zsh/ported/params.rs
1//! Parameter management for zshrs
2//!
3//! Port from zsh/Src/params.c (6511 lines → full Rust port)
4//!
5//! Provides shell parameters (variables), special parameters, arrays,
6//! associative arrays, parameter attributes, namerefs, scoping,
7//! tied parameters, and all special parameter get/set functions.
8
9use crate::config_h::DEFAULT_TMPPREFIX;
10use crate::func_body_fmt::FuncBodyFmt;
11use crate::lex::parse_subscript;
12use crate::ported::builtin::{LASTVAL, PPARAMS};
13use crate::ported::config_h::{MACHTYPE, OSTYPE, VENDOR};
14use crate::ported::exec::FORKLEVEL;
15use crate::ported::hashtable::emptycmdnamtable;
16use crate::ported::hashtable::hashtable_nodes;
17use crate::ported::hist::{
18 bangchar, casemodify, hashchar, hatchar, histsiz, resizehistents, saveandpophiststack,
19 savehistsiz,
20};
21use crate::ported::init::SHTTY;
22use crate::ported::lex::untokenize;
23use crate::ported::math::lastbase;
24#[allow(unused_imports)]
25use crate::ported::math::{
26 matheval, mathevali, MN_FLOAT, MN_FLOAT as MN_FLT, MN_INTEGER, MN_INTEGER as MN_INT,
27};
28use crate::ported::mem::{popheap, pushheap};
29use crate::ported::modules::parameter::FUNCSTACK;
30#[allow(unused_imports)]
31use crate::ported::options::{opt_state_get, opt_state_set, optlookup};
32use crate::ported::patchlevel::{ZSH_PATCHLEVEL, ZSH_VERSION};
33use crate::ported::pattern::{patcompile, pattry};
34#[allow(unused_imports)]
35use crate::ported::signals::{queue_signals, unqueue_signals};
36use crate::ported::signals_h::SIGS;
37use crate::ported::string::ztrdup;
38use crate::ported::utils::{
39 adduserdir, arrlen_ge, dec_locallevel, inc_locallevel, metafy, quotedzputs, xsymlink,
40};
41#[allow(unused_imports)]
42use crate::ported::utils::{
43 adjustwinsize, argzero, colonsplit, errflag, get_username, inittyptab, itype_end,
44 locallevel as locallevel_fn, posixzero, set_argzero, set_locallevel, set_posixzero, unmeta,
45 zerr, ztrdup_metafy, zwarn,
46};
47use crate::ported::zsh_h::PAT_HEAPDUP;
48#[allow(unused_imports)]
49use crate::ported::zsh_h::{
50 gsu_array, gsu_float, gsu_hash, gsu_integer, gsu_scalar, hashnode, hashtable, isset, mnumber,
51 param, paramdef, unset, value, HashTable, Marker, Param, ALLEXPORT, ASSPM_AUGMENT,
52 ASSPM_ENV_IMPORT, ASSPM_KEY_VALUE, ASSPM_WARN, AUTONAMEDIRS, EMULATE_KSH, EMULATE_SH,
53 EMULATE_ZSH, EMULATION, ERRFLAG_ERROR, EXECOPT, FS_FUNC, KSHARRAYS, PM_ARRAY, PM_AUTOLOAD,
54 PM_DECLARED, PM_DEFAULTED, PM_DONTIMPORT, PM_DONTIMPORT_SUID, PM_EFLOAT, PM_EXPORTED,
55 PM_FFLOAT, PM_HASHED, PM_HASHELEM, PM_HIDE, PM_HIDEVAL, PM_INTEGER, PM_LEFT, PM_LOCAL,
56 PM_NAMEDDIR, PM_NAMEREF, PM_NORESTORE, PM_READONLY, PM_READONLY_SPECIAL, PM_REMOVABLE,
57 PM_RIGHT_B, PM_RIGHT_Z, PM_RO_BY_DESIGN, PM_SCALAR, PM_SPECIAL, PM_TAGGED, PM_TIED, PM_TYPE,
58 PM_UNIQUE, PM_UNSET, PM_UPPER, POSIXARGZERO, PRINT_INCLUDEVALUE, PRINT_KV_PAIR, PRINT_LINE,
59 PRINT_NAMEONLY, PRINT_POSIX_EXPORT, PRINT_POSIX_READONLY, PRINT_TYPE, PRINT_TYPESET,
60 SCANPM_ARRONLY, SCANPM_CHECKING, SCANPM_ISVAR_AT, SCANPM_KEYMATCH, SCANPM_MATCHKEY,
61 SCANPM_MATCHMANY, SCANPM_MATCHVAL, SCANPM_NONAMEREF, SCANPM_WANTINDEX, SCANPM_WANTKEYS,
62 SCANPM_WANTVALS, TERM_BAD, TERM_UNKNOWN, VALFLAG_EMPTY, VALFLAG_INV, VALFLAG_SUBST,
63 WARNCREATEGLOBAL, WARNNESTEDVAR,
64};
65use crate::ported::zsh_h::{
66 HashNode, Inbrack, Meta, CBASES, CHASELINKS, HFILE_USE_OPTIONS, INTERACTIVE, OCTALZEROES,
67 PM_LOWER, PRIVILEGED, SCANPM_ASSIGNING,
68};
69use crate::ported::zsh_h::{CASMOD_LOWER, CASMOD_UPPER};
70use crate::ported::zsh_system_h::DEFAULT_TIMEFMT;
71use crate::{DPUTS, DPUTS2};
72use fusevm::Value;
73use indexmap::IndexMap;
74use std::collections::{HashMap, HashSet};
75use std::env;
76use std::sync::atomic::{AtomicI64, Ordering};
77use std::sync::{Arc, Mutex, OnceLock, RwLock};
78use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
79
80/// Port of `static int lc_update_needed` from `Src/params.c:5850`
81/// (under `#ifdef USE_LOCALE`). Set to 1 by `scanendscope` when a
82/// LC_*/LANG param's scope ends; consumed by `endparamscope` to
83/// trigger a `setlocale()` refresh.
84pub static LC_UPDATE_NEEDED: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
85
86/// Port of `static Param foundparam` from `Src/params.c:640`.
87/// Set by `scanparamvals` to the last param it touched, read by
88/// `assignsparam` / `assignnparam` for the assoc-element path.
89/// Stores the param name; the live `¶m` lookup is done by
90/// the caller through paramtab.
91pub static FOUNDPARAM: OnceLock<Mutex<Option<String>>> = OnceLock::new();
92
93/// Port of `rprompt_indent_unsetfn()` from `Src/params.c:4237`. C
94/// body: `stdunsetfn(pm, exp); rprompt_indent = 1;` — keeps in
95/// sync with init_term().
96pub fn rprompt_indent_unsetfn(pm: &mut param, exp: i32) {
97 stdunsetfn(pm, exp);
98 *RPROMPT_INDENT.lock().unwrap() = 1;
99}
100
101// =============================================================================
102// IPDEF{1,2,4,5,5U,6,7,7R,7U,8,9,10} + LCIPDEF — special-parameter
103// table entry constructors. All defined as macros in
104// `Src/params.c:296-406`. Each produces one row of the
105// `special_params[]` table; the differences are flag combinations
106// + which gsu (getter/setter union) the entry binds.
107//
108// In C, `BR(p)` is `{(void *)(p)}` for the param's `u` data field;
109// `GSU(g)` is the `&g` of the named gsu_scalar/gsu_integer/etc.
110// The Rust port stores `var` and `gsu` as `usize` slot indexes
111// into per-evaluator tables, matching the existing PARAMDEF helper
112// above. The flag bit combinations mirror the C macros line-by-line.
113// =============================================================================
114
115/// Port of `IPDEF1()` from `Src/params.c:296` —
116/// `{{NULL,A,PM_INTEGER|PM_SPECIAL|C},BR(NULL),GSU(B),10,0,...}`.
117#[inline]
118#[allow(non_snake_case)]
119pub fn IPDEF1(A: &str, B: usize, C: i32) -> paramdef {
120 // c:params.c:296
121 paramdef {
122 name: A.into(),
123 flags: (PM_INTEGER | PM_SPECIAL) as i32 | C,
124 gsu: B,
125 ..Default::default()
126 }
127}
128
129/// Port of `IPDEF2()` from `Src/params.c:309` —
130/// `{{NULL,A,PM_SCALAR|PM_SPECIAL|C},BR(NULL),GSU(B),0,0,...}`.
131#[inline]
132#[allow(non_snake_case)]
133pub fn IPDEF2(A: &str, B: usize, C: i32) -> paramdef {
134 // c:params.c:309
135 paramdef {
136 name: A.into(),
137 flags: (PM_SCALAR | PM_SPECIAL) as i32 | C,
138 gsu: B,
139 ..Default::default()
140 }
141}
142
143// ---------------------------------------------------------------------------
144// Parameter flags (from zsh.h PM_* flags)
145// ---------------------------------------------------------------------------
146
147// What level of localness we are at. // c:47
148// // c:48
149// Hand-wavingly, this is incremented at every function call and decremented // c:49
150// at every function return. See startparamscope(). // c:50
151
152/// Port of `mod_export int locallevel;` from `Src/params.c:54`.
153/// Tracks function-local-scope nesting depth. Bumped by
154/// `startparamscope()` (params.c:5879) on every function call,
155/// decremented by `endparamscope()` (params.c:5950) on return.
156///
157/// Storage is [`crate::thread_shell_state::LocalLevelCell`], not a bare
158/// `AtomicI32`: the counter does two jobs that C can merge because it has
159/// one thread. `load()` is the SHARED stamp the one param table's entries
160/// are compared against and is unchanged; `here()` is the calling thread's
161/// own depth, which is what the error formatters must read so an
162/// `async_precmd` hook running on a worker cannot make a top-level error
163/// print as if it had happened inside that hook. See that module for the
164/// misattribution this prevents.
165#[allow(non_upper_case_globals)]
166pub static locallevel: crate::thread_shell_state::LocalLevelCell = // c:54
167 crate::thread_shell_state::LocalLevelCell::new();
168
169/// Port of `mod_export zlong mypid;` from `Src/params.c:102` — `$$`
170/// (`IPDEF4("$", &mypid)`, c:353). Set ONCE by `setupvals`
171/// (c:Src/init.c:1227 `mypid = (zlong) getpid();`); a forked child
172/// (pipeline stage, `&` job, coproc) inherits the parent's value, so `$$`
173/// names the parent shell there — zshrs read `getpid()` live and printed
174/// the child's pid (`echo $$ | cat`).
175#[allow(non_upper_case_globals)]
176pub static mypid: std::sync::atomic::AtomicI64 = std::sync::atomic::AtomicI64::new(0); // c:102
177
178/// Port of `mod_export zlong ppid;` from `Src/params.c:107` — `$PPID`
179/// (`IPDEF4("PPID", &ppid)`, c:350), set by `setupvals`
180/// (c:Src/init.c:1226 `ppid = (zlong) getppid();`). Same fork rule as
181/// [`mypid`]: a forked stage keeps the shell's parent, not its own.
182#[allow(non_upper_case_globals)]
183pub static ppid: std::sync::atomic::AtomicI64 = std::sync::atomic::AtomicI64::new(0); // c:107
184
185// ---------------------------------------------------------------------------
186// Real `param` struct lives in Src/zsh.h:1829 (port at zsh_h.rs:750).
187// It uses C-union flattening: u_str / u_arr / u_val / u_dval / u_hash
188// dispatched on `PM_TYPE(node.flags)`. There is NO `ParamValue` enum in
189// C; do not reintroduce one.
190// ---------------------------------------------------------------------------
191
192/// Port of `LCIPDEF()` from `Src/params.c:324` —
193/// `IPDEF2(name, lc_blah_gsu, PM_UNSET)`.
194#[inline]
195#[allow(non_snake_case)]
196pub fn LCIPDEF(name: &str) -> paramdef {
197 // c:params.c:324
198 IPDEF2(name, 0, PM_UNSET as i32) // c:324 lc_blah_gsu (slot 0)
199}
200
201/// Port of `IPDEF4()` from `Src/params.c:344` —
202/// `{{NULL,A,PM_INTEGER|PM_READONLY_SPECIAL},BR((void*)B),
203/// GSU(varint_readonly_gsu),10,0,...}`.
204#[inline]
205#[allow(non_snake_case)]
206pub fn IPDEF4(A: &str, B: usize) -> paramdef {
207 // c:params.c:344
208 paramdef {
209 name: A.into(),
210 flags: (PM_INTEGER | PM_READONLY_SPECIAL) as i32,
211 var: B,
212 ..Default::default()
213 }
214}
215
216/// Port of `IPDEF5()` from `Src/params.c:353` —
217/// `{{NULL,A,PM_INTEGER|PM_SPECIAL},BR((void*)B),GSU(F),10,0,...}`.
218#[inline]
219#[allow(non_snake_case)]
220pub fn IPDEF5(A: &str, B: usize, F: usize) -> paramdef {
221 // c:params.c:353
222 paramdef {
223 name: A.into(),
224 flags: (PM_INTEGER | PM_SPECIAL) as i32,
225 var: B,
226 gsu: F,
227 ..Default::default()
228 }
229}
230
231/// Port of `IPDEF5U()` from `Src/params.c:354` — c:353 + PM_UNSET.
232#[inline]
233#[allow(non_snake_case)]
234pub fn IPDEF5U(A: &str, B: usize, F: usize) -> paramdef {
235 // c:params.c:354
236 paramdef {
237 name: A.into(),
238 flags: (PM_INTEGER | PM_SPECIAL | PM_UNSET) as i32,
239 var: B,
240 gsu: F,
241 ..Default::default()
242 }
243}
244
245/// Port of `IPDEF6()` from `Src/params.c:362` — c:353 + PM_DONTIMPORT.
246#[inline]
247#[allow(non_snake_case)]
248pub fn IPDEF6(A: &str, B: usize, F: usize) -> paramdef {
249 // c:params.c:362
250 paramdef {
251 name: A.into(),
252 flags: (PM_INTEGER | PM_SPECIAL | PM_DONTIMPORT) as i32,
253 var: B,
254 gsu: F,
255 ..Default::default()
256 }
257}
258
259/// Port of `IPDEF7()` from `Src/params.c:367` —
260/// `{{NULL,A,PM_SCALAR|PM_SPECIAL},BR((void*)B),GSU(varscalar_gsu),0,0,...}`.
261#[inline]
262#[allow(non_snake_case)]
263pub fn IPDEF7(A: &str, B: usize) -> paramdef {
264 // c:params.c:367
265 paramdef {
266 name: A.into(),
267 flags: (PM_SCALAR | PM_SPECIAL) as i32,
268 var: B,
269 ..Default::default()
270 }
271}
272
273/// Port of `IPDEF7U()` from `Src/params.c:369` — c:367 + PM_UNSET.
274#[inline]
275#[allow(non_snake_case)]
276pub fn IPDEF7U(A: &str, B: usize) -> paramdef {
277 // c:params.c:369
278 paramdef {
279 name: A.into(),
280 flags: (PM_SCALAR | PM_SPECIAL | PM_UNSET) as i32,
281 var: B,
282 ..Default::default()
283 }
284}
285
286/// Port of `IPDEF7R()` from `Src/params.c:368` — c:367 + PM_DONTIMPORT_SUID.
287#[inline]
288#[allow(non_snake_case)]
289pub fn IPDEF7R(A: &str, B: usize) -> paramdef {
290 // c:params.c:368
291 paramdef {
292 name: A.into(),
293 flags: (PM_SCALAR | PM_SPECIAL | PM_DONTIMPORT_SUID) as i32,
294 var: B,
295 ..Default::default()
296 }
297}
298
299/// Port of `IPDEF9()` from `Src/params.c:384` —
300/// `{{NULL,A,D|PM_ARRAY|PM_SPECIAL|PM_DONTIMPORT},BR((void*)B),
301/// GSU(vararray_gsu),0,0,NULL,C,NULL,0}`.
302#[inline]
303#[allow(non_snake_case)]
304pub fn IPDEF9(A: &str, B: usize, C: usize, D: i32) -> paramdef {
305 // c:params.c:384
306 paramdef {
307 name: A.into(),
308 flags: (PM_ARRAY | PM_SPECIAL | PM_DONTIMPORT) as i32 | D,
309 var: B,
310 ..Default::default()
311 }
312}
313
314/// Port of `IPDEF8()` from `Src/params.c:394` —
315/// `{{NULL,A,D|PM_SCALAR|PM_SPECIAL},BR((void*)B),GSU(colonarr_gsu),
316/// 0,0,NULL,C,NULL,0}`.
317/// `C` is the colon-arr field; the Rust port stores it in `getnfn`
318/// since `paramdef` lacks a dedicated colon-arr slot until that's
319/// ported.
320#[inline]
321#[allow(non_snake_case)]
322pub fn IPDEF8(A: &str, B: usize, C: usize, D: i32) -> paramdef {
323 // c:params.c:394
324 paramdef {
325 name: A.into(),
326 flags: (PM_SCALAR | PM_SPECIAL) as i32 | D,
327 var: B,
328 ..Default::default()
329 }
330}
331
332/// Port of `IPDEF10()` from `Src/params.c:406` —
333/// `{{NULL,A,PM_ARRAY|PM_SPECIAL},BR(NULL),GSU(B),10,0,...}`.
334#[inline]
335#[allow(non_snake_case)]
336pub fn IPDEF10(A: &str, B: usize) -> paramdef {
337 // c:params.c:406
338 paramdef {
339 name: A.into(),
340 flags: (PM_ARRAY | PM_SPECIAL) as i32,
341 gsu: B,
342 ..Default::default()
343 }
344}
345
346/// Port of `newparamtable()` from `Src/params.c:519`. C body
347/// allocates a HashTable via `newhashtable(size, name, NULL)`
348/// and wires the vtable. Rust port constructs a fresh
349/// `Box<hashtable>` with the param-specific callbacks left as
350/// `None` (the hashtable.rs vtable cannot host the typed
351/// param-callback signatures yet — wiring them requires the
352/// hashtable backend refactor).
353#[allow(unused_variables)]
354pub fn newparamtable(size: i32, name: &str) -> Option<HashTable> {
355 let hsize = if size == 0 { 17 } else { size };
356 let mut nodes: Vec<Option<HashNode>> = Vec::with_capacity(hsize as usize);
357 for _ in 0..hsize {
358 nodes.push(None);
359 }
360 Some(Box::new(hashtable {
361 hsize,
362 ct: 0,
363 nodes,
364 tmpdata: 0,
365 hash: None,
366 emptytable: None,
367 filltable: None,
368 cmpnodes: None,
369 addnode: None,
370 getnode: None,
371 getnode2: None,
372 removenode: None,
373 disablenode: None,
374 enablenode: None,
375 freenode: None,
376 printnode: None,
377 scantab: None,
378 }))
379}
380
381/// Port of `loadparamnode()` from `Src/params.c:544`. If `pm` is
382/// an AUTOLOAD stub, fires the module loader and re-fetches the
383/// node from ht; otherwise returns pm unchanged.
384///
385/// C body:
386/// if (pm && (pm->flags & PM_AUTOLOAD) && pm->u.str) {
387/// int level = pm->level;
388/// char *mn = dupstring(pm->u.str);
389/// (void)ensurefeature(mn, "p:", nam);
390/// pm = (Param)gethashnode2(ht, nam);
391/// while (pm && pm->level > level) pm = pm->old;
392/// if (pm && (pm->level != level || (pm->flags & PM_AUTOLOAD)))
393/// pm = NULL;
394/// if (!pm) zerr("autoloading module %s failed...", mn, nam);
395/// }
396/// return pm;
397/// Port of `loadparamnode(HashTable ht, Param pm, const char *nam)` from `Src/params.c:544`.
398/// WARNING: param names don't match C — Rust=(pm, nam) vs C=(ht, pm, nam)
399pub fn loadparamnode(
400 // c:544
401 _ht: &HashTable,
402 pm: Option<Param>,
403 nam: &str,
404) -> Option<Param> {
405 // c:546 — `if (pm && (pm->flags & PM_AUTOLOAD) && pm->u.str)`.
406 let (level, modname) = match &pm {
407 Some(p) if p.node.flags & PM_AUTOLOAD as i32 != 0 && p.u_str.is_some() => {
408 (p.level, p.u_str.clone().unwrap())
409 }
410 _ => return pm, // c:566 fall through
411 };
412
413 // c:549 — `(void)ensurefeature(mn, "p:", (pm->node.flags & PM_AUTOALL)
414 // ? NULL : nam);` fires the module loader (same shape as
415 // exec::resolvebuiltin's c:2711 call). A feature the module lacks
416 // reports "autoload cancelled" there and drops the stub.
417 let autoall = pm
418 .as_ref()
419 .is_some_and(|p| (p.node.flags as u32 & crate::ported::zsh_h::PM_AUTOALL) != 0);
420 if let Ok(mut mtab) = crate::ported::module::MODULESTAB.lock() {
421 let _ = crate::ported::module::ensurefeature(
422 &mut mtab,
423 &modname,
424 "p:",
425 if autoall { None } else { Some(nam) },
426 );
427 }
428 // c:550 — re-fetch the node from ht after autoload.
429 let mut pm = paramtab().write().unwrap().get(nam).cloned();
430 // c:551 — walk pm->old back to original level.
431 while let Some(ref p) = pm {
432 if p.level > level {
433 pm = p.old.clone().map(|b| Param::from(b));
434 } else {
435 break;
436 }
437 }
438 // c:553-554 — if pm is at wrong level or still AUTOLOAD, treat
439 // as load failure.
440 let still_bad = match &pm {
441 Some(p) => p.level != level || p.node.flags & PM_AUTOLOAD as i32 != 0,
442 None => true,
443 };
444 if still_bad {
445 pm = None;
446 // c:561-563 — `zerr("autoloading module %s failed to define
447 // parameter: %s", mn, nam)`.
448 zerr(&format!(
449 "autoloading module {} failed to define parameter: {}",
450 modname, nam
451 ));
452 }
453 pm // c:566
454}
455
456// ---------------------------------------------------------------------------
457// Numeric type for parameters (from params.c mnumber)
458// ---------------------------------------------------------------------------
459
460// ---------------------------------------------------------------------------
461// Value struct - mirrors C's struct value for subscript access
462// ---------------------------------------------------------------------------
463// ---------------------------------------------------------------------------
464// Shell parameter
465// ---------------------------------------------------------------------------
466
467// ---------------------------------------------------------------------------
468// Tied parameter data
469// ---------------------------------------------------------------------------
470
471// TiedData removed: was a Rust-only sidecar for the deleted `ParamTable`'s
472// `tied: HashMap<String, TiedData>` field. C source stores tied-pair
473// metadata via `pm->ename` (the partner name) and `pm->u.data` (the
474// separator char) on the real `param` struct (Src/zsh.h:750 / Src/params.c
475// `bin_typeset()` typeset -T branch).
476
477// ---------------------------------------------------------------------------
478// Parameter table print types (from printparamnode)
479// ---------------------------------------------------------------------------
480
481// ---------------------------------------------------------------------------
482// Special parameter definitions table (mirrors special_params[] in C)
483// ---------------------------------------------------------------------------
484
485/// Special-parameter definition — Rust extension paralleling the
486/// `IPDEF*` macro entries in `Src/params.c:297-392`. C uses
487/// `struct paramdef` (`Src/zsh.h:2082`, mirrored at `zsh_h.rs:950`)
488/// with `var` + `gsu` pointers; the Rust port carries a trimmed
489/// shape with `pm_type`/`pm_flags`/`tied_name` until the full
490/// `gsu`-callback plumbing lands. Canonical `paramdef` is the
491/// long-term target.
492#[allow(non_camel_case_types)]
493#[derive(Clone, Debug)]
494pub struct special_paramdef {
495 /// `name` field.
496 pub name: &'static str,
497 pub pm_type: u32, // PM_INTEGER | PM_SCALAR | PM_ARRAY
498 pub pm_flags: u32, // PM_READONLY_SPECIAL, PM_DONTIMPORT, etc.
499 /// `tied_name` field.
500 pub tied_name: Option<&'static str>,
501}
502
503/// Index of the first entry in `special_params` that lives in the
504/// zsh-only section (after the `{{NULL,NULL,0}, BR(NULL), ...}`
505/// sentinel at `Src/params.c:392`). Entries before this index are
506/// always loaded; entries at and after this index are only loaded
507/// under non-sh/non-ksh emulation. Mirrors the C two-section table
508/// terminated by an inner NULL sentinel.
509pub const SPECIAL_PARAMS_ZSH_START: usize = 54; // c:392
510
511/// All special parameters from params.c special_params[]
512pub const special_params: &[special_paramdef] = &[
513 // Integer specials with custom GSU
514 special_paramdef {
515 name: "#",
516 pm_type: PM_INTEGER,
517 pm_flags: PM_READONLY,
518 tied_name: None,
519 },
520 special_paramdef {
521 name: "ERRNO",
522 pm_type: PM_INTEGER,
523 pm_flags: PM_UNSET,
524 tied_name: None,
525 },
526 special_paramdef {
527 name: "GID",
528 pm_type: PM_INTEGER,
529 pm_flags: PM_DONTIMPORT,
530 tied_name: None,
531 },
532 special_paramdef {
533 name: "EGID",
534 pm_type: PM_INTEGER,
535 pm_flags: PM_DONTIMPORT,
536 tied_name: None,
537 },
538 special_paramdef {
539 name: "HISTSIZE",
540 pm_type: PM_INTEGER,
541 pm_flags: 0,
542 tied_name: None,
543 },
544 special_paramdef {
545 name: "RANDOM",
546 pm_type: PM_INTEGER,
547 pm_flags: 0,
548 tied_name: None,
549 },
550 special_paramdef {
551 name: "SAVEHIST",
552 pm_type: PM_INTEGER,
553 pm_flags: 0,
554 tied_name: None,
555 },
556 special_paramdef {
557 name: "SECONDS",
558 pm_type: PM_INTEGER,
559 pm_flags: 0,
560 tied_name: None,
561 },
562 special_paramdef {
563 name: "UID",
564 pm_type: PM_INTEGER,
565 pm_flags: PM_DONTIMPORT,
566 tied_name: None,
567 },
568 special_paramdef {
569 name: "EUID",
570 pm_type: PM_INTEGER,
571 pm_flags: PM_DONTIMPORT,
572 tied_name: None,
573 },
574 special_paramdef {
575 name: "TTYIDLE",
576 pm_type: PM_INTEGER,
577 pm_flags: PM_READONLY,
578 tied_name: None,
579 },
580 // Scalar specials with custom GSU
581 special_paramdef {
582 name: "USERNAME",
583 pm_type: PM_SCALAR,
584 pm_flags: PM_DONTIMPORT,
585 tied_name: None,
586 },
587 special_paramdef {
588 name: "-",
589 pm_type: PM_SCALAR,
590 pm_flags: PM_READONLY,
591 tied_name: None,
592 },
593 special_paramdef {
594 name: "histchars",
595 pm_type: PM_SCALAR,
596 pm_flags: PM_DONTIMPORT,
597 tied_name: None,
598 },
599 special_paramdef {
600 name: "HOME",
601 pm_type: PM_SCALAR,
602 pm_flags: PM_UNSET,
603 tied_name: None,
604 },
605 special_paramdef {
606 name: "TERM",
607 pm_type: PM_SCALAR,
608 pm_flags: PM_UNSET,
609 tied_name: None,
610 },
611 special_paramdef {
612 name: "TERMINFO",
613 pm_type: PM_SCALAR,
614 pm_flags: PM_UNSET,
615 tied_name: None,
616 },
617 special_paramdef {
618 name: "TERMINFO_DIRS",
619 pm_type: PM_SCALAR,
620 pm_flags: PM_UNSET,
621 tied_name: None,
622 },
623 special_paramdef {
624 name: "WORDCHARS",
625 pm_type: PM_SCALAR,
626 pm_flags: 0,
627 tied_name: None,
628 },
629 special_paramdef {
630 name: "IFS",
631 pm_type: PM_SCALAR,
632 pm_flags: PM_DONTIMPORT,
633 tied_name: None,
634 },
635 special_paramdef {
636 name: "_",
637 pm_type: PM_SCALAR,
638 pm_flags: PM_DONTIMPORT,
639 tied_name: None,
640 },
641 special_paramdef {
642 name: "KEYBOARD_HACK",
643 pm_type: PM_SCALAR,
644 pm_flags: PM_DONTIMPORT,
645 tied_name: None,
646 },
647 special_paramdef {
648 name: "0",
649 pm_type: PM_SCALAR,
650 pm_flags: 0,
651 tied_name: None,
652 },
653 // Readonly integer variables bound to C globals
654 special_paramdef {
655 name: "!",
656 pm_type: PM_INTEGER,
657 pm_flags: PM_READONLY,
658 tied_name: None,
659 },
660 special_paramdef {
661 name: "$",
662 pm_type: PM_INTEGER,
663 pm_flags: PM_READONLY,
664 tied_name: None,
665 },
666 special_paramdef {
667 name: "?",
668 pm_type: PM_INTEGER,
669 pm_flags: PM_READONLY,
670 tied_name: None,
671 },
672 special_paramdef {
673 name: "HISTCMD",
674 pm_type: PM_INTEGER,
675 pm_flags: PM_READONLY,
676 tied_name: None,
677 },
678 special_paramdef {
679 name: "LINENO",
680 pm_type: PM_INTEGER,
681 pm_flags: PM_READONLY,
682 tied_name: None,
683 },
684 special_paramdef {
685 name: "PPID",
686 pm_type: PM_INTEGER,
687 pm_flags: PM_READONLY,
688 tied_name: None,
689 },
690 special_paramdef {
691 name: "ZSH_SUBSHELL",
692 pm_type: PM_INTEGER,
693 pm_flags: PM_READONLY,
694 tied_name: None,
695 },
696 // Settable integer variables
697 special_paramdef {
698 name: "COLUMNS",
699 pm_type: PM_INTEGER,
700 pm_flags: 0,
701 tied_name: None,
702 },
703 special_paramdef {
704 name: "LINES",
705 pm_type: PM_INTEGER,
706 pm_flags: 0,
707 tied_name: None,
708 },
709 special_paramdef {
710 name: "ZLE_RPROMPT_INDENT",
711 pm_type: PM_INTEGER,
712 pm_flags: PM_UNSET,
713 tied_name: None,
714 },
715 special_paramdef {
716 name: "SHLVL",
717 pm_type: PM_INTEGER,
718 pm_flags: 0,
719 tied_name: None,
720 },
721 special_paramdef {
722 name: "FUNCNEST",
723 pm_type: PM_INTEGER,
724 pm_flags: 0,
725 tied_name: None,
726 },
727 special_paramdef {
728 name: "OPTIND",
729 pm_type: PM_INTEGER,
730 pm_flags: PM_DONTIMPORT,
731 tied_name: None,
732 },
733 special_paramdef {
734 // c:Src/params.c:364 — `IPDEF6("TRY_BLOCK_ERROR", &try_errflag,
735 // varinteger_gsu)` = PM_INTEGER | PM_SPECIAL | PM_DONTIMPORT.
736 // No PM_UNSET on the table entry — C reads -1 via the getfn
737 // reading the global `try_errflag`. zshrs's earlier port set
738 // PM_UNSET as a "no-write-yet" sentinel which broke
739 // `${(k)parameters}` parity (zsh emits the name; with PM_UNSET
740 // here scanpmparameters skipped it). The -1 default is now
741 // surfaced via the special-var getter (params.rs sentinel
742 // override on PM_UNSET removal).
743 name: "TRY_BLOCK_ERROR",
744 pm_type: PM_INTEGER,
745 pm_flags: PM_DONTIMPORT,
746 tied_name: None,
747 },
748 special_paramdef {
749 // c:Src/loop.c — `try_interrupt = -1`. Same pattern as
750 // TRY_BLOCK_ERROR: no PM_UNSET on the table entry; -1
751 // default emerges via the special-var getter.
752 name: "TRY_BLOCK_INTERRUPT",
753 pm_type: PM_INTEGER,
754 pm_flags: PM_DONTIMPORT,
755 tied_name: None,
756 },
757 // Scalar variables bound to C globals
758 special_paramdef {
759 name: "OPTARG",
760 pm_type: PM_SCALAR,
761 pm_flags: 0,
762 tied_name: None,
763 },
764 special_paramdef {
765 name: "NULLCMD",
766 pm_type: PM_SCALAR,
767 pm_flags: 0,
768 tied_name: None,
769 },
770 special_paramdef {
771 name: "POSTEDIT",
772 pm_type: PM_SCALAR,
773 pm_flags: PM_UNSET,
774 tied_name: None,
775 },
776 special_paramdef {
777 name: "READNULLCMD",
778 pm_type: PM_SCALAR,
779 pm_flags: 0,
780 tied_name: None,
781 },
782 special_paramdef {
783 name: "PS1",
784 pm_type: PM_SCALAR,
785 pm_flags: 0,
786 tied_name: None,
787 },
788 special_paramdef {
789 name: "RPS1",
790 pm_type: PM_SCALAR,
791 pm_flags: PM_UNSET,
792 tied_name: None,
793 },
794 special_paramdef {
795 name: "RPROMPT",
796 pm_type: PM_SCALAR,
797 pm_flags: PM_UNSET,
798 tied_name: None,
799 },
800 special_paramdef {
801 name: "PS2",
802 pm_type: PM_SCALAR,
803 pm_flags: 0,
804 tied_name: None,
805 },
806 special_paramdef {
807 name: "RPS2",
808 pm_type: PM_SCALAR,
809 pm_flags: PM_UNSET,
810 tied_name: None,
811 },
812 special_paramdef {
813 name: "RPROMPT2",
814 pm_type: PM_SCALAR,
815 pm_flags: PM_UNSET,
816 tied_name: None,
817 },
818 special_paramdef {
819 name: "PS3",
820 pm_type: PM_SCALAR,
821 pm_flags: 0,
822 tied_name: None,
823 },
824 special_paramdef {
825 name: "PS4",
826 pm_type: PM_SCALAR,
827 pm_flags: PM_DONTIMPORT_SUID,
828 tied_name: None,
829 },
830 special_paramdef {
831 name: "SPROMPT",
832 pm_type: PM_SCALAR,
833 pm_flags: 0,
834 tied_name: None,
835 },
836 // Readonly arrays
837 special_paramdef {
838 name: "*",
839 pm_type: PM_ARRAY,
840 pm_flags: PM_READONLY | PM_DONTIMPORT,
841 tied_name: None,
842 },
843 special_paramdef {
844 name: "@",
845 pm_type: PM_ARRAY,
846 pm_flags: PM_READONLY | PM_DONTIMPORT,
847 tied_name: None,
848 },
849 // ===================================================================
850 // c:388-392 — `/* This empty row indicates the end of parameters
851 // available in all emulations. */` NULL sentinel terminates the
852 // "always loaded" section. Entries below this line are only added
853 // under zsh emulation (else-branch of EMULATION(EMULATE_SH|EMULATE_KSH)
854 // at createparamtable c:840-846).
855 // SPECIAL_PARAMS_ZSH_START tracks this section boundary.
856 // ===================================================================
857 // Tied colon-separated/array pairs
858 special_paramdef {
859 name: "CDPATH",
860 pm_type: PM_SCALAR,
861 pm_flags: PM_TIED,
862 tied_name: Some("cdpath"),
863 },
864 special_paramdef {
865 name: "FIGNORE",
866 pm_type: PM_SCALAR,
867 pm_flags: PM_TIED,
868 tied_name: Some("fignore"),
869 },
870 special_paramdef {
871 name: "FPATH",
872 pm_type: PM_SCALAR,
873 pm_flags: PM_TIED,
874 tied_name: Some("fpath"),
875 },
876 special_paramdef {
877 name: "MAILPATH",
878 pm_type: PM_SCALAR,
879 pm_flags: PM_TIED,
880 tied_name: Some("mailpath"),
881 },
882 special_paramdef {
883 name: "PATH",
884 pm_type: PM_SCALAR,
885 pm_flags: PM_TIED,
886 tied_name: Some("path"),
887 },
888 special_paramdef {
889 name: "PSVAR",
890 pm_type: PM_SCALAR,
891 pm_flags: PM_TIED,
892 tied_name: Some("psvar"),
893 },
894 special_paramdef {
895 name: "ZSH_EVAL_CONTEXT",
896 pm_type: PM_SCALAR,
897 pm_flags: PM_READONLY | PM_TIED,
898 tied_name: Some("zsh_eval_context"),
899 },
900 special_paramdef {
901 name: "MODULE_PATH",
902 pm_type: PM_SCALAR,
903 pm_flags: PM_DONTIMPORT | PM_TIED,
904 tied_name: Some("module_path"),
905 },
906 special_paramdef {
907 name: "MANPATH",
908 pm_type: PM_SCALAR,
909 pm_flags: PM_TIED,
910 tied_name: Some("manpath"),
911 },
912 // Locale
913 special_paramdef {
914 name: "LANG",
915 pm_type: PM_SCALAR,
916 pm_flags: PM_UNSET,
917 tied_name: None,
918 },
919 special_paramdef {
920 name: "LC_ALL",
921 pm_type: PM_SCALAR,
922 pm_flags: PM_UNSET,
923 tied_name: None,
924 },
925 special_paramdef {
926 name: "LC_COLLATE",
927 pm_type: PM_SCALAR,
928 pm_flags: PM_UNSET,
929 tied_name: None,
930 },
931 special_paramdef {
932 name: "LC_CTYPE",
933 pm_type: PM_SCALAR,
934 pm_flags: PM_UNSET,
935 tied_name: None,
936 },
937 special_paramdef {
938 name: "LC_MESSAGES",
939 pm_type: PM_SCALAR,
940 pm_flags: PM_UNSET,
941 tied_name: None,
942 },
943 special_paramdef {
944 name: "LC_NUMERIC",
945 pm_type: PM_SCALAR,
946 pm_flags: PM_UNSET,
947 tied_name: None,
948 },
949 special_paramdef {
950 name: "LC_TIME",
951 pm_type: PM_SCALAR,
952 pm_flags: PM_UNSET,
953 tied_name: None,
954 },
955 // Zsh-only aliases
956 special_paramdef {
957 name: "ARGC",
958 pm_type: PM_INTEGER,
959 pm_flags: PM_READONLY,
960 tied_name: None,
961 },
962 special_paramdef {
963 name: "HISTCHARS",
964 pm_type: PM_SCALAR,
965 pm_flags: PM_DONTIMPORT,
966 tied_name: None,
967 },
968 special_paramdef {
969 name: "status",
970 pm_type: PM_INTEGER,
971 pm_flags: PM_READONLY,
972 tied_name: None,
973 },
974 special_paramdef {
975 name: "prompt",
976 pm_type: PM_SCALAR,
977 pm_flags: 0,
978 tied_name: None,
979 },
980 special_paramdef {
981 name: "PROMPT",
982 pm_type: PM_SCALAR,
983 pm_flags: 0,
984 tied_name: None,
985 },
986 special_paramdef {
987 name: "PROMPT2",
988 pm_type: PM_SCALAR,
989 pm_flags: 0,
990 tied_name: None,
991 },
992 special_paramdef {
993 name: "PROMPT3",
994 pm_type: PM_SCALAR,
995 pm_flags: 0,
996 tied_name: None,
997 },
998 special_paramdef {
999 name: "PROMPT4",
1000 pm_type: PM_SCALAR,
1001 pm_flags: 0,
1002 tied_name: None,
1003 },
1004 special_paramdef {
1005 name: "argv",
1006 pm_type: PM_ARRAY,
1007 pm_flags: 0,
1008 tied_name: None,
1009 },
1010 // c:Src/params.c:425-434 — IPDEF9 lowercase array tied counterparts
1011 // of the uppercase scalar specials. Each is a PM_ARRAY|PM_TIED|
1012 // PM_SPECIAL entry that points back to its colon-scalar partner
1013 // via tied_name. Without these in special_params, `${(t)path}` /
1014 // `${(t)cdpath}` / `${(t)fpath}` etc. miss the `-tied-special`
1015 // suffix when the paramtab.get(name) lookup runs in subst.rs:5605.
1016 special_paramdef {
1017 name: "fignore",
1018 pm_type: PM_ARRAY,
1019 pm_flags: PM_TIED,
1020 tied_name: Some("FIGNORE"),
1021 },
1022 special_paramdef {
1023 name: "cdpath",
1024 pm_type: PM_ARRAY,
1025 pm_flags: PM_TIED,
1026 tied_name: Some("CDPATH"),
1027 },
1028 special_paramdef {
1029 name: "fpath",
1030 pm_type: PM_ARRAY,
1031 pm_flags: PM_TIED,
1032 tied_name: Some("FPATH"),
1033 },
1034 special_paramdef {
1035 name: "mailpath",
1036 pm_type: PM_ARRAY,
1037 pm_flags: PM_TIED,
1038 tied_name: Some("MAILPATH"),
1039 },
1040 special_paramdef {
1041 name: "manpath",
1042 pm_type: PM_ARRAY,
1043 pm_flags: PM_TIED,
1044 tied_name: Some("MANPATH"),
1045 },
1046 special_paramdef {
1047 name: "psvar",
1048 pm_type: PM_ARRAY,
1049 pm_flags: PM_TIED,
1050 tied_name: Some("PSVAR"),
1051 },
1052 special_paramdef {
1053 name: "zsh_eval_context",
1054 pm_type: PM_ARRAY,
1055 pm_flags: PM_TIED | PM_READONLY,
1056 tied_name: Some("ZSH_EVAL_CONTEXT"),
1057 },
1058 special_paramdef {
1059 name: "module_path",
1060 pm_type: PM_ARRAY,
1061 pm_flags: PM_TIED,
1062 tied_name: Some("MODULE_PATH"),
1063 },
1064 special_paramdef {
1065 name: "path",
1066 pm_type: PM_ARRAY,
1067 pm_flags: PM_TIED,
1068 tied_name: Some("PATH"),
1069 },
1070 // pipestatus array
1071 special_paramdef {
1072 name: "pipestatus",
1073 pm_type: PM_ARRAY,
1074 pm_flags: 0,
1075 tied_name: None,
1076 },
1077];
1078
1079/// Port of `static initparam special_params_sh[]` from
1080/// `Src/params.c:447-460`. "Alternative versions of colon-separated
1081/// path parameters for sh emulation. These don't link to the array
1082/// versions." Loaded by `createparamtable` (c:840-844) when
1083/// `EMULATION(EMULATE_SH|EMULATE_KSH)` is non-zero, instead of the
1084/// zsh-only section of `special_params`. All entries are scalars
1085/// (`IPDEF8` macro adds `PM_SCALAR|PM_SPECIAL`); the C-side
1086/// `tied_name` is NULL so these aren't tied to lowercase array
1087/// counterparts.
1088pub const special_params_sh: &[special_paramdef] = &[
1089 special_paramdef {
1090 // c:448
1091 name: "CDPATH",
1092 pm_type: PM_SCALAR,
1093 pm_flags: 0,
1094 tied_name: None,
1095 },
1096 special_paramdef {
1097 // c:449
1098 name: "FIGNORE",
1099 pm_type: PM_SCALAR,
1100 pm_flags: 0,
1101 tied_name: None,
1102 },
1103 special_paramdef {
1104 // c:450
1105 name: "FPATH",
1106 pm_type: PM_SCALAR,
1107 pm_flags: 0,
1108 tied_name: None,
1109 },
1110 special_paramdef {
1111 // c:451
1112 name: "MAILPATH",
1113 pm_type: PM_SCALAR,
1114 pm_flags: 0,
1115 tied_name: None,
1116 },
1117 special_paramdef {
1118 // c:452
1119 name: "PATH",
1120 pm_type: PM_SCALAR,
1121 pm_flags: 0,
1122 tied_name: None,
1123 },
1124 special_paramdef {
1125 // c:453
1126 name: "PSVAR",
1127 pm_type: PM_SCALAR,
1128 pm_flags: 0,
1129 tied_name: None,
1130 },
1131 special_paramdef {
1132 // c:454
1133 name: "ZSH_EVAL_CONTEXT",
1134 pm_type: PM_SCALAR,
1135 pm_flags: PM_READONLY,
1136 tied_name: None,
1137 },
1138 special_paramdef {
1139 // c:457 (security comment)
1140 name: "MODULE_PATH",
1141 pm_type: PM_SCALAR,
1142 pm_flags: PM_DONTIMPORT,
1143 tied_name: None,
1144 },
1145];
1146
1147/// Port of `getparamnode()` from `Src/params.c:570`. C body:
1148/// `pm = loadparamnode(ht, gethashnode2(ht, nam), nam);
1149/// if (pm && ht == realparamtab && !PM_UNSET) pm = resolve_nameref(pm);
1150/// return (HashNode)pm;`
1151/// WARNING: param names don't match C — Rust=() vs C=(ht, nam)
1152pub fn getparamnode(ht: &HashTable, nam: &str) -> Option<Param> {
1153 // c:572 — `pm = loadparamnode(ht, gethashnode2(ht, nam), nam)`.
1154 let pm = paramtab().read().unwrap().get(nam).cloned();
1155 let pm = loadparamnode(ht, pm, nam);
1156 // c:573 — `if (pm && ht == realparamtab && !PM_UNSET) pm = resolve_nameref(pm)`.
1157 if let Some(p) = pm {
1158 if p.node.flags & PM_UNSET as i32 == 0 {
1159 // ht == realparamtab check — both Rust accessors point at
1160 // the same backing store today, so this is always true.
1161 return resolve_nameref(Some(p));
1162 }
1163 return Some(p);
1164 }
1165 None
1166}
1167
1168/// Port of `scancopyparams()` from `Src/params.c:584`. C body:
1169/// ```c
1170/// Param tpm = (Param) zshcalloc(sizeof *tpm);
1171/// tpm->node.nam = ztrdup(pm->node.nam);
1172/// copyparam(tpm, pm, 0);
1173/// addhashnode(outtable, tpm->node.nam, tpm);
1174/// ```
1175/// Real port: clone the param via `Box::new(pm.clone())` (Rust
1176/// equivalent of zshcalloc + copyparam) and push it into the
1177/// caller-supplied destination table. The original C uses the
1178/// global `outtable`; Rust port plumbs it in explicitly.
1179/// WARNING: param names don't match C — Rust=(pm, _flags, outtable) vs C=(hn, flags)
1180pub fn scancopyparams(pm: &mut param, _flags: i32, outtable: &mut HashMap<String, Box<param>>) {
1181 // c:586-588 — `tpm = (Param) zshcalloc(...); copyparam(tpm, pm, 0); addnode(...)`.
1182 let mut tpm = Box::new(pm.clone()); // c:586 zshcalloc
1183 tpm.old = None;
1184 tpm.env = None;
1185 tpm.ename = None; // c:1242 (calloc-zero fields copyparam doesn't set)
1186 copyparam(&mut tpm, pm, 0); // c:587
1187 let nam = tpm.node.nam.clone();
1188 outtable.insert(nam, tpm); // c:588 addnode(outtable, ztrdup(pm->node.nam), tpm)
1189}
1190
1191/// Port of `copyparamtable()` from `Src/params.c:596`. C body:
1192/// allocates a fresh paramtable via `newparamtable(ht->hsize, name)`,
1193/// sets the global `outtable = nht`, then scans the source via
1194/// `scanhashtable(ht, 0, 0, 0, scancopyparams, 0)` and clears
1195/// `outtable` on exit. Rust port returns the freshly-allocated
1196/// table; the per-node clone walk requires the HashTable iterator
1197/// which isn't wired yet (callers receive the empty allocated
1198/// table — same shape the C source returns when `ht` is empty).
1199pub fn copyparamtable(ht: Option<&HashTable>, name: &str) -> Option<HashTable> {
1200 let ht = ht?;
1201 newparamtable(ht.hsize, name)
1202}
1203
1204/// Port of `deleteparamtable()` from `Src/params.c:616`. C body:
1205/// `int odelunset = delunset; delunset = 1; deletehashtable(t);
1206/// delunset = odelunset;` — flips the global before tearing down
1207/// each entry so unset callbacks fire. Rust port: `Drop` cascades
1208/// through `Box<hashtable>` to clear all `nodes`; consume the
1209/// table by value to mirror the C ownership transfer.
1210pub fn deleteparamtable(t: Option<HashTable>) {
1211 // c:616-623 — `int odelunset = delunset; delunset = 1;` save/
1212 // restore so the inner free path fires every entry's unsetfn.
1213 let odelunset = DELUNSET.swap(1, Ordering::Relaxed); // c:620-621
1214 if let Some(table) = t {
1215 // Box dropped here → fields freed; param freenode callbacks
1216 // are invoked transparently via Drop on each `param` entry.
1217 drop(table);
1218 }
1219 DELUNSET.store(odelunset, Ordering::Relaxed); // c:623
1220}
1221
1222/// Port of `scancountparams()` from `Src/params.c:630`. C body:
1223/// ```c
1224/// ++numparamvals;
1225/// if ((flags & SCANPM_WANTKEYS) && (flags & SCANPM_WANTVALS))
1226/// ++numparamvals;
1227/// ```
1228/// Increments the static `numparamvals` global used by
1229/// `paramvalarr`. Rust port mirrors against a counter passed by
1230/// reference (no static-mutable in safe Rust).
1231/// WARNING: param names don't match C — Rust=(_hn, flags, numparamvals) vs C=(hn, flags)
1232pub fn scancountparams(_hn: ¶m, flags: i32, numparamvals: &mut u32) {
1233 *numparamvals += 1;
1234 if (flags as u32 & SCANPM_WANTKEYS) != 0 && (flags as u32 & SCANPM_WANTVALS) != 0 {
1235 *numparamvals += 1;
1236 }
1237}
1238
1239/// Port of `scanparamvals()` from `Src/params.c:644`. Real C body
1240/// is the per-node callback for `paramvalarr`: applies SCANPM_MATCHKEY
1241/// (pattry on name) / SCANPM_MATCHVAL (pattry on value) / SCANPM_KEYMATCH
1242/// (compile pm.nam as pattern, match against scanstr) / SCANPM_WANTKEYS
1243/// / SCANPM_WANTVALS / SCANPM_MATCHMANY filters, populating the
1244/// `paramvals[]` slice with the param's name and/or `getstrvalue`
1245/// result, and stashing `foundparam = pm`. State lives in the C
1246/// file-scope statics ported above as `NUMPARAMVALS` / `SCANPROG` /
1247/// `SCANSTR` / `PARAMVALS` / `FOUNDPARAM`.
1248/// WARNING: param names don't match C — Rust=(flags) vs C=(hn, flags)
1249pub fn scanparamvals(
1250 // c:644
1251 pm: &mut param,
1252 flags: i32,
1253) {
1254 let f = flags as u32;
1255 if NUMPARAMVALS.load(Ordering::Relaxed) != 0
1256 && (f & SCANPM_MATCHMANY) == 0
1257 && (f & (SCANPM_MATCHVAL | SCANPM_MATCHKEY | SCANPM_KEYMATCH)) != 0
1258 {
1259 return;
1260 }
1261 if (f & SCANPM_KEYMATCH) != 0 {
1262 // patcompile(pm.node.nam) + pattry(prog, scanstr)
1263 let scanstr = scanstr_lock().lock().unwrap().clone();
1264 if let Some(s) = scanstr {
1265 let matched = patcompile(
1266 &{
1267 let mut __pat_tok = (&pm.node.nam).to_string();
1268 crate::ported::glob::tokenize(&mut __pat_tok);
1269 __pat_tok
1270 },
1271 PAT_HEAPDUP as i32,
1272 None,
1273 )
1274 .map_or(false, |p| pattry(&p, &s));
1275 if !matched {
1276 return;
1277 }
1278 } else {
1279 return;
1280 }
1281 } else if (f & SCANPM_MATCHKEY) != 0 {
1282 let prog = scanprog_lock().lock().unwrap().clone();
1283 if let Some(p) = prog {
1284 let matched = patcompile(
1285 &{
1286 let mut __pat_tok = (&p).to_string();
1287 crate::ported::glob::tokenize(&mut __pat_tok);
1288 __pat_tok
1289 },
1290 PAT_HEAPDUP as i32,
1291 None,
1292 )
1293 .map_or(false, |prog| pattry(&prog, &pm.node.nam));
1294 if !matched {
1295 return;
1296 }
1297 } else {
1298 return;
1299 }
1300 }
1301 set_foundparam(Some(pm.node.nam.clone()));
1302 if (f & SCANPM_WANTKEYS) != 0 {
1303 paramvals_lock().lock().unwrap().push(pm.node.nam.clone());
1304 NUMPARAMVALS.fetch_add(1, Ordering::Relaxed);
1305 if (f & (SCANPM_WANTVALS | SCANPM_MATCHVAL)) == 0 {
1306 return;
1307 }
1308 }
1309 let mut vbuf = value {
1310 pm: None, // placeholder; real C re-binds
1311 arr: Vec::new(),
1312 scanflags: 0,
1313 valflags: 0,
1314 start: 0,
1315 end: -1,
1316 };
1317 // C: paramvals[numparamvals] = getstrvalue(&v);
1318 // We don't move pm into vbuf to preserve the borrow; mirror the
1319 // C semantics by reading u_str directly via strgetfn for the
1320 // PM_SCALAR fast path and falling back through getstrvalue when
1321 // wired.
1322 let s = strgetfn(pm);
1323 let _ = vbuf;
1324 if (f & SCANPM_MATCHVAL) != 0 {
1325 let prog = scanprog_lock().lock().unwrap().clone();
1326 let matched = prog
1327 .and_then(|p| {
1328 patcompile(
1329 &{
1330 let mut __pat_tok = (&p).to_string();
1331 crate::ported::glob::tokenize(&mut __pat_tok);
1332 __pat_tok
1333 },
1334 PAT_HEAPDUP as i32,
1335 None,
1336 )
1337 })
1338 .map_or(false, |prog| pattry(&prog, &s));
1339 if matched {
1340 // c:672-674 — `numparamvals += ((flags & SCANPM_WANTVALS) ? 1 :
1341 // !(flags & SCANPM_WANTKEYS));`. C wrote the value into
1342 // `paramvals[numparamvals]` BEFORE this test (c:670), so an
1343 // increment of 0 means the slot is reused by the next node and
1344 // the value is discarded. The Rust store is a Vec whose length
1345 // IS `numparamvals`, so a 0-increment must simply not push —
1346 // otherwise `-k h[(R)pat]` (WANTKEYS + MATCHVAL, inc 0)
1347 // interleaved every matching VALUE with its key.
1348 let inc = if (f & SCANPM_WANTVALS) != 0 {
1349 1
1350 } else if (f & SCANPM_WANTKEYS) == 0 {
1351 1
1352 } else {
1353 0
1354 };
1355 if inc != 0 {
1356 paramvals_lock().lock().unwrap().push(s);
1357 NUMPARAMVALS.fetch_add(inc, Ordering::Relaxed);
1358 }
1359 } else if (f & SCANPM_WANTKEYS) != 0 {
1360 // c:675-676 — `--numparamvals; /* Value didn't match, discard key */`
1361 paramvals_lock().lock().unwrap().pop();
1362 NUMPARAMVALS.fetch_sub(1, Ordering::Relaxed);
1363 }
1364 } else {
1365 paramvals_lock().lock().unwrap().push(s);
1366 NUMPARAMVALS.fetch_add(1, Ordering::Relaxed);
1367 }
1368 set_foundparam(None);
1369}
1370
1371/// Port of `paramvalarr()` from `Src/params.c:689`. Scans the param hash twice (count,
1372/// then collect) and returns a heap-allocated string array. C body:
1373/// ```c
1374/// numparamvals = 0;
1375/// if (ht) scanhashtable(ht, 0, 0, PM_UNSET, scancountparams, flags);
1376/// paramvals = zhalloc((numparamvals + 1) * sizeof(char *));
1377/// if (ht) { numparamvals = 0;
1378/// scanhashtable(ht, 0, 0, PM_UNSET, scanparamvals, flags); }
1379/// paramvals[numparamvals] = 0;
1380/// return paramvals;
1381/// ```
1382/// SCANPM_MATCHKEY / SCANPM_MATCHVAL filter against `scanprog`
1383/// (the active glob/regex from the caller's `${(k)var[(I)pattern]}`
1384/// subscript); SCANPM_WANTKEYS / SCANPM_WANTVALS / SCANPM_WANTINDEX
1385/// control which fields land in the output array.
1386///
1387/// zshrs port note on the `ht` argument: C's assoc backing is a
1388/// `HashTable` whose nodes are `PM_SCALAR` child Params created by
1389/// `arrhashsetfn` (`createparam(k, PM_SCALAR|PM_UNSET)` + `assignstrvalue`,
1390/// c:4156-4166). zshrs's `arrhashsetfn` (params.rs, same C line) writes the
1391/// pairs into `paramtab_hashed_storage` instead and leaves `pm->u_hash`
1392/// empty, so the `IndexMap<String, String>` IS this port's assoc hash
1393/// table. Each entry is materialised back into the `PM_SCALAR` node C's
1394/// scan callbacks expect, so `scancountparams`/`scanparamvals` are the
1395/// unmodified C callbacks working over the same node shape.
1396///
1397/// Previously this scanned the GLOBAL `paramtab` and reimplemented the
1398/// per-node emit inline (never calling `scanparamvals`), so it could not
1399/// be pointed at a named assoc at all — `${h[(R)pat]}`-shaped scans and
1400/// `compadd -k 'h[(R)pat]'` had no backend.
1401/// Port of `paramvalarr(HashTable ht, int flags)` from `Src/params.c:689`.
1402pub fn paramvalarr(ht: &IndexMap<String, String>, flags: i32) -> Vec<String> {
1403 // c:689
1404 // c:691-692 — DPUTS((flags & (SCANPM_MATCHKEY|SCANPM_MATCHVAL)) && !scanprog,
1405 // "BUG: scanning hash without scanprog set");
1406 DPUTS!(
1407 // c:691
1408 (flags as u32 & (SCANPM_MATCHKEY | SCANPM_MATCHVAL)) != 0
1409 && scanprog_lock().lock().unwrap().is_none(), // c:691 !scanprog test
1410 "BUG: scanning hash without scanprog set" // c:692
1411 );
1412
1413 // c:Src/params.c:4156-4166 — an assoc entry is a `PM_SCALAR` Param
1414 // whose `node.nam` is the key and whose value is the element string.
1415 let as_node = |k: &String, val: &String| -> param {
1416 let mut pm = param::default();
1417 pm.node.nam = k.clone();
1418 pm.node.flags = PM_SCALAR as i32;
1419 pm.u_str = Some(val.clone());
1420 pm
1421 };
1422
1423 // c:693-695 — first pass sizes the result:
1424 // numparamvals = 0;
1425 // if (ht) scanhashtable(ht, 0, 0, PM_UNSET, scancountparams, flags);
1426 NUMPARAMVALS.store(0, Ordering::Relaxed); // c:693
1427 let mut count: u32 = 0;
1428 for (k, val) in ht.iter() {
1429 // c:695
1430 scancountparams(&as_node(k, val), flags, &mut count);
1431 }
1432
1433 // c:696 — `paramvals = zhalloc((numparamvals + 1) * sizeof(char *));`
1434 {
1435 let mut pv = paramvals_lock().lock().unwrap();
1436 pv.clear();
1437 pv.reserve(count as usize + 1);
1438 }
1439
1440 // c:698-700 — second pass fills it:
1441 // numparamvals = 0;
1442 // scanhashtable(ht, 0, 0, PM_UNSET, scanparamvals, flags);
1443 NUMPARAMVALS.store(0, Ordering::Relaxed); // c:698
1444 for (k, val) in ht.iter() {
1445 // c:700
1446 scanparamvals(&mut as_node(k, val), flags);
1447 }
1448
1449 // c:701-702 — `paramvals[numparamvals] = 0; return paramvals;`. The
1450 // Rust store is a Vec whose length is `numparamvals`, so there is no
1451 // NULL terminator to write.
1452 paramvals_lock().lock().unwrap().clone() // c:702
1453}
1454
1455/// Port of `getvaluearr()` from `Src/params.c:710`. C body:
1456/// ```c
1457/// if (v->arr) return v->arr;
1458/// else if (PM_TYPE == PM_ARRAY) return v->arr = pm->gsu.a->getfn(pm);
1459/// else if (PM_TYPE == PM_HASHED) {
1460/// v->arr = paramvalarr(pm->gsu.h->getfn(pm), v->scanflags);
1461/// v->start = 0; v->end = numparamvals + 1; return v->arr;
1462/// } else return NULL;
1463/// ```
1464pub fn getvaluearr(v: Option<&mut value>) -> Vec<String> {
1465 let v = match v {
1466 Some(v) => v,
1467 None => return Vec::new(),
1468 };
1469 if !v.arr.is_empty() {
1470 return v.arr.clone();
1471 }
1472 let (t, nam) = match v.pm.as_ref() {
1473 Some(p) => (PM_TYPE(p.node.flags as u32), p.node.nam.clone()),
1474 None => return Vec::new(),
1475 };
1476 if t == PM_ARRAY {
1477 // c:713 — `return v->arr = v->pm->gsu.a->getfn(v->pm);`. That is a
1478 // VTABLE dispatch: `arrgetfn` (c:4054 `return pm->u.arr;`) is only
1479 // the `stdarray_gsu` getfn. The zsh/parameter magic arrays
1480 // (`reswords`, `funcstack`, `dirstack`, …) carry a MODULE getfn and
1481 // have no `u.arr` at all, and this port keeps those rows outside the
1482 // pm, so `getaparam` (c:3101) is where the dispatch is spelled out —
1483 // exactly the split it documents at its own `u_arr` branch.
1484 let has_u_arr = v.pm.as_ref().map_or(false, |p| p.u_arr.is_some());
1485 let arr = if has_u_arr {
1486 arrgetfn(v.pm.as_ref().expect("pm checked above")) // c:4054
1487 } else {
1488 getaparam(&nam).unwrap_or_default() // c:3107
1489 };
1490 v.arr = arr;
1491 return v.arr.clone();
1492 }
1493 if t == PM_HASHED {
1494 // c:715 — `v->arr = paramvalarr(v->pm->gsu.h->getfn(v->pm),
1495 // v->scanflags);`
1496 //
1497 // `hashgetfn` (c:4084 `return pm->u.hash;`) is not the assoc
1498 // backing in this port: `arrhashsetfn` (c:4113) stores the pairs in
1499 // `paramtab_hashed_storage` and leaves `u_hash` empty. The
1500 // equivalent of `gsu.h->getfn(pm)` is therefore
1501 // `subst::assoc_get`, which resolves namerefs (c:570-575), imposes
1502 // the `local NAME` shadow over a magic special (c:1090-1115), and
1503 // returns the entries in zsh HASH-BUCKET order — the visit order
1504 // C's `scanhashtable` (Src/hashtable.c:426) produces, which is the
1505 // order the scan emits matches in.
1506 let ht = crate::ported::subst::assoc_get(&nam).unwrap_or_default();
1507 v.arr = paramvalarr(&ht, v.scanflags); // c:715
1508 // c:716-718 — `/* Can't take numeric slices of associative
1509 // arrays */ v->start = 0; v->end = numparamvals + 1;`.
1510 v.start = 0; // c:716
1511 v.end = (v.arr.len() + 1) as i32; // c:717
1512 return v.arr.clone();
1513 }
1514 Vec::new() // c:719
1515}
1516
1517/// ```c
1518/// struct value vbuf; Value v; int slice; char **arr;
1519/// if (!(v = getvalue(&vbuf, &name, 1)) || *name) return 0;
1520/// if (v->scanflags & ~SCANPM_ARRONLY) return v->end > 1;
1521/// slice = v->start != 0 || v->end != -1;
1522/// if (PM_TYPE(v->pm->node.flags) != PM_ARRAY || !slice)
1523/// return !slice && !(v->pm->node.flags & PM_UNSET);
1524/// if (!v->end) return 0;
1525/// if (!(arr = getvaluearr(v))) return 0;
1526/// return arrlen_ge(arr, v->end < 0 ? - v->end : v->end);
1527/// ```
1528/// Returns 1 if `name` resolves to a set parameter (or a non-empty
1529/// slice/element of one). Used by `[[ -v NAME ]]`/`[[ -n …]]`
1530/// dispatch in cond.c and the readonly-check inside builtin.c.
1531/// Port of `issetvar()` from `Src/params.c:732`.
1532pub fn issetvar(name: &str) -> i32 {
1533 // c:732
1534 let mut vbuf = value {
1535 pm: None,
1536 arr: Vec::new(),
1537 scanflags: 0,
1538 valflags: 0,
1539 start: 0,
1540 end: -1,
1541 };
1542 let mut cursor: &str = name;
1543 let v = match getvalue(Some(&mut vbuf), &mut cursor, 1) {
1544 // c:739
1545 Some(v) => v,
1546 None => return 0,
1547 };
1548 if !cursor.is_empty() {
1549 // c:739
1550 return 0; // c:740 no value or more chars after the variable name
1551 }
1552 // c:741 — `if (v->scanflags & ~SCANPM_ARRONLY) return v->end > 1`.
1553 // The extracted-elements branch keys on EXTRACTION scanflags. zshrs's
1554 // getvalue additionally sets SCANPM_CHECKING (the issetvar/`-v` call
1555 // passes the no-create flag) which C's getvalue does NOT leave in
1556 // v->scanflags — so mask it out here too, else a whole-array `-v arr`
1557 // (scanflags = CHECKING only, end = -1) wrongly took this branch and
1558 // returned `end > 1` = 0.
1559 if (v.scanflags as u32 & !(SCANPM_ARRONLY | SCANPM_CHECKING)) != 0 {
1560 // c:741
1561 return if v.end > 1 { 1 } else { 0 }; // c:742
1562 }
1563
1564 let slice = v.start != 0 || v.end != -1; // c:744
1565 let pm = match v.pm.as_ref() {
1566 Some(p) => p,
1567 None => {
1568 // c:2222-2224 — positional `-v N`: C sets `v->pm = argvparam`
1569 // (the PM_ARRAY over the positionals) and falls through to the
1570 // array range check below. zshrs keeps positionals in PPARAMS
1571 // rather than a real pm, so fetchvalue leaves `pm = None` with
1572 // `start = N-1`, `end = N`; range-check `end` against the
1573 // pparams length to report whether `$N` is set.
1574 if v.end >= 1 && !name.is_empty() && name.bytes().all(|b| b.is_ascii_digit()) {
1575 let len = crate::ported::builtin::PPARAMS
1576 .lock()
1577 .map(|p| p.len())
1578 .unwrap_or(0);
1579 return if (v.end as usize) <= len { 1 } else { 0 };
1580 }
1581 return 0;
1582 }
1583 };
1584 if PM_TYPE(pm.node.flags as u32) != PM_ARRAY || !slice {
1585 // c:745
1586 return if !slice && (pm.node.flags as u32 & PM_UNSET) == 0 {
1587 1
1588 } else {
1589 0
1590 }; // c:746
1591 }
1592
1593 if v.end == 0 {
1594 // c:748 empty array slice
1595 return 0; // c:749
1596 }
1597 // c:751 — get the array and check end is within range
1598 let arr = getvaluearr(Some(v));
1599 if arr.is_empty() {
1600 // c:751
1601 return 0; // c:752
1602 }
1603 // c:753
1604 let bound: usize = if v.end < 0 {
1605 (-v.end) as usize
1606 } else {
1607 v.end as usize
1608 };
1609 if arrlen_ge(&arr, bound) {
1610 1
1611 } else {
1612 0
1613 }
1614}
1615
1616/// Port of `split_env_string()` from `Src/params.c:763`.
1617///
1618/// Walks `env` until either `=` or end. Returns `None` (C `0`) if:
1619/// - any byte before `=` has the high bit set (c:771-777 — names
1620/// outside the portable character set are silently rejected),
1621/// - no `=` is present (c:783-785 fall-through),
1622/// - or the name is empty (`*str == '=' && str == tenv`, c:782).
1623/// Otherwise returns `Some((name, value))` (C `1` + out-params).
1624///
1625/// Out-param style differs from C (we return a tuple); the
1626/// rejection rules are 1:1.
1627pub fn split_env_string(env: &str) -> Option<(String, String)> {
1628 // c:763
1629 if env.is_empty() {
1630 // c:763 !env
1631 return None;
1632 }
1633 let bytes = env.as_bytes();
1634 // c:770-779 — walk name bytes, reject if high bit set.
1635 let mut i = 0;
1636 while i < bytes.len() && bytes[i] != b'=' {
1637 // c:770
1638 if bytes[i] >= 128 {
1639 // c:771 (unsigned char) >= 128
1640 return None; // c:777
1641 }
1642 i += 1;
1643 }
1644 // c:780-785 — accept only if `=` was found at non-zero offset.
1645 if i > 0 && i < bytes.len() && bytes[i] == b'=' {
1646 // c:780
1647 let name = String::from_utf8_lossy(&bytes[..i]).into_owned(); // c:781-782
1648 let value = String::from_utf8_lossy(&bytes[i + 1..]).into_owned(); // c:783
1649 Some((name, value)) // c:784
1650 } else {
1651 None // c:786
1652 }
1653}
1654
1655// parameter entries as well as setting up parameter table // c:812
1656// entries for environment variables we inherit. // c:813
1657/// Direct port of `createparamtable()` from `Src/params.c:817-988`.
1658///
1659/// Walks the same five-stage init sequence as the C source:
1660/// 1. Touch paramtab/realparamtab so the OnceLocks initialise
1661/// (c:835 — newparamtable(151,"paramtab")).
1662/// 2. Register every `special_params[]` entry as a PM_SPECIAL
1663/// node in the global paramtab (c:838-847). EMULATE_SH/KSH
1664/// override list (`special_params_sh`) is wired below.
1665/// 3. Initialise non-special params that must precede env
1666/// import: MAILCHECK / KEYTIMEOUT / LISTMAX / TMPPREFIX /
1667/// TIMEFMT / HOST / LOGNAME (c:854-879).
1668/// 4. Walk std::env::vars() and import each name that is a legal
1669/// ident and not blocked via `dontimport`. Mark PM_EXPORTED
1670/// and stamp the param's env field (c:893-925).
1671/// 5. Post-import wiring: HOME PM_UNSET clear + LOGNAME/SHLVL
1672/// env sync, CPUTYPE / MACHTYPE / OSTYPE / TTY / VENDOR /
1673/// ZSH_ARGZERO / ZSH_VERSION / ZSH_PATCHLEVEL (c:931-979).
1674///
1675/// The `noerrs = 2` guard (c:872) is held until the signals[] build
1676/// completes (c:1009). The rest of the C body (ALLEXPORT toggle,
1677/// set_pwd_env, signals[] build with SIGRTMIN..MAX) follows below.
1678/// Port of `extern char **environ` (POSIX, read by `createparamtable`
1679/// at Src/params.c:893). C reads the environment EXACTLY as it was at
1680/// process entry — nothing mutates `environ` before zsh walks it. In
1681/// the Rust binary, linked frameworks can rewrite the live environment
1682/// during their lazy init before our import runs (observed on macOS:
1683/// CoreFoundation recomputes `__CF_USER_TEXT_ENCODING`, so `export -p`
1684/// showed `0x1F5:0x0:0x0` while zsh, inheriting the same env, printed
1685/// the original `0x0:0:0`, and CF *adds* the variable outright when the
1686/// parent never exported it). `main()` fills this OnceLock as its first
1687/// statement; the import loops below prefer the snapshot and fall back to
1688/// the live env (lib tests never run `main`).
1689///
1690/// On macOS the snapshot is not `std::env::vars()` but the process-entry
1691/// `envp` dyld hands to a `__mod_init_func` entry, which CoreFoundation's
1692/// `setenv` cannot have grown — see `initial_env` in `bins/zshrs.rs` for
1693/// the mechanism, the measurements, and the residual value-only caveat.
1694/// Elsewhere it is the live environment, which is already the
1695/// process-entry one.
1696#[allow(non_upper_case_globals)]
1697pub static environ: OnceLock<Vec<(String, String)>> = OnceLock::new();
1698
1699/// Port of `createparamtable()` from `Src/params.c:817`.
1700pub fn createparamtable() {
1701 // c:817
1702
1703 // c:835 — `paramtab = realparamtab = newparamtable(151, "paramtab")`.
1704 let _ = paramtab();
1705 let _ = realparamtab();
1706
1707 // Helper closure (single definition; mirrors the C
1708 // `paramtab->addnode(paramtab, ztrdup(name), ip)` site).
1709 let add_special = |ip: &special_paramdef, tab: &mut hashtable_nodes<Param>| {
1710 // !!! DASH-FAITHFUL GATE (no C counterpart) !!! dash has no $RANDOM /
1711 // $SECONDS: they are ordinary variables there.
1712 if crate::dash_mode::dash_faithful() && matches!(ip.name, "RANDOM" | "SECONDS") {
1713 return;
1714 }
1715 // c:840 — `paramdef->gsu` selects which gsu_scalar vtable the
1716 // new param gets. C uses the per-IPDEF macro's BR(...) field;
1717 // since the Rust special_paramdef doesn't carry a gsu slot
1718 // yet, dispatch by name to the matching `*_GSU` constant
1719 // (HOME_GSU/IFS_GSU/...). Non-special scalars (no match)
1720 // leave gsu_s as None and fall back to strsetfn/strgetfn.
1721 let gsu_s: Option<Box<gsu_scalar>> = match ip.name {
1722 "0" => Some(Box::new(ARGZERO_GSU.clone())), // c:225-226 / IPDEF2("0", argzero_gsu, 0)
1723 "HOME" => Some(Box::new(HOME_GSU.clone())), // c:248
1724 "IFS" => Some(Box::new(IFS_GSU.clone())), // c:245
1725 "TERM" => Some(Box::new(TERM_GSU.clone())), // c:250
1726 "TERMINFO" => Some(Box::new(TERMINFO_GSU.clone())), // c:251
1727 "TERMINFO_DIRS" => Some(Box::new(TERMINFODIRS_GSU.clone())), // c:252
1728 "WORDCHARS" => Some(Box::new(WORDCHARS_GSU.clone())), // c:249
1729 "USERNAME" => Some(Box::new(USERNAME_GSU.clone())), // c:247
1730 "KEYBOARD_HACK" => Some(Box::new(KEYBOARDHACK_GSU.clone())), // c:253
1731 "HISTCHARS" | "histchars" => Some(Box::new(HISTCHARS_GSU.clone())), // c:246
1732 "LANG" => Some(Box::new(LANG_GSU.clone())), // c:332
1733 "LC_ALL" => Some(Box::new(LC_ALL_GSU.clone())), // c:333
1734 // c:331 `# define LCIPDEF(name) IPDEF2(name, lc_blah_gsu, PM_UNSET)`
1735 // — c:334-347 lists exactly these five categories.
1736 "LC_COLLATE" | "LC_CTYPE" | "LC_MESSAGES" | "LC_NUMERIC" | "LC_TIME" => {
1737 Some(Box::new(LC_BLAH_GSU.clone())) // c:335-346
1738 }
1739 _ => None,
1740 };
1741 let pm = Box::new(param {
1742 node: hashnode {
1743 next: None,
1744 nam: ip.name.to_string(),
1745 flags: (ip.pm_type | ip.pm_flags | PM_SPECIAL) as i32,
1746 },
1747 u_data: 0,
1748 u_tied: None,
1749 u_arr: None,
1750 u_str: None,
1751 u_val: 0,
1752 u_dval: 0.0,
1753 u_hash: None,
1754 gsu_s, // c:840 gsu_s wired
1755 gsu_i: None,
1756 gsu_f: None,
1757 gsu_a: None,
1758 gsu_h: None,
1759 // c:303/351/360/361/369/413 — every INTEGER IPDEF macro
1760 // (IPDEF1, IPDEF4, IPDEF5, IPDEF5U, IPDEF6) writes `10` into
1761 // the initparam `base` slot; the scalar/array ones (IPDEF2,
1762 // IPDEF7*, IPDEF8, IPDEF9) write `0`. printparamnode's
1763 // PMTF_USE_BASE arm (c:6242-6243) prints that base after the
1764 // `integer` word, which is why zsh says `integer 10 UID`.
1765 // Hardcoding 0 here dropped the base for every special
1766 // integer on the createparamtable path (interactive/setupvals);
1767 // the `-fc` path seeds it separately, so only the REPL diverged.
1768 base: if (ip.pm_type & PM_INTEGER) != 0 {
1769 10
1770 } else {
1771 0
1772 },
1773 width: 0,
1774 env: None,
1775 // c:391/401 — IPDEF9/IPDEF8 pass the tied partner's name as
1776 // the `ename` slot (`IPDEF8("PATH", &path, "path", PM_TIED)`).
1777 // printparamnode's PM_TIED arm (c:6256-6288) looks the peer up
1778 // by `p->ename` and prints its name before this one's, giving
1779 // `tied path PATH` / `array tied PATH path`. Leaving it None
1780 // made the peer lookup fail, so the partner name vanished.
1781 ename: ip.tied_name.map(|s| s.to_string()),
1782 old: None,
1783 level: 0,
1784 });
1785 tab.insert(ip.name.to_string(), pm);
1786 };
1787
1788 // c:838-840 — `for (ip = special_params; ip->node.nam; ip++)
1789 // paramtab->addnode(...)`. Section 1: always loaded.
1790 {
1791 let mut tab = paramtab().write().unwrap();
1792 for ip in special_params[..SPECIAL_PARAMS_ZSH_START].iter() {
1793 add_special(ip, &mut tab);
1794 }
1795 }
1796
1797 // c:840-847 — emulation branch. Under EMULATE_SH/EMULATE_KSH,
1798 // load special_params_sh (scalar versions). Otherwise load
1799 // special_params zsh-only section (the continuation past the
1800 // inner NULL sentinel).
1801 let is_sh_ksh = EMULATION(EMULATE_SH | EMULATE_KSH);
1802 {
1803 let mut tab = paramtab().write().unwrap();
1804 if is_sh_ksh {
1805 // c:841-843 — sh/ksh: scalar replacements.
1806 for ip in special_params_sh.iter() {
1807 add_special(ip, &mut tab);
1808 }
1809 } else {
1810 // c:845-847 — zsh: continuation tail (array-tied + lowercase
1811 // aliases + pipestatus).
1812 for ip in special_params[SPECIAL_PARAMS_ZSH_START..].iter() {
1813 add_special(ip, &mut tab);
1814 }
1815 }
1816 }
1817 // c:Src/params.c:113 — `zlong zsh_funcnest = MAX_FUNCTION_DEPTH;`.
1818 // The IPDEF5("FUNCNEST", &zsh_funcnest, varinteger_gsu) special
1819 // (c:359) reads this statically-initialized global, so $FUNCNEST
1820 // reports MAX_FUNCTION_DEPTH from the first instant the table
1821 // exists. zshrs models FUNCNEST as a stored integer special (no
1822 // backing global / getfn wired in add_special), which defaults to
1823 // u_val=0 — that 0 trips the funcnest guard on the very first
1824 // function call. Seed it here to mirror the C global's static
1825 // initializer. config.h MAX_FUNCTION_DEPTH=500 (Homebrew
1826 // arm-darwin oracle reports `$FUNCNEST` == 500).
1827 setiparam("FUNCNEST", 500); // c:params.c:113
1828
1829 // c:848 — `argvparam = (Param) &argvparam_pm;` is the C handle a
1830 // positional-param fetchvalue path follows to reach
1831 // `pparams`. The Rust port resolves $1..$N directly from
1832 // `PPARAMS` via `value.start`/`value.end` indices (see
1833 // fetchvalue at params.rs:6395-6407), so no separate
1834 // Param descriptor is wired up here.
1835 *crate::ported::utils::noerrs_lock().lock().unwrap() = 2; // c:872 noerrs = 2
1836
1837 // c:858-860 — standard non-special params (must precede env import).
1838 setiparam("MAILCHECK", 60); // c:858
1839 setiparam("KEYTIMEOUT", 40); // c:859
1840 setiparam("LISTMAX", 100); // c:860
1841
1842 // c:870-871 — TMPPREFIX / TIMEFMT defaults. C wraps each string
1843 // through ztrdup_metafy() to escape Meta bytes before storing in
1844 // the param table; the Rust port mirrors this.
1845 setsparam("TMPPREFIX", &ztrdup_metafy(DEFAULT_TMPPREFIX)); // c:870
1846 setsparam("TIMEFMT", &ztrdup_metafy(DEFAULT_TIMEFMT)); // c:871
1847
1848 // c:Src/init.c:1214-1215 — `nullcmd = ztrdup("cat");
1849 // readnullcmd = ztrdup(DEFAULT_READNULLCMD);`. In C these are
1850 // globals that the NULLCMD/READNULLCMD IPDEF specials read
1851 // lazily; the stored-param port instead needs the value written
1852 // into the table. The add_special() loop above re-inserts these
1853 // specials with a null u_str, so the seed MUST run after it — do
1854 // it here (before the env-import loop below) so an exported
1855 // NULLCMD/READNULLCMD still wins, exactly like TIMEFMT. Without
1856 // this, the interactive/setupvals path (which calls
1857 // createparamtable, unlike -fc) left NULLCMD unset: sourcing a
1858 // completion cache that uses the `$(<<\EOF ...)` NULLCMD idiom
1859 // (e.g. _docker's docker_subcommands cache) hit "redirection with
1860 // no command" and populated 0 matches under `use-cache on`.
1861 setsparam("NULLCMD", "cat"); // c:1214
1862 setsparam("READNULLCMD", crate::ported::config_h::DEFAULT_READNULLCMD); // c:1215
1863
1864 // c:873-876 — HOST from gethostname() (ztrdup_metafy wrap c:875).
1865 let mut host_buf = [0u8; 256];
1866 let host_rc = unsafe { libc::gethostname(host_buf.as_mut_ptr() as *mut libc::c_char, 256) };
1867 let hostname = if host_rc == 0 {
1868 std::ffi::CStr::from_bytes_until_nul(&host_buf)
1869 .ok()
1870 .and_then(|c| c.to_str().ok())
1871 .unwrap_or("")
1872 .to_string()
1873 } else {
1874 String::new()
1875 };
1876 setsparam("HOST", &ztrdup_metafy(&hostname)); // c:875
1877
1878 // c:878-882 — LOGNAME from `getlogin()` libc syscall (with
1879 // \`cached_username\` as fallback when DISABLE_DYNAMIC_NSS).
1880 //
1881 // The previous Rust port read \`env::var(\"LOGNAME\")\` /
1882 // \`env::var(\"USER\")\` — different source. \`getlogin\` returns the
1883 // kernel's record of the controlling-terminal login user; env
1884 // LOGNAME/USER is whatever the parent process passed in (can be
1885 // spoofed). For audit / SUID-aware code paths, the kernel's view
1886 // is the right one.
1887 let logname = unsafe {
1888 let p = libc::getlogin();
1889 if p.is_null() {
1890 String::new()
1891 } else {
1892 std::ffi::CStr::from_ptr(p).to_string_lossy().into_owned()
1893 }
1894 }; // c:880 getlogin()
1895 let logname = if logname.is_empty() {
1896 // c:882 — `ztrdup(cached_username)` fallback.
1897 get_username()
1898 } else {
1899 logname
1900 };
1901 setsparam("LOGNAME", &ztrdup_metafy(&logname)); // c:878
1902
1903 // c:891 — pushheap() / c:921 — popheap(). Wraps the env-import
1904 // loop so per-iter allocations land on the heap zone.
1905 pushheap(); // c:891
1906
1907 // c:893-924 — environment import loop. Walk the process-entry
1908 // `environ` snapshot like C, not the live (possibly framework-
1909 // mutated) environment — see the `environ` static above.
1910 let environ_vars: Vec<(String, String)> = environ
1911 .get()
1912 .cloned()
1913 .unwrap_or_else(|| env::vars().collect());
1914 for (iname, ivalue) in environ_vars {
1915 if iname.is_empty() {
1916 continue;
1917 }
1918 // c:897 — leading-digit reject (`!idigit(*iname)`).
1919 if iname.as_bytes()[0].is_ascii_digit() {
1920 continue;
1921 }
1922 // c:897 — must be a valid identifier.
1923 if !isident(&iname) {
1924 continue;
1925 }
1926 // c:897 — `!strchr(iname, '[')` reject subscripted names.
1927 if iname.contains('[') {
1928 continue;
1929 }
1930 // c:902-906 — block if PM_DONTIMPORT-family flags say so.
1931 let blocked = {
1932 let tab = paramtab().read().unwrap();
1933 tab.get(&iname)
1934 .map(|pm| dontimport(pm.node.flags) != 0)
1935 .unwrap_or(false)
1936 };
1937 if blocked {
1938 continue;
1939 }
1940 // c:907-908 — assignsparam(..., ASSPM_ENV_IMPORT).
1941 let metafied = metafy(&ivalue);
1942 let _ = assignsparam(&iname, &metafied, ASSPM_ENV_IMPORT);
1943 // c:909-915 — stamp PM_EXPORTED and the env-side string.
1944 let mut tab = paramtab().write().unwrap();
1945 if let Some(pm) = tab.get_mut(&iname) {
1946 pm.node.flags |= PM_EXPORTED as i32;
1947 let env_str = if pm.node.flags & PM_SPECIAL as i32 != 0 {
1948 // c:912 — `pm->env = mkenvstr(pm->node.nam,
1949 // getsparam(pm->node.nam), pm->node.flags)`. For
1950 // special params the C body re-fetches the
1951 // canonical string via getsparam; we use ivalue
1952 // here (already metafied above).
1953 mkenvstr(&iname, &ivalue, pm.node.flags)
1954 } else {
1955 // c:914 — `pm->env = ztrdup(*envp2)` for non-special:
1956 // direct env-line copy.
1957 format!("{}={}", iname, ivalue)
1958 };
1959 pm.env = Some(env_str);
1960 }
1961 }
1962
1963 popheap(); // c:921
1964
1965 // c:933-944 — HOME / LOGNAME / SHLVL post-import wiring.
1966 //
1967 // C body (verbatim):
1968 // pm = paramtab->getnode(paramtab, "HOME");
1969 // if (EMULATION(EMULATE_ZSH)) {
1970 // pm->node.flags &= ~PM_UNSET;
1971 // if (!(pm->node.flags & PM_EXPORTED))
1972 // addenv(pm, home);
1973 // } else if (!home)
1974 // pm->node.flags |= PM_UNSET;
1975 // pm = paramtab->getnode(paramtab, "LOGNAME");
1976 // if (!(pm->node.flags & PM_EXPORTED))
1977 // addenv(pm, pm->u.str);
1978 // pm = paramtab->getnode(paramtab, "SHLVL");
1979 // sprintf(buf, "%d", (int)++shlvl);
1980 // addenv(pm, buf);
1981
1982 // c:938-945 — HOME. EMULATE_ZSH path clears PM_UNSET and
1983 // addenv(home) when not already exported; non-zsh path sets
1984 // PM_UNSET when `home` is empty/unset.
1985 let is_zsh = EMULATION(EMULATE_ZSH);
1986 let home_val = home_lock().lock().expect("home poisoned").clone();
1987 let home_action: Option<bool> = {
1988 let mut tab = paramtab().write().unwrap();
1989 if let Some(pm) = tab.get_mut("HOME") {
1990 if is_zsh {
1991 // c:939
1992 pm.node.flags &= !(PM_UNSET as i32); // c:941
1993 if pm.node.flags & PM_EXPORTED as i32 == 0 {
1994 // c:942
1995 Some(true)
1996 } else {
1997 Some(false)
1998 }
1999 } else if home_val.is_empty() {
2000 // c:944
2001 pm.node.flags |= PM_UNSET as i32; // c:945
2002 Some(false)
2003 } else {
2004 Some(false)
2005 }
2006 } else {
2007 None
2008 }
2009 };
2010 if let Some(true) = home_action {
2011 addenv("HOME", &home_val); // c:943
2012 }
2013
2014 // c:946-948 — LOGNAME. If not already exported, addenv(pm, pm->u.str).
2015 let logname_export: Option<String> = {
2016 let tab = paramtab().read().unwrap();
2017 tab.get("LOGNAME").and_then(|pm| {
2018 if pm.node.flags & PM_EXPORTED as i32 == 0 {
2019 pm.u_str.clone()
2020 } else {
2021 None
2022 }
2023 })
2024 };
2025 if let Some(ustr) = logname_export {
2026 addenv("LOGNAME", &ustr); // c:948
2027 }
2028
2029 // c:949-953 — SHLVL: unconditionally addenv with the incremented
2030 // value. C uses the \`shlvl\` integer global (IPDEF5 declared at
2031 // params.c:358 with varinteger_gsu) which was populated during
2032 // env-import. C: \`++shlvl\` then \`sprintf(buf, \"%d\", (int)shlvl)\`.
2033 //
2034 // The previous Rust port read SHLVL fresh from env::var; the
2035 // canonical read is through paramtab (which has the parsed
2036 // integer post-import). Falls back to env for the rare case
2037 // where paramtab hasn't seen the import yet.
2038 let new_shlvl: i32 = getsparam("SHLVL")
2039 .or_else(|| env::var("SHLVL").ok())
2040 .and_then(|s| s.parse().ok())
2041 .unwrap_or(0)
2042 + 1; // c:951 `++shlvl`
2043 setiparam("SHLVL", new_shlvl as i64);
2044 addenv("SHLVL", &new_shlvl.to_string()); // c:953
2045
2046 // c:949-967 — CPUTYPE / MACHTYPE / OSTYPE / TTY / VENDOR /
2047 // ZSH_ARGZERO / ZSH_VERSION / ZSH_PATCHLEVEL. C body wraps each
2048 // through ztrdup_metafy() — Rust mirrors that. CPUTYPE is set
2049 // from uname()'s `machine` field at runtime (c:957-961); the
2050 // other three (MACHTYPE / OSTYPE / VENDOR) come from config.h
2051 // values frozen at configure-time (c:961, c:963, c:964).
2052 let utsname = nix::sys::utsname::uname().ok();
2053 let cputype = utsname
2054 .as_ref()
2055 .map(|u| u.machine().to_string_lossy().to_string())
2056 .unwrap_or_else(|| "unknown".to_string());
2057 setsparam("CPUTYPE", &ztrdup_metafy(&cputype)); // c:954/960
2058 setsparam(
2059 // c:961
2060 "MACHTYPE",
2061 &ztrdup_metafy(MACHTYPE),
2062 );
2063 setsparam(
2064 // c:962
2065 "OSTYPE",
2066 &ztrdup_metafy(OSTYPE),
2067 );
2068 let tty_str = {
2069 let p = unsafe { libc::ttyname(0) };
2070 if !p.is_null() {
2071 unsafe { std::ffi::CStr::from_ptr(p) }
2072 .to_string_lossy()
2073 .to_string()
2074 } else {
2075 String::new()
2076 }
2077 };
2078 setsparam("TTY", &ztrdup_metafy(&tty_str)); // c:963
2079 setsparam(
2080 // c:964
2081 "VENDOR",
2082 &ztrdup_metafy(VENDOR),
2083 );
2084 let argv0 = env::args().next().unwrap_or_default();
2085 setsparam("ZSH_ARGZERO", &ztrdup(&argv0)); // c:965 (ztrdup, not _metafy: posixzero)
2086 setsparam("ZSH_VERSION", &ztrdup_metafy(ZSH_VERSION)); // c:966 (Config/version.mk VERSION via patchlevel::ZSH_VERSION)
2087 setsparam("ZSH_PATCHLEVEL", &ztrdup_metafy(ZSH_PATCHLEVEL)); // c:967
2088 // zshrs-only identity. No C counterpart. Surfaced so scripts can
2089 // detect zshrs (vs. upstream zsh) cleanly without inspecting a
2090 // `-test` suffix on `$ZSH_VERSION`. See `patchlevel::ZSHRS_VERSION`
2091 // for the value and bug #73 in docs/BUGS.md for the rationale.
2092 //
2093 // Suppressed whenever the zsh-compatible namespace must not
2094 // carry zshrs-original names, so `${(k)parameters}` matches
2095 // reference zsh's name set (PM_HIDE doesn't filter from the (k)
2096 // listing path — C's scanpmparameters only skips PM_UNSET, not
2097 // PM_HIDE; outright skipping the setsparam call is the only way
2098 // to keep the name out of the listing). Direct access falls back
2099 // to an empty value, same as any other unset name.
2100 //
2101 // `hide_ext_builtins()` is that predicate: `--zsh` OR the
2102 // `ZSHRS_HIDE_EXT_BUILTINS` measurement knob the parity
2103 // harnesses already set (extensions/ext_builtins.rs:48). Gating
2104 // on `--zsh` ALONE left ZSHRS_VERSION in `${(ko)parameters}` for
2105 // the native binary, which is what the harnesses actually run.
2106 if !crate::ext_builtins::hide_ext_builtins() {
2107 setsparam(
2108 "ZSHRS_VERSION",
2109 &ztrdup_metafy(crate::ported::patchlevel::ZSHRS_VERSION),
2110 );
2111 }
2112
2113 // c:968-979 — `setaparam("signals", sigptr = zalloc((TRAPCOUNT
2114 // + 1) * sizeof(char *))); t = sigs; while (t - sigs <= SIGCOUNT)
2115 // *sigptr++ = ztrdup_metafy(*t++); { for (sig = SIGRTMIN; sig <=
2116 // SIGRTMAX; sig++) *sigptr++ = ztrdup_metafy(rtsigname(sig, 0));
2117 // } while ((*sigptr++ = ztrdup_metafy(*t++))) ;`. Builds the
2118 // $signals array: indices 0..=SIGCOUNT walked from the static
2119 // sigs[] name table, then SIGRTMIN..SIGRTMAX names, then the
2120 // trailing tail (DEBUG / ERR / EXIT / ZERR sentinels).
2121 // c:signames.c sigs[] (generated) — index 0 is "EXIT", entries
2122 // 1..=SIGCOUNT in PLATFORM SIGNAL-NUMBER order, tail "ZERR",
2123 // "DEBUG" (zsh.h SIGZERR/SIGDEBUG). SIGS is declared in Linux
2124 // textual order — sort by libc number to reproduce the generated
2125 // table on every platform. Same construction as vm_helper.rs —
2126 // keep in sync.
2127 let mut by_num: Vec<(&str, i32)> = SIGS.to_vec();
2128 by_num.sort_by_key(|&(_, n)| n);
2129 let mut signals_arr: Vec<String> = Vec::with_capacity(by_num.len() + 3);
2130 signals_arr.push(ztrdup_metafy("EXIT")); // c:sigs[0]
2131 for &(name, _num) in by_num.iter() {
2132 signals_arr.push(ztrdup_metafy(name));
2133 }
2134 // RT-signal range (Linux-only; macOS SIGS table already includes
2135 // the realtime names and rtsigname returns "" out of range).
2136 #[cfg(target_os = "linux")]
2137 {
2138 for sig in libc::SIGRTMIN()..=libc::SIGRTMAX() {
2139 let nm = crate::ported::signals::rtsigname(sig);
2140 if !nm.is_empty() {
2141 signals_arr.push(ztrdup_metafy(&nm));
2142 }
2143 }
2144 }
2145 signals_arr.push(ztrdup_metafy("ZERR")); // c:sigs tail
2146 signals_arr.push(ztrdup_metafy("DEBUG")); // c:sigs tail
2147 {
2148 let mut tab = paramtab().write().unwrap();
2149 let pm = Box::new(param {
2150 node: hashnode {
2151 next: None,
2152 nam: "signals".to_string(),
2153 // c:974 builds it with plain `setaparam`, i.e.
2154 // createparam(name, PM_ARRAY) — `signals` is an ORDINARY
2155 // array holding a snapshot of the signal names, not a
2156 // special with a gsu. Stamping PM_SPECIAL made
2157 // `${(t)signals}` read `array-special` against zsh's
2158 // bare `array`.
2159 flags: PM_ARRAY as i32,
2160 },
2161 u_data: 0,
2162 u_tied: None,
2163 u_arr: Some(signals_arr),
2164 u_str: None,
2165 u_val: 0,
2166 u_dval: 0.0,
2167 u_hash: None,
2168 gsu_s: None,
2169 gsu_i: None,
2170 gsu_f: None,
2171 gsu_a: None,
2172 gsu_h: None,
2173 base: 0,
2174 width: 0,
2175 env: None,
2176 ename: None,
2177 old: None,
2178 level: 0,
2179 });
2180 tab.insert("signals".to_string(), pm);
2181 }
2182
2183 *crate::ported::utils::noerrs_lock().lock().unwrap() = 0; // c:1009 noerrs = 0
2184}
2185
2186/// Parallel storage for PM_HASHED parameter values. `param.u_hash`
2187/// is typed `Option<HashTable>` per Src/zsh.h:1841 but the full
2188/// HashTable substrate isn't wired yet; the assoc-array values live
2189/// here keyed on param name until that lands.
2190static PARAMTAB_HASHED_STORAGE_INNER: OnceLock<
2191 Mutex<crate::cow_map::CowHashMap<String, IndexMap<String, String>>>,
2192> = OnceLock::new();
2193
2194/// Port of `assigngetset()` from `Src/params.c:994`. C body
2195/// installs the standard get/set/unset vtable matching the
2196/// param's PM_TYPE so subsequent assignment dispatches go
2197/// through `pm->gsu.X->setfn`.
2198pub fn assigngetset(pm: &mut param) {
2199 match PM_TYPE(pm.node.flags as u32) {
2200 x if x == PM_SCALAR || x == PM_NAMEREF => {
2201 pm.gsu_s = Some(Box::new(gsu_scalar {
2202 getfn: strgetfn,
2203 setfn: strsetfn,
2204 unsetfn: stdunsetfn,
2205 }));
2206 }
2207 x if x == PM_INTEGER => {
2208 pm.gsu_i = Some(Box::new(gsu_integer {
2209 getfn: intgetfn,
2210 setfn: intsetfn,
2211 unsetfn: stdunsetfn,
2212 }));
2213 }
2214 x if x == PM_EFLOAT || x == PM_FFLOAT => {
2215 pm.gsu_f = Some(Box::new(gsu_float {
2216 getfn: floatgetfn,
2217 setfn: floatsetfn,
2218 unsetfn: stdunsetfn,
2219 }));
2220 }
2221 x if x == PM_ARRAY => {
2222 pm.gsu_a = Some(Box::new(gsu_array {
2223 getfn: arrgetfn,
2224 setfn: arrsetfn,
2225 unsetfn: stdunsetfn,
2226 }));
2227 }
2228 x if x == PM_HASHED => {
2229 pm.gsu_h = Some(Box::new(gsu_hash {
2230 getfn: hashgetfn,
2231 setfn: hashsetfn,
2232 unsetfn: stdunsetfn,
2233 }));
2234 }
2235 _ => {
2236 // c:1015 — DPUTS(1, "BUG: tried to create param node without valid flag")
2237 DPUTS!(true, "BUG: tried to create param node without valid flag");
2238 // c:1015
2239 }
2240 }
2241}
2242
2243/// Port of `createparam()` from `Src/params.c:1030`. C body
2244/// (~130 lines, see comment header at c:1020-1027) creates a
2245/// parameter so that it can be assigned to. Returns NULL if the
2246/// parameter already exists or can't be created, otherwise
2247/// returns the new node. If a parameter of the same name exists
2248/// in an outer scope, it is hidden by the new one. An already
2249/// existing node at the current level may be "created" and
2250/// returned provided it is unset and not special. If the
2251/// parameter can't be created because it already exists,
2252/// PM_UNSET is cleared.
2253///
2254/// Faithful port covers:
2255/// - PM_HASHELEM / PM_EXPORTED tweak when paramtab != realparamtab (c:1034)
2256/// - PM_RO_BY_DESIGN read-only rejection (c:1043-1052)
2257/// - PM_NAMEREF chain follow via `resolve_nameref_rec` (c:1062-1104)
2258/// - hidden vs reuse-old branches (c:1108-1147)
2259/// - `pm->node.flags = flags & ~PM_LOCAL` finalization (c:1155)
2260/// - `assigngetset(pm)` for non-special params (c:1157-1158)
2261///
2262/// Paramtab-backed branches (c:1034 paramtab compare, c:1038
2263/// gethashnode2, c:1144-1146 paramtab.removenode/addnode) cannot
2264/// fully execute until the paramtab vtable lands; they are
2265/// preserved as architectural intent. The faithful behaviour
2266/// emerges as soon as paramtab is wired (no signature drift
2267/// at this site).
2268pub fn createparam(
2269 // c:1030
2270 name: &str,
2271 mut flags: i32,
2272) -> Option<Param> {
2273 // c:1034-1035 — when paramtab != realparamtab (we're inside
2274 // a hash-element scope), strip PM_EXPORTED + add PM_HASHELEM.
2275 // Without paramtab/realparamtab live yet, this branch is
2276 // skipped — the caller is expected to be in the
2277 // realparamtab scope which is the common case.
2278
2279 // c:1037 — `if (name != nulstring) { ... } else { hcalloc; nulstring }`
2280 // c:1038-1041 — oldpm = gethashnode2(paramtab, name)
2281 // Without paramtab backend, we cannot consult the table; treat
2282 // the param as new. The PM_RO_BY_DESIGN / PM_NAMEREF / hidden
2283 // branches (c:1043-1147) collapse to "allocate fresh".
2284 // c:1037-1041 — `oldpm = gethashnode2(paramtab, name)`. Look up
2285 // any existing Param at this name so the c:1108/1135 branches
2286 // can decide reuse-vs-shadow. PM_RO_BY_DESIGN / PM_NAMEREF
2287 // chase branches (c:1043-1104) elided — covered when nameref
2288 // / readonly-by-design Params are wired.
2289 let mut oldpm: Option<Param> = if !name.is_empty() {
2290 paramtab().read().ok().and_then(|t| t.get(name).cloned())
2291 } else {
2292 None
2293 };
2294
2295 // c:1064-1080 — PM_RO_BY_DESIGN rejection, verbatim:
2296 //
2297 // if (oldpm && (oldpm->node.flags & PM_RO_BY_DESIGN)) {
2298 // if (!(flags & PM_LOCAL)) {
2299 // /* Must call the API for namerefs and specials to work */
2300 // pm = (Param) paramtab->getnode2(paramtab, oldpm->node.nam);
2301 // if (!pm || ((pm->node.flags & PM_NAMEREF) &&
2302 // pm->level != locallevel)) {
2303 // zerr("%s: can't modify read-only parameter", name);
2304 // return NULL;
2305 // }
2306 // }
2307 // /**
2308 // * Implementation note: In the case of a readonly nameref,
2309 // * the right thing might be to insert a new global into
2310 // * the paramtab and point the local pm->old at it, rather
2311 // * than error. That is why gethashnode2() is called
2312 // * first, to avoid skipping up the stack prematurely.
2313 // **/
2314 // }
2315 //
2316 // `paramtab->getnode2` is `getprivatenode2`
2317 // (c:Src/Modules/param_private.c:617, installed by setup_ at c:679)
2318 // whenever zsh/param/private is loaded, which is what makes a private
2319 // belonging to an OUTER scope resolve to NULL here and turn a deeper
2320 // scope's assignment into this error instead of a silent new global
2321 // (V10private.ztst:41,42). Without the block zshrs fell through to
2322 // assignstrvalue's generic PM_READONLY guard, whose wording
2323 // (`read-only variable: q`) is a different message.
2324 if let Some(op) = oldpm.clone() {
2325 // c:1064
2326 if (op.node.flags as u32 & PM_RO_BY_DESIGN) != 0 && (flags as u32 & PM_LOCAL) == 0 {
2327 // c:1064-1065
2328 // c:1067 — `pm = paramtab->getnode2(paramtab, oldpm->node.nam);`
2329 let visible =
2330 crate::ported::modules::param_private::getprivatenode2(&*op as *const param);
2331 let reject = if visible.is_null() {
2332 true // c:1068 `!pm`
2333 } else {
2334 let vpm: ¶m = unsafe { &*visible };
2335 (vpm.node.flags as u32 & PM_NAMEREF) != 0
2336 && vpm.level != locallevel.load(Ordering::Relaxed) as i32
2337 // c:1068-1069
2338 };
2339 if reject {
2340 zerr(&format!("{}: can't modify read-only parameter", name)); // c:1071
2341 return None; // c:1072
2342 }
2343 }
2344 }
2345
2346 // c:1086-1125 — the PM_NAMEREF chase. Previously elided; ported
2347 // here verbatim.
2348 //
2349 // if (oldpm && !(flags & PM_NAMEREF) &&
2350 // (oldpm->level == locallevel ?
2351 // !(oldpm->node.flags & PM_RO_BY_DESIGN) : !(flags & PM_LOCAL)) &&
2352 // (oldpm->node.flags & PM_NAMEREF) &&
2353 // (!(oldpm->node.flags & PM_UNSET) ||
2354 // (oldpm->node.flags & PM_DECLARED))) {
2355 //
2356 // c:1091-1095 — "Here we only have to deal with namerefs that refer
2357 // to not-yet-defined or unset variable. All other namerefs have
2358 // already been taken care of by the resolve_nameref in
2359 // typeset_single. It's unclear why these can't be handled there
2360 // too."
2361 //
2362 // C rebinds the local `name` pointer (c:1105) / the local `oldpm`
2363 // (c:1121) and lets the reuse arm below act on the RETARGETED node;
2364 // because that arm keeps the same struct, the retargeted node stays
2365 // in `paramtab` under its OWN key. zshrs's table is name-keyed and
2366 // re-inserts at the end, so the rebind has to carry the key too.
2367 let mut name_owned: String;
2368 let mut name: &str = name;
2369 if let Some(op) = oldpm.clone() {
2370 let opf = op.node.flags as u32;
2371 let ll = locallevel.load(Ordering::Relaxed) as i32;
2372 // c:1086-1090
2373 if (flags as u32 & PM_NAMEREF) == 0
2374 && (if op.level == ll {
2375 (opf & PM_RO_BY_DESIGN) == 0 // c:1088
2376 } else {
2377 (flags as u32 & PM_LOCAL) == 0 // c:1088
2378 })
2379 && (opf & PM_NAMEREF) != 0 // c:1089
2380 && ((opf & PM_UNSET) == 0 || (opf & PM_DECLARED) != 0)
2381 // c:1090
2382 {
2383 // c:1097 — `Param lastpm = resolve_nameref_rec(oldpm, NULL, 1);`
2384 let lastpm = resolve_nameref_rec(Some(op), None, 1);
2385 if let Some(lastpm) = lastpm {
2386 // c:1098
2387 let lf = lastpm.node.flags as u32;
2388 // c:1099-1101
2389 if (lf & PM_NAMEREF) != 0 && ((lf & PM_UNSET) == 0 || (lf & PM_DECLARED) != 0) {
2390 // c:1102 — `char *refname = GETREFNAME(lastpm);`
2391 let refname = lastpm.u_str.clone().unwrap_or_default();
2392 if !refname.is_empty() {
2393 // c:1103 — `if (refname && *refname)`
2394 // c:1104 — nameref pointing to a not-yet-defined variable
2395 name_owned = refname; // c:1105 `name = refname;`
2396 name = &name_owned;
2397 oldpm = None; // c:1106
2398 } else {
2399 // c:1108 — nameref pointing to an uninitialized nameref
2400 if (lf & PM_READONLY) == 0 {
2401 // c:1109
2402 if (flags & !(PM_LOCAL as i32)) != 0 {
2403 // c:1110
2404 // c:1111 — Only plain scalar assignment allowed
2405 // c:1112-1113 — Differs from ksh93u+
2406 zerr(&format!("{}: can't change type of named reference", name));
2407 return None; // c:1114
2408 }
2409 }
2410 return Some(lastpm); // c:1117
2411 }
2412 } else {
2413 // c:1120 — nameref pointing to an unset local
2414 DPUTS!(
2415 (lf & PM_UNSET) == 0, // c:1121
2416 "BUG: local parameter is not unset" // c:1122
2417 );
2418 name_owned = lastpm.node.nam.clone(); // Rust-only: name-keyed table
2419 name = &name_owned;
2420 oldpm = Some(lastpm); // c:1123
2421 }
2422 }
2423 }
2424 }
2425
2426 if !name.is_empty() {
2427 // c:1149-1150 — `if (isset(ALLEXPORT) && !(flags & PM_HASHELEM)) flags |= PM_EXPORTED;`
2428 if isset(ALLEXPORT) && (flags as u32 & PM_HASHELEM) == 0 {
2429 flags |= PM_EXPORTED as i32;
2430 }
2431 }
2432
2433 // c:1108 — `if (oldpm && (oldpm->level == locallevel || !(flags
2434 // & PM_LOCAL)))`: reuse the existing Param in place. c:1135 —
2435 // else allocate a fresh pm and chain pm.old = oldpm (the
2436 // local-shadow path). The reuse arm just returns the existing
2437 // pm with reset base/width; the shadow arm does the chain
2438 // installation that endparamscope later unwinds.
2439 let cur_locallevel = locallevel.load(Ordering::Relaxed);
2440 // c:1106-1107 — DPUTS(oldpm && oldpm->level > locallevel,
2441 // "BUG: old local parameter not deleted");
2442 DPUTS!(
2443 // c:1106
2444 match &oldpm {
2445 // c:1106
2446 Some(op) => op.level > cur_locallevel, // c:1106
2447 None => false, // c:1106
2448 },
2449 "BUG: old local parameter not deleted" // c:1107
2450 );
2451 let reuse = match &oldpm {
2452 Some(op) => op.level == cur_locallevel || (flags as u32 & PM_LOCAL) == 0,
2453 None => false,
2454 };
2455
2456 // c:1130-1152 — an existing node at this level is only taken over when
2457 // it is unset and not special; otherwise createparam declines:
2458 //
2459 // if (isset(POSIXBUILTINS) && (oldpm->node.flags & PM_READONLY)) {
2460 // zerr("read-only variable: %s", name);
2461 // return NULL;
2462 // }
2463 // if (!(oldpm->node.flags & PM_UNSET) ||
2464 // (oldpm->node.flags & PM_SPECIAL) ||
2465 // /* POSIXBUILTINS horror: we need to retain 'export' flags */
2466 // (isset(POSIXBUILTINS) && (oldpm->node.flags & PM_EXPORTED))) {
2467 // if (oldpm->node.flags & PM_RO_BY_DESIGN) { zerr(...); return NULL; }
2468 // oldpm->node.flags &= ~PM_UNSET;
2469 // if ((oldpm->node.flags & PM_SPECIAL) && oldpm->ename) {
2470 // Param altpm = (Param) paramtab->getnode(paramtab, oldpm->ename);
2471 // if (altpm)
2472 // altpm->node.flags &= ~PM_UNSET;
2473 // }
2474 // return NULL;
2475 // }
2476 //
2477 // "If the parameter can't be created because it already exists, the
2478 // PM_UNSET flag is cleared" (c:1045-1046). The caller then finds the
2479 // revived node with getnode — e.g. assignnparam's c:3696 fallback — so an
2480 // unset special such as SHLVL keeps its PM_SPECIAL|PM_EXPORTED identity
2481 // when `(( SHLVL++ ))` recreates it.
2482 if reuse {
2483 let op = oldpm.as_ref().unwrap(); // reuse=true requires Some
2484 let opf = op.node.flags as u32;
2485 if isset(crate::ported::zsh_h::POSIXBUILTINS) && (opf & PM_READONLY) != 0 {
2486 // c:1131
2487 zerr(&format!("read-only variable: {}", name)); // c:1132
2488 return None; // c:1133
2489 }
2490 if (opf & PM_UNSET) == 0
2491 || (opf & PM_SPECIAL) != 0
2492 || (isset(crate::ported::zsh_h::POSIXBUILTINS) && (opf & PM_EXPORTED) != 0)
2493 {
2494 // c:1135-1138
2495 if (opf & PM_RO_BY_DESIGN) != 0 {
2496 // c:1139
2497 zerr(&format!("{}: can't modify read-only parameter", name)); // c:1140-1141
2498 return None; // c:1142
2499 }
2500 if let Ok(mut tab) = paramtab().write() {
2501 if let Some(live) = tab.get_mut(name) {
2502 live.node.flags &= !(PM_UNSET as i32); // c:1144
2503 }
2504 if (opf & PM_SPECIAL) != 0 {
2505 // c:1145
2506 if let Some(alt) = op.ename.as_deref() {
2507 if let Some(altpm) = tab.get_mut(alt) {
2508 // c:1146-1149
2509 altpm.node.flags &= !(PM_UNSET as i32); // c:1149
2510 }
2511 }
2512 }
2513 }
2514 return None; // c:1151
2515 }
2516 }
2517
2518 // c:Src/builtin.c:2608-2609 — set by the shadow arm below when the
2519 // tied peer is already local at this level (c:2394-2408), which makes
2520 // `pm->old` carry PM_NORESTORE and so suppresses the
2521 // "re-initialise the special parameter to empty" step:
2522 // } else if (newspecial != NS_NONE &&
2523 // !(pm->old->node.flags & (PM_NORESTORE|PM_READONLY))) {
2524 // ... pm->gsu.a->setfn(pm, mkarray(NULL)); ...
2525 // C keeps the SAME Param struct for a special, so "not zeroed" means
2526 // the live value simply stays; zshrs allocates a fresh shadow struct,
2527 // so the live value has to be carried over explicitly.
2528 let mut inherit_live_value = false;
2529
2530 // c:1137-1146 — the shadow arm removes the displaced node from the
2531 // bucket chain BEFORE re-adding the fresh one:
2532 // if ((pm->old = oldpm)) {
2533 // if (oldpm->env) delenv(oldpm);
2534 // paramtab->removenode(paramtab, name);
2535 // }
2536 // paramtab->addnode(paramtab, ztrdup(name), pm);
2537 // `addhashnode2` (Src/hashtable.c:189-213) replaces a key IN PLACE, so
2538 // without the removenode the name keeps its old chain position; with it
2539 // the name moves to the FRONT of its bucket (c:216-218). That position is
2540 // observable — `${(k)parameters}` walks bucket 0..hsize-1, chain
2541 // head→tail (`scanmatchtable`, Src/hashtable.c:426-435) — and drives the
2542 // group/column layout `_parameters` produces.
2543 let mut shadow_displaced = false;
2544
2545 let mut pm: Param = if reuse {
2546 // c:1132-1134 — `pm = oldpm; pm->base = pm->width = 0;
2547 // oldpm = pm->old;` Reuse the entry already in paramtab.
2548 let mut existing = oldpm.unwrap(); // safe: reuse=true requires Some
2549 existing.base = 0; // c:1133
2550 existing.width = 0; // c:1133
2551 existing
2552 } else {
2553 // c:1136 zshcalloc(sizeof *pm) — fresh allocation; chain the
2554 // outer Param into pm.old (c:1137) so endparamscope can
2555 // restore it. c:1144 paramtab->removenode is implicit since
2556 // we re-insert below.
2557 //
2558 // c:Src/builtin.c:2382-2424 newspecial path — for PM_SPECIAL
2559 // shadows (`local IFS=...`), the C source allocates a
2560 // separate `tpm`, calls `copyparam(tpm, pm, 1)` which uses
2561 // the GSU getfn to read the current value into tpm.u.str,
2562 // then sets `pm->old = tpm`. zshrs's createparam path moves
2563 // `oldpm` into pm.old as-is; for specials whose value lives
2564 // in a global (ifs_lock, paramtab-external) the bare
2565 // `oldpm.u_str` is empty and endparamscope can't restore the
2566 // real outer value. Snapshot the current value via the GSU
2567 // getfn now so the saved chain carries the right string.
2568 // Bug #8 in docs/BUGS.md (`local IFS=:` leaked past return).
2569 //
2570 // c:Src/builtin.c:2382-2424 copyparam for PM_HASHED — same
2571 // shape, different storage. zshrs's PM_HASHED data lives in
2572 // the parallel `paramtab_hashed_storage` map keyed by name (no
2573 // scope dimension), so a `local -A h` shadow that writes
2574 // through set_assoc would clobber the outer scope's bag and
2575 // endparamscope's pm.old restoration alone wouldn't recover
2576 // it. Push the current paramtab_hashed_storage[name] onto the
2577 // shadow stack BEFORE the fresh pm gets installed, so when
2578 // endparamscope unwinds the PM_HASHED stale entry it can pop
2579 // and restore. Bug #415.
2580 let oldpm = if let Some(mut op) = oldpm {
2581 // c:1137 — `if ((pm->old = oldpm))` guards the c:1144
2582 // `paramtab->removenode(paramtab, name)` applied below.
2583 shadow_displaced = true;
2584 // c:Src/builtin.c:2394-2411 — the tied-pair NORESTORE test, run
2585 // BEFORE the copyparam snapshot because it REPLACES it:
2586 // if (pm->ename &&
2587 // (pm2 = (Param) paramtab->getnode(paramtab, pm->ename)) &&
2588 // pm2->level == locallevel) {
2589 // /* This is getting silly, but anyway: if one of a
2590 // * path/PATH pair has already been made local at the
2591 // * current level, we have to make sure that the other one
2592 // * does not have its value saved: since that comes from
2593 // * an internal variable it will already reflect the local
2594 // * value, so restoring it on exit would be wrong. */
2595 // tpm->node.flags = pm->node.flags | PM_NORESTORE;
2596 // } else {
2597 // copyparam(tpm, pm, 1);
2598 // }
2599 // The C test is only reached from the `newspecial != NS_NONE` arm
2600 // (c:2377); the caller signals that decision by ORing PM_SPECIAL
2601 // into `flags` (see the `keep_special` computation at
2602 // builtin.rs:5681, which is c:2083-2085 verbatim).
2603 let tie_peer_local = (flags as u32 & PM_SPECIAL) != 0
2604 && op.ename.as_deref().is_some_and(|peer| {
2605 paramtab()
2606 .read()
2607 .ok()
2608 .and_then(|t| t.get(peer).map(|p2| p2.level == cur_locallevel))
2609 .unwrap_or(false) // c:2396 pm2->level == locallevel
2610 });
2611 if tie_peer_local {
2612 // c:2408 — `tpm->node.flags = pm->node.flags | PM_NORESTORE;`
2613 // (and NO copyparam, so no value snapshot is taken).
2614 op.node.flags |= PM_NORESTORE as i32;
2615 inherit_live_value = true;
2616 } else if (op.node.flags as u32 & (PM_SPECIAL | PM_TIED)) != 0 {
2617 // c:Src/builtin.c:2382-2424 copyparam — snapshot the
2618 // CURRENT live value into the shadow chain. PM_TIED
2619 // scalars (FPATH/PATH/CDPATH…) included: their value
2620 // derives from the tied array's global storage, which
2621 // the local's writes flow through — without the
2622 // snapshot, `f() { local FPATH=/tmp }; f` left the
2623 // GLOBAL fpath at ( /tmp ) and every later autoload
2624 // failed (zinit's :zinit-tmp-subst-autoload does
2625 // exactly this dance). Fall back to the name-routed
2626 // getsparam when the pm carries no scalar gsu.
2627 // c:Src/builtin.c:2410 `copyparam(tpm, pm, 1)` copies the
2628 // `u` UNION out of the struct it already holds — it never
2629 // resolves the NAME, so it cannot run `getparamnode` →
2630 // `loadparamnode` (c:Src/params.c:563-585) and cannot
2631 // `ensurefeature` the owning module. C is explicit about this
2632 // for the whole builtin: c:Src/builtin.c:3096-3097 comments the
2633 // literal-arg lookup as "getnode2() to avoid autoloading", and
2634 // the `-m` loop at c:3085-3088 hands `typeset_single` the pm
2635 // POINTER it already has. zshrs's snapshot below goes through
2636 // `getaparam`/`getsparam`, which DO resolve the name and boot
2637 // the module — so a bare `local zsh_scheduled_events` (or the
2638 // `typeset -h +g -m '*'` stress at B02typeset.ztst:37) loaded
2639 // zsh/sched and zsh/zleparameter, where `zmodload -e zsh/sched`
2640 // answers 1 in zsh and 0 in zshrs. It also stuck the name in
2641 // MATERIALIZED_MODULE_PARAMS for the rest of the shell, so a
2642 // later `readonly -p` listed `typeset -g -ar keymaps` /
2643 // `typeset -g -ar zsh_scheduled_events` (B02typeset.ztst:47).
2644 // An unbooted module row has no value to preserve — in C it is
2645 // a PM_AUTOLOAD stub whose `u.str` is just the module name — so
2646 // skip the snapshot entirely for it. Same predicate as the
2647 // print-side gate in `printparamnode` below; kept inlined
2648 // rather than shared because build.rs forbids Rust-only helper
2649 // fns under src/ported/.
2650 let unbooted_module_row = crate::vm_helper::AUTOLOAD_PARAMS
2651 .iter()
2652 .find(|(pnam, _)| *pnam == op.node.nam)
2653 .is_some_and(|(_, modname)| {
2654 crate::ported::module::MODULESTAB
2655 .lock()
2656 .ok()
2657 .map(|t| {
2658 // c:Src/module.c:218-241 — "boot ran" is
2659 // MOD_INIT_B && !MOD_UNLOAD.
2660 !t.modules.get(*modname).is_some_and(|md| {
2661 (md.node.flags & crate::ported::zsh_h::MOD_INIT_B) != 0
2662 && (md.node.flags & crate::ported::zsh_h::MOD_UNLOAD) == 0
2663 })
2664 })
2665 .unwrap_or(false)
2666 });
2667 if unbooted_module_row {
2668 // c:2410 — nothing to copy; the stub carries no value.
2669 } else if (PM_TYPE(op.node.flags as u32) & PM_ARRAY) != 0 {
2670 // ARRAY side of a tied pair (fpath/path/cdpath):
2671 // snapshot the ELEMENT VECTOR. Forcing the scalar
2672 // getsparam fallback here stored Some("") and the
2673 // endparamscope refire then assigned that empty
2674 // scalar — zinit's autoload stubs (`local -a
2675 // fpath; fpath=( … )`) collapsed the GLOBAL fpath
2676 // to one empty element on every stub exit.
2677 if let Some(arr) = getaparam(&op.node.nam) {
2678 op.u_arr = Some(arr);
2679 }
2680 } else {
2681 let getfn_ptr = op.gsu_s.as_ref().map(|g| g.getfn);
2682 if let Some(getfn) = getfn_ptr {
2683 op.u_str = Some(getfn(&op));
2684 } else if let Some(v) = getsparam(&op.node.nam) {
2685 op.u_str = Some(v);
2686 }
2687 }
2688 }
2689 // c:Src/builtin.c:2386-2425 — the newspecial shadow keeps the
2690 // SAME Param, so `pm->u.hash` still points at the special's own
2691 // table and the local reads through it. The magic module hashes
2692 // (Src/Modules/parameter.c partab[]: commands / functions /
2693 // aliases / …) have no `paramtab_hashed_storage` backing at all —
2694 // `gethkparam` (params.rs:5919) reaches them by falling THROUGH
2695 // that map to the row's `scanfn`. Inserting an empty local bag
2696 // here therefore did not "shadow" such a hash, it BLANKED it:
2697 // `local -A +h commands` read 0 keys where zsh reads the live
2698 // command table. Only the newspecial case is excluded (the caller
2699 // ORs PM_SPECIAL into `flags` per c:2083-2085); a plain
2700 // `local -A commands` still decays to an empty local assoc, which
2701 // is what zsh does too.
2702 let keeps_special_hash = (flags as u32 & PM_SPECIAL) != 0
2703 && crate::ported::modules::parameter::PARTAB
2704 .iter()
2705 .any(|e| e.name == name);
2706 // c:Src/params.c:3800 + c:3908-3925 — the unwind of a plain local
2707 // is `unsetparam_pm` → `pm->gsu.s->unsetfn(pm, exp)` → stdunsetfn's
2708 // `case PM_HASHED: pm->gsu.h->setfn(pm, NULL)` (hashsetfn, c:4045,
2709 // `deleteparamtable(pm->u.hash)`). That switch dispatches on the
2710 // type of the LOCAL being destroyed and never looks at `pm->old`,
2711 // because in C the pairs hang off the local's own `u.hash`
2712 // (c:1157 `zshcalloc` leaves the fresh local's slot NULL, so the
2713 // outer's table is still hanging off the `pm->old` struct,
2714 // untouched). RUST-ONLY HAZARD: zshrs keeps the pairs in the
2715 // name-keyed `paramtab_hashed_storage` with no scope dimension, so
2716 // the save/clear here is what stands in for both halves — and
2717 // gating it on the OUTER being PM_HASHED had no C counterpart. A
2718 // `typeset -A H` under an outer `local H` (or `local -a H`)
2719 // therefore pushed no frame, so nothing ever removed the row:
2720 // outer(){ local H; inner; print ${(t)H} ${#H} }
2721 // inner(){ typeset -A H; H[x]=1 }
2722 // outer; print ${(t)H}
2723 // read `scalar-local 1` inside and left `association` behind in
2724 // GLOBAL scope after `outer` returned. Fire whenever EITHER side is
2725 // hashed: outer-hashed needs the outer's bag preserved, and
2726 // local-hashed needs the local's bag torn down.
2727 if ((op.node.flags as u32 & PM_HASHED) != 0 || (flags as u32 & PM_HASHED) != 0)
2728 && (flags as u32 & PM_LOCAL) != 0
2729 && !keeps_special_hash
2730 {
2731 // Push current paramtab_hashed_storage[name] (Some/None)
2732 // onto the shadow stack so endparamscope can restore.
2733 // Then CLEAR the storage so the local shadow starts
2734 // with an empty bag — without this, `local -A h` (no
2735 // value) leaves the outer's data visible and a
2736 // subsequent `h[x]=v` appends to it instead of
2737 // creating a fresh local assoc. C's copyparam handles
2738 // this via separate `tpm` / `pm` u.hash slots so the
2739 // pm.u.hash that fresh writes go through is
2740 // zero-initialised; zshrs's parallel storage is one
2741 // map per name, so the save+clear pair is the
2742 // equivalent.
2743 let saved: Option<IndexMap<String, String>> = paramtab_hashed_storage()
2744 .lock()
2745 .ok()
2746 .and_then(|m| m.get(name).cloned());
2747 let stk_mtx =
2748 PARAMTAB_HASHED_SHADOW_STACK.get_or_init(|| Mutex::new(HashMap::new()));
2749 if let Ok(mut stk) = stk_mtx.lock() {
2750 // The frame carries the level of the local it belongs to
2751 // (the same `cur_locallevel` stamped into `pm.level` below,
2752 // c:Src/builtin.c:2576 `pm->level = locallevel`), so
2753 // endparamscope pops a frame only for the scope that
2754 // actually pushed one. Without that tag the pop side had to
2755 // re-derive the push condition from the param flags, and
2756 // the two spellings drifted apart — which is how the leak
2757 // above survived.
2758 stk.entry(name.to_string())
2759 .or_default()
2760 .push((cur_locallevel, saved));
2761 }
2762 if let Ok(mut m) = paramtab_hashed_storage().lock() {
2763 // c:Src/params.c:2270 — `if (PM_TYPE(pm->node.flags) &
2764 // (PM_ARRAY|PM_HASHED))`: a subscripted read dispatches on
2765 // the TYPE OF THE NODE paramtab returns, then `getindex`
2766 // (c:2282 / c:2112). Only a shadow that is ITSELF PM_HASHED
2767 // owns a bag in this parallel storage; a NON-hash shadow
2768 // must leave NO row at all, or `assoc_contains`
2769 // (subst.rs, `contains_key`) still answers "association"
2770 // for the empty map and every subscripted read of the local
2771 // ARRAY routes to the hash path and comes back empty:
2772 //
2773 // g(){ local -a h; h=(a b c); print ${h[2]} }
2774 // f(){ local -A h; h[x]=1; g }; f
2775 // zsh: b zshrs: <empty>
2776 //
2777 // That is `Completion/Base/Core/_normal:4`'s `local -A opts`
2778 // shadowed by `_ruby`'s `local -a opts`, whose
2779 // `$opts[(r)*-d\[*]` (sh:158) then found nothing and
2780 // dropped `-d` from `irb -`. `endparamscope` already
2781 // restores `Some(map) => insert` / `None => remove`, so
2782 // removing the row here is undone on scope exit.
2783 if (flags as u32 & PM_HASHED) != 0 {
2784 m.insert(name.to_string(), IndexMap::new());
2785 } else {
2786 m.remove(name);
2787 }
2788 }
2789 }
2790 // c:Src/params.c:1138-1144 — the shadow arm, where C spells the
2791 // intent out: "needed to avoid freeing oldpm, but we do take it out
2792 // of the environment when it's hidden" — `if (oldpm->env)
2793 // delenv(oldpm);`. Without it the name stays in `environ` for the
2794 // whole life of the local, so the local's assignment republished
2795 // over that existing entry and CHILD PROCESSES saw the local value:
2796 // export EV=out; f(){ local EV=in; sh -c 'echo [$EV]' }; f
2797 // printed `[in]` where zsh prints `[]`. Note the FLAG was already
2798 // correct (`${(t)EV}` = `scalar-local`, not exported) — only the
2799 // environment entry leaked, which is why nothing parameter-side
2800 // could see it. docs/BUGS.md #1040. The symmetric re-export of the
2801 // outer value on scope exit is #1038, in endparamscope.
2802 if (flags as u32 & PM_LOCAL) != 0
2803 && (op.env.is_some() || (op.node.flags as u32 & PM_EXPORTED) != 0)
2804 {
2805 delenvvalue(name); // c:1142 delenv(oldpm)
2806 }
2807 Some(op)
2808 } else {
2809 None
2810 };
2811 Box::new(param {
2812 node: hashnode {
2813 next: None,
2814 nam: name.to_string(),
2815 flags: 0,
2816 },
2817 u_data: 0,
2818 u_tied: None,
2819 u_arr: None,
2820 u_str: None,
2821 u_val: 0,
2822 u_dval: 0.0,
2823 u_hash: None,
2824 gsu_s: None,
2825 gsu_i: None,
2826 gsu_f: None,
2827 gsu_a: None,
2828 gsu_h: None,
2829 base: 0,
2830 width: 0,
2831 env: None,
2832 // c:Src/builtin.c:2385-2393 — for the newspecial shadow (flagged by
2833 // the caller ORing PM_SPECIAL into `flags`) C keeps the special's
2834 // OWN struct and moves only the saved state into `tpm`, so
2835 // `pm->ename` — the tied peer, MANPATH→manpath — survives. A plain
2836 // parameter is `zshcalloc`ed (c:1136) with no peer.
2837 ename: if (flags as u32 & PM_SPECIAL) != 0 {
2838 oldpm.as_ref().and_then(|o| o.ename.clone())
2839 } else {
2840 None
2841 },
2842 old: oldpm, // c:1137 pm->old = oldpm
2843 // c:1136 — C: `pm = zshcalloc(sizeof *pm)`. calloc
2844 // zeroes pm.level so a freshly created GLOBAL assignment
2845 // (`x=foo` inside a function) gets level=0 and survives
2846 // endparamscope. The `pm->level = locallevel` set happens
2847 // ONLY through builtin.c:2576 (PM_LOCAL path: `local x=…`).
2848 level: if (flags as u32 & PM_LOCAL) != 0 {
2849 cur_locallevel
2850 } else {
2851 0
2852 },
2853 })
2854 };
2855
2856 pm.node.flags = flags & !(PM_LOCAL as i32); // c:1155
2857 if (pm.node.flags as u32 & PM_SPECIAL) == 0 {
2858 // c:1157
2859 assigngetset(&mut pm); // c:1158
2860 }
2861 // c:Src/params.c:1146 — when shadowing a special parameter
2862 // (e.g. `local IFS=...` inside a function), the new pm must
2863 // inherit the canonical special-var GSU (ifssetfn etc.) so
2864 // writes route through the global storage that `$IFS`
2865 // expansion reads. Without this, the shadow's setfn was the
2866 // generic strsetfn and `local IFS=:` updated paramtab.u_str
2867 // but NOT the canonical `ifs` global — `$IFS` expansion in
2868 // the same function read the default value through ifsgetfn.
2869 //
2870 // Detect special-var names and override the gsu_s + stamp
2871 // PM_SPECIAL. Mirrors the back-fill at assignsparam:4981 but
2872 // covers the create-time path (local / typeset of fresh
2873 // shadow), not just the assign-existing path.
2874 // A shadow that typeset_single did NOT keep special (`local -h HOME`,
2875 // c:Src/builtin.c:2084-2086: -h leaves newspecial NS_NONE) is an
2876 // ordinary parameter: c:1155-1158 gives it plain accessors only.
2877 if !name.is_empty() && !(shadow_displaced && (flags as u32 & PM_SPECIAL) == 0) {
2878 let special_gsu: Option<Box<gsu_scalar>> = match name {
2879 "HOME" => Some(Box::new(HOME_GSU.clone())),
2880 "IFS" => Some(Box::new(IFS_GSU.clone())),
2881 "TERM" => Some(Box::new(TERM_GSU.clone())),
2882 "TERMINFO" => Some(Box::new(TERMINFO_GSU.clone())),
2883 "TERMINFO_DIRS" => Some(Box::new(TERMINFODIRS_GSU.clone())),
2884 "WORDCHARS" => Some(Box::new(WORDCHARS_GSU.clone())),
2885 "USERNAME" => Some(Box::new(USERNAME_GSU.clone())),
2886 "KEYBOARD_HACK" => Some(Box::new(KEYBOARDHACK_GSU.clone())),
2887 "HISTCHARS" | "histchars" => Some(Box::new(HISTCHARS_GSU.clone())),
2888 "LANG" => Some(Box::new(LANG_GSU.clone())), // c:332
2889 "LC_ALL" => Some(Box::new(LC_ALL_GSU.clone())), // c:333
2890 "LC_COLLATE" | "LC_CTYPE" | "LC_MESSAGES" | "LC_NUMERIC" | "LC_TIME" => {
2891 Some(Box::new(LC_BLAH_GSU.clone())) // c:335-346
2892 }
2893 _ => None,
2894 };
2895 if let Some(gsu) = special_gsu {
2896 pm.gsu_s = Some(gsu);
2897 pm.node.flags |= PM_SPECIAL as i32;
2898 }
2899 }
2900 // c:Src/builtin.c:2087-2089 + 2381-2392 — when `local` shadows a
2901 // PM_SPECIAL parameter, typeset_single sets `newspecial = NS_NORMAL` and
2902 // the apply branch keeps the SAME struct: "For specials, we keep the same
2903 // struct but zero everything. Maybe it would be easier to create a new
2904 // struct but copy the get/set methods." So the local inherits the
2905 // shadowed param's accessors AND its type — it does not decay to a plain
2906 // scalar.
2907 //
2908 // The name table above only enumerates special SCALARS, so every INTEGER
2909 // special fell through and was created generic: `local HISTSIZE=5` built a
2910 // `scalar-local` whose setter never reached the real HISTSIZE storage, so
2911 // the assignment was silently dropped and `${(t)HISTSIZE}` read
2912 // `scalar-local` where zsh reports `integer-local-special`. Same for
2913 // SAVEHIST / SECONDS / RANDOM / LINES / COLUMNS. Inherit from the shadowed
2914 // param generically rather than growing the list — that is what C does.
2915 // docs/BUGS.md #1039 (D).
2916 // c:Src/builtin.c:2083-2085 — `newspecial`. The caller that KNOWS the
2917 // `off` mask (bin_typeset's per-arg loop) evaluates the full test and
2918 // signals the answer by ORing PM_SPECIAL into `flags`; createparam only
2919 // sees `on`, so it cannot re-derive the `& ~off` term itself.
2920 let caller_keeps_special = (flags as u32 & PM_SPECIAL) != 0;
2921 // c:Src/params.c:1155-1158 — C's createparam ends with
2922 // pm->node.flags = flags & ~PM_LOCAL;
2923 // if (!(pm->node.flags & PM_SPECIAL)) assigngetset(pm);
2924 // and inherits NOTHING from `oldpm`. Keeping the special's accessors and
2925 // type is done by typeset_single's `newspecial` arm (c:2381-2425), which
2926 // only runs when c:2083-2085 chose NS_NORMAL. So the inheritance below
2927 // must fire ONLY when the caller signalled that decision by ORing
2928 // PM_SPECIAL into `flags`. The old `(pm.flags & PM_SPECIAL) == 0 ||`
2929 // disjunction made it fire for the NS_NONE case too, so `-h` (which is
2930 // exactly the flag that suppresses NS_NORMAL, c:2083 `!(on & PM_HIDE)`)
2931 // could not hide a special: `f(){ typeset -h +g LINENO }` produced
2932 // `integer-local-readonly-hide-special` where zsh gives
2933 // `scalar-local-hide`, and the following `unset LINENO` then failed with
2934 // "read-only variable".
2935 if caller_keeps_special {
2936 let inherited = pm.old.as_ref().and_then(|old| {
2937 // c:2087-2089 — a HIDDEN special (PM_HIDE, e.g. the `commands`
2938 // special-hash) is NOT kept special when a `local`/`typeset`
2939 // shadows it: C sets newspecial=NS_NORMAL for such a name, so the
2940 // shadow decays to the plainly-requested type. Re-stamping it
2941 // special made `local -a commands` in the brew completer fail with
2942 // "can't change type of a special parameter".
2943 // …unless the statement also carries `+h` (PM_HIDE in `off`),
2944 // which cancels c:2084's `& ~off` term and preserves the special —
2945 // `local -A +h commands` reads `association-local-special` in zsh.
2946 if (old.node.flags as u32 & PM_SPECIAL) != 0
2947 && ((old.node.flags as u32 & PM_HIDE) == 0 || caller_keeps_special)
2948 {
2949 Some((
2950 old.gsu_s.clone(),
2951 old.gsu_i.clone(),
2952 old.gsu_f.clone(),
2953 // c:Src/builtin.c:2384 `on |= pm->node.flags & PM_TIED;` rides
2954 // along with the type: the shadow of MANPATH/PATH is still the
2955 // tied half, so bin_typeset's re-tie test (c:2899) sees it.
2956 PM_TYPE(old.node.flags as u32) | (old.node.flags as u32 & PM_TIED),
2957 ))
2958 } else {
2959 None
2960 }
2961 });
2962 if let Some((gs, gi, gf, ty)) = inherited {
2963 pm.gsu_s = gs;
2964 pm.gsu_i = gi;
2965 pm.gsu_f = gf;
2966 // Re-stamp the type from the shadowed special: createparam was
2967 // called with the caller's flags (PM_SCALAR for a bare `local X=1`),
2968 // which would otherwise override the special's real type.
2969 pm.node.flags &= !(PM_TYPE(u32::MAX) as i32);
2970 pm.node.flags |= PM_SPECIAL as i32 | ty as i32;
2971 }
2972 }
2973 // c:Src/builtin.c:2608-2633 — the zeroing step is SKIPPED when
2974 // `pm->old` carries PM_NORESTORE, i.e. when the tied peer is already
2975 // local at this level. In C nothing further is needed: `pm` is the
2976 // special's own struct and its value lives in the internal variable
2977 // the tie writes through, so it already holds the peer's local value.
2978 // zshrs's shadow is a fresh struct, so read the live value across.
2979 // The old node is still installed in paramtab at this point (the
2980 // addnode below is what displaces it), so the plain getters see it.
2981 if inherit_live_value {
2982 match PM_TYPE(pm.node.flags as u32) {
2983 t if t == PM_ARRAY => {
2984 if let Some(arr) = getaparam(name) {
2985 pm.u_arr = Some(arr);
2986 }
2987 }
2988 t if t == PM_SCALAR => {
2989 if let Some(val) = getsparam(name) {
2990 pm.u_str = Some(val);
2991 }
2992 }
2993 _ => {}
2994 }
2995 }
2996 // c:1146 `paramtab->addnode(paramtab, ztrdup(name), pm)`. For
2997 // the reuse arm this overwrites the same entry; for the shadow
2998 // arm it installs the new chained pm on top of the (now-
2999 // displaced) old.
3000 if !name.is_empty() {
3001 let cloned = pm.clone();
3002 // c:Src/builtin.c:2381-2451 — the `newspecial != NS_NONE` arm is an
3003 // if-arm that RETURNS through c:2444-2451; it never falls through to
3004 // the c:2519-2525 `createparam` call. C therefore keeps the special's
3005 // own node — already installed in paramtab — exactly where it is and
3006 // only pushes a copy of the old values down into `pm->old`
3007 // ("For specials, we keep the same struct but zero everything",
3008 // c:2390-2394). zshrs builds that shadow through createparam instead,
3009 // so the removenode below has to be suppressed for it or the special
3010 // would move to the head of its chain where zsh leaves it in place.
3011 // `keep_special` (builtin.rs:6032, c:2087-2089) is the caller's
3012 // newspecial signal; PM_REMOVABLE specials (zle/compsys params, whose
3013 // C sites DO call createparam — Src/Zle/complete.c:1300,
3014 // Src/Zle/zle_params.c:200) are not part of it.
3015 let newspecial_shadow = caller_keeps_special
3016 && (flags as u32 & PM_REMOVABLE) == 0
3017 && (flags as u32 & PM_LOCAL) != 0;
3018 let mut tab = paramtab().write().unwrap();
3019 if shadow_displaced && !newspecial_shadow {
3020 tab.remove(name); // c:1144
3021 }
3022 tab.insert(name.to_string(), pm); // c:1146
3023 drop(tab);
3024 return Some(cloned);
3025 }
3026 Some(pm) // c:1159
3027}
3028
3029/// Empty special-hash sentinel.
3030/// Port of `shempty()` from Src/params.c:1166. The C source uses
3031/// it as a no-op getfn callback for special hashes that need an
3032/// addressable function pointer but no actual work. Provided here
3033/// so future callers that match the C source's signature can call
3034/// it directly.
3035pub fn shempty() {}
3036
3037/// Port of `setsparam()` from `Src/params.c:3350`.
3038/// C body: `return assignsparam(s, val, ASSPM_WARN);`
3039/// WARNING: param names don't match C — Rust=() vs C=(s, val)
3040pub fn setsparam(s: &str, val: &str) -> Option<Param> {
3041 assignsparam(s, val, ASSPM_WARN as i32) // c:3352
3042}
3043
3044/// Port of `createspecialhash()` from `Src/params.c:1182`.
3045/// Creates a PM_SPECIAL|PM_HASHED parameter with the supplied get
3046/// and scan callbacks, attaches an empty hash table, and returns
3047/// the new Param (or None if `createparam` fails).
3048///
3049/// C body wiring:
3050/// - `pm = createparam(name, PM_SPECIAL|PM_HASHED|flags)` (c:1186)
3051/// - If shadowing an old param at function scope, `pm->level =
3052/// locallevel` (c:1204-1205) so the old one is exposed after
3053/// leaving the fn.
3054/// - `pm->gsu.h = (flags & PM_READONLY) ? &stdhash_gsu :
3055/// &nullsethash_gsu` (c:1206-1207)
3056/// - `pm->u.hash = newhashtable(0, name, NULL)` (c:1208) with
3057/// no-op add/empty/remove/free callbacks (`shempty`) plus the
3058/// supplied `get` / `scan` callbacks.
3059///
3060/// The Rust port drops `GetNodeFunc` / `ScanTabFunc` fn-pointer
3061/// parameters because the Rust HashTable model uses owned
3062/// HashMap<String, T> rather than C-style vtable dispatch; the
3063/// returned Param carries the empty hash and PM_HASHED flag so
3064/// callers can fill it via the standard array/hash setfn path.
3065pub fn createspecialhash(name: &str, flags: i32) -> Option<Param> {
3066 // c:1186 — `createparam(name, PM_SPECIAL|PM_HASHED|flags)`.
3067 let mut pm = createparam(name, (PM_SPECIAL | PM_HASHED) as i32 | flags)?;
3068
3069 // c:1204-1205 — if shadowing an old param, set level=locallevel.
3070 if pm.old.is_some() {
3071 // C: `pm->level = locallevel`. The previous Rust port had
3072 // `let ll = 0_i32;` as a hardcoded placeholder — meaning
3073 // shadowed special-hash params (`fpath`, `path`, `psvar`,
3074 // etc. assigned inside a function via local) would NEVER
3075 // get their level tagged for restoration. After the function
3076 // returned, the original param would be inaccessible because
3077 // the shadow record's level (always 0) wouldn't trigger the
3078 // endparamscope unset. Now reads the canonical `locallevel`
3079 // global from params.rs (matching the C global).
3080 pm.level = locallevel.load(Ordering::Relaxed) as i32;
3081 // c:1205
3082 }
3083
3084 // c:1206-1207 — GSU selection. We can't set the gsu_h pointer
3085 // without the full GSU port wired; leave it None and let the
3086 // standard setfn dispatch route through the existing hashsetfn
3087 // / nullsethashfn helpers.
3088
3089 // c:1208 — `pm->u.hash = newhashtable(0, name, NULL)`. Rust
3090 // stores an empty HashTable in u_hash. The C body then sets
3091 // hash/empty/add/get/get2/remove/disable/enable/free/print
3092 // callbacks (c:1210-1221) which in our Rust model are implicit
3093 // (HashMap handles add/get/remove; freenode is Drop).
3094 let ht = Box::new(hashtable {
3095 hsize: 0,
3096 ct: 0,
3097 nodes: Vec::new(),
3098 tmpdata: 0,
3099 hash: None,
3100 emptytable: None,
3101 filltable: None,
3102 cmpnodes: None,
3103 addnode: None,
3104 getnode: None,
3105 getnode2: None,
3106 removenode: None,
3107 disablenode: None,
3108 enablenode: None,
3109 freenode: None,
3110 printnode: None,
3111 scantab: None,
3112 });
3113 pm.u_hash = Some(ht);
3114 let _ = name;
3115
3116 Some(pm) // c:1223
3117}
3118
3119/// ```c
3120/// tpm->node.flags = pm->node.flags;
3121/// tpm->base = pm->base;
3122/// tpm->width = pm->width;
3123/// tpm->level = pm->level;
3124/// if (!fakecopy) {
3125/// tpm->old = pm->old;
3126/// tpm->node.flags &= ~PM_SPECIAL;
3127/// }
3128/// switch (PM_TYPE(pm->node.flags)) {
3129/// case PM_SCALAR: case PM_NAMEREF:
3130/// tpm->u.str = ztrdup(pm->gsu.s->getfn(pm)); break;
3131/// case PM_INTEGER:
3132/// tpm->u.val = pm->gsu.i->getfn(pm); break;
3133/// case PM_EFLOAT: case PM_FFLOAT:
3134/// tpm->u.dval = pm->gsu.f->getfn(pm); break;
3135/// case PM_ARRAY:
3136/// tpm->u.arr = zarrdup(pm->gsu.a->getfn(pm)); break;
3137/// case PM_HASHED:
3138/// tpm->u.hash = copyparamtable(pm->gsu.h->getfn(pm), pm->node.nam);
3139/// break;
3140/// }
3141/// if (!fakecopy)
3142/// assigngetset(tpm);
3143/// ```
3144/// Copies `pm`'s value + level/base/width/flags into `tpm`.
3145/// `fakecopy = 1` means we're saving a snapshot (e.g. for special
3146/// param scope-save) and don't need callable get/set callbacks; in
3147/// that case `tpm->old`/PM_SPECIAL are preserved untouched and
3148/// `assigngetset` is skipped.
3149/// Port of `copyparam()` from `Src/params.c:1236`.
3150/// WARNING: param names don't match C — Rust=(pm, fakecopy) vs C=(tpm, pm, fakecopy)
3151pub fn copyparam(
3152 // c:1236
3153 tpm: &mut param,
3154 pm: &mut param,
3155 fakecopy: i32,
3156) {
3157 tpm.node.flags = pm.node.flags; // c:1244
3158 tpm.base = pm.base; // c:1245
3159 tpm.width = pm.width; // c:1246
3160 tpm.level = pm.level; // c:1247
3161 if fakecopy == 0 {
3162 // c:1248
3163 tpm.old = pm.old.take(); // c:1249
3164 tpm.node.flags &= !(PM_SPECIAL as i32); // c:1250
3165 }
3166 match PM_TYPE(pm.node.flags as u32) {
3167 // c:1252
3168 t if t == PM_SCALAR || t == PM_NAMEREF => {
3169 // c:1255 — `tpm->u.str = ztrdup(pm->gsu.s->getfn(pm));`.
3170 // C dispatches through the GSU getfn pointer so PM_SPECIAL
3171 // params (IFS, PATH, HOME, ...) return their canonical
3172 // global value (ifsgetfn reads `ifs_lock`, etc.). Without
3173 // this dispatch, `local IFS=:` saved an empty pm.u_str
3174 // (the bare strgetfn read) and endparamscope could not
3175 // restore the real outer value on scope exit (bug #8 in
3176 // docs/BUGS.md).
3177 let getfn_ptr = pm.gsu_s.as_ref().map(|g| g.getfn);
3178 tpm.u_str = Some(if let Some(getfn) = getfn_ptr {
3179 getfn(pm)
3180 } else {
3181 strgetfn(pm)
3182 });
3183 }
3184 t if t == PM_INTEGER => {
3185 // c:1257
3186 tpm.u_val = intgetfn(pm); // c:1258
3187 }
3188 t if t == PM_EFLOAT || t == PM_FFLOAT => {
3189 // c:1260-1261
3190 tpm.u_dval = floatgetfn(pm); // c:1262
3191 }
3192 t if t == PM_ARRAY => {
3193 // c:1264
3194 tpm.u_arr = Some(arrgetfn(pm)); // c:1265
3195 }
3196 t if t == PM_HASHED => {
3197 // c:1267
3198 // copyparamtable(pm->gsu.h->getfn(pm), pm->node.nam) // c:1268
3199 tpm.u_hash = copyparamtable(pm.u_hash.as_ref(), &pm.node.nam);
3200 }
3201 _ => {}
3202 }
3203 if fakecopy == 0 {
3204 // c:1280
3205 assigngetset(tpm); // c:1281
3206 }
3207}
3208
3209// ---------------------------------------------------------------------------
3210// Utility functions
3211// ---------------------------------------------------------------------------
3212
3213/// Check if string is valid identifier (from params.c isident)
3214// Return 1 if the string s is a valid identifier, else return 0. // c:1288
3215/// Port of `isident()` from `Src/params.c:1288`.
3216/// `isident` — see implementation.
3217pub fn isident(s: &str) -> bool {
3218 use crate::ported::utils::itype_end;
3219 use crate::ported::ztype_h::{IIDENT, INAMESPC};
3220
3221 let b = s.as_bytes();
3222 // c:1313 — `if (!*s)` — empty string is definitely not valid
3223 if b.is_empty() {
3224 return false; // c:1314
3225 }
3226
3227 // c:1316-1319 — This partly duplicates code in itype_end(), but we need to
3228 // distinguish the leading namespace at this point to check the
3229 // correctness of the identifier that follows
3230 //
3231 // C walks `char *s` / `char *ss` as pointers; the Rust port tracks the
3232 // same two cursors as BYTE OFFSETS into `s` (`s_off` / `ss_off`).
3233 let mut s_off = 0usize;
3234 if b[0] == b'.' {
3235 // c:1320
3236 if b.get(1).is_some_and(|c| c.is_ascii_digit()) {
3237 // c:1321
3238 return false; // c:1322 — Namespace must not start with a digit
3239 }
3240 // c:1323-1325 — Reject identifiers beginning with a digit in namespaces.
3241 // Move this out below this block to also reject v.1x form.
3242 // c:1326 — `if ((ss = itype_end(s + (*s == '.'), IIDENT, 0)))`
3243 let ss = 1 + itype_end(&s[1..], IIDENT as u32, false);
3244 if b.get(ss) == Some(&b'.') {
3245 // c:1327
3246 if b.get(ss + 1).is_none() {
3247 // c:1328
3248 return false; // c:1329
3249 }
3250 if b[ss + 1].is_ascii_digit() {
3251 // c:1330
3252 s_off = ss + 1; // c:1331
3253 }
3254 }
3255 }
3256
3257 let ss_off: usize;
3258 if b[s_off].is_ascii_digit() {
3259 // c:1336
3260 // c:1337 — If the first character is `s' is a digit, then all must be
3261 s_off += 1; // c:1338 — `++s`
3262 let mut i = s_off; // c:1338 — `ss = ++s`
3263 while i < b.len() {
3264 // c:1338
3265 if !b[i].is_ascii_digit() {
3266 // c:1339
3267 break; // c:1340
3268 }
3269 i += 1;
3270 }
3271 ss_off = i;
3272 } else {
3273 // c:1341
3274 // c:1342 — Find the first character in `s' not in the iident type table
3275 ss_off = s_off + itype_end(&s[s_off..], INAMESPC, false); // c:1343
3276 }
3277
3278 // c:1346-1347 — If the next character is not [, then it is
3279 // definitely not a valid identifier.
3280 if ss_off >= b.len() {
3281 // c:1348
3282 return true; // c:1349
3283 }
3284 if s_off == ss_off {
3285 // c:1350
3286 return false; // c:1351
3287 }
3288 if b[ss_off] != b'[' {
3289 // c:1352
3290 return false; // c:1353
3291 }
3292
3293 // c:1355 — Require balanced [ ] pairs with something between
3294 // c:1356-1358 — `if (!(ss = parse_subscript(++ss, 1, ']')))
3295 // parse_subscript(++ss, 1, ']'))) return 0;`
3296 // Subscript MUST be balanced — `foo[` (missing `]`)
3297 // is NOT a valid identifier. The previous Rust port
3298 // accepted `[` at the end unconditionally, missing
3299 // the balanced-pair requirement.
3300 //
3301 // Routing through the full `parse_subscript` (which
3302 // drives a nested lex context) would be overkill at
3303 // this site — a simple bracket-balance walk over the
3304 // remaining bytes suffices. Count `[` / `]` and require
3305 // the depth to return to 0 before end-of-string.
3306 let mut depth = 1i32;
3307 // c:Src/params.c:1334 — `if (!(ss = parse_subscript(++ss,
3308 // 1, ']'))) return 0;`. C's parse_subscript rejects empty
3309 // subscripts: `h[]` returns NULL. Mirror by tracking
3310 // whether ANY non-bracket char appears before the depth
3311 // returns to 0. Without this, `h[]=val` was accepted as a
3312 // valid assoc element write — but zsh errors
3313 // `not an identifier: h[]` (bug #288 in docs/BUGS.md).
3314 //
3315 // c:Src/params.c parse_subscript (params.c:1480+) also
3316 // recognises backslash-escaped brackets: `A[\[k\]]` is a
3317 // single subscript with key `[k]`. The `\[` doesn't count
3318 // toward bracket depth and `\]` doesn't close. Track a
3319 // bslash flag so the depth walk matches C semantics.
3320 let mut saw_content = false;
3321 // Bug fix: `s.split('[').skip(1).next()` only returned
3322 // the segment between the first and second `[`, dropping
3323 // everything after. Use find()+slice to get the entire
3324 // tail starting after the first `[`.
3325 // c:1356 — `parse_subscript(++ss, …)`: the walk starts at the
3326 // byte AFTER the `[` that the name walk stopped on (`ss_off`),
3327 // not at the first `[` anywhere in `s` — with a namespace name
3328 // the two can differ once `s` has been advanced at c:1331.
3329 let tail_start = Some(ss_off + 1);
3330 let saw_close = tail_start.map(|start| &s[start..]).is_some_and(|tail| {
3331 let mut bslash = false;
3332 for ch in tail.chars() {
3333 if bslash {
3334 // c:parse_subscript — escaped char is
3335 // content, doesn't affect depth.
3336 saw_content = true;
3337 bslash = false;
3338 continue;
3339 }
3340 match ch {
3341 '\\' => {
3342 bslash = true;
3343 saw_content = true;
3344 }
3345 '[' => {
3346 depth += 1;
3347 saw_content = true;
3348 }
3349 ']' => {
3350 depth -= 1;
3351 if depth == 0 {
3352 return true;
3353 }
3354 saw_content = true;
3355 }
3356 _ => saw_content = true,
3357 }
3358 }
3359 false
3360 });
3361 if !saw_content {
3362 return false; // c:1334 empty subscript rejected
3363 }
3364 saw_close
3365}
3366
3367/// Subscript-argument parser.
3368///
3369/// Port of `getarg()` from `Src/params.c:1367`. The C function is a
3370/// 618-line monolith handling the entire `[...]` body of a
3371/// subscripted parameter expansion.
3372///
3373/// Ported phases:
3374/// - Flag-block parse (c:1389-1480) — extract `(...)` chars.
3375/// - Hash pattern search (c:1581-1660) when `assoc` is `Some`.
3376/// - Array pattern search (c:1672-1719) when `arr` is `Some`.
3377/// - Scalar word-mode arm (c:1761-1797) when `scalar` is `Some`.
3378///
3379/// Later C phases not yet exercised by this entry point:
3380/// - Brace-depth walk to closing `]` (c:1507-1535)
3381/// - parsestr + singsub on subscript body (c:1545-1580)
3382/// - mathevalarg integer parse (c:1601-1604)
3383/// - Multibyte char-search arm (c:1798-1985)
3384pub(crate) fn getarg<'a>(
3385 idx: &'a str,
3386 arr: Option<&[String]>,
3387 assoc: Option<&IndexMap<String, String>>,
3388 scalar: Option<&str>,
3389) -> Option<getarg_out<'a>> {
3390 let rest = idx.strip_prefix('(')?;
3391 // c:Src/params.c:1389-1391 — the flag scan walks `s` from just past
3392 // the `(` and stops at the FIRST `)`/Outpar; everything after it is
3393 // the search PATTERN, which C hands straight to patcompile (c:1697).
3394 // A `[` therefore has no meaning to the flag scanner at all: it is
3395 // either an ordinary pattern char-class (`(r)[dUurRtT]`) or, if it
3396 // shows up among the flags themselves, an unknown flag that the
3397 // `_ => bad` arm below already rejects the same way C's default arm
3398 // does (c:1477-1483). A blanket `rest.contains('[')` bail therefore
3399 // rejected every legitimate char-class pattern: `${${x}[(r)[abc]]}`
3400 // returned None here, and paramsubst's subexp arm (subst.rs:6447-6463)
3401 // falls back to the WHOLE value on None — so `_typeset`'s
3402 // `${${(s::)use[$i]}[(r)[dUurRtT]]}` answered non-empty for every
3403 // character and appended `$func` to every allargs key, dropping the
3404 // specs whose key has no `f`/`p` variant (`functions -W`, `autoload -k`).
3405 if rest.starts_with(')') {
3406 return None;
3407 }
3408 // Flag scanner per zshparam(1) "Subscript Flags" /
3409 // params.c:1389-1480 switch:
3410 // r/R (reverse value-search → value/all values),
3411 // i/I (value-search → key/all keys),
3412 // k/K (key-search → value/all values),
3413 // e (exact match — disables glob),
3414 // n<DELIM>EXPR<DELIM> (Nth match — params.c:1452-1463),
3415 // b<DELIM>EXPR<DELIM> (begin offset — params.c:1464-1475),
3416 // w (word index on scalar),
3417 // f (word index split by newline; alias for `w` + sep="\n"),
3418 // p (escapes for next get_strarg),
3419 // s<DELIM>SEP<DELIM> (split-by-separator).
3420 // The `n` / `b` / `s` forms use `get_strarg`'s balanced-delimiter
3421 // pair: any non-flag char closes its pair (`(n.5.)`, `(n:5:)` etc.).
3422 let bytes = rest.as_bytes();
3423 let mut i: usize = 0;
3424 let mut num: i64 = 1;
3425 let mut beg: i64 = 0;
3426 let mut has_beg = false;
3427 let flags_start = 0_usize;
3428 let mut flags_end = 0_usize;
3429 let mut bad = false;
3430 // Sequential direction state per c:1390-1483 (see the flag arm below).
3431 let mut seq_ind = false;
3432 let mut seq_down = false;
3433 // c:1440-1447 `word` and c:1447/1479-1495 `sep`; `(p)` (c:1476) print-decodes
3434 // a following `(s:SEP:)`.
3435 let mut word = false;
3436 let mut sep_explicit: Option<String> = None;
3437 let mut escapes = false;
3438 while i < bytes.len() && bytes[i] != b')' {
3439 let c = bytes[i] as char;
3440 match c {
3441 'r' | 'R' | 'i' | 'I' | 'e' | 'k' | 'K' | 'w' | 'f' | 'p' => {
3442 // c:Src/params.c:1390-1483 — the switch is SEQUENTIAL and each
3443 // direction arm RESETS the others, so only the LAST direction
3444 // letter survives: `(Ir)` ends down=0 (FIRST match) while `(rI)`
3445 // ends down=1 (LAST). Tracking it HERE rather than re-scanning
3446 // the finished `flags` slice matters because that slice still
3447 // contains the `n`/`b` ARGUMENT text (e.g. "n:2:"), whose
3448 // characters must not be re-read as flags. `keymatch` is gated
3449 // on ishash (c:1400/1405); on the array path it stays 0, so k/K
3450 // reduce to r/R. Bug #1050.
3451 match c {
3452 'r' | 'k' => {
3453 seq_ind = false;
3454 seq_down = false;
3455 }
3456 'R' | 'K' => {
3457 seq_ind = false;
3458 seq_down = true;
3459 }
3460 'i' => {
3461 seq_ind = true;
3462 seq_down = false;
3463 }
3464 'I' => {
3465 seq_ind = true;
3466 seq_down = true;
3467 }
3468 'w' => word = true, // c:1440
3469 'f' => {
3470 word = true;
3471 sep_explicit = Some("\n".to_string()); // c:1447
3472 }
3473 'p' => escapes = true, // c:1476
3474 _ => {}
3475 }
3476 i += 1;
3477 flags_end = i;
3478 }
3479 'n' | 'b' => {
3480 // Consume `n<DELIM>NUM<DELIM>` per c:1432 get_strarg.
3481 if i + 1 >= bytes.len() {
3482 bad = true;
3483 break;
3484 }
3485 let delim = bytes[i + 1];
3486 let arg_start = i + 2;
3487 let mut arg_end = arg_start;
3488 while arg_end < bytes.len() && bytes[arg_end] != delim {
3489 arg_end += 1;
3490 }
3491 if arg_end >= bytes.len() {
3492 bad = true;
3493 break;
3494 }
3495 // c:Src/params.c:1452-1463 (`n`) / c:1464-1475 (`b`) — the text
3496 // between the `get_strarg` delimiters is an ARITHMETIC
3497 // expression, not a decimal literal. C evaluates it:
3498 // c:1458 `num = mathevalarg(s + arglen, &d);` and
3499 // c:1471 `if ((beg = mathevalarg(s + arglen, &d)) > 0) beg--;`.
3500 // This port ran `str::parse` and bailed out of the WHOLE
3501 // function with `ok()?` when it failed, so any non-literal
3502 // argument discarded the entire flag group and the raw
3503 // subscript text fell through to the math lexer instead —
3504 // `_git-archive` sh:7's `words[(b:CURRENT-1:I)--format=*]`
3505 // reported "bad math expression" where zsh searches from the
3506 // computed offset. Keep the decimal fast path (mathevalarg of
3507 // "2" is 2) and route every other form through the canonical
3508 // evaluator, exactly as the c:1597 numeric bound below does.
3509 // mathevalarg owns its own diagnostics, so an empty argument
3510 // still raises c:Src/math.c:1530-1532's "bad math expression:
3511 // empty string" rather than being silently dropped here.
3512 let arg = std::str::from_utf8(&bytes[arg_start..arg_end]).ok()?;
3513 let parsed: i64 = match arg.trim().parse::<i64>() {
3514 Ok(n) => n, // fast path; identical to mathevalarg on a decimal literal
3515 Err(_) => crate::ported::math::mathevalarg(arg), // c:1458 / c:1471
3516 };
3517 if c == 'n' {
3518 num = if parsed == 0 { 1 } else { parsed };
3519 } else {
3520 has_beg = true;
3521 beg = if parsed > 0 { parsed - 1 } else { parsed };
3522 }
3523 i = arg_end + 1;
3524 flags_end = i;
3525 }
3526 's' => {
3527 // c:1479-1495 — `s<DELIM>SEP<DELIM>` via get_strarg; the flag
3528 // scan continues after it, so `(ws.:.i)` and `(s.:.w)` both
3529 // reach the word arms below.
3530 let Some(&delim) = bytes.get(i + 1).filter(|b| b.is_ascii()) else {
3531 bad = true;
3532 break;
3533 };
3534 let Some(len) = bytes[i + 2..].iter().position(|&b| b == delim) else {
3535 bad = true;
3536 break;
3537 };
3538 let arg = &rest[i + 2..i + 2 + len];
3539 sep_explicit = Some(if escapes {
3540 crate::ported::utils::getkeystring_with(
3541 arg,
3542 (crate::ported::zsh_h::GETKEY_OCTAL_ESC | crate::ported::zsh_h::GETKEY_EMACS)
3543 as u32,
3544 None,
3545 )
3546 .0 // c:1490
3547 } else {
3548 arg.to_string() // c:1494
3549 });
3550 i += len + 3;
3551 flags_end = i;
3552 }
3553 _ => {
3554 bad = true;
3555 break;
3556 }
3557 }
3558 }
3559 // c:1477-1483 — flag-error fallback: reset all flags, treat as no
3560 // subscript flags.
3561 if bad {
3562 return None;
3563 }
3564 if i >= bytes.len() || bytes[i] != b')' {
3565 return None;
3566 }
3567 if flags_end == flags_start {
3568 return None;
3569 }
3570 let flags = &rest[flags_start..flags_end];
3571 let pat = &rest[i + 1..];
3572
3573 // c:1488-1491 — negative `num` flips the search direction.
3574 let neg_num_flips = num < 0;
3575 if neg_num_flips {
3576 num = -num;
3577 }
3578
3579 // Phase 3 — hash pattern search arm (c:1581-1660 / 1672-1734).
3580 // Per C source case-arms:
3581 // `r`: rev=1 → match against VALUES, return matching VALUE
3582 // `R`: rev+down=1 → match VALUES, return ALL matching VALUEs
3583 // `i`: rev+ind=1 → match VALUES, return KEY of first match
3584 // `I`: rev+ind+down=1 → match VALUES, return ALL matching KEYs
3585 // `k`: keymatch+rev=1 → match KEYS, return VALUE of first match
3586 // `K`: keymatch+rev+down=1 → match KEYS, return ALL matching VALUEs
3587 if let Some(map) = assoc {
3588 let exact = flags.contains('e');
3589 let key_match = flags.contains('k') || flags.contains('K');
3590 let return_index = seq_ind; // c:1412/1416 ind, sequential
3591 // c:Src/params.c — on a HASH, i/I/k/K all match against KEYS while
3592 // r/R match against VALUES (zsh 5.9: `${h[(i)KEY]}`→KEY,
3593 // `${h[(r)VAL]}`→VAL, `${h[(i)VAL]}`→empty). k/K are exact key
3594 // compares (pprog=NULL, c:1707-1708); i/I glob the key; r/R glob the
3595 // value; (e) forces exact. Return: i/I→KEY, k/K→VALUE, r/R→VALUE.
3596 let match_against_key = key_match || return_index;
3597 let key_glob = return_index; // i/I glob the key; k/K are exact-only
3598 // c:Src/params.c:1689,1708-1729 — the value/key SCAN only runs when
3599 // `v->scanflags` is set, which requires a search-direction flag
3600 // (i/I/r/R/k/K). With none of those (e.g. a bare `(e)key`), C never
3601 // builds scanflags: getindex falls back to a plain `gethashnode`
3602 // exact KEY lookup. `(e)` (quote_arg, c:1450) only makes that key
3603 // literal (no glob). Without this, `(e)*` wrongly scanned VALUES for
3604 // an exact "*" and returned empty instead of the value at key "*".
3605 let has_search = flags.contains('i')
3606 || flags.contains('I')
3607 || flags.contains('r')
3608 || flags.contains('R')
3609 || flags.contains('k')
3610 || flags.contains('K');
3611 if !has_search {
3612 return Some(getarg_out::Value(Value::str(
3613 map.get(pat).cloned().unwrap_or_default(),
3614 )));
3615 }
3616 // C params.c:1488-1491 — negative `num` flips `down`. Since
3617 // R/I/K already set down=1, neg_num XORs the bit (r/i/k +
3618 // neg → return_all; R/I/K + neg → single-match again).
3619 let is_uppercase = flags.contains('I') || flags.contains('R') || flags.contains('K');
3620 let return_all = is_uppercase ^ neg_num_flips;
3621
3622 // c:1740-1747 — `b<NUM>` start offset on the values array. The
3623 // hash is iterated in insertion order (IndexMap); skip first
3624 // `beg` entries before counting matches.
3625 let len = map.len() as i64;
3626 let mut start = beg;
3627 if start < 0 {
3628 start += len;
3629 }
3630 if !return_all && start >= len {
3631 return Some(getarg_out::Value(Value::str("")));
3632 }
3633 let skip = if start < 0 { 0 } else { start as usize };
3634
3635 // Per C params.c:1707-1709 + zsh 5.9 empirical:
3636 // k/K — keymatch path: pprog=NULL, no glob; exact key
3637 // lookup. `(K)*` returns "" because there's no key
3638 // literally named "*".
3639 // r/R/i/I — value path: pprog=patcompile, glob/exact.
3640 // c:params.c:1697 — `pprog = patcompile(s, 0, NULL)` runs ONCE
3641 // before the scan loop. Compiling inside the per-entry compare
3642 // re-tokenized + re-compiled the pattern for EVERY element —
3643 // `${history[(R)(#i)*pat*]}` over a 566k-entry history ran
3644 // ~4 CPU-minutes per hsmw ^R (the reported freeze) instead of
3645 // one compile + 566k pattry calls.
3646 let compiled_pat = if exact || (match_against_key && !key_glob) {
3647 None
3648 } else {
3649 patcompile(
3650 &{
3651 let mut __pat_tok = crate::pattern_data_escape::escape_data_backslashes(pat); // c:1725 tokenize(s): a backslash tokenize leaves raw is DATA (c:Src/glob.c:3651)
3652 crate::ported::glob::tokenize(&mut __pat_tok);
3653 __pat_tok
3654 },
3655 PAT_HEAPDUP as i32,
3656 None,
3657 )
3658 };
3659 let key_compare = |target: &str| -> bool {
3660 if key_match && !exact {
3661 // c:Src/params.c:653-660 — SCANPM_KEYMATCH (set for `k`/`K`
3662 // at c:1714-1715) inverts the usual direction: the STORED KEY
3663 // is compiled as the pattern and matched against the SUBSCRIPT
3664 // text (`scanstr`), not the other way round:
3665 // char *tmp = dupstring(v.pm->node.nam);
3666 // tokenize(tmp); remnulargs(tmp);
3667 // if (!(prog = patcompile(tmp, 0, NULL)) ||
3668 // !pattry(prog, scanstr)) return;
3669 // That is the whole point of the flag — `_netcat` stores glob
3670 // keys (`'*-l*'`, `'*-i secs*'`, …) and asks
3671 // `$optionmap[(K)$help]` for the options the binary's `-h`
3672 // output mentions. A plain `target == pat` compare answered no
3673 // for every key, so `nc -<TAB>`/`netstat -<TAB>` fell back to
3674 // the raw assoc instead of the help-filtered option set.
3675 // C compiles per node here too (c:654-659), so the one-shot
3676 // `compiled_pat` above (which the r/R value scan needs for its
3677 // 566k-entry history case) does not apply.
3678 patcompile(
3679 &{
3680 let mut t = target.to_string();
3681 crate::ported::glob::tokenize(&mut t);
3682 t
3683 },
3684 PAT_HEAPDUP as i32,
3685 None,
3686 )
3687 .map_or(false, |p| pattry(&p, pat))
3688 } else if exact || (match_against_key && !key_glob) {
3689 // (e) literal key compare.
3690 target == pat
3691 } else {
3692 // i/I glob the key; r/R glob the value.
3693 compiled_pat.as_ref().map_or(false, |p| pattry(p, target))
3694 }
3695 };
3696 if return_all {
3697 let mut out: Vec<String> = Vec::new();
3698 for (k, v) in map.iter().skip(skip) {
3699 let target = if match_against_key {
3700 k.as_str()
3701 } else {
3702 v.as_str()
3703 };
3704 if key_compare(target) {
3705 // i/I → KEY; k/K → VALUE; r/R → VALUE.
3706 out.push(if return_index { k.clone() } else { v.clone() });
3707 }
3708 }
3709 // c:Src/params.c:1724-1734 — the MATCHMANY forms (`I`/`R`/`K`, or a
3710 // negative `n` that flips `down`) set SCANPM_MATCHMANY, so getindex
3711 // takes the `getvaluearr(v)` branch and the expansion is ARRAY-
3712 // shaped: unquoted `$optionmap[(K)$help]` yields one word per match,
3713 // which is how `_netcat` feeds `_arguments` twelve separate option
3714 // specs. Joining here collapsed all twelve into a single word and
3715 // `comparguments` rejected it ("invalid option definition"). Array
3716 // and scalar callers are both served: `Value::Array::to_str`
3717 // (fusevm value.rs:149-151) joins with " ", the exact string this
3718 // used to return.
3719 return Some(getarg_out::Value(Value::array(
3720 out.into_iter().map(Value::str).collect(),
3721 )));
3722 }
3723 // c:1753 — `!--num` skips matches until the Nth.
3724 let mut remaining = num;
3725 for (k, v) in map.iter().skip(skip) {
3726 let target = if match_against_key {
3727 k.as_str()
3728 } else {
3729 v.as_str()
3730 };
3731 if key_compare(target) {
3732 remaining -= 1;
3733 if remaining == 0 {
3734 return Some(getarg_out::Value(Value::str(if return_index {
3735 k.clone()
3736 } else {
3737 v.clone()
3738 })));
3739 }
3740 }
3741 }
3742 return Some(getarg_out::Value(Value::str("")));
3743 }
3744
3745 // Phase 2 — array pattern search arm (c:1672-1719). The C body
3746 // does `pprog = patcompile(s, 0, NULL)` then forward/reverse
3747 // `for (r = 1 + beg, p = ta + beg; *p; r++, p++) if (pprog &&
3748 // pattry(pprog, *p)) return r`.
3749 if let Some(arr) = arr {
3750 // C params.c:1761-1797 — `(w)N` / `(f)N` word-mode arm.
3751 // `getstrvalue(v)` joins the array; `sepsplit` re-splits by
3752 // sep (`f` → "\n", `w` → IFS-default whitespace, `s:SEP:`
3753 // → user sep), then the Nth split word is returned. So
3754 // `arr=("a b" "c d"); ${arr[(w)2]}` → "b" (joined "a b c d",
3755 // split → ["a","b","c","d"], pick idx 1).
3756 if flags.contains('w') || flags.contains('f') {
3757 if let Ok(n) = pat.parse::<i64>() {
3758 let sep_chars: &[char] = if flags.contains('f') {
3759 &['\n']
3760 } else {
3761 &[' ', '\t', '\n']
3762 };
3763 let joined = arr.join(" ");
3764 let words: Vec<&str> = joined
3765 .split(|c: char| sep_chars.contains(&c))
3766 .filter(|w| !w.is_empty())
3767 .collect();
3768 let len = words.len() as i64;
3769 let idx_into = if n > 0 {
3770 (n - 1) as usize
3771 } else if n < 0 {
3772 let off = len + n;
3773 if off < 0 {
3774 return Some(getarg_out::Value(Value::str("")));
3775 }
3776 off as usize
3777 } else {
3778 return Some(getarg_out::Value(Value::str("")));
3779 };
3780 return Some(getarg_out::Value(Value::str(
3781 words
3782 .get(idx_into)
3783 .map(|s| s.to_string())
3784 .unwrap_or_default(),
3785 )));
3786 }
3787 }
3788 let exact = flags.contains('e');
3789 let word = flags.contains('w') || flags.contains('f');
3790 let _ = word;
3791 let return_index = seq_ind; // c:1412/1416 ind, sequential
3792 // c:Src/params.c:2091 — `if (start > 0 && (isset(KSHARRAYS) ||
3793 // (v->pm->node.flags & PM_HASHED))) start--;`. The search loop below
3794 // yields a 1-BASED index, but under KSHARRAYS the shell's indices are
3795 // 0-based, so every index that leaves here has to be shifted down. This
3796 // was missed entirely, which showed up two ways:
3797 // `setopt ksharrays; a=(x y z); ${a[(i)y]}` → 2, zsh says 1, and the
3798 // no-match answer came out len+1 (4) instead of len (3);
3799 // a RANGE bound built from a search (`${a[(r)b,(r)d]}`) fed the
3800 // unshifted index straight into the 0-based slice machinery, so the
3801 // window slid one element right — `c d e` for zsh's `b c d`.
3802 // The `> 0` guard is what keeps the `(I)` no-match answer at 0.
3803 // (PM_HASHED is not folded in here: an assoc `(i)` returns the KEY, not
3804 // an index, so there is nothing to shift — verified against zsh.)
3805 let ksh_idx = |i: i64| -> String {
3806 if i > 0 && isset(KSHARRAYS) {
3807 (i - 1).to_string()
3808 } else {
3809 i.to_string()
3810 }
3811 };
3812 // C params.c:1575 `if (!rev)` — without a direction flag
3813 // (r/R/i/I/k/K), getarg does NOT enter the search loop on
3814 // arrays; pat is mathevalarg'd as an integer index instead.
3815 // Verified empirically: `arr=(foo bar); ${arr[(e)foo]}`
3816 // returns empty in real zsh (mathevalarg fails, no element).
3817 let any_search_flag = flags.contains('r')
3818 || flags.contains('R')
3819 || flags.contains('i')
3820 || flags.contains('I')
3821 || flags.contains('k')
3822 || flags.contains('K');
3823 if !any_search_flag {
3824 return None;
3825 }
3826 // c:1488-1491 — negative `num` flips reverse direction.
3827 // c:1488-1491 — negative num flips the direction; `down` itself is the
3828 // sequential last-direction-letter value, not an order-blind contains().
3829 let reverse = seq_down ^ neg_num_flips;
3830 // C params.c:1668-1685 implicit `*` wrap fires only when
3831 // `v->scanflags` is unset; in standard subscript callsites
3832 // scanflags IS set, so the wrap does NOT engage. Verified
3833 // empirically: `arr=(foobar baz); ${arr[(r)foo]}` returns
3834 // empty in real zsh (exact match), not "foobar". Pattern is
3835 // used verbatim — globbing only when user supplies `*`.
3836 let pat_used: &str = pat;
3837
3838 // c:1740-1760 — `b<NUM>` starting offset + bounds checks.
3839 // beg is already 0-based after parse (parsed-1 for positive).
3840 let len = arr.len() as i64;
3841 let mut start = beg;
3842 if start < 0 {
3843 start += len;
3844 }
3845 // c:1743-1747 — out-of-bounds returns.
3846 if reverse {
3847 if start < 0 {
3848 return Some(getarg_out::Value(if return_index {
3849 Value::str("0")
3850 } else {
3851 Value::str("")
3852 }));
3853 }
3854 } else if start >= len {
3855 return Some(getarg_out::Value(if return_index {
3856 Value::str(ksh_idx(arr.len() as i64 + 1))
3857 } else {
3858 Value::str("")
3859 }));
3860 }
3861 // c:1750-1751 — reverse w/o explicit b starts from len-1.
3862 if reverse && !has_beg {
3863 start = len - 1;
3864 }
3865 // c:1748 — `if (beg >= 0 && beg < len) { …search… }`
3866 if start >= len {
3867 return Some(getarg_out::Value(if return_index {
3868 Value::str("0")
3869 } else {
3870 Value::str("")
3871 }));
3872 }
3873 let iter: Box<dyn Iterator<Item = (usize, &String)>> = if reverse {
3874 // c:1752 — `for (p = ta + beg; p >= ta; p--)`: clamp start
3875 // into the valid range then walk backwards. An empty array
3876 // has no element to walk (C's loop body never dereferences a
3877 // valid `p`), so yield an empty iterator rather than indexing
3878 // `arr[..=0]` on a zero-length slice.
3879 if arr.is_empty() {
3880 Box::new(std::iter::empty())
3881 } else {
3882 let s_idx = if start < 0 { 0 } else { start as usize };
3883 let s_idx = s_idx.min(arr.len() - 1);
3884 Box::new(arr[..=s_idx].iter().enumerate().rev())
3885 }
3886 } else {
3887 // c:1757 — `for (p = ta + beg; *p; p++)`: skip first beg.
3888 let s_idx = if start < 0 { 0 } else { start as usize };
3889 Box::new(arr.iter().enumerate().skip(s_idx))
3890 };
3891 // c:1758 — `!--num` skips matches until the Nth.
3892 // c:1697 — pattern compiled ONCE before the scan (see the
3893 // assoc arm above; same per-element recompile hazard).
3894 let compiled_arr_pat = if exact {
3895 None
3896 } else {
3897 patcompile(
3898 &{
3899 let mut __pat_tok = crate::pattern_data_escape::escape_data_backslashes(pat_used); // c:1725 tokenize(s): a backslash tokenize leaves raw is DATA (c:Src/glob.c:3651)
3900 crate::ported::glob::tokenize(&mut __pat_tok);
3901 __pat_tok
3902 },
3903 PAT_HEAPDUP as i32,
3904 None,
3905 )
3906 };
3907 let mut remaining = num;
3908 for (i, s) in iter {
3909 let hit = if exact {
3910 s == pat
3911 } else {
3912 compiled_arr_pat.as_ref().map_or(false, |p| pattry(p, s))
3913 };
3914 if hit {
3915 remaining -= 1;
3916 if remaining == 0 {
3917 return Some(getarg_out::Value(if return_index {
3918 Value::str(ksh_idx(i as i64 + 1))
3919 } else {
3920 Value::str(s.clone())
3921 }));
3922 }
3923 }
3924 }
3925 return Some(getarg_out::Value(if return_index {
3926 // zsh: `i` returns len+1 if not found, `I` returns 0.
3927 if flags.contains('I') {
3928 Value::str("0")
3929 } else {
3930 Value::str(ksh_idx(arr.len() as i64 + 1))
3931 }
3932 } else {
3933 Value::str("")
3934 }));
3935 }
3936
3937 // C params.c:1761-1797 — scalar word-mode arm. `(w)N` joins
3938 // the source string and re-splits by sep (whitespace by default
3939 // for `w`, "\n" for `f`). When `pat` is a numeric N, the Nth
3940 // word is returned. Pattern-search variants on scalars use the
3941 // c:1798-1980 char-search arm ported below.
3942 if let Some(s) = scalar {
3943 // c:1783 `sepsplit(s, sep, 1, 1)`: with no separator the split is on
3944 // IFS whitespace and yields no empty word; an explicit one keeps the
3945 // empties between adjacent separators. Each entry is a char range.
3946 let s_chars: Vec<char> = s.chars().collect();
3947 let sepsplit_ranges = || -> Vec<(usize, usize)> {
3948 let sep: Vec<char> = sep_explicit.as_deref().unwrap_or("").chars().collect();
3949 let sep_len_at = |i: usize| -> usize {
3950 if sep_explicit.is_none() {
3951 matches!(s_chars[i], ' ' | '\t' | '\n') as usize
3952 } else if !sep.is_empty() && s_chars[i..].starts_with(&sep) {
3953 sep.len()
3954 } else {
3955 0
3956 }
3957 };
3958 let (mut ta, mut i, mut start) = (Vec::new(), 0usize, 0usize);
3959 while i < s_chars.len() {
3960 match sep_len_at(i) {
3961 0 => i += 1,
3962 l => {
3963 ta.push((start, i));
3964 i += l;
3965 start = i;
3966 }
3967 }
3968 }
3969 ta.push((start, s_chars.len()));
3970 if sep_explicit.is_none() {
3971 ta.retain(|&(a, b)| a < b);
3972 }
3973 ta
3974 };
3975 let word_text = |(a, b): (usize, usize)| -> String { s_chars[a..b].iter().collect() };
3976 if word {
3977 if let Ok(n) = pat.parse::<i64>() {
3978 let words: Vec<String> = sepsplit_ranges()
3979 .into_iter()
3980 .filter(|&(a, b)| a < b)
3981 .map(word_text)
3982 .collect();
3983 // c:1636-1648 — KSHARRAYS shifts a non-negative index and a
3984 // negative one counts from the end. Since workers/54773 an
3985 // index outside [1, wordcount] yields nothing, like any other
3986 // out-of-range subscript: `${${:-a b c}[(w)5]}`, `(w)-5` and
3987 // `(w)0` are all empty.
3988 let len = words.len() as i64;
3989 let mut r = if isset(KSHARRAYS) && n >= 0 { n + 1 } else { n };
3990 if r < 0 {
3991 r += len + 1; // c:1645-1646
3992 }
3993 return Some(getarg_out::Value(Value::str(if r < 1 || r > len {
3994 String::new() // c:1647-1648 `if (r < 1 || r > i) return 0;`
3995 } else {
3996 words[r as usize - 1].clone()
3997 })));
3998 }
3999 }
4000 let any_search = flags.contains('r')
4001 || flags.contains('R')
4002 || flags.contains('i')
4003 || flags.contains('I')
4004 || flags.contains('k') // c:1422-1426 (keymatch = ishash = 0 ⇒ rev)
4005 || flags.contains('K'); // c:1427-1431 (keymatch = ishash = 0 ⇒ rev+down)
4006 // c:2091 — a search index leaves 0-based under KSHARRAYS.
4007 let ksh_idx = |i: i64| -> i64 {
4008 if i > 0 && isset(KSHARRAYS) {
4009 i - 1
4010 } else {
4011 i
4012 }
4013 };
4014 // c:1782-1818 — a search flag with a word flag matches whole WORDS
4015 // (no implicit trailing Star: c:1689 skips it for `word`), then maps the
4016 // word number back to a character offset through `findword`.
4017 if word && any_search && sep_explicit.as_deref() != Some("") {
4018 let down = seq_down ^ neg_num_flips; // c:1509-1512
4019 let ta = sepsplit_ranges();
4020 let visited: Vec<(usize, usize)> =
4021 ta.iter().copied().filter(|&(a, b)| a < b).collect(); // findword skips empties
4022 let exact = flags.contains('e');
4023 let prog = if exact {
4024 None
4025 } else {
4026 let mut tok = crate::pattern_data_escape::escape_data_backslashes(pat); // c:1725 tokenize(s): a raw backslash tokenize declines is DATA (c:Src/glob.c:3651)
4027 crate::ported::glob::tokenize(&mut tok);
4028 patcompile(&tok, PAT_HEAPDUP as i32, None) // c:1727
4029 };
4030 let untok = crate::lex::untokenize(pat); // c:1709 quote_arg
4031 let hit = |k: usize| -> bool {
4032 let w = word_text(ta[k]);
4033 if exact {
4034 w == untok
4035 } else {
4036 prog.as_ref().is_some_and(|p| pattry(p, &w))
4037 }
4038 };
4039 let len = ta.len() as i64;
4040 let b0 = if beg < 0 { beg + len } else { beg }; // c:1785-1786
4041 // c:1791 `return len + 1` — straight out, `*w` never set.
4042 let direct = !down && b0 >= len;
4043 let mut left = num;
4044 let mut nth = |k: &usize| {
4045 hit(*k) && {
4046 left -= 1;
4047 left == 0
4048 }
4049 };
4050 let r: i64 = if (down && b0 < 0) || (!direct && !(b0 >= 0 && b0 < len)) {
4051 0 // c:1788-1789
4052 } else if direct {
4053 len + 1
4054 } else if down {
4055 let top = if has_beg { b0 } else { len - 1 }; // c:1794-1795
4056 (0..=top as usize).rev().find(&mut nth).map_or(0, |k| k as i64 + 1) // c:1796-1800
4057 } else {
4058 (b0 as usize..ta.len()).find(&mut nth).map_or(0, |k| k as i64 + 1) // c:1802-1806
4059 };
4060 // c:1811-1816 — the r-th visited word gives the 1-based offset; its
4061 // extent is `strlen(ta[i])`, `i` counting findword's visits.
4062 let (off, end) = if direct {
4063 (r, r)
4064 } else if r > 0 && (r as usize) <= visited.len() {
4065 let k = r as usize - 1;
4066 let a = visited[k].0 as i64;
4067 (a + 1, a + (ta[k].1 - ta[k].0) as i64)
4068 } else {
4069 (0, 0)
4070 };
4071 return Some(getarg_out::Value(Value::str(if seq_ind {
4072 ksh_idx(off).to_string()
4073 } else if off > 0 {
4074 // getindex: offset through `*w`, or the one character at the
4075 // offset when `*w` was never set.
4076 let a = off as usize - 1;
4077 let e = (end.max(off) as usize).min(s_chars.len());
4078 s_chars.get(a..e).map(|c| c.iter().collect()).unwrap_or_default()
4079 } else {
4080 String::new()
4081 })));
4082 }
4083 // C params.c:1798-1980 — scalar char-search arm. `(i)/(I)/
4084 // (r)/(R)` on a scalar runs a sliding-window glob match over
4085 // the CHARACTER array (`s_chars`), so positions are character-
4086 // based — matching C's prevcharlen/nextcharlen cursor stepping
4087 // (c:1948-1971) under MULTIBYTE. (i)/(I) return the 1-based
4088 // char position of the first/last match; (r)/(R) return the
4089 // char at the match. Verified against zsh: `s=ábc; ${s[(i)b]}`
4090 // → 2 (char pos), not 3 (byte pos).
4091 // c:Src/params.c:1422-1431 — `case 'k'` / `case 'K'` set `keymatch =
4092 // ishash`, and c:1402-1403 makes `ishash` 0 for a non-hash, so on a
4093 // SCALAR they leave only `rev` (and `down` for `K`): they ARE `r`/`R`
4094 // here, and c:1707's `if (!keymatch)` still compiles the pattern.
4095 // Omitting them left `${s[(k)pat]}` short of the c:1819 char-search arm.
4096 if any_search {
4097 let return_index = seq_ind; // c:1412/1416 ind, sequential
4098 let want_last = flags.contains('I') || flags.contains('R') || flags.contains('K');
4099 // Negative `num` flips direction (c:1488-1491).
4100 let want_last = want_last ^ neg_num_flips;
4101 let n = s_chars.len();
4102 let positions: Box<dyn Iterator<Item = usize>> = if want_last {
4103 Box::new((0..=n).rev())
4104 } else {
4105 Box::new(0..=n)
4106 };
4107 // c:1929+ / c:1964 — `!--num` skips matches until the Nth.
4108 // Per `b<NUM>` (c:1740-1747) — start from offset, only
4109 // when has_beg is set. Without `b`, walk all positions.
4110 let beg_idx_opt: Option<usize> = if has_beg {
4111 let beg_norm = if beg < 0 { beg + n as i64 } else { beg };
4112 Some(if beg_norm < 0 {
4113 0
4114 } else {
4115 (beg_norm as usize).min(n)
4116 })
4117 } else {
4118 None
4119 };
4120 // c:1689-1706 — a SCALAR search compiles the pattern with an
4121 // implicit trailing `Star` appended (c:1698 `d[l] = Star`)
4122 // and then tries it against the SUFFIX at each position
4123 // (c:1955-1976 `pattry(pprog, t)` where `t` walks forward
4124 // through the string), so a hit means "the pattern matches a
4125 // prefix of what starts here" — which is why
4126 // `s=barfooxyz; ${s[(i)foo]}` is 4.
4127 //
4128 // Testing every SPAN instead (`s[start..start+len]` for all
4129 // len) answers the same, but builds O(n²) candidate strings
4130 // of average length n/2: `${${(k)parameters}[(r)PATH]}` —
4131 // a 6KB scalar — ran for over 40 seconds where zsh replies
4132 // instantly. Compile once, then match suffixes by slice.
4133 let compiled_span_pat = if flags.contains('e') {
4134 None
4135 } else {
4136 patcompile(
4137 &{
4138 let mut __pat_tok = crate::pattern_data_escape::escape_data_backslashes(pat); // c:1725 tokenize(s): a backslash tokenize leaves raw is DATA (c:Src/glob.c:3651)
4139 crate::ported::glob::tokenize(&mut __pat_tok);
4140 __pat_tok.push(crate::ported::zsh_h::Star); // c:1698
4141 __pat_tok
4142 },
4143 PAT_HEAPDUP as i32,
4144 None,
4145 )
4146 };
4147 // Byte offset of each character, so a suffix is a slice of
4148 // `s` rather than a fresh String per position.
4149 let char_offsets: Vec<usize> = s
4150 .char_indices()
4151 .map(|(i, _)| i)
4152 .chain(std::iter::once(s.len()))
4153 .collect();
4154 let mut found: Option<(usize, usize)> = None;
4155 let mut remaining = num;
4156 'outer: for start in positions {
4157 if let Some(b_idx) = beg_idx_opt {
4158 if want_last {
4159 if start > b_idx {
4160 continue;
4161 }
4162 } else if start < b_idx {
4163 continue;
4164 }
4165 }
4166 let suffix = &s[char_offsets[start]..];
4167 let hit = if flags.contains('e') {
4168 // c:1687-1702 — `(e)` untokenizes the pattern and
4169 // still appends the Star, i.e. a literal prefix test.
4170 suffix.starts_with(pat)
4171 } else {
4172 compiled_span_pat
4173 .as_ref()
4174 .map_or(false, |p| pattry(p, suffix))
4175 };
4176 if hit {
4177 remaining -= 1;
4178 if remaining == 0 {
4179 // c:1957 `!--num` — the position IS the answer.
4180 found = Some((start, start));
4181 break 'outer;
4182 }
4183 }
4184 }
4185 return Some(getarg_out::Value(match (found, return_index) {
4186 (Some((s_pos, _)), true) => Value::str(ksh_idx(s_pos as i64 + 1).to_string()),
4187 // C params.c:1798-1980 char-search returns the char AT
4188 // the match position, not the full matched substring.
4189 // Verified empirically: `s="barfooxyz"; ${s[(r)foo]}`
4190 // returns "f" in real zsh, not "foo".
4191 (Some((s_pos, _)), false) => Value::str(
4192 s_chars
4193 .get(s_pos)
4194 .map(|c| c.to_string())
4195 .unwrap_or_default(),
4196 ),
4197 (None, true) => Value::str(if flags.contains('i') {
4198 ksh_idx(n as i64 + 1).to_string()
4199 } else {
4200 "0".to_string()
4201 }),
4202 (None, false) => Value::str(String::new()),
4203 }));
4204 }
4205 }
4206
4207 // No search context — return parsed flags for caller dispatch.
4208 Some(getarg_out::Flags { flags, rest: pat })
4209}
4210
4211/// Port of `getindex()` from `Src/params.c:2001`. Returns 0 on
4212/// success, non-zero on parse error. C body parses `[N]`/`[N,M]`/
4213/// `[(flags)pat]` after a Value's name and updates v->start/end/
4214/// scanflags.
4215/// Direct port of `int getindex(char **pptr, Value v, int
4216/// scanflags)` from `Src/params.c:2001-2167`. Parses the bracket
4217/// subscript after a Value's name and updates v->start/v->end/
4218/// v->scanflags. Returns 0 on success, 1 on parse error.
4219///
4220/// Handles:
4221/// - `[*]` / `[@]` — full range, with `[@]` setting
4222/// SCANPM_ISVAR_AT (c:2027-2032).
4223/// - `[N]` / `[N,M]` — operands evaluated through `mathevalarg`
4224/// (c:1618), KSH_ARRAYS adjustment (c:1619-1620), and the
4225/// KSHZEROSUBSCRIPT zero-subscript rule (c:2126-2150).
4226/// - Hash subscripts: exact key (c:1575-1595) and the `(r R k K i I
4227/// e n)` search flags (c:1391-1760), parsed inline because this
4228/// port's `getarg` has no `Value` out-parameter.
4229/// - Array `(i)`/`(I)` index searches (c:2110-2114) and `(r)`/`(R)`
4230/// element searches, resolved through `getarg`.
4231/// - Subscripts index by character, so the MB_METACHARLEN
4232/// previous/next-length offsets of c:2144-2145 are always 1.
4233pub fn getindex(pptr: &mut &str, v: &mut value, scanflags: i32) -> i32 {
4234 // c:2001
4235
4236 let s = *pptr;
4237 // c:2006 — `*s++ = '['`. Caller asserts s[0] is '[' (or its
4238 // tokenised form Inbrack); skip it. The byte test here read
4239 // `s.as_bytes()[0] != 0xa9`, which is neither `Inbrack` (U+e191,
4240 // encoded 0xC2 0x91 in a Rust `str`) nor anything else in the
4241 // token range, so a lexer-tokenised subscript never got past this
4242 // guard. Compare the leading CHARACTER instead.
4243 let lbrack = match s.chars().next() {
4244 Some(c) if c == '[' || c == crate::ported::zsh_h::Inbrack => c,
4245 _ => return 1,
4246 };
4247 let after_lbrack = &s[lbrack.len_utf8()..];
4248
4249 // c:2008 — `parse_subscript(s, dq, ']')`. Routes through the
4250 // existing lex-layer port at `crate::ported::lex::parse_subscript`
4251 // which honours `[...]` / `(...)` / `{...}` nesting and single/
4252 // double quoting (parse/src/lex.rs:3074).
4253 //
4254 // c:Src/lex.c:1749 `untokenize(t = dupstring_wlen(s, l));` plus
4255 // c:2011-2018 (`if (itok(*tbrack)) *tbrack = ztokens[...]`) mean
4256 // every offset C computes from here on indexes UNTOKENISED text.
4257 // C gets that for free because its tokens are single bytes that
4258 // untokenize in place; zshrs's are non-ASCII `char`s whose UTF-8
4259 // length shrinks from 2 to 1, so the offsets `parse_subscript`
4260 // returns do not index the tokenised string. Untokenize first and
4261 // work in that string, then map the end offset back by CHARACTER
4262 // count — untokenize is strictly one char in, one char out — to
4263 // advance `*pptr` inside the caller's still-tokenised text.
4264 let untok = crate::ported::lex::untokenize(after_lbrack);
4265 let close_pos = parse_subscript(&untok, ']');
4266 let close_pos = match close_pos {
4267 Some(p) => p,
4268 None => {
4269 // c:2020 — `zerr("invalid subscript")`.
4270 zerr("invalid subscript");
4271 *pptr = ""; // c:2021
4272 return 1; // c:2022
4273 }
4274 };
4275 let body = &untok[..close_pos];
4276 // c:2156/2164 — `*tbrack = ']'; *pptr = s`, i.e. the text just past
4277 // the closing bracket, in the ORIGINAL (tokenised) string.
4278 let past_close: &str = {
4279 let nchars = body.chars().count() + 1; // body + the `]` itself
4280 match after_lbrack.char_indices().nth(nchars) {
4281 Some((i, _)) => &after_lbrack[i..],
4282 None => "",
4283 }
4284 };
4285
4286 // c:2027 — special-case `[*]` / `[@]`.
4287 if body == "*" || body == "@" {
4288 if body == "@" && (v.scanflags != 0 || v.pm.is_none()) {
4289 // c:2028
4290 v.scanflags |= SCANPM_ISVAR_AT as i32; // c:2029
4291 }
4292 v.start = 0; // c:2030
4293 v.end = -1; // c:2031
4294 // c:2156 — `*tbrack = ']'; *pptr = s` (s points past `]`).
4295 *pptr = past_close;
4296 return 0; // c:2160
4297 }
4298
4299 let _ = scanflags;
4300 // c:2058-2114 — flag subscripts `[(i)pat]` / `[(I)pat]` on an
4301 // array. The numeric parser below can't handle these; route
4302 // through getarg (c:2058 `getarg(&s, &inv, v, 0, ...)`). The
4303 // index-returning reverse flags (i/I) yield the 1-based array
4304 // index of the match (len+1 / 0 when not found). C's getindex
4305 // inv branch (c:2110-2114) turns that into a single-element
4306 // access: `v->valflags |= VALFLAG_INV; v->scanflags = 0;
4307 // v->start = start; v->end = start + 1`. issetvar then range-
4308 // checks `end` against the array length, so `arr[(i)x]` (index
4309 // = len+1, out of range) correctly reports unset. Non-index
4310 // flags (r/R/k/K) still fall through to the substitution
4311 // pipeline's getarg.
4312 // c:1597-1615 — hash subscript resolution. For a PM_HASHED param
4313 // the subscript is either an exact KEY (c:1575-1595) or a flag
4314 // SEARCH over the keys/values (c:1686-1735).
4315 // Resolve the PM_HASHED test into an owned name so the `v.pm` borrow is
4316 // released before the arm below mutates `v`.
4317 let hashed_name = v.pm.as_ref().and_then(|p| {
4318 if PM_TYPE(p.node.flags as u32) == PM_HASHED {
4319 Some(p.node.nam.clone())
4320 } else {
4321 None
4322 }
4323 });
4324 {
4325 if let Some(name) = hashed_name {
4326 let past_bracket = past_close;
4327
4328 // ---------------------------------------------------------
4329 // c:1391-1483 — the subscript flag block. C parses it inside
4330 // `getarg`, but this port's `getarg` has no `Value` parameter
4331 // (params.rs `getarg`: `(idx, arr, assoc, scalar)`) so it can
4332 // neither read nor set `v->scanflags`, which is what selects
4333 // KEYS-vs-VALUES output for the scan. Parse it here, where the
4334 // Value is in hand, exactly as C's switch does: the switch is
4335 // SEQUENTIAL and every direction arm RESETS the others, so only
4336 // the LAST direction letter survives (`(ir)` matches values,
4337 // `(ri)` matches keys).
4338 // ---------------------------------------------------------
4339 let mut rev = false; // c:1394 — any search flag seen
4340 let mut ind = false; // c:1412/1416 — i/I: match the KEY
4341 let mut down = false; // c:1397 — R/I/K: every match, not the first
4342 let mut keymatch = false; // c:1400/1405 — k/K: the stored KEY is the pattern
4343 let mut quote_arg = false; // c:1449 — (e): literal compare, no glob
4344 let mut num: i64 = 1; // c:1430 — (n<D>NUM<D>): Nth match
4345 let mut flag_err = false; // c:1477 — unknown flag
4346 let mut pat: &str = body;
4347
4348 if body.starts_with('(') {
4349 let bytes = body.as_bytes();
4350 let mut i = 1usize;
4351 while i < bytes.len() && bytes[i] != b')' {
4352 match bytes[i] as char {
4353 'r' => {
4354 // c:1394-1396
4355 rev = true;
4356 keymatch = false;
4357 down = false;
4358 ind = false;
4359 }
4360 'R' => {
4361 // c:1397-1399
4362 rev = true;
4363 down = true;
4364 keymatch = false;
4365 ind = false;
4366 }
4367 'k' => {
4368 // c:1400-1404 — `keymatch = ishash` (always true here)
4369 keymatch = true;
4370 rev = true;
4371 down = false;
4372 ind = false;
4373 }
4374 'K' => {
4375 // c:1405-1410
4376 keymatch = true;
4377 rev = true;
4378 down = true;
4379 ind = false;
4380 }
4381 'i' => {
4382 // c:1411-1414
4383 rev = true;
4384 ind = true;
4385 down = false;
4386 keymatch = false;
4387 }
4388 'I' => {
4389 // c:1415-1418
4390 rev = true;
4391 ind = true;
4392 down = true;
4393 keymatch = false;
4394 }
4395 'e' => quote_arg = true, // c:1449-1451
4396 'w' | 'f' | 'p' => {
4397 // c:1419-1428/1448 — `word`/`sep`/`escapes`. Word
4398 // mode only applies to a SCALAR value (c:1601
4399 // `if (word && !v->scanflags)`), which a PM_HASHED
4400 // Value never is, so accept and ignore rather than
4401 // taking C's `flagerr` path.
4402 }
4403 'n' | 'b' | 's' => {
4404 // c:1429-1447/1471 — `get_strarg`'s balanced
4405 // `<DELIM>ARG<DELIM>` pair, used by `n` (Nth
4406 // match), `b` (begin offset) and `s` (word
4407 // separator). `b`/`s` only steer the NON-scan
4408 // array/scalar walks (c:1601-1760), which a hash
4409 // scan never reaches. `n`'s SIGN does matter
4410 // (c:1487-1490 flips `down`), so parse that one.
4411 let is_n = bytes[i] == b'n';
4412 if i + 1 >= bytes.len() {
4413 flag_err = true;
4414 break;
4415 }
4416 let delim = bytes[i + 1];
4417 let arg_start = i + 2;
4418 let mut arg_end = arg_start;
4419 while arg_end < bytes.len() && bytes[arg_end] != delim {
4420 arg_end += 1;
4421 }
4422 if arg_end >= bytes.len() {
4423 flag_err = true;
4424 break;
4425 }
4426 if is_n {
4427 // c:1459 — `num = mathevalarg(...); if (!num) num = 1;`
4428 num = body[arg_start..arg_end].trim().parse().unwrap_or(1);
4429 if num == 0 {
4430 num = 1;
4431 }
4432 }
4433 i = arg_end;
4434 }
4435 _ => {
4436 // c:1476-1483 — `flagerr:` resets every flag and
4437 // rewinds, so the whole subscript is re-read as
4438 // plain key text.
4439 flag_err = true;
4440 break;
4441 }
4442 }
4443 i += 1;
4444 }
4445 if !flag_err && i < bytes.len() && bytes[i] == b')' {
4446 pat = &body[i + 1..]; // c:1485 `if (s != *str) s++;`
4447 } else {
4448 flag_err = true;
4449 }
4450 }
4451 if flag_err {
4452 // c:1478-1482 — `num = 1; word = rev = ind = down = keymatch = 0;`
4453 rev = false;
4454 ind = false;
4455 down = false;
4456 keymatch = false;
4457 quote_arg = false;
4458 num = 1;
4459 pat = body;
4460 }
4461 // c:1487-1490 — `if (num < 0) { down = !down; num = -num; }`
4462 if num < 0 {
4463 down = !down;
4464 }
4465
4466 if !rev {
4467 // c:1575-1595 — no search flag: the subscript is an exact KEY.
4468 // C rebinds `v->pm` to the ELEMENT's own Param
4469 // (`ht->getnode(ht, s)`), or on a miss to a fresh
4470 // `PM_SCALAR|PM_UNSET` one, then leaves the whole-value
4471 // bounds. That element type is what makes the read collapse:
4472 // `getvaluearr` has no arm for PM_SCALAR (c:719) and
4473 // `getarrvalue`'s `v->start == 0 && v->end == -1` early-out
4474 // (c:2565) hands the empty result straight back, while
4475 // `IS_UNSET_VALUE` (c:472-474) catches the miss. Materialise
4476 // the same element node here — this port keeps assoc values as
4477 // plain strings, so build the `PM_SCALAR` node C's hash
4478 // carries (c:4156-4166).
4479 // c:1585 — `if (!(v->pm = (Param) ht->getnode(ht, s)))`. C
4480 // resolves an exact key with ONE hash-node lookup; it never
4481 // enumerates the table. For a PM_HASHED SPECIAL (`history`,
4482 // `functions`, `commands`, `parameters`, … — the partab[] rows
4483 // at Src/Modules/parameter.c:2235+) `ht->getnode` is that
4484 // row's `getfn`, e.g. `getpmhistory` (c:Src/Modules/
4485 // parameter.c:1156), which is a single `quietgethist`.
4486 // Routing this through `assoc_get` instead materialized the
4487 // WHOLE association to read one key: on `$history[$num]` that
4488 // meant paging the entire HISTFILE into the ring and building
4489 // a 574k-entry map for one event, seconds per completion.
4490 // Only the magic rows change path here — a plain assoc still
4491 // reads through `assoc_get`, since its values live in
4492 // `paramtab_hashed_storage` and carry no getfn.
4493 // c:570-575 — nameref deref before the read (what `assoc_get`
4494 // does on its own way in).
4495 let hname = match resolve_nameref_name(&name, None) {
4496 nameref_resolution::Target { name: t_, .. } => t_,
4497 _ => name.clone(),
4498 };
4499 let entry = if paramtab_hashed_storage()
4500 .lock()
4501 .map_or(true, |st| st.contains_key(hname.as_str()))
4502 {
4503 crate::ported::subst::assoc_get(&hname).and_then(|m| m.get(pat).cloned())
4504 } else {
4505 crate::vm_helper::partab_get(&hname, pat) // c:1585 ht->getnode
4506 };
4507 let mut elem = param::default();
4508 elem.node.nam = pat.to_string();
4509 elem.node.flags = PM_SCALAR as i32;
4510 match entry {
4511 Some(val) => elem.u_str = Some(val), // c:1585
4512 None => elem.node.flags |= PM_UNSET as i32, // c:1588
4513 }
4514 v.pm = Some(Box::new(elem));
4515 v.scanflags = 0; // c:1591 (`*inv` is 0 on this path)
4516 v.start = 0; // c:1592
4517 v.end = -1; // c:1594
4518 *pptr = past_bracket; // c:2164
4519 return 0; // c:2166
4520 }
4521
4522 // ---------------------------------------------------------
4523 // c:1493-1512 — resolve which SIDE of each entry is emitted.
4524 // The caller's WANTKEYS/WANTVALS win when present (that is how
4525 // `compadd -k 'h[(R)pat]'` asks for the KEYS of VALUE-matching
4526 // entries, Src/Zle/compcore.c:2030-2033); otherwise the
4527 // direction flag picks.
4528 // ---------------------------------------------------------
4529 let sf = v.scanflags as u32;
4530 if (sf & SCANPM_WANTKEYS) == 0 && (sf & SCANPM_WANTVALS) == 0 && v.scanflags != 0 {
4531 if ind {
4532 // c:1503-1505
4533 v.scanflags |= SCANPM_WANTKEYS as i32;
4534 v.scanflags &= !(SCANPM_WANTVALS as i32);
4535 } else if rev {
4536 // c:1506
4537 v.scanflags |= SCANPM_WANTVALS as i32;
4538 }
4539 if !down && keymatch {
4540 // c:1511 — `if (!down && keymatch && ishash)`
4541 v.scanflags &= !(SCANPM_MATCHMANY as i32);
4542 }
4543 }
4544
4545 // c:1564-1572 — `parsestr(&s); singsub(&s)` on the subscript
4546 // body, so a `$var` inside the pattern is expanded before it is
4547 // compiled.
4548 let mut s: String = pat.to_string();
4549 if s.contains('$') || s.contains('`') {
4550 s = crate::ported::subst::singsub(&s); // c:1571
4551 }
4552
4553 // c:1686-1708 — build the pattern program once, before the scan.
4554 // `k`/`K` leave it NULL (c:1707-1708): SCANPM_KEYMATCH compiles
4555 // the STORED KEY per node instead (c:653-659).
4556 let pprog = if keymatch {
4557 None // c:1708
4558 } else {
4559 let mut p = s.clone();
4560 if !quote_arg {
4561 crate::ported::glob::tokenize(&mut p); // c:1704
4562 }
4563 crate::ported::glob::remnulargs(&mut p); // c:1705
4564 patcompile(&p, PAT_HEAPDUP as i32, None) // c:1706
4565 };
4566
4567 // c:1710-1735 — the hash scan. `v->scanflags` is always non-zero
4568 // for a PM_HASHED value (fetchvalue c:2274-2280 promotes 0 to
4569 // SCANPM_ARRONLY), so C's `else` array-walk arm is unreachable
4570 // from here.
4571 if !keymatch && pprog.is_none() {
4572 // c:1717-1718 — `if (!pprog) return 1;` aborts before the
4573 // scan, leaving `v->arr` NULL. An uncompilable pattern can
4574 // match nothing; report it the way the exact-key miss above
4575 // does so no consumer re-derives a full-table scan.
4576 *pptr = past_bracket;
4577 v.pm = None;
4578 return 0;
4579 }
4580 // c:1712-1713 — `scanprog = pprog; scanstr = s;`. This port's
4581 // statics hold the pattern TEXT (`scanparamvals` compiles it),
4582 // so store `s` and leave SCANPROG unset for the keymatch case
4583 // exactly as C leaves `pprog` NULL there.
4584 *scanprog_lock().lock().unwrap() = if keymatch { None } else { Some(s.clone()) };
4585 *scanstr_lock().lock().unwrap() = Some(s);
4586 if keymatch {
4587 v.scanflags |= SCANPM_KEYMATCH as i32; // c:1715
4588 } else if ind {
4589 v.scanflags |= SCANPM_MATCHKEY as i32; // c:1720
4590 } else {
4591 v.scanflags |= SCANPM_MATCHVAL as i32; // c:1722
4592 }
4593 if down {
4594 v.scanflags |= SCANPM_MATCHMANY as i32; // c:1725
4595 }
4596 // c:1726 — `ta = getvaluearr(v)` runs the scan and leaves the
4597 // result in `v->arr` with `v->start`/`v->end` covering all of it
4598 // (c:716-717), which is what C's getindex tail (c:2151-2153)
4599 // then re-derives from getarg's `r`/`*w`.
4600 let _ = getvaluearr(Some(&mut *v)); // c:1726
4601 *scanprog_lock().lock().unwrap() = None; // c:1732
4602 *scanstr_lock().lock().unwrap() = None;
4603
4604 // ---------------------------------------------------------
4605 // c:2128-2160 — getindex's non-inverse tail. C runs it on
4606 // every subscript; this port used to return straight out of
4607 // the hash scan and skip it, so `v->scanflags` was never
4608 // cleared and `assignstrvalue`'s c:2701 guard could not tell
4609 // "the search matched, this is a slice" from "the search
4610 // matched nothing".
4611 //
4612 // The two numbers C feeds it come from `getarg`:
4613 // `start` is getarg's return — 1 on both hash exits, the
4614 // `return 1` at c:1754 (scan produced a result) and the
4615 // `return !down` at c:1760 (it did not, and !down is 1
4616 // whenever that line is reachable: a `down` scan always
4617 // satisfies c:1747 through SCANPM_MATCHMANY and leaves by
4618 // c:1754).
4619 // `we` is `*w`, assigned ONLY on the c:1754 exit as
4620 // `v->end`, which `getvaluearr` has just set to
4621 // `numparamvals + 1` (c:717). On the c:1760 exit it keeps
4622 // the caller's 0.
4623 // ---------------------------------------------------------
4624 let sf = v.scanflags as u32; // c:2175
4625 let start: i32 = 1; // c:2036 (getarg → c:1754 / c:1760)
4626 // c:1747-1750 — `if ((ta = getvaluearr(v)) && (*ta ||
4627 // ((v->scanflags & SCANPM_MATCHMANY) && (v->scanflags &
4628 // (SCANPM_MATCHKEY|SCANPM_MATCHVAL|SCANPM_KEYMATCH)))))`.
4629 let we: i32 = if !v.arr.is_empty()
4630 || ((sf & SCANPM_MATCHMANY) != 0
4631 && (sf & (SCANPM_MATCHKEY | SCANPM_MATCHVAL | SCANPM_KEYMATCH)) != 0)
4632 {
4633 v.end // c:1752 `*w = v->end;`
4634 } else {
4635 0
4636 };
4637 // c:2131 — `com = (*s == ',')`. Never true for a hash: the
4638 // c:1515 walk does not stop at a comma when `ishash`.
4639 let mut com = false; // c:2131
4640 let end: i32 = if we != 0 { we } else { start }; // c:2135
4641 if start != end {
4642 com = true; // c:2137-2138
4643 }
4644 // c:2144-2145 — `if (start > 0) start -= startprevlen;` with the
4645 // initial `*prevcharlen = 1` (c:1405): the hash exits never
4646 // reach c:1650's character walk.
4647 let start = start - 1;
4648 if v.scanflags != 0
4649 && !com
4650 && ((v.scanflags as u32 & SCANPM_MATCHMANY) == 0
4651 || (v.scanflags as u32 & (SCANPM_MATCHKEY | SCANPM_MATCHVAL | SCANPM_KEYMATCH))
4652 == 0)
4653 {
4654 v.scanflags = 0; // c:2179
4655 }
4656 v.start = start; // c:2180
4657 v.end = end; // c:2181
4658 *pptr = past_bracket; // c:2164
4659 return 0; // c:2166
4660 }
4661 }
4662 if body.starts_with('(') {
4663 let pm_is_array =
4664 v.pm.as_ref()
4665 .map_or(false, |p| PM_TYPE(p.node.flags as u32) == PM_ARRAY);
4666 if pm_is_array {
4667 let flag_part = &body[1..body.find(')').unwrap_or(1)];
4668 let return_index = flag_part.contains('i') || flag_part.contains('I');
4669 if return_index {
4670 let arr: Vec<String> = v.pm.as_ref().map(|p| arrgetfn(p)).unwrap_or_default();
4671 if let Some(getarg_out::Value(val)) = getarg(body, Some(&arr), None, None) {
4672 if let Ok(idx) = val.to_str().parse::<i64>() {
4673 // c:2110-2114 — inv branch single-element access.
4674 v.valflags |= VALFLAG_INV;
4675 v.scanflags = 0;
4676 v.start = idx as i32;
4677 v.end = (idx + 1) as i32;
4678 *pptr = past_close;
4679 return 0;
4680 }
4681 }
4682 } else if flag_part.contains('r')
4683 || flag_part.contains('R')
4684 || flag_part.contains('k')
4685 || flag_part.contains('K')
4686 {
4687 // c:1736-1760 — the ARRAY search arm (`ishash` false, so
4688 // c:1711's hash scan is skipped and `ta = getarrvalue(v)` is
4689 // walked directly). C's `getarg` returns the 1-based INDEX
4690 // `r` of the match and getindex's tail (c:2151-2153) turns it
4691 // into `start`/`end`; this port's `getarg` has no Value
4692 // out-param and hands back the matched ELEMENT(S) instead, so
4693 // put them in `v->arr` — the same cache `getvaluearr` reads
4694 // first (c:711-712) — and set the bounds to cover exactly
4695 // them. `k`/`K` degrade to `r`/`R` here because C gates
4696 // `keymatch = ishash` (c:1401/1406).
4697 let arr: Vec<String> = v.pm.as_ref().map(|p| arrgetfn(p)).unwrap_or_default();
4698 if let Some(getarg_out::Value(val)) = getarg(body, Some(&arr), None, None) {
4699 let hits: Vec<String> = match val {
4700 // c:1724-1734 — the MATCHMANY forms are array-shaped.
4701 Value::Array(parts) => parts.iter().map(|p| p.to_str()).collect(),
4702 other => {
4703 // c:1758 — a miss returns index 0, i.e. no
4704 // element; this port signals it with "".
4705 let s = other.to_str();
4706 if s.is_empty() {
4707 Vec::new()
4708 } else {
4709 vec![s]
4710 }
4711 }
4712 };
4713 // c:1394-1399 — the LAST direction letter decides `down`
4714 // (every arm resets the others); `k`/`K` act as `r`/`R`
4715 // on an array (c:1401/1406 `keymatch = ishash`).
4716 let down = flag_part
4717 .chars()
4718 .rev()
4719 .find(|c| matches!(c, 'r' | 'R' | 'k' | 'K'))
4720 .is_some_and(|c| c.is_ascii_uppercase());
4721 if hits.is_empty() && down {
4722 // c:1758-1760 / c:1782 — a reverse search that matches
4723 // nothing returns index 0, so getindex's tail sees
4724 // `start == 0 && end == 0` (c:2126) and the
4725 // zero-subscript rule decides what that means:
4726 if crate::ported::zsh_h::isset(crate::ported::zsh_h::KSHZEROSUBSCRIPT) {
4727 // c:2134-2140 — "Treat this as accessing the
4728 // first element of the array."
4729 v.start = 0;
4730 v.end = 1; // c:2140 `end = startnextlen`
4731 } else {
4732 // c:2141-2150 — strict: an empty range.
4733 v.valflags |= VALFLAG_EMPTY; // c:2147
4734 v.start = -1; // c:2148
4735 v.end = 0;
4736 }
4737 *pptr = past_close; // c:2164
4738 return 0; // c:2166
4739 }
4740 v.start = 0; // c:2144-2145 (getarg's 1, less startprevlen)
4741 v.end = hits.len() as i32; // c:2135
4742 v.arr = hits;
4743 *pptr = past_close; // c:2164
4744 return 0; // c:2166
4745 }
4746 }
4747 }
4748 }
4749 // c:2035-2040 — general path: getarg() would parse the start
4750 // index. The Rust `getarg` has a different signature (flag
4751 // dispatcher returning getarg_out, not C's char**+int*+zlong
4752 // out-params), so the bracket-subscript here inline-parses
4753 // the simple cases: `N`, `N,M`, `-N`. Flag-based subscripts
4754 // (`[(I)pat]`, `[(r)val]`) still route through getarg
4755 // separately when called by the substitution pipeline.
4756
4757 let (start_str, end_str) = match body.split_once(',') {
4758 Some((a, b)) => (a, Some(b)),
4759 None => (body, None),
4760 };
4761 // c:Src/params.c:1597 — `r = mathevalarg(s, &s);`. A non-hashed
4762 // subscript operand is an ARITHMETIC expression, not a decimal
4763 // literal: `a[i]`, `a[1+1]`, `a[$#x]` all reach getarg's numeric
4764 // arm and are handed to `mathevalarg`. This port bailed out with
4765 // `return 0` (leaving `v` untouched) whenever `str::parse` failed,
4766 // which silently swallowed BOTH of C's outcomes — an undefined
4767 // name evaluating to 0 (c:Src/math.c:1534) and the empty operand
4768 // that `mathevalarg` rejects outright with `zerr("bad math
4769 // expression: empty string")` (c:Src/math.c:1530-1532). Keep the
4770 // decimal fast path (matheval of "5" is 5) and route everything
4771 // else through the canonical evaluator so both outcomes survive.
4772 let evalarg = |t: &str| -> i64 {
4773 let r = match t.parse::<i64>() {
4774 Ok(n) => n, // fast path; identical to mathevalarg on a decimal literal
4775 Err(_) => crate::ported::math::mathevalarg(t), // c:1618
4776 };
4777 // c:1619-1620 — `if (isset(KSHARRAYS) && r >= 0) r++;`. getarg
4778 // runs this for both the start (a2=0) and the `,end` (a2=1) operand.
4779 if crate::ported::zsh_h::isset(KSHARRAYS) && r >= 0 {
4780 r + 1
4781 } else {
4782 r
4783 }
4784 };
4785 let start: i64 = evalarg(start_str);
4786 let mut end: i64 = match end_str {
4787 // c:2112 — `end = getarg(&s, &inv, v, 1, ...)`, i.e. the same
4788 // arithmetic evaluation for the range's second operand.
4789 Some(s) => evalarg(s),
4790 // c:2114 — `end = we ? we : start;`
4791 None => start,
4792 };
4793
4794 let mut start = start;
4795 let mut com = end_str.is_some() || start != end; // c:2131-2138
4796
4797 // c:2144-2145 — `if (start > 0) start -= startprevlen;`. getarg
4798 // returned the 1-based position just PAST the start character
4799 // (c:1650-1655 walks `r` characters); `startprevlen` is that last
4800 // character's length, so this turns the position into the 0-based
4801 // offset every consumer (getstrvalue c:2362/c:2527, setstrvalue,
4802 // glob's `first`) indexes with. This port subscripts by character,
4803 // where that length is always one — including for an array, whose
4804 // getarg leaves `*prevcharlen` at its initial 1 (c:1405).
4805 if start > 0 {
4806 start -= 1; // c:2145
4807 } else if start == 0 && end == 0 {
4808 // c:2126
4809 // c:2134 — `if (isset(KSHZEROSUBSCRIPT))` non-strict mode.
4810 // Treats `a[0]` as the first element (end = startnextlen,
4811 // which is 1 for ASCII). c:2141-2150 strict mode keeps the
4812 // VALFLAG_EMPTY + start=-1 sentinel for empty access.
4813 if crate::ported::zsh_h::isset(crate::ported::zsh_h::KSHZEROSUBSCRIPT) {
4814 end = 1; // c:2140 — `end = startnextlen` (1 for ASCII)
4815 } else {
4816 v.valflags |= VALFLAG_EMPTY; // c:2147
4817 start = -1; // c:2148
4818 com = true; // c:2170
4819 }
4820 }
4821 // c:2156-2158 — clear scanflags for non-comma simple subscript
4822 // when match flags absent.
4823 if v.scanflags != 0
4824 && !com
4825 && (v.scanflags as u32 & SCANPM_MATCHMANY == 0
4826 || v.scanflags as u32 & (SCANPM_MATCHKEY | SCANPM_MATCHVAL | SCANPM_KEYMATCH) == 0)
4827 {
4828 v.scanflags = 0;
4829 }
4830 let _ = (SCANPM_ISVAR_AT, SCANPM_WANTINDEX, VALFLAG_INV);
4831 v.start = start as i32; // c:2159
4832 v.end = end as i32; // c:2160
4833
4834 // c:2164-2165 — advance `*pptr` past the close bracket.
4835 *pptr = past_close;
4836 0 // c:2166
4837}
4838
4839/// Port of `getvalue()` from `Src/params.c:2173`. C body:
4840/// `return fetchvalue(v, pptr, bracks, SCANPM_CHECKING);` — pure
4841/// wrapper around `fetchvalue` with the SCANPM_CHECKING flag set
4842/// so unset params don't trigger creation.
4843pub fn getvalue<'a>(
4844 v: Option<&'a mut value>,
4845 pptr: &mut &str,
4846 bracks: i32,
4847) -> Option<&'a mut value> {
4848 fetchvalue(v, pptr, bracks, SCANPM_CHECKING as i32)
4849}
4850
4851/// Port of `fetchvalue()` from `Src/params.c:2180`.
4852///
4853/// Walks the parameter expression starting at `*pptr`, consuming
4854/// the identifier (or special-char like `?`/`#`/`$`/`!`/`@`/`*`/
4855/// `-`) and updating `*pptr` to point past the name. Looks up the
4856/// param in paramtab and populates the Value's pm/start/end/
4857/// scanflags fields.
4858///
4859/// Identifier, special-char and digit names are parsed and looked up.
4860/// Nameref resolution (the `getnode` vs `getnode2` choice on
4861/// SCANPM_NONAMEREF, c:2227-2236), bracket subscripts (`getindex` at
4862/// c:2288) and the SCANPM_ARRONLY scanflags promotion for hash/array
4863/// params are handled. C's fetchvalue has no separate nameref-slice
4864/// path; `getparamnode` (c:570-575) resolves references through
4865/// `resolve_nameref`.
4866pub fn fetchvalue<'a>(
4867 // c:2180
4868 v: Option<&'a mut value>,
4869 pptr: &mut &str,
4870 bracks: i32,
4871 scanflags: i32,
4872) -> Option<&'a mut value> {
4873 let s = *pptr;
4874 let bytes = s.as_bytes();
4875 if bytes.is_empty() {
4876 return None; // c:2214 fall-through
4877 }
4878 let c = bytes[0];
4879 let mut ppar: i32 = 0;
4880 let mut end_pos = 0usize;
4881
4882 if c.is_ascii_digit() {
4883 // c:2190
4884 // c:2191-2194 — zstrtol parse of positional parameter index.
4885 if bracks >= 0 {
4886 let mut idx = 0;
4887 while idx < bytes.len() && bytes[idx].is_ascii_digit() {
4888 ppar = ppar * 10 + (bytes[idx] - b'0') as i32;
4889 idx += 1;
4890 }
4891 end_pos = idx;
4892 } else {
4893 // c:2194 — single-digit positional ($0..$9 short form).
4894 ppar = (c - b'0') as i32;
4895 end_pos = 1;
4896 }
4897 } else if itype_end(s, crate::ported::ztype_h::IIDENT as u32, false) > 0 {
4898 // c:2196 — `itype_end(s, IIDENT, 0)` — walk identifier chars.
4899 end_pos = itype_end(s, crate::ported::ztype_h::IIDENT as u32, false);
4900 } else if matches!(c, b'?' | b'#' | b'$' | b'!' | b'@' | b'*' | b'-') {
4901 // c:2198-2210
4902 end_pos = 1;
4903 } else {
4904 return None; // c:2213
4905 }
4906
4907 let name = &s[..end_pos];
4908 *pptr = &s[end_pos..];
4909
4910 if ppar > 0 {
4911 // c:2217-2225 positional
4912 if let Some(v) = v {
4913 *v = value {
4914 pm: None,
4915 arr: Vec::new(),
4916 scanflags: 0,
4917 valflags: 0,
4918 start: ppar - 1,
4919 end: ppar,
4920 };
4921 return Some(v);
4922 }
4923 return None;
4924 }
4925
4926 // c:2227-2236 — paramtab lookup honouring SCANPM_NONAMEREF for
4927 // getnode vs getnode2 (the second skips nameref resolution).
4928 let pm = {
4929 let tab = paramtab().read().unwrap();
4930 let key = if name == "0" { "0" } else { name };
4931 tab.get(key).cloned()
4932 };
4933 // c:2227 — the lookup is `paramtab->getnode`, i.e. getparamnode
4934 // (c:570-572), which runs loadparamnode on a PM_AUTOLOAD stub so the
4935 // first fetch of `zmodload -ap MOD NAME`'s NAME loads MOD (or reports
4936 // "autoloading module MOD failed to define parameter: NAME").
4937 // loadparamnode does not read its table argument (zshrs has one
4938 // paramtab), so the stub-only branch hands it a fresh empty one.
4939 let pm = match pm {
4940 Some(p) if (p.node.flags as u32 & PM_AUTOLOAD) != 0 => newparamtable(0, "paramtab")
4941 .and_then(|ht| loadparamnode(&ht, Some(p), name)),
4942 other => other,
4943 };
4944 let pm = pm?; // c:2237-2241
4945
4946 // c:2241-2243 — `if (PM_UNSET && !PM_DECLARED) return NULL`.
4947 if pm.node.flags & PM_UNSET as i32 != 0 && pm.node.flags & PM_DECLARED as i32 == 0 {
4948 return None;
4949 }
4950
4951 // c:2246-2270 — nameref deref. Partially handled: we route
4952 // through resolve_nameref if PM_NAMEREF is set and the caller
4953 // didn't pass SCANPM_NONAMEREF.
4954 let pm = if pm.node.flags & PM_NAMEREF as i32 != 0 && (scanflags as u32) & SCANPM_NONAMEREF == 0
4955 {
4956 resolve_nameref(Some(pm))?
4957 } else {
4958 pm
4959 };
4960
4961 // c:430 `IPDEF9("argv", &pparams, NULL, 0)` — the positional alias's
4962 // array getter is `arrvargetfn` (c:4231-4236), which dereferences the
4963 // LIVE `pparams`. zshrs keeps that vector in `builtin::PPARAMS` and the
4964 // node's `u_arr` is stale, so every getindex/getarg/getstrvalue reader
4965 // of this Value (they all go through `arrgetfn`) saw the wrong words:
4966 // `(( argv[(i)-] ))` answered 1 where zsh answers 2.
4967 let mut pm = pm;
4968 if matches!(name, "argv" | "@" | "*") {
4969 pm.u_arr = Some(PPARAMS.lock().map(|p| p.clone()).unwrap_or_default());
4970 }
4971
4972 if let Some(v) = v {
4973 // c:2274-2282 — populate Value from pm.
4974 *v = value {
4975 pm: Some(pm.clone()),
4976 arr: Vec::new(),
4977 scanflags: 0,
4978 valflags: 0,
4979 start: 0,
4980 end: -1,
4981 };
4982 let pmflags = pm.node.flags;
4983 let isvar_at = name == "@";
4984 if PM_TYPE(pmflags as u32) & (PM_ARRAY | PM_HASHED) != 0 {
4985 // c:2274-2280 — scanflags overload for hashed arrays.
4986 let mut sf = scanflags;
4987 if isvar_at {
4988 sf |= SCANPM_ISVAR_AT as i32;
4989 }
4990 if sf == 0 {
4991 sf = SCANPM_ARRONLY as i32;
4992 }
4993 v.scanflags = sf;
4994 }
4995 // c:2289-2293 — bracket-subscript dispatch. When the unparsed
4996 // remainder starts with `[` (or the lexer's `Inbrack` token),
4997 // hand off to `getindex` which fills `v.start`/`v.end`/
4998 // `v.scanflags` and advances `pptr`.
4999 if bracks > 0 && (pptr.starts_with('[') || pptr.starts_with(Inbrack)) {
5000 if getindex(pptr, v, scanflags) != 0 {
5001 // c:2290
5002 return Some(v); // c:2292
5003 }
5004 } else if (scanflags & SCANPM_ASSIGNING as i32) == 0
5005 && v.scanflags != 0
5006 // c:2288 `itype_end(t, INAMESPC, 1) != t` — the clamp needs an identifier
5007 // name; `$@` / `$*` are not one, so `"${*:-x}"` keeps every word.
5008 && name.chars().next().is_some_and(|c| c.is_alphabetic() || c == '_')
5009 && isset(KSHARRAYS)
5010 {
5011 // c:2294-2296 — KSHARRAYS implicit `[0]` for bare arr.
5012 v.end = 1;
5013 v.scanflags = 0;
5014 } else {
5015 }
5016 return Some(v);
5017 }
5018 None
5019}
5020
5021/// Port of `getstrvalue()` from `Src/params.c:2335`.
5022/// Full C body dispatches on `PM_TYPE(v->pm->node.flags)`:
5023/// PM_HASHED (KSH path: `[0]` index lookup), PM_ARRAY (sepjoin
5024/// when v->scanflags else `ss[v->start]`), PM_INTEGER (`convbase`),
5025/// PM_EFLOAT|PM_FFLOAT (`convfloat`), PM_SCALAR|PM_NAMEREF
5026/// (`pm->gsu.s->getfn(pm)`). Then PM_LEFT/PM_RIGHT_B/PM_RIGHT_Z
5027/// padding when VALFLAG_SUBST is set.
5028pub fn getstrvalue(v: Option<&mut value>) -> String {
5029 let v = match v {
5030 Some(v) => v,
5031 None => return String::new(),
5032 };
5033 // c:2344-2348 — `if (VALFLAG_INV && !PM_HASHED) return sprintf("%d", v->start)`.
5034 if (v.valflags & VALFLAG_INV) != 0 {
5035 let hashed =
5036 v.pm.as_ref()
5037 .map(|p| (p.node.flags as u32 & PM_HASHED) != 0)
5038 .unwrap_or(false);
5039 if !hashed {
5040 return v.start.to_string();
5041 }
5042 }
5043 let pm = match v.pm.as_mut() {
5044 Some(p) => p,
5045 None => return String::new(),
5046 };
5047 let t = PM_TYPE(pm.node.flags as u32);
5048 let pmflags = pm.node.flags as u32;
5049
5050 // c:2350-2370 — PM_TYPE dispatch.
5051 let mut s: String = if t == PM_HASHED && v.scanflags == 0 && EMULATION(EMULATE_KSH) {
5052 // c:Src/params.c:2351-2358 — `case PM_HASHED: if (!v->scanflags &&
5053 // EMULATION(EMULATE_KSH))` — a bare `$assoc` (no subscript) under
5054 // KSH EMULATION is `${assoc[0]}`: it builds `s = "[0]"`, does a
5055 // KEY-"0" `getindex`, and returns that element's value. So it is
5056 // EMPTY unless the hash actually holds a key "0". This is
5057 // emulation-gated, NOT KSHARRAYS-option-gated: `emulate -L ksh;
5058 // typeset -A h=(a 1 b 2); print $h` is empty, whereas `setopt
5059 // ksharrays; …; print $h` falls through below to the first bucket
5060 // value `1`. (`setopt ksharrays` keeps v->scanflags non-zero for a
5061 // bare assoc, so it never reaches this arm; the `scanflags==0`
5062 // guard mirrors the C `!v->scanflags` "impossible unless emulating
5063 // ksh" invariant.)
5064 paramtab_hashed_storage()
5065 .lock()
5066 .ok()
5067 .and_then(|store| store.get(&pm.node.nam).and_then(|m| m.get("0").cloned()))
5068 .unwrap_or_default()
5069 } else if t == PM_HASHED || t == PM_ARRAY {
5070 // c:2359-2369 (PM_ARRAY, and the ksh-emulation PM_HASHED fall-through).
5071 // Both arms end in `return s;` (c:2369): an element is neither
5072 // padded nor cut by the scalar tail below.
5073 let arr = arrgetfn(pm);
5074 if v.scanflags != 0 {
5075 // c:2361
5076 return arr.join(" ");
5077 } else {
5078 let mut start = v.start;
5079 if start < 0 {
5080 start += arr.len() as i32;
5081 } // c:2364
5082 if start < 0 || (start as usize) >= arr.len() {
5083 // c:2365-2366
5084 return String::new();
5085 } else {
5086 return arr[start as usize].clone();
5087 }
5088 }
5089 } else if t == PM_INTEGER {
5090 // c:2371
5091 // c:2373 — `convbase(buf, pm->gsu.i->getfn(pm), pm->base)`.
5092 // The previous Rust port used `intgetfn(pm).to_string()` (naked
5093 // base-10). With `convbase` now ported (params.rs:6577), honor
5094 // `pm.base` so `typeset -i 16 x=255` renders as `0xff` rather
5095 // than `255` per zsh's `$x`-expansion + `typeset -p`.
5096 // !!! POSIX-FAITHFUL GATE !!! zsh gives integer variables a per-var
5097 // output base (`(( z=0xff ))` → `typeset -i16 z=255` → `$z` prints
5098 // `16#FF`). bash/ksh/POSIX have no such feature: integers always
5099 // print in decimal. Under `--sh`/`--ksh`/`--dash`/`--bash`
5100 // (posix_faithful) force base 10 so `(( z=0xff )); echo $z` → `255`,
5101 // matching the real shell. `--... --zsh` keeps zsh's based display.
5102 let base = if pm.base > 0 && !crate::dash_mode::posix_faithful() {
5103 pm.base // c:2373 pm->base
5104 } else {
5105 10
5106 };
5107 convbase_underscore(
5108 intgetfn(pm),
5109 base,
5110 pm.width, // c:2373 pm->width for underscore grouping
5111 )
5112 } else if t == PM_EFLOAT || t == PM_FFLOAT {
5113 // c:2366-2370 — `convfloat(getfn(pm), pm->base, pm->node.flags,
5114 // NULL)`. The format flag (PM_EFLOAT → e-notation, PM_FFLOAT →
5115 // fixed) and the precision (`pm->base` holds digits for floats)
5116 // BOTH come straight from the param. The previous Rust port used
5117 // `convfloat_underscore(floatgetfn(pm), pm.width)`, which drops
5118 // the format flag (convfloat_underscore calls `convfloat(d,0,0)`
5119 // → 'g' default) and wrongly applies width grouping that C does
5120 // NOT do here (underscore grouping happens in typeset -p output
5121 // c:2866, not getstrvalue). That made getstrvalue render a
5122 // default `float f=2.5` as "2.5" instead of "2.500000000e+00",
5123 // surfacing when `assignaparam`'s AUGMENT prepend (c:3350) routes
5124 // an old float value through getstrvalue.
5125 convfloat(floatgetfn(pm), pm.base, pm.node.flags as u32)
5126 } else if t == PM_SCALAR || t == PM_NAMEREF {
5127 // c:2380
5128 strgetfn(pm)
5129 } else {
5130 // c:2384
5131 DPUTS!(true, "BUG: param node without valid type"); // c:2385
5132 String::new() // c:2386 s = "" (line c:2384)
5133 };
5134
5135 // c:2390-2538 — VALFLAG_SUBST padding (PM_LEFT / PM_RIGHT_B /
5136 // PM_RIGHT_Z). Multibyte is approximated via `chars().count()`
5137 // (codepoint count) since the Rust port stores strings as
5138 // UTF-8 rather than the C meta-byte encoding.
5139 if v.valflags & VALFLAG_SUBST != 0 {
5140 let pad_flags = pmflags & (PM_LEFT | PM_RIGHT_B | PM_RIGHT_Z);
5141 if pad_flags != 0 {
5142 let fwidth = if pm.width > 0 {
5143 pm.width as usize
5144 } else {
5145 s.chars().count()
5146 };
5147 if pad_flags == PM_LEFT || pad_flags == (PM_LEFT | PM_RIGHT_Z) {
5148 // c:2393-2424 — left-justify: optional zero/blank trim,
5149 // truncate to fwidth, right-pad with spaces.
5150 let trimmed: &str = if pad_flags & PM_RIGHT_Z != 0 {
5151 s.trim_start_matches('0')
5152 } else {
5153 s.trim_start_matches(|c: char| c == ' ' || c == '\t')
5154 };
5155 let len = trimmed.chars().count();
5156 let take = len.min(fwidth);
5157 let mut out: String = trimmed.chars().take(take).collect();
5158 if fwidth > take {
5159 out.extend(std::iter::repeat(' ').take(fwidth - take));
5160 }
5161 s = out;
5162 } else if pad_flags & (PM_RIGHT_B | PM_RIGHT_Z) != 0 {
5163 // c:2426-2510 — right-justify with optional zero-padding
5164 // honouring leading-blank/minus/0x/base# prefix
5165 // detection for numeric values.
5166 let charlen = s.chars().count();
5167 if charlen < fwidth {
5168 let mut zero = true;
5169 let mut valprefend: usize = 0;
5170 let numeric_pm = (pmflags & (PM_INTEGER | PM_EFLOAT | PM_FFLOAT)) != 0;
5171 if pad_flags & PM_RIGHT_Z != 0 {
5172 // c:2446-2466 — find the prefix to keep
5173 // (blanks → minus → 0x / base#).
5174 let bytes = s.as_bytes();
5175 let mut t = 0usize;
5176 while t < bytes.len() && (bytes[t] == b' ' || bytes[t] == b'\t') {
5177 t += 1; // c:2446-2447
5178 }
5179 if numeric_pm && t < bytes.len() && bytes[t] == b'-' {
5180 t += 1; // c:2454-2455
5181 }
5182 if (pmflags & PM_INTEGER) != 0 {
5183 let cbases = optlookup("cbases") > 0;
5184 if cbases
5185 && t + 1 < bytes.len()
5186 && bytes[t] == b'0'
5187 && bytes[t + 1] == b'x'
5188 {
5189 t += 2; // c:2462-2463
5190 } else if let Some(hash_off) =
5191 bytes[t..].iter().position(|&b| b == b'#')
5192 {
5193 t += hash_off + 1; // c:2464-2465
5194 }
5195 }
5196 valprefend = t;
5197 if t == bytes.len() {
5198 zero = false; // c:2468-2469
5199 } else if !numeric_pm && !bytes[t].is_ascii_digit() {
5200 zero = false; // c:2473-2474
5201 }
5202 }
5203 // c:2483 — pad char picks: ' ' if PM_RIGHT_B or
5204 // numeric-prefix detection failed, else '0'.
5205 let pad_char = if (pad_flags & PM_RIGHT_B) != 0 || !zero {
5206 ' '
5207 } else {
5208 '0'
5209 };
5210 let need = fwidth - charlen;
5211 let prefix = &s[..valprefend];
5212 let rest = &s[valprefend..];
5213 let mut out = String::with_capacity(need + s.len());
5214 out.push_str(prefix); // c:2491
5215 out.extend(std::iter::repeat(pad_char).take(need)); // c:2483-2485
5216 out.push_str(rest); // c:2492-2493
5217 s = out;
5218 } else if charlen > fwidth {
5219 // c:2496-2500 — truncate from the front to fit fwidth
5220 // codepoints (C uses MB_METACHARLEN; Rust uses chars).
5221 let skip = charlen - fwidth;
5222 s = s.chars().skip(skip).collect();
5223 }
5224 }
5225 }
5226 // c:2497-2506 — still inside the VALFLAG_SUBST block, right after
5227 // the padding switch. PM_LOWER / PM_UPPER are READ-time attributes:
5228 // storage keeps the value verbatim and only a substitution folds it.
5229 // PM_UPPER is skipped for PM_NAMEREF (the string is a target name).
5230 match pmflags & (PM_LOWER | PM_UPPER) {
5231 f if f == PM_LOWER => {
5232 s = casemodify(&s, CASMOD_LOWER); // c:2499
5233 }
5234 f if f == PM_UPPER => {
5235 if (pmflags & PM_NAMEREF) == 0 {
5236 // c:2502
5237 s = casemodify(&s, CASMOD_UPPER); // c:2503
5238 }
5239 }
5240 _ => {}
5241 }
5242 }
5243
5244 // c:2507-2508 — the whole value.
5245 if v.start == 0 && v.end == -1 {
5246 return s;
5247 }
5248
5249 // c:2510-2532 — a subscripted scalar: cut `s` to [start, end). C's
5250 // offsets are bytes of the metafied string (getarg converted the
5251 // character subscript, c:1650-1655); this port's getindex yields
5252 // character offsets, so the cut walks characters and each
5253 // MB_METACHARLEN (c:2520) is one.
5254 let chars: Vec<char> = s.chars().collect();
5255 let len = chars.len() as i32; // c:2510
5256 if v.start < 0 {
5257 // c:2511-2515
5258 v.start += len;
5259 if v.start < 0 {
5260 v.start = 0;
5261 }
5262 }
5263 if v.end < 0 {
5264 // c:2516-2522
5265 v.end += len;
5266 if v.end >= 0 && v.end < len {
5267 v.end += 1; // c:2520 `v->end += MB_METACHARLEN(eptr)`
5268 }
5269 }
5270 if v.start > len {
5271 return String::new(); // c:2524-2525
5272 }
5273 let tail = &chars[v.start as usize..]; // c:2526
5274 if v.end <= v.start {
5275 String::new() // c:2528-2529
5276 } else if v.end - v.start <= len - v.start {
5277 tail[..(v.end - v.start) as usize].iter().collect() // c:2530-2531
5278 } else {
5279 tail.iter().collect()
5280 }
5281}
5282
5283/// Slice an indexed array using zsh 1-based inclusive semantics.
5284/// Port of `getarrvalue()` from `Src/params.c:2548` — the slice
5285/// branch that resolves the start/end pair into a Vec. Negative
5286/// indices count from the end (`-1` is the last element);
5287/// out-of-range bounds collapse to empty (`${a[5,10]}` on len=3
5288/// returns empty, not clamped); `start > end` returns empty.
5289///
5290/// 0 has asymmetric meaning per C source's getarrvalue:
5291/// start=0 → "before first element" → resolved to 1
5292/// end=0 → "before first element" → empty slice
5293/// WARNING: param names don't match C — Rust=(arr, start, end) vs C=(v)
5294pub fn getarrvalue(arr: &[String], start: i64, end: i64) -> Vec<String> {
5295 let len = arr.len() as i64;
5296 // c:Src/params.c:2570-2585 getarrvalue — a slice whose START is at or
5297 // beyond the array end does NOT collapse to an empty array; it returns
5298 // EMPTY-STRING padding from the global `nular` (a one-element `{"", NULL}`
5299 // array), so the result is capped at a SINGLE empty element. The C
5300 // branch order (after `v->start += arrlen` / `v->end += arrlen + 1` for
5301 // negatives) is: `v->end <= v->start` → 0 elems; else `v->start < 0` → 1
5302 // elem; else `arrlen_le(s, v->start)` → `v->end - (v->start + 1)` elems
5303 // (capped by nular's length to ≤ 1). Only observable via `${#}` (counts
5304 // the empty element → `${#arr[1,2]}` is 1 for `arr=()`) and quoted splat;
5305 // unquoted array-capture elides the empty word. Mirror it here so the
5306 // out-of-range cases that previously returned `[]` pad correctly.
5307 let nular_pad = || -> Vec<String> {
5308 let v_start0 = start - 1; // C v->start is 0-based (subscript - 1)
5309 let v_end = if end < 0 { end + len + 1 } else { end }; // c:2565
5310 let v_start = if v_start0 < 0 {
5311 v_start0 + len
5312 } else {
5313 v_start0
5314 }; // c:2563
5315 if v_end <= v_start {
5316 Vec::new() // c:2578 (empty/inverted range)
5317 } else if v_start < 0 {
5318 vec![String::new()] // c:2580 (arrdup_max(nular, 1))
5319 } else if v_start >= len {
5320 // c:2582 — arrdup_max(nular, v->end - (v->start + 1)); nular has a
5321 // single element, so the count is capped at 1.
5322 if v_end - (v_start + 1) >= 1 {
5323 vec![String::new()]
5324 } else {
5325 Vec::new()
5326 }
5327 } else {
5328 Vec::new()
5329 }
5330 };
5331 if len == 0 {
5332 return nular_pad();
5333 }
5334 // Out-of-range starts (positive past len, or negative below
5335 // -len) return nular padding per Src/params.c getarrvalue's
5336 // slice-resolution branches (c:2570-2585).
5337 if start > len {
5338 return nular_pad();
5339 }
5340 if end < 0 && (len + end + 1) < 1 {
5341 return nular_pad();
5342 }
5343 if start < 0 && end < 0 && start > end {
5344 return nular_pad();
5345 }
5346 if start < 0 && start < -len {
5347 return nular_pad();
5348 }
5349 let resolve_start = |i: i64| -> i64 {
5350 if i < 0 {
5351 (len + i + 1).max(1)
5352 } else if i == 0 {
5353 1
5354 } else {
5355 i.min(len)
5356 }
5357 };
5358 let resolve_end = |i: i64| -> i64 {
5359 if i < 0 {
5360 (len + i + 1).max(0)
5361 } else if i == 0 {
5362 0
5363 } else {
5364 i.min(len)
5365 }
5366 };
5367 let s = resolve_start(start);
5368 let e = resolve_end(end);
5369 if e < 1 || s > e {
5370 return Vec::new();
5371 }
5372 let s_idx = (s - 1) as usize;
5373 let e_idx = e as usize;
5374 arr[s_idx..e_idx.min(arr.len())].to_vec()
5375}
5376
5377// ---------------------------------------------------------------------------
5378// Parameter table
5379// ---------------------------------------------------------------------------
5380
5381/// Parameter table.
5382/// Port of the `paramtab` HashTable Src/params.c maintains —
5383/// `createparamtable()` (line 817) initializes it with all the
5384/// IPDEF*-declared special params; `createparam()` (line 1030)
5385/// adds user variables.
5386// ---------------------------------------------------------------------------
5387// Free functions matching the C API
5388// ---------------------------------------------------------------------------
5389
5390/// Port of `getintvalue()` from `Src/params.c:2601`.
5391/// C body:
5392/// ```c
5393/// if (!v) return 0;
5394/// if (v->valflags & VALFLAG_INV) return v->start;
5395/// if (v->scanflags) {
5396/// char **arr = getarrvalue(v);
5397/// if (arr) { char *scal = sepjoin(arr, NULL, 1); return mathevali(scal); }
5398/// return 0;
5399/// }
5400/// if (PM_TYPE(v->pm->node.flags) == PM_INTEGER)
5401/// return v->pm->gsu.i->getfn(v->pm);
5402/// if (v->pm->node.flags & (PM_EFLOAT|PM_FFLOAT))
5403/// return (zlong)v->pm->gsu.f->getfn(v->pm);
5404/// return mathevali(getstrvalue(v));
5405/// ```
5406pub fn getintvalue(v: Option<&mut value>) -> i64 {
5407 let v = match v {
5408 Some(v) => v,
5409 None => return 0,
5410 };
5411 if (v.valflags & VALFLAG_INV) != 0 {
5412 return v.start as i64;
5413 }
5414 if v.scanflags != 0 {
5415 // c:2599-2605 — `arr = getarrvalue(v); if (arr) { scal = sepjoin(arr,
5416 // NULL, 1); return mathevali(scal); } else return 0;`. Same
5417 // array/slice selection as getnumvalue below.
5418 if v.pm.is_none() {
5419 return 0;
5420 }
5421 let whole = !v.arr.is_empty() || (v.start == 0 && v.end == -1);
5422 let (start, end) = (v.start as i64, v.end as i64);
5423 let full = getvaluearr(Some(&mut *v));
5424 let arr = if whole {
5425 full
5426 } else {
5427 getarrvalue(&full, if start >= 0 { start + 1 } else { start }, end)
5428 };
5429 let scal = crate::ported::utils::sepjoin(&arr, None);
5430 return mathevali(&scal).unwrap_or_else(|e| {
5431 zerr(&e);
5432 0
5433 });
5434 }
5435 let pm = match v.pm.as_mut() {
5436 Some(p) => p,
5437 None => return 0,
5438 };
5439 if PM_TYPE(pm.node.flags as u32) == PM_INTEGER {
5440 return intgetfn(pm);
5441 }
5442 if (pm.node.flags as u32 & (PM_EFLOAT | PM_FFLOAT)) != 0 {
5443 return floatgetfn(pm) as i64;
5444 }
5445 // c:2618 — `return mathevali(getstrvalue(v));`. The previous
5446 // Rust port used `s.parse::<i64>().unwrap_or(0)` which silently
5447 // returned 0 for any non-trivial arithmetic on the scalar
5448 // value side (e.g. `typeset x="1+2"; ((y = x))` would yield
5449 // y=0 instead of 3). Route through `math::mathevali` to
5450 // match C's arithmetic-expression evaluation.
5451 let pm = v.pm.as_mut().unwrap();
5452 let s = strgetfn(pm);
5453 mathevali(&s).unwrap_or(0) // c:2618 mathevali(...)
5454}
5455
5456/// Port of `getnumvalue()` from `Src/params.c:2624`. Returns an
5457/// `mnumber` (tagged int/float). C body dispatches on `valflags &
5458/// VALFLAG_INV` (returns start as int), `scanflags` (sepjoin →
5459/// matheval), then PM_TYPE: PM_INTEGER → mn.l = pm->gsu.i->getfn,
5460/// PM_EFLOAT|PM_FFLOAT → mn.type=MN_FLOAT; mn.d = pm->gsu.f->getfn,
5461/// else matheval(getstrvalue(v)).
5462pub fn getnumvalue(v: Option<&mut value>) -> mnumber {
5463 let v = match v {
5464 Some(v) => v,
5465 None => {
5466 return mnumber {
5467 l: 0,
5468 d: 0.0,
5469 type_: MN_INTEGER,
5470 }
5471 }
5472 };
5473 if (v.valflags & VALFLAG_INV) != 0 {
5474 return mnumber {
5475 l: v.start as i64,
5476 d: 0.0,
5477 type_: MN_INTEGER,
5478 };
5479 }
5480 if v.scanflags != 0 {
5481 // c:2626-2632 — `arr = getarrvalue(v); if (arr) { scal = sepjoin(arr,
5482 // NULL, 1); return matheval(scal); } else mn.u.l = 0;`. An array
5483 // search such as `(( a[(r)2*] + 1 ))` is evaluated from the joined
5484 // elements; the port returned 0 for every scanflags value.
5485 if v.pm.is_none() {
5486 return mnumber {
5487 l: 0,
5488 d: 0.0,
5489 type_: MN_INTEGER,
5490 };
5491 }
5492 // c:2556 — `s = getvaluearr(v)`, which returns `v->arr` first (c:731).
5493 // !!! WARNING: RUST-ONLY SLICE RULE !!! this port's getindex `(r)`/`(R)`
5494 // arm stores exactly the matched elements in `v.arr` (params.rs
5495 // getindex), where C stores an index into the whole array; those
5496 // elements ARE the slice, so they are taken whole. Otherwise C's
5497 // c:2557-2558 whole-array test, then the slice. `getarrvalue` here
5498 // takes a 1-based start; a non-negative `v.start` is getindex's
5499 // 0-based offset (c:2145), a negative one already counts from the end.
5500 let whole = !v.arr.is_empty() || (v.start == 0 && v.end == -1);
5501 let (start, end) = (v.start as i64, v.end as i64);
5502 let full = getvaluearr(Some(&mut *v));
5503 let arr = if whole {
5504 full
5505 } else {
5506 getarrvalue(&full, if start >= 0 { start + 1 } else { start }, end)
5507 };
5508 let scal = crate::ported::utils::sepjoin(&arr, None);
5509 // c:2630 — `return matheval(scal);`; a malformed join is reported
5510 // by the evaluator (zerr) and reads as zero.
5511 return matheval(&scal).unwrap_or_else(|e| {
5512 zerr(&e);
5513 mnumber {
5514 l: 0,
5515 d: 0.0,
5516 type_: MN_INTEGER,
5517 }
5518 });
5519 }
5520 let pm = match v.pm.as_mut() {
5521 Some(p) => p,
5522 None => {
5523 return mnumber {
5524 l: 0,
5525 d: 0.0,
5526 type_: MN_INTEGER,
5527 }
5528 }
5529 };
5530 let t = PM_TYPE(pm.node.flags as u32);
5531 if t == PM_INTEGER {
5532 return mnumber {
5533 l: intgetfn(pm),
5534 d: 0.0,
5535 type_: MN_INTEGER,
5536 };
5537 }
5538 if t == PM_EFLOAT || t == PM_FFLOAT {
5539 return mnumber {
5540 l: 0,
5541 d: floatgetfn(pm),
5542 type_: MN_FLOAT,
5543 };
5544 }
5545 // c:2639 — `return matheval(getstrvalue(v));`. getstrvalue, not the
5546 // raw scalar getter: it selects the array ELEMENT (c:2362) and cuts a
5547 // subscripted scalar (c:2510-2532), so `a=(1+2 x); $(( a[1] ))` is 3
5548 // and `b=12; $(( b[2] ))` is 2. Reading `strgetfn(pm)` gave an
5549 // array's (empty) scalar slot and a scalar's whole value.
5550 let s = getstrvalue(Some(v));
5551 matheval(&s) // c:2639 matheval(...)
5552 .unwrap_or_else(|e| {
5553 zerr(&e);
5554 mnumber {
5555 l: 0,
5556 d: 0.0,
5557 type_: MN_INTEGER,
5558 }
5559 })
5560}
5561
5562/// Port of `export_param()` from `Src/params.c:2653`.
5563///
5564/// C body converts `pm`'s value to its scalar form per `PM_TYPE`:
5565/// PM_INTEGER: convbase(buf, getfn, pm->base)
5566/// PM_EFLOAT/FFLOAT: convfloat(getfn, pm->base, pm->node.flags, NULL)
5567/// PM_SCALAR/etc.: gsu.s->getfn(pm)
5568/// Then calls `addenv(pm, val)`. PM_ARRAY/PM_HASHED early-return.
5569///
5570/// The previous Rust port used `format!("{}", intgetfn(pm))` for
5571/// integers and `format!("{}", floatgetfn(pm))` for floats — Rust's
5572/// DEFAULT formatting. C uses convbase/convfloat which respect
5573/// `pm.base` and `pm.flags`:
5574/// - `typeset -i16 x=255; export x` should put "16#FF" in the
5575/// env (per pm.base==16). The previous Rust port wrote "255".
5576/// - `typeset -F3 y=3.14; export y` should put "3.140" (per
5577/// pm.base==3 precision + PM_FFLOAT flag). Rust wrote "3.14".
5578///
5579/// Both formatter ports exist (`params::convbase`, `utils::convfloat`).
5580/// Wire them so the env-side representation matches C.
5581pub fn export_param(pm: &mut param) {
5582 // c:2653
5583 let t = PM_TYPE(pm.node.flags as u32);
5584 if (t & (PM_ARRAY | PM_HASHED)) != 0 {
5585 // c:2659 array/hash skip
5586 return;
5587 }
5588 let val: String = if t == PM_INTEGER {
5589 // c:2664 — `convbase(buf, pm->gsu.i->getfn(pm), pm->base)`.
5590 let base = if pm.base > 0 { pm.base } else { 10 };
5591 convbase(intgetfn(pm), base as u32) // c:2664
5592 } else if (pm.node.flags as u32 & (PM_EFLOAT | PM_FFLOAT)) != 0 {
5593 // c:2668 — `convfloat(pm->gsu.f->getfn(pm), pm->base,
5594 // pm->node.flags, NULL)`.
5595 convfloat(floatgetfn(pm), pm.base, pm.node.flags as u32)
5596 // c:2668
5597 } else if (pm.node.flags as u32 & PM_SPECIAL) != 0 {
5598 // c:2670 — `val = pm->gsu.s->getfn(pm)`, the param's OWN getfn.
5599 //
5600 // A PM_SPECIAL scalar keeps its value in a process global reached
5601 // through that getfn — `term`, `home`, `wordchars`, `ifs`,
5602 // `keyboardhackchar` (`Src/params.c:5185` and neighbours) — and
5603 // NOT in `pm->u.str`, which stays empty for them. Calling
5604 // `strgetfn` (c:4031 `return pm->u.str`) unconditionally exported
5605 // an EMPTY string for every one of them: an assignment to `TERM`
5606 // wrote `TERM=` into the environment, so `clear`, `tput`, `less`
5607 // and `vim` in the next child reported no terminal while `$TERM`
5608 // itself still read `xterm-256color` out of the global.
5609 //
5610 // `lookup_special_var` is the same getfn dispatch `getsparam`
5611 // (c:3163 `getstrvalue`) uses, so the exported text and the text
5612 // the shell reports for the parameter cannot disagree. It answers
5613 // `None` for a PM_UNSET special and for a PM_SPECIAL whose value
5614 // really does live in `u.str` (`PATH`, …) — both fall back to
5615 // `strgetfn`, which is that param's getfn.
5616 lookup_special_var(&pm.node.nam).unwrap_or_else(|| strgetfn(pm))
5617 } else {
5618 strgetfn(pm) // c:2670
5619 };
5620 addenv(&pm.node.nam, &val);
5621 // c:2672 `addenv(pm, val)` → c:5477-5478 —
5622 // pm->env = newenv;
5623 // pm->node.flags |= PM_EXPORTED;
5624 // C's `addenv` takes the Param and marks it; this port's `addenv`
5625 // takes `(name, value)` and cannot, so the two lines it owes live at
5626 // its Param-holding caller. Without the flag the only caller that
5627 // reaches here with an UNexported param — `assignstrvalue`'s
5628 // `isset(ALLEXPORT)` tail at c:2836-2841 — put the value in the
5629 // environment and left the parameter reading `scalar`, so
5630 // `setopt allexport; typeset v=1; v=2; print ${(t)v}` answered
5631 // `scalar` where zsh answers `scalar-export`, and `export -p` did not
5632 // list it.
5633 pm.node.flags |= PM_EXPORTED as i32; // c:5478
5634 pm.env = Some(val);
5635}
5636
5637/// Port of `setstrvalue()` from `Src/params.c:2685`. C body is a
5638/// one-liner: `assignstrvalue(v, val, 0);` — the real workhorse
5639/// is `assignstrvalue` (params.c:2692).
5640pub fn setstrvalue(v: Option<&mut value>, val: &str) {
5641 assignstrvalue(v, Some(val.to_string()), 0);
5642}
5643
5644/// 1:1 port of the C body covering: EXECOPT short-circuit,
5645/// PM_READONLY/PM_HASHED/VALFLAG_EMPTY guards, PM_UNSET clear,
5646/// per-PM_TYPE dispatch including the SCALAR/NAMEREF subscript
5647/// splice (KSHARRAYS-aware index normalization, MULTIBYTE end
5648/// adjust, full-string overwrite vs in-place memcpy fast path,
5649/// AUTONAMEDIRS/PM_NAMEDDIR re-registration), PM_INTEGER (with
5650/// ASSPM_ENV_IMPORT → `zstrtol_underscore`, else `mathevali`,
5651/// `lastbase` propagation), PM_EFLOAT/PM_FFLOAT (env vs `matheval`,
5652/// MN_FLOAT/MN_INTEGER coercion), PM_ARRAY (single-element wrap
5653/// via `setarrvalue`), PM_HASHED (`foundparam` indirection); then
5654/// `setscope(pm)`, errflag/env/ALLEXPORT/PM_ARRAY/ename gate, and
5655/// `export_param`. Width tracking for PM_LEFT/PM_RIGHT_B/PM_RIGHT_Z
5656/// preserved.
5657/// Port of `assignstrvalue()` from `Src/params.c:2692`.
5658pub fn assignstrvalue(v: Option<&mut value>, val: Option<String>, flags: i32) {
5659 if unset(EXECOPT) {
5660 return;
5661 }
5662
5663 let v = match v {
5664 Some(v) => v,
5665 None => return,
5666 };
5667 let pm = match v.pm.as_mut() {
5668 Some(p) => p,
5669 None => return,
5670 };
5671
5672 if (pm.node.flags as u32 & PM_READONLY) != 0 {
5673 // c:2701 — `zerr("read-only variable: %s", pm->node.nam)`.
5674 zerr(&format!("read-only variable: {}", pm.node.nam)); // c:2701
5675 return;
5676 }
5677 if (pm.node.flags as u32 & PM_HASHED) != 0
5678 && (v.scanflags as u32 & (SCANPM_MATCHMANY | SCANPM_ARRONLY)) != 0
5679 {
5680 // c:2706 — `zerr("%s: attempt to set slice of associative array", ...)`.
5681 zerr(&format!(
5682 "{}: attempt to set slice of associative array",
5683 pm.node.nam
5684 )); // c:2706
5685 return;
5686 }
5687 if (v.valflags & VALFLAG_EMPTY) != 0 {
5688 // c:2710 — `zerr("%s: assignment to invalid subscript range", ...)`.
5689 zerr(&format!(
5690 "{}: assignment to invalid subscript range",
5691 pm.node.nam
5692 )); // c:2710
5693 return;
5694 }
5695 pm.node.flags &= !(PM_UNSET as i32);
5696
5697 let mut val = val;
5698 match PM_TYPE(pm.node.flags as u32) {
5699 t if t == PM_SCALAR || t == PM_NAMEREF => {
5700 let v_str = val.take().unwrap_or_default();
5701 // c:2748 — no PM_LOWER / PM_UPPER arm exists in setstrvalue.
5702 // The C source folds case in exactly two places, neither of
5703 // them a write path: `getstrvalue`'s VALFLAG_SUBST tail
5704 // (c:2497-2506 — the READ done for a parameter substitution)
5705 // and `copyenvstr` (c:5434-5442 — the env mirror). Storage is
5706 // verbatim, so dropping the attribute exposes the original
5707 // text again (`typeset -l v=ABC; typeset +l v; print $v` → ABC)
5708 // and `typeset -p` — which reads through `printparamvalue`'s
5709 // direct `gsu.s->getfn` (c:6049), not getstrvalue — reports
5710 // `typeset -l v=ABC`. Folding here made all three wrong.
5711 if v.start == 0 && v.end == -1 {
5712 // c:2748 — `v->pm->gsu.s->setfn(v->pm, val);`. C
5713 // dispatches through the param's GSU vtable so
5714 // PM_SPECIAL params route to their canonical setfn
5715 // (homesetfn, ifssetfn, ...). The Rust port stores
5716 // the vtable on `pm.gsu_s` (set in `createparamtable`
5717 // via `gsu_scalar_for_special`); when set, dispatch
5718 // through it. When unset, fall back to the default
5719 // `strsetfn` path (C's stdscalar_gsu.setfn).
5720 // c:2742-2746 — ASSPM_AUGMENT: prepend the existing
5721 // value before storing. Without this, `PATH+=":/foo"`
5722 // would replace PATH instead of appending.
5723 let final_str = if (flags & ASSPM_AUGMENT) != 0 {
5724 // c:2742-2746 — the old text comes from `gsu.s->getfn`, not
5725 // from the raw slot: a tied scalar (`typeset -T FOO foo`)
5726 // keeps its value in the partner array, so `FOO+=:d` read
5727 // an empty `u_str` and dropped the existing elements.
5728 let prev = match pm.gsu_s.as_ref().map(|g| g.getfn) {
5729 Some(getfn) => getfn(pm),
5730 None => pm.u_str.clone().unwrap_or_default(),
5731 };
5732 format!("{}{}", prev, v_str)
5733 } else {
5734 v_str
5735 };
5736 let len = final_str.len();
5737 let setfn_ptr = pm.gsu_s.as_ref().map(|g| g.setfn);
5738 if let Some(setfn) = setfn_ptr {
5739 setfn(pm, final_str); // c:2748
5740 } else {
5741 strsetfn(pm, final_str); // c:2748 (default)
5742 }
5743 if (pm.node.flags as u32 & (PM_LEFT | PM_RIGHT_B | PM_RIGHT_Z)) != 0
5744 && pm.width == 0
5745 {
5746 pm.width = len as i32;
5747 }
5748 } else {
5749 // Subscript splice (c:Src/params.c:2748+ assignstrvalue). In C
5750 // `v->start`/`v->end` are BYTE offsets into the metafied string
5751 // (getindex already converted the character subscript to a byte
5752 // offset via MB_METACHARLEN). zshrs's subscript resolution
5753 // instead yields CHARACTER positions, so operate on the char
5754 // sequence throughout — otherwise a multibyte scalar value
5755 // (e.g. a Powerline glyph U+E0B0, 3 bytes) panics when the byte
5756 // slice `z[..start]` lands inside a codepoint. p10k's
5757 // `_p9k_get_icon` hit exactly this (crashing the shell).
5758 let z = strgetfn(pm);
5759 let z_chars: Vec<char> = z.chars().collect();
5760 let zlen = z_chars.len() as i32;
5761 let mut start = v.start;
5762 let mut end = v.end;
5763 if (v.valflags & VALFLAG_INV) != 0 && !isset(KSHARRAYS) {
5764 start -= 1;
5765 end -= 1;
5766 }
5767 if start < 0 {
5768 start += zlen;
5769 if start < 0 {
5770 start = 0;
5771 }
5772 }
5773 if start > zlen {
5774 start = zlen;
5775 }
5776 if end < 0 {
5777 end += zlen;
5778 if end < 0 {
5779 end = 0;
5780 } else if end >= zlen {
5781 end = zlen;
5782 } else {
5783 // MULTIBYTE branch: increment by metachar length;
5784 // single-byte path increments by 1.
5785 end += 1;
5786 }
5787 }
5788 // c:2763-2766 (54700) — `if (v->end < v->start) v->end =
5789 // v->start; else if (v->end > oldlen) v->end = oldlen;`: a
5790 // reversed range (`a[6,3]=xyz`) inserts at start and keeps
5791 // the whole tail.
5792 if end < start {
5793 end = start;
5794 } else if end > zlen {
5795 end = zlen;
5796 }
5797 // Slice by CHARACTER index into z_chars (never a raw byte
5798 // slice of z, which would split a multibyte codepoint).
5799 let s = (start.max(0) as usize).min(z_chars.len());
5800 let e = (end.max(0) as usize).min(z_chars.len());
5801 let mut x = String::with_capacity(z.len() + v_str.len());
5802 x.extend(z_chars[..s].iter());
5803 x.push_str(&v_str);
5804 if e <= z_chars.len() {
5805 x.extend(z_chars[e..].iter());
5806 }
5807 // c:3997-4000 — strsetfn does the PM_NAMEDDIR / adduserdir step.
5808 strsetfn(pm, x);
5809 }
5810 }
5811 t if t == PM_INTEGER => {
5812 if let Some(ref s) = val {
5813 let ival: i64 = if (flags & ASSPM_ENV_IMPORT) != 0 {
5814 s.parse::<i64>().unwrap_or(0)
5815 } else {
5816 // c:Src/params.c:2774 — `mathevali(val)`. C's
5817 // matheval calls zerr (Src/math.c:1462+) on parse
5818 // failures, which sets `errflag |= ERRFLAG_ERROR`
5819 // and the caller propagates the abort. The Rust
5820 // port returns Result<i64,String>; `unwrap_or(0)`
5821 // silently swallowed "operator expected at 'def'"
5822 // and stored 0 instead. Bug #75 in docs/BUGS.md.
5823 //
5824 // Mirror the C semantic: surface the error via
5825 // zerr (which sets errflag), then fall back to 0
5826 // for the stored value (C also stores whatever the
5827 // partial parse computed, which is typically 0).
5828 match mathevali(s) {
5829 Ok(v) => v,
5830 Err(msg) => {
5831 zerr(&msg);
5832 0
5833 }
5834 }
5835 };
5836 // c:2775-2778 — `if (flags & ASSPM_AUGMENT) pm->u.val += val.l;
5837 // else pm->u.val = val.l;`. The augment
5838 // path is what makes `integer x=42; x+=8` store 50 instead
5839 // of 8. Without this the integer `+=` operator silently
5840 // replaced.
5841 let final_val = if (flags & ASSPM_AUGMENT) != 0 {
5842 pm.u_val.wrapping_add(ival)
5843 } else {
5844 ival
5845 };
5846 intsetfn(pm, final_val);
5847 if (pm.node.flags as u32 & (PM_LEFT | PM_RIGHT_B | PM_RIGHT_Z)) != 0
5848 && pm.width == 0
5849 {
5850 pm.width = s.len() as i32;
5851 }
5852 // !!! POSIX-FAITHFUL GATE !!! zsh gives an integer var the
5853 // output base of the literal it was assigned (`(( z=0xff ))`
5854 // → base 16 → `$z` prints `16#FF`). bash/ksh/POSIX have no
5855 // such per-var base — integers always print decimal. Under
5856 // posix_faithful, never attach a base so `echo $z` → `255`.
5857 if pm.base == 0 && !crate::dash_mode::posix_faithful() {
5858 let lb = lastbase();
5859 if lb != -1 {
5860 pm.base = lb;
5861 }
5862 }
5863 }
5864 }
5865 t if t == PM_EFLOAT || t == PM_FFLOAT => {
5866 if let Some(ref s) = val {
5867 let mn = if (flags & ASSPM_ENV_IMPORT) != 0 {
5868 mnumber {
5869 l: 0,
5870 d: s.parse::<f64>().unwrap_or(0.0),
5871 type_: MN_FLOAT,
5872 }
5873 } else {
5874 // c:Src/params.c — float assignment runs the RHS
5875 // through matheval; on parse failure, zsh emits the
5876 // engine's diagnostic ("bad math expression: operator
5877 // expected at `abc'") via zerr. Mirror the integer
5878 // arm above which already calls zerr on Err. Bug
5879 // #506 — zshrs previously swallowed the error and
5880 // stored 0.0 silently for `float f="3.14abc"`.
5881 match matheval(s) {
5882 Ok(v) => v,
5883 Err(msg) => {
5884 zerr(&msg);
5885 mnumber {
5886 l: 0,
5887 d: 0.0,
5888 type_: MN_FLOAT,
5889 }
5890 }
5891 }
5892 };
5893 let d = if (mn.type_ & MN_FLOAT) != 0 {
5894 mn.d
5895 } else {
5896 mn.l as f64
5897 };
5898 // c:2775-2778 — ASSPM_AUGMENT path: `float x=1.5; x+=0.25`
5899 // adds 0.25 to the current u_dval rather than replacing.
5900 // Mirrors the integer-augment block above; without it
5901 // `+=` was a plain `=`.
5902 let final_d = if (flags & ASSPM_AUGMENT) != 0 {
5903 pm.u_dval + d
5904 } else {
5905 d
5906 };
5907 floatsetfn(pm, final_d);
5908 if (pm.node.flags as u32 & (PM_LEFT | PM_RIGHT_B | PM_RIGHT_Z)) != 0
5909 && pm.width == 0
5910 {
5911 pm.width = s.len() as i32;
5912 }
5913 }
5914 }
5915 t if t == PM_ARRAY => {
5916 // c:2826-2828 — `char **ss = zalloc(2*sizeof(char*));
5917 // ss[0]=val; ss[1]=NULL; setarrvalue(v, ss);` — wrap the
5918 // single value in a 1-element array. The C-faithful
5919 // setarrvalue takes &mut Value; we already hold a &mut
5920 // borrow of pm from v.pm.as_mut() higher up, so inline
5921 // the dispatch directly against pm here to avoid the
5922 // double-borrow.
5923 let one = vec![val.take().unwrap_or_default()];
5924 if v.start == 0 && v.end == -1 {
5925 if isset(KSHARRAYS) {
5926 // c:Src/params.c getvalue — under KSHARRAYS a bare
5927 // array name addresses element 0 (ksh semantics), so
5928 // a SCALAR assignment `a=X` to an existing array sets
5929 // the FIRST element keeping the rest (a=(X second)),
5930 // not a whole-array replace. The non-KSHARRAYS path
5931 // already reset the array to a scalar before reaching
5932 // here (assignsparam c:3179-3192, gated on
5933 // unset(KSHARRAYS)); only the KSHARRAYS case arrives
5934 // with the param still PM_ARRAY.
5935 let arr = pm.u_arr.get_or_insert_with(Vec::new);
5936 let first = one.into_iter().next().unwrap_or_default();
5937 if arr.is_empty() {
5938 arr.push(first);
5939 } else {
5940 arr[0] = first;
5941 }
5942 } else {
5943 // c:2922 — full replace.
5944 pm.u_arr = Some(one);
5945 }
5946 } else {
5947 // c:2933+ — slice splice path with bounds adjust.
5948 let arr = pm.u_arr.get_or_insert_with(Vec::new);
5949 let len = arr.len() as i64;
5950 let start_raw = v.start as i64;
5951 let end_raw = v.end as i64;
5952 let start = if start_raw < 0 {
5953 (len + start_raw + 1).max(0)
5954 } else {
5955 start_raw
5956 };
5957 let end = if end_raw < 0 {
5958 (len + end_raw + 1).max(0)
5959 } else {
5960 end_raw
5961 };
5962 let start_idx = (start.max(1) - 1) as usize;
5963 let end_idx = end.max(0) as usize;
5964 while arr.len() < start_idx {
5965 arr.push(String::new());
5966 }
5967 let end_idx = end_idx.min(arr.len());
5968 if start_idx <= end_idx {
5969 arr.splice(start_idx..end_idx, one);
5970 } else {
5971 for (i, x) in one.into_iter().enumerate() {
5972 if start_idx + i < arr.len() {
5973 arr[start_idx + i] = x;
5974 } else {
5975 arr.push(x);
5976 }
5977 }
5978 }
5979 }
5980 }
5981 t if t == PM_HASHED => {
5982 // c:2831-2843 — the value still refers to the WHOLE hash, so
5983 // the only writable target is the member `scanparamvals`
5984 // stashed in `foundparam` (c:663). With no such member the
5985 // assignment is an attempt to overwrite the association
5986 // itself with a scalar:
5987 //
5988 // if (foundparam == NULL) {
5989 // zerr("%s: attempt to set associative array to scalar",
5990 // v->pm->node.nam);
5991 // zsfree(val);
5992 // return;
5993 // } else
5994 // foundparam->gsu.s->setfn(foundparam, val);
5995 //
5996 // This arm used to be inert, so a reverse subscript whose
5997 // search found nothing (`h[(r)nomatch]=Z`) silently created a
5998 // literal key instead of reporting the error.
5999 match foundparam() {
6000 None => {
6001 // c:2835-2836
6002 zerr(&format!(
6003 "{}: attempt to set associative array to scalar",
6004 pm.node.nam
6005 ));
6006 return; // c:2838
6007 }
6008 Some(nam) => {
6009 // c:2841 — `foundparam->gsu.s->setfn(foundparam, val)`.
6010 // This port keeps assoc members as plain strings in
6011 // `paramtab_hashed_storage` rather than as Params with
6012 // their own `strsetfn` (see the same substitution at
6013 // c:4113 `arrhashsetfn`), so the member write is a
6014 // store into that map.
6015 let mut store = paramtab_hashed_storage().lock().unwrap();
6016 store
6017 .entry(pm.node.nam.clone())
6018 .or_default()
6019 .insert(nam, val.take().unwrap_or_default()); // c:2841
6020 }
6021 }
6022 set_foundparam(None);
6023 }
6024 _ => {}
6025 }
6026 setscope(pm);
6027 if errflag.load(Ordering::Relaxed) != 0
6028 || ((pm.env.is_none()
6029 && (pm.node.flags as u32 & PM_EXPORTED) == 0
6030 && !(isset(ALLEXPORT) && (pm.node.flags as u32 & PM_HASHELEM) == 0))
6031 || (pm.node.flags as u32 & PM_ARRAY) != 0
6032 || pm.ename.is_some())
6033 {
6034 return;
6035 }
6036 export_param(pm);
6037}
6038
6039/// Port of `setnumvalue()` from `Src/params.c:2856`. C body
6040/// dispatches on `PM_TYPE(v->pm->node.flags)`:
6041/// PM_SCALAR/PM_NAMEREF/PM_ARRAY → convbase_underscore /
6042/// convfloat_underscore + setstrvalue; PM_INTEGER →
6043/// `pm->gsu.i->setfn(pm, val.u.l)`; PM_EFLOAT|PM_FFLOAT →
6044/// `pm->gsu.f->setfn(pm, val.u.d)`. EXECOPT/PM_READONLY checks
6045/// at top.
6046pub fn setnumvalue(v: Option<&mut value>, val: mnumber) {
6047 // c:2860 — `if (unset(EXECOPT)) return;`. In NO_EXEC mode, param
6048 // mutations must be skipped so dry-run shell evaluation doesn't
6049 // leak state into the param table. The previous Rust port skipped
6050 // this check; `zsh -n -c '(( x=5 ))'` would mutate $x silently.
6051 if unset(EXECOPT) {
6052 // c:2860
6053 return;
6054 }
6055 let v = match v {
6056 Some(v) => v,
6057 None => return,
6058 };
6059 let pm = match v.pm.as_mut() {
6060 Some(p) => p,
6061 None => return,
6062 };
6063 if (pm.node.flags as u32 & PM_READONLY) != 0 {
6064 zerr(&format!("read-only variable: {}", pm.node.nam)); // c:2862
6065 return;
6066 }
6067 let t = PM_TYPE(pm.node.flags as u32);
6068 if t == PM_SCALAR || t == PM_NAMEREF || t == PM_ARRAY {
6069 // c:2862-2872 — convbase_underscore for integers (honors
6070 // pm.base for the radix prefix + pm.width for underscore
6071 // grouping), convfloat_underscore for floats. The previous
6072 // Rust port computed `val.l.to_string()` then DROPPED the
6073 // result via `let _ = s;` — meaning a numeric assignment
6074 // to a SCALAR param stored NOTHING. `typeset s; (( s = 42 ))`
6075 // would leave $s empty.
6076 let s = if (val.type_ & MN_INTEGER) != 0 {
6077 // c:2862
6078 // c:2864 — `convbase_underscore(val.u.l, pm->base, pm->width)`.
6079 convbase_underscore(val.l, if pm.base > 0 { pm.base } else { 10 }, pm.width)
6080 } else {
6081 // c:2867
6082 // c:2869 — `convfloat_underscore(val.u.d, pm->width)`.
6083 convfloat_underscore(val.d, pm.width)
6084 };
6085 pm.u_str = Some(s); // c:2871 setstrvalue → store
6086 } else if t == PM_INTEGER {
6087 // c:2874 — `pm->gsu.i->setfn(pm, val.u.l)`. For MN_FLOAT
6088 // input, C truncates to integer via `(zlong)val.u.d`.
6089 let n = if (val.type_ & MN_INTEGER) != 0 {
6090 val.l
6091 } else {
6092 crate::zlong_cast::zlong_from_double(val.d)
6093 };
6094 // c:2874 dispatches the param's OWN integer set-function. Most
6095 // integer params carry `varinteger_gsu`, whose setfn is the plain
6096 // store below — but `$LINES` / `$COLUMNS` are declared
6097 // `IPDEF5("COLUMNS", &zterm_columns, zlevar_gsu)`
6098 // (c:Src/params.c:362-363), and `zlevarsetfn` (c:4226) additionally
6099 // publishes the value into the `zterm_lines` / `zterm_columns`
6100 // GLOBAL that every C-side reader consults.
6101 //
6102 // The port inlined the store and never dispatched, which left
6103 // `zlevarsetfn` with zero call sites: an explicit `COLUMNS=61` moved
6104 // the parameter and nothing else. Readers that go through
6105 // `adjustcolumns()` (c:1858, a cached read of `zterm_columns`) kept
6106 // the geometry the last `adjustwinsize` probe cached — so in any
6107 // shell that had probed a tty, `COLUMNS=61` was ignored by the
6108 // completion-list layout while `$COLUMNS` itself read 61.
6109 if matches!(pm.node.nam.as_str(), "LINES" | "COLUMNS") {
6110 zlevarsetfn(pm, n); // c:2874 via zlevar_gsu
6111 } else {
6112 pm.u_val = n; // c:2874 via varinteger_gsu
6113 }
6114 } else if t == PM_EFLOAT || t == PM_FFLOAT {
6115 // c:2878 — `pm->gsu.f->setfn(pm, val.u.d)`. MN_INTEGER input
6116 // gets promoted via `(double)val.u.l`.
6117 pm.u_dval = if (val.type_ & MN_INTEGER) != 0 {
6118 val.l as f64
6119 } else {
6120 val.d
6121 };
6122 }
6123}
6124
6125/// Port of `setarrvalue()` from `Src/params.c:2895`. Sets an array (or assoc-array via
6126/// arrhashsetfn) into the param identified by v.pm, honouring
6127/// PM_READONLY / type-guards / VALFLAG_EMPTY rejections and the
6128/// slice-bounds adjust for `[N,M]` subscripts.
6129///
6130/// C dispatch:
6131/// - !EXECOPT → silent return (c:2897-2898)
6132/// - PM_READONLY → zerr + return (c:2899-2904)
6133/// - !PM_ARRAY && !PM_HASHED → zerr (c:2905-2911)
6134/// - VALFLAG_EMPTY → zerr (c:2913-2917)
6135/// - start==0,end==-1 && PM_HASHED → arrhashsetfn(0) (c:2919-2922)
6136/// - start==0,end==-1 && PM_ARRAY → gsu.a->setfn (c:2922-2923)
6137/// - start==-1,end==0 && PM_HASHED → arrhashsetfn(AUGMENT) (c:2925-2928)
6138/// - PM_HASHED with other bounds → zerr slice-of-assoc (c:2929-2932)
6139/// - PM_ARRAY with slice → bounds adjust + splice (c:2933+)
6140///
6141/// VALFLAG_INV + !KSHARRAYS off-by-one (c:2938-2942) is ported below.
6142/// PM_UNIQUE dedupe (c:2966-2967) is ported at the tail. C's
6143/// setarrvalue has no ASSPM_AUGMENT handling for slices; the only
6144/// AUGMENT arm is the whole-hash `[-1,0]` form (c:2925-2928).
6145pub fn setarrvalue(v: &mut value, val: Vec<String>) {
6146 // c:2895
6147 // c:2897-2898 — `if (unset(EXECOPT)) return;`. Match the same
6148 // NO_EXEC bail as setnumvalue at c:2860. Without it,
6149 // `zsh -n -c 'arr=(a b c)'` would mutate arr during a parse-
6150 // only run.
6151 if unset(EXECOPT) {
6152 // c:2897
6153 return;
6154 }
6155
6156 let pm = match v.pm.as_mut() {
6157 Some(p) => p,
6158 None => return,
6159 };
6160
6161 // c:2899-2904 — PM_READONLY rejection.
6162 if pm.node.flags & PM_READONLY as i32 != 0 {
6163 zerr(&format!("read-only variable: {}", pm.node.nam));
6164 return;
6165 }
6166 // c:2905-2911 — type guard.
6167 let t = PM_TYPE(pm.node.flags as u32);
6168 if t & (PM_ARRAY | PM_HASHED) == 0 {
6169 zerr(&format!(
6170 "{}: attempt to assign array value to non-array",
6171 pm.node.nam
6172 ));
6173 return;
6174 }
6175 // c:2913-2917 — VALFLAG_EMPTY rejection.
6176 if v.valflags & VALFLAG_EMPTY != 0 {
6177 zerr(&format!(
6178 "{}: assignment to invalid subscript range",
6179 pm.node.nam
6180 ));
6181 return;
6182 }
6183
6184 // c:2919-2932 — full-replace / AUGMENT / hash-slice-reject paths.
6185 if v.start == 0 && v.end == -1 {
6186 if t == PM_HASHED {
6187 // c:2920 — arrhashsetfn(pm, val, 0).
6188 arrhashsetfn(pm, val, 0);
6189 } else {
6190 // c:2922 — `pm->gsu.a->setfn(pm, val)`. Route through
6191 // arrsetfn so PM_UNIQUE dedupe + arrfixenv side-effects
6192 // fire (params.c:4066-4076).
6193 arrsetfn(pm, val);
6194 }
6195 return;
6196 }
6197 if v.start == -1 && v.end == 0 && t == PM_HASHED {
6198 arrhashsetfn(pm, val, ASSPM_AUGMENT);
6199 return;
6200 }
6201 if t == PM_HASHED {
6202 zerr(&format!(
6203 "{}: attempt to set slice of associative array",
6204 pm.node.nam
6205 ));
6206 return;
6207 }
6208
6209 // c:2938-2942 — VALFLAG_INV + !KSHARRAYS off-by-one. Inverse
6210 // subscripts (`a[(i)pat]=val`) are 1-based when KSHARRAYS is
6211 // off; shift start/end down by 1 to match the 0-based slice
6212 // arithmetic below.
6213 if v.valflags & VALFLAG_INV != 0 && !isset(KSHARRAYS) {
6214 if v.start > 0 {
6215 v.start -= 1;
6216 }
6217 v.end -= 1;
6218 }
6219
6220 // c:2933+ — PM_ARRAY slice path.
6221 let arr = pm.u_arr.get_or_insert_with(Vec::new);
6222 let len = arr.len() as i64;
6223 // c:2944-2949 — negative start: add pre_assignment_length; clamp to 0.
6224 // !!! WARNING: RUST-ONLY INDEX CONVENTION — C's getindex has already made a
6225 // positive start 0-based (`start -= startprevlen`, c:2144-2145), but this
6226 // port keeps it 1-based (`start_idx = start - 1` below; the fusevm splice
6227 // passes 1-based starts and assignaparam lifts getindex's). So a
6228 // negative start, which C turns into a 0-based index here, becomes the
6229 // matching 1-based one: `+ 1`, clamped at 1 instead of 0. Using C's 0-based
6230 // result made `typeset a[-1]=(z)` overwrite the last TWO elements.
6231 let start = if v.start < 0 {
6232 (len + v.start as i64 + 1).max(1)
6233 } else {
6234 v.start as i64
6235 };
6236 // c:2950-2953 — negative end: add pre_assignment_length + 1; clamp to 0.
6237 let end = if v.end < 0 {
6238 (len + v.end as i64 + 1).max(0)
6239 } else {
6240 v.end as i64
6241 };
6242 let start_idx = (start.max(1) - 1) as usize;
6243 let end_idx = end.max(0) as usize;
6244
6245 // c:2980 — pad with empty strings up to start.
6246 while arr.len() < start_idx {
6247 arr.push(String::new());
6248 }
6249
6250 // c:2940-2943 — `if (v->end < v->start || v->start > oldlen)
6251 // v->end = v->start; else if (v->end > oldlen)
6252 // v->end = oldlen;`. A REVERSED range (start > end,
6253 // e.g. `a[4,2]=(42 43 44)`) collapses to an EMPTY range at start, so
6254 // the splice INSERTS the value there keeping every element
6255 // ("1 2 3 42 43 44 4 5") — it does NOT overwrite from start. The
6256 // previous Rust port took a custom `start_idx > end_idx` else-branch
6257 // that overwrote/extended from start, dropping the tail. `clamp`
6258 // implements both C arms: lower-bound start_idx (reversed → empty),
6259 // upper-bound arr.len() (end past tail → clamp). After the pad above
6260 // arr.len() >= start_idx, so the clamp range is always valid.
6261 let end_idx = end_idx.clamp(start_idx, arr.len());
6262 let val_len = val.len(); // c:3030 post-assign sanity
6263 let pre_len = arr.len(); // c:3030 (snapshot)
6264 // c:2989-2998 — splice val into [start..end] range.
6265 arr.splice(start_idx..end_idx, val);
6266 // c:3030 — DPUTS2(p - new != post_assignment_length, …). The
6267 // post-splice arr.len() is C's `p - new`; expected follows C's
6268 // post_assignment_length = start + vallen + MAX(0, oldlen - end).
6269 let expected = pre_len - (end_idx - start_idx) + val_len; // c:3030
6270 DPUTS2!(
6271 // c:3030
6272 arr.len() != expected, // c:3030
6273 "setarrvalue: wrong allocation: {} 1= {}",
6274 expected,
6275 arr.len() // c:3030-3031
6276 );
6277
6278 // c:2966-2967 — `if (pm->node.flags & PM_UNIQUE) arrunique(pm->u.arr);`
6279 // Dedupe in-place preserving first occurrence. Without this,
6280 // `typeset -U arr; arr[2]=foo` leaves duplicates that violate the
6281 // PM_UNIQUE invariant.
6282 if (pm.node.flags as u32 & PM_UNIQUE) != 0 {
6283 let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
6284 arr.retain(|s| seen.insert(s.clone())); // c:2967
6285 }
6286 // c:Src/params.c:2922 — a SUBSCRIPT-RANGE assignment (`path[i,j]=(…)`,
6287 // `path[N]=(…)`, `path[i,j]=()`) reaches here (the whole-replace form
6288 // returned early via arrsetfn above). For a tied colon-array the scalar
6289 // side must re-derive, just like the single-element path in
6290 // assignsparam; see TIED_COLON_ARRAYS. Snapshot after the splice.
6291 let tie_sync: Option<(&'static str, String)> = TIED_COLON_ARRAYS
6292 .iter()
6293 .find(|(a, _)| *a == pm.node.nam)
6294 .map(|(_, scalar)| (*scalar, pm.u_arr.as_deref().unwrap_or(&[]).join(":")));
6295 if let Some((scalar, joined)) = tie_sync {
6296 assignsparam(scalar, &joined, 0);
6297 }
6298}
6299
6300/// Retrieve integer parameter.
6301/// Port of `getiparam()` from `Src/params.c:3044`. C: getvalue +
6302/// getintvalue. Our adaptation reads the scalar string and parses;
6303/// returns 0 on missing or unparseable, matching getintvalue's
6304/// failure-returns-0 convention (params.c:2601).
6305pub fn getiparam(s: &str) -> i64 {
6306 // c:Src/params.c:348 + c:4202 — `IPDEF4("HISTCMD", &curhist)` binds
6307 // `intvargetfn` (`return *pm->u.valptr;`) to `&curhist`
6308 // (`Src/hist.c:88`), so the value is the LIVE history counter, never
6309 // stored on the node. zshrs's `createparamtable` inserts HISTCMD as a
6310 // plain PM_INTEGER whose `u_val` nothing writes, so this must precede
6311 // the `u_val` fast path below — otherwise every read reports the 0 the
6312 // node was created with.
6313 if s == "HISTCMD" {
6314 return crate::ported::hist::curhist.load(Ordering::SeqCst); // c:348 + c:4202
6315 }
6316 // c:362-363 + c:4202 — `$COLUMNS`/`$LINES` are the other valptr-bound
6317 // specials, and for the same reason they have to be resolved through
6318 // `intvargetfn` before the `u_val` fast path below. `unset COLUMNS`
6319 // leaves the node in the table flagged PM_UNSET (c:3877, PM_SPECIAL
6320 // without PM_REMOVABLE), and an unset parameter reads empty rather than
6321 // as the global, so the flag is consulted first.
6322 if s == "COLUMNS" || s == "LINES" {
6323 if let Ok(tab) = paramtab().read() {
6324 if let Some(pm) = tab.get(s) {
6325 if (pm.node.flags as u32 & PM_UNSET) == 0 && !is_unset_special(s) {
6326 return intvargetfn(pm); // c:4202
6327 }
6328 }
6329 }
6330 }
6331 // C also honours PM_INTEGER's `pm->u.val` payload directly when
6332 // the param is typed numeric; check paramtab first for that case.
6333 if let Ok(tab) = paramtab().read() {
6334 if let Some(pm) = tab.get(s) {
6335 if (pm.node.flags as u32 & PM_INTEGER) != 0 {
6336 return pm.u_val;
6337 }
6338 }
6339 }
6340 getsparam(s)
6341 .and_then(|s| s.parse::<i64>().ok())
6342 .unwrap_or(0)
6343}
6344
6345/// Retrieve numeric (int-or-float) parameter.
6346/// Port of `getnparam()` from `Src/params.c:3058`. C returns an
6347/// `mnumber` (tagged int/float union); our adaptation returns
6348/// `(i64, f64, bool)` where the bool is true for float. Unset
6349/// returns `(0, 0.0, false)`, matching the MN_INTEGER zero
6350/// fallback in the C source's not-found branch.
6351pub fn getnparam(s: &str) -> (i64, f64, bool) {
6352 // c:Src/params.c:348 + c:4202 — same valptr-bound special as in
6353 // `getiparam` above: IPDEF4 binds `intvargetfn` to `&curhist`
6354 // (`Src/hist.c:88`), so read the live counter rather than the
6355 // never-written `u_val` on the paramtab node.
6356 if s == "HISTCMD" {
6357 let v = crate::ported::hist::curhist.load(Ordering::SeqCst); // c:348 + c:4202
6358 return (v, v as f64, false);
6359 }
6360 // c:362-363 + c:4202 — the same valptr-bound pair, resolved through
6361 // `intvargetfn`; see `getiparam` for the PM_UNSET gate.
6362 if s == "COLUMNS" || s == "LINES" {
6363 if let Ok(tab) = paramtab().read() {
6364 if let Some(pm) = tab.get(s) {
6365 if (pm.node.flags as u32 & PM_UNSET) == 0 && !is_unset_special(s) {
6366 let v = intvargetfn(pm); // c:4202
6367 return (v, v as f64, false);
6368 }
6369 }
6370 }
6371 }
6372 if let Ok(tab) = paramtab().read() {
6373 if let Some(pm) = tab.get(s) {
6374 let fl = pm.node.flags as u32;
6375 if (fl & (PM_EFLOAT | PM_FFLOAT)) != 0 {
6376 return (pm.u_dval as i64, pm.u_dval, true);
6377 }
6378 if (fl & PM_INTEGER) != 0 {
6379 return (pm.u_val, pm.u_val as f64, false);
6380 }
6381 }
6382 }
6383 let s = match getsparam(s) {
6384 Some(s) => s,
6385 None => return (0, 0.0, false),
6386 };
6387 if s.contains('.') || s.contains('e') || s.contains('E') {
6388 if let Ok(f) = s.parse::<f64>() {
6389 return (f as i64, f, true);
6390 }
6391 }
6392 if let Ok(i) = s.parse::<i64>() {
6393 return (i, i as f64, false);
6394 }
6395 (0, 0.0, false)
6396}
6397
6398/// Port of `getsparam()` from `Src/params.c:3076`.
6399///
6400/// C body:
6401/// ```c
6402/// char *getsparam(char *s) {
6403/// struct value vbuf;
6404/// Value v = getvalue(&vbuf, &s, 0);
6405/// if (!v) return NULL;
6406/// return getstrvalue(v);
6407/// }
6408/// ```
6409///
6410/// `getvalue` (params.c:2173) builds a `Value` for the parameter,
6411/// dispatching through `Param.gsu->getfn` for special parameters.
6412/// `getstrvalue` (params.c:2335) extracts the scalar form: for
6413/// PM_INTEGER calls `pm->gsu.i->getfn(pm)` and convbase's the
6414/// result; for PM_SCALAR calls `pm->gsu.s->getfn(pm)`; for
6415/// PM_ARRAY joins the elements.
6416///
6417/// **Sole funnel.** Every scalar parameter read in zshrs routes
6418/// through this fn — `subst.rs` parameter expansion AND
6419/// `fusevm_bridge::expand_param` both call `getsparam`. The
6420/// dispatch chain lives in exactly one place, mirroring C's
6421/// "every read goes through getsparam" architecture.
6422///
6423/// Lookup order (mirrors C's `getvalue` → `getstrvalue` cascade):
6424/// 1. **GSU dispatch** via [`lookup_special_var`] — special
6425/// parameters route through their getfn callback (`uidgetfn` /
6426/// `randomgetfn` / `usernamegetfn` / etc.). Same role as
6427/// C's `Param.gsu->getfn` virtual dispatch.
6428/// 2. **Local variable** — `variables[name]`. C reads `pm->u.str`
6429/// for PM_SCALAR; here we hold the scalar in the variables
6430/// HashMap.
6431/// 3. **Environment fallback** — `std::env::var(name)`. C imports
6432/// env vars into the param table at startup so they go through
6433/// the same dispatch as everything else; zshrs reads from the
6434/// OS env on miss to match.
6435/// 4. **Array → scalar** — `arrays[name].join(" ")`. Mirrors
6436/// C's PM_ARRAY case in getstrvalue (params.c:2358) which
6437/// joins via `sepjoin(ss, NULL, 1)`.
6438///
6439// Retrieve a scalar (string) parameter // c:3076
6440/// Returns `None` only if all four paths miss (parameter genuinely
6441/// unset).
6442pub fn getsparam(name: &str) -> Option<String> {
6443 // c:3076
6444 // 1. GSU dispatch — `Param.gsu->getfn(pm)` equivalent. Special
6445 // parameters (UID/RANDOM/USERNAME/...) live behind getfn
6446 // hooks that the table read below would otherwise miss.
6447 if let Some(v) = lookup_special_var(name) {
6448 return Some(v);
6449 }
6450 // 1b. c:Src/params.c:570-575 — getparamnode resolves PM_NAMEREF
6451 // chains before the value read (`pm = resolve_nameref(pm)`).
6452 // Redirect the lookup to the resolved target.
6453 // c:570-575 getparamnode → resolve_nameref: scalar-read
6454 // redirection through a nameref chain, INLINE (former
6455 // nameref_read_redirect, single-caller fold 2026-06-12).
6456 if let Some(res) = (|name: &str| -> Option<Option<String>> {
6457 if !is_nameref(name) {
6458 return None;
6459 }
6460 match resolve_nameref_name(name, None) {
6461 nameref_resolution::NotRef => None,
6462 nameref_resolution::Placeholder(..)
6463 | nameref_resolution::SelfRef
6464 | nameref_resolution::OutOfScope => Some(None),
6465 nameref_resolution::Target {
6466 name: t,
6467 subscript,
6468 pm,
6469 level,
6470 } => {
6471 let pm = match pm {
6472 Some(p) => p,
6473 None => return Some(None), // dangling target
6474 };
6475 // c:2261-2263 — fetchvalue drops an unset referent unless it
6476 // is DECLARED; a declared one still has its element read.
6477 let f = pm.node.flags as u32;
6478 if (f & PM_UNSET) != 0 && (subscript.is_none() || (f & PM_DECLARED) == 0) {
6479 return Some(None);
6480 }
6481 match subscript {
6482 None => {
6483 // Visible binding → recurse through the normal
6484 // read path (padding, gsu, specials all apply).
6485 let visible_level = paramtab()
6486 .read()
6487 .ok()
6488 .and_then(|tab| tab.get(&t).map(|p| p.level));
6489 if visible_level == Some(level) {
6490 Some(getsparam(&t))
6491 } else {
6492 // Hidden (old-chain) binding — read the clone.
6493 Some(param_scalar_value(&pm))
6494 }
6495 }
6496 Some(k) => Some(nameref_element_read(&pm, &t, &k)),
6497 }
6498 }
6499 }
6500 })(name)
6501 {
6502 return res;
6503 }
6504 // 2. Paramtab read — `(Value)gethashnode2(paramtab, name)`.
6505 // Walk the global paramtab for the named param, returning
6506 // `pm->u.str` for PM_SCALAR/PM_NAMEREF or `sepjoin(pm->u.arr)`
6507 // for PM_ARRAY (matches `getstrvalue` at params.c:2358).
6508 // c:2352 — set when the node is PM_ARRAY but its elements live behind a
6509 // MODULE getfn instead of `u.arr`; the dispatch runs after the read guard
6510 // below is dropped (see the arm that sets it).
6511 // c:Src/params.c:570-572 — getvalue reaches the node through
6512 // getparamnode, whose loadparamnode loads the owning module of a
6513 // PM_AUTOLOAD stub (`zmodload -ap MOD NAME`) before the value is read;
6514 // when the module does not define NAME the stub is gone and the read
6515 // fails. Taken before the read guard below: the load re-enters paramtab.
6516 let autoload_stub = paramtab()
6517 .read()
6518 .ok()
6519 .and_then(|t| t.get(name).filter(|p| (p.node.flags as u32 & PM_AUTOLOAD) != 0).cloned());
6520 if let Some(stub) = autoload_stub {
6521 // loadparamnode does not read its table argument (one paramtab).
6522 let loaded = newparamtable(0, "paramtab").and_then(|ht| loadparamnode(&ht, Some(stub), name));
6523 if loaded.is_none() {
6524 return None;
6525 }
6526 }
6527 let mut needs_partab_dispatch = false;
6528 if let Ok(tab) = paramtab().read() {
6529 if let Some(pm) = tab.get(name) {
6530 // c:Src/Modules/param_private.c:568-617 + c:678 — C swaps
6531 // `realparamtab->getnode` to getprivatenode when
6532 // zsh/param/private is active, so every lookup skips
6533 // private params belonging to an OUTER scope when read
6534 // from a DEEPER one (`private x` in f is invisible to g
6535 // called by f; g sees the global). zshrs's HashMap
6536 // paramtab has no getnode vtable, so call the canonical
6537 // walk at the lookup site. SAFETY: getprivatenode only
6538 // follows the `pm->old` chain read-only; the chain is
6539 // owned by this entry, which stays alive under the read
6540 // guard held for the rest of this block.
6541 let visible =
6542 crate::ported::modules::param_private::getprivatenode(&**pm as *const param); // c:678 getnode hook
6543 if visible.is_null() {
6544 return None; // c:609 walk exhausted — no visible node
6545 }
6546 let pm: ¶m = unsafe { &*visible };
6547 // c:Src/params.c:2335-2358 — `if (pm->node.flags & PM_UNSET)
6548 // return ""`. Unset specials kept in paramtab (PM_SPECIAL
6549 // retention path, c:3911) read as empty, not as their stale
6550 // u.val / u.str. Without this, `unset SECONDS; echo
6551 // "[$SECONDS]"` printed `[0]` instead of `[]` — the
6552 // PM_UNSET pm fell through to the integer path and
6553 // returned convbase(0). Bug #418 in docs/BUGS.md.
6554 if (pm.node.flags as u32 & PM_UNSET) != 0 {
6555 return None;
6556 }
6557 let t = PM_TYPE(pm.node.flags as u32);
6558 // c:2390-2538 — when PM_LEFT/PM_RIGHT_B/PM_RIGHT_Z + width
6559 // are set, getstrvalue (called from getsparam in C) applies
6560 // padding. Mirror that here so `$var` expansion respects
6561 // `typeset -Z N` / `-L N` / `-R N` even when the caller
6562 // didn't go through getstrvalue. Numeric prefix detection
6563 // (leading blanks/minus/0x/base#) follows params.rs:3156.
6564 let pad_flags = (pm.node.flags as u32) & (PM_LEFT | PM_RIGHT_B | PM_RIGHT_Z);
6565 // c:Src/params.c:2358 — getstrvalue for a PM_SCALAR
6566 // dispatches through `pm->gsu.s->getfn(pm)` so tied /
6567 // colon-array / special-callback scalars (PATH/path,
6568 // user `typeset -T` pairs) return their live computed
6569 // value rather than a stale cached `u_str`. Reads of
6570 // `u_str` for scalars without a GSU vtable (the common
6571 // case) fall through unchanged. Bug #24 in docs/BUGS.md.
6572 let raw: Option<String> = if t == PM_INTEGER {
6573 let base = if pm.base > 0 { pm.base } else { 10 };
6574 // c:2364 `convbase(buf, v->pm->gsu.i->getfn(v->pm), …)` — a
6575 // module integer (`$SRANDOM`) answers through its getfn.
6576 let val = pm.gsu_i.as_ref().map_or(pm.u_val, |g| (g.getfn)(pm));
6577 Some(convbase(val, base as u32))
6578 } else if t == PM_EFLOAT || t == PM_FFLOAT {
6579 Some(convfloat(pm.u_dval, pm.base, pm.node.flags as u32))
6580 } else if t == PM_SCALAR && pm.gsu_s.is_some() {
6581 pm.gsu_s.as_ref().map(|gsu| (gsu.getfn)(pm))
6582 } else if let Some(s) = pm.u_str.as_ref() {
6583 // c:2350-2357 — getstrvalue switches on PM_TYPE FIRST: a
6584 // PM_ARRAY node's scalar value is
6585 // `sepjoin(getvaluearr(v), NULL, 1)`, never a scalar field.
6586 // `getvaluearr` (c:724-732) reads the elements through
6587 // `v->pm->gsu.a->getfn(v->pm)` — `arrgetfn` (c:4054 `return
6588 // pm->u.arr`) for an ordinary array, but a MODULE getfn
6589 // (`reswordsgetfn`, Src/Modules/parameter.c:878) for the
6590 // zsh/parameter magic arrays, which carry no `u.arr` at all.
6591 // That is the same split `getaparam` spells out below.
6592 // Reading `u_str` for those returned the seeded PM_SPECIAL
6593 // stub's EMPTY string, so every colon modifier — whose test
6594 // is `vunset = isarr ? !*aval : !*val` (c:Src/subst.c:3188) —
6595 // saw a null value: `${reswords:-D}` / `${keymaps:+S}` /
6596 // `${(P)r:-D}` took the unset branch where zsh takes the set
6597 // one. Gated on "the stub is empty and this name really is a
6598 // PARTAB_ARRAY row" so no node that already answers keeps its
6599 // answer.
6600 if s.is_empty() && t == PM_ARRAY && pm.u_arr.is_none() {
6601 needs_partab_dispatch = crate::ported::modules::parameter::PARTAB_ARRAY
6602 .iter()
6603 .any(|e| e.name == name); // c:2352
6604 }
6605 if needs_partab_dispatch {
6606 None
6607 } else {
6608 Some(s.clone())
6609 }
6610 } else {
6611 match pm.u_arr.as_ref() {
6612 // c:2354 — stdarray gsu → arrgetfn (c:4054).
6613 // c:2353 — `s = sepjoin(ss, NULL, 1)`: the scalar view of
6614 // an array joins with `$IFS[1]`, not with a hardcoded
6615 // space (sepjoin's NULL sep, c:Src/utils.c:3928-3945).
6616 // This value is what every DQ pattern operator matches
6617 // against, so `IFS=-; a=(a b); print -r -- "${a#a}"` was
6618 // stripping from "a b" and printing " b" where zsh joins
6619 // to "a-b" first and prints "-b".
6620 Some(arr) => Some(crate::ported::utils::sepjoin(arr, None)),
6621 None => {
6622 // Same module-getfn dispatch as above for a stub that
6623 // carries neither string nor array storage.
6624 if t == PM_ARRAY {
6625 needs_partab_dispatch = crate::ported::modules::parameter::PARTAB_ARRAY
6626 .iter()
6627 .any(|e| e.name == name); // c:2352
6628 }
6629 None
6630 }
6631 }
6632 };
6633 if let Some(mut s) = raw {
6634 if pad_flags != 0 && pm.width > 0 {
6635 let fwidth = pm.width as usize;
6636 let numeric_pm =
6637 (pm.node.flags as u32 & (PM_INTEGER | PM_EFLOAT | PM_FFLOAT)) != 0;
6638 if pad_flags == PM_LEFT || pad_flags == (PM_LEFT | PM_RIGHT_Z) {
6639 let trimmed: &str = if pad_flags & PM_RIGHT_Z != 0 {
6640 s.trim_start_matches('0')
6641 } else {
6642 s.trim_start_matches(|c: char| c == ' ' || c == '\t')
6643 };
6644 let len = trimmed.chars().count();
6645 let take = len.min(fwidth);
6646 let mut out: String = trimmed.chars().take(take).collect();
6647 if fwidth > take {
6648 out.extend(std::iter::repeat(' ').take(fwidth - take));
6649 }
6650 s = out;
6651 } else if pad_flags & (PM_RIGHT_B | PM_RIGHT_Z) != 0 {
6652 let charlen = s.chars().count();
6653 if charlen < fwidth {
6654 let mut zero = true;
6655 let mut valprefend: usize = 0;
6656 if pad_flags & PM_RIGHT_Z != 0 {
6657 let bytes = s.as_bytes();
6658 let mut tpos = 0usize;
6659 while tpos < bytes.len()
6660 && (bytes[tpos] == b' ' || bytes[tpos] == b'\t')
6661 {
6662 tpos += 1;
6663 }
6664 if numeric_pm && tpos < bytes.len() && bytes[tpos] == b'-' {
6665 tpos += 1;
6666 }
6667 if (pm.node.flags as u32 & PM_INTEGER) != 0
6668 && tpos + 1 < bytes.len()
6669 && bytes[tpos] == b'0'
6670 && bytes[tpos + 1] == b'x'
6671 {
6672 tpos += 2;
6673 } else if (pm.node.flags as u32 & PM_INTEGER) != 0 {
6674 if let Some(hash_off) =
6675 bytes[tpos..].iter().position(|&b| b == b'#')
6676 {
6677 tpos += hash_off + 1;
6678 }
6679 }
6680 valprefend = tpos;
6681 if tpos == bytes.len() {
6682 zero = false;
6683 } else if !numeric_pm && !bytes[tpos].is_ascii_digit() {
6684 zero = false;
6685 }
6686 }
6687 let pad_char = if (pad_flags & PM_RIGHT_B) != 0 || !zero {
6688 ' '
6689 } else {
6690 '0'
6691 };
6692 let pad_count = fwidth - charlen;
6693 let prefix = &s[..valprefend];
6694 let rest = &s[valprefend..];
6695 let mut out = String::with_capacity(fwidth);
6696 out.push_str(prefix);
6697 out.extend(std::iter::repeat(pad_char).take(pad_count));
6698 out.push_str(rest);
6699 s = out;
6700 } else if charlen > fwidth {
6701 // c:Src/params.c:2496-2500 — right-justify
6702 // with charlen > fwidth: truncate from the
6703 // FRONT to fit fwidth codepoints. Bug #100
6704 // in docs/BUGS.md — the getsparam path had
6705 // the pad arm but missed the truncation
6706 // arm. The parallel getstrvalue path at
6707 // ~line 3355 has it; this is the
6708 // missing-symmetry fix.
6709 let skip = charlen - fwidth;
6710 s = s.chars().skip(skip).collect();
6711 }
6712 }
6713 }
6714 // c:2497-2506 — the tail of getstrvalue's VALFLAG_SUBST
6715 // block, immediately after the padding switch:
6716 // switch (v->pm->node.flags & (PM_LOWER | PM_UPPER)) {
6717 // case PM_LOWER: s = casemodify(s, CASMOD_LOWER); break;
6718 // case PM_UPPER:
6719 // if (!(v->pm->node.flags & PM_NAMEREF))
6720 // s = casemodify(s, CASMOD_UPPER);
6721 // break;
6722 // }
6723 // `switch` on the masked pair means BOTH bits set folds
6724 // neither way — bin_typeset clears the opposite bit
6725 // (c:Src/builtin.c:2731-2734) so that state is unreachable
6726 // from `typeset`, but a raw flag write could produce it.
6727 // PM_UPPER is skipped for a nameref because the stored
6728 // string is the *target's name*, not a value.
6729 match pm.node.flags as u32 & (PM_LOWER | PM_UPPER) {
6730 f if f == PM_LOWER => {
6731 s = casemodify(&s, CASMOD_LOWER); // c:2499
6732 }
6733 f if f == PM_UPPER => {
6734 if (pm.node.flags as u32 & PM_NAMEREF) == 0 {
6735 // c:2502
6736 s = casemodify(&s, CASMOD_UPPER); // c:2503
6737 }
6738 }
6739 _ => {}
6740 }
6741 return Some(s);
6742 }
6743 }
6744 }
6745 if needs_partab_dispatch {
6746 // c:2352-2354 — `ss = getvaluearr(v); s = sepjoin(ss, NULL, 1);` over
6747 // the module getfn's array. Dispatched here, outside the paramtab read
6748 // guard, because `partab_array_get` re-locks `paramtab` for its shadow
6749 // check — the same deferral `getaparam` documents below.
6750 return crate::vm_helper::partab_array_get(name).map(|a| a.join(" ")); // c:2354
6751 }
6752 // 3. Env fallback — C imports env into paramtab at init so the
6753 // read above would hit. If the import hasn't happened yet
6754 // (e.g. during very early init) fall back to the live env.
6755 env::var(name).ok()
6756}
6757
6758/// Port of `getsparam_u()` from `Src/params.c:3088`. C body
6759/// (c:3091-3094):
6760/// ```c
6761/// /* getsparam() returns pointer into global params table, so ... */
6762/// if ((s = getsparam(s)))
6763/// return unmeta(s); /* returns static pointer to copy */
6764/// return s;
6765/// ```
6766///
6767/// The previous Rust "port" was an entirely fabricated impl — it
6768/// took `Option<&mut value>` and gated on `PM_TYPE == PM_SCALAR`,
6769/// which matches no part of the C body. C just calls getsparam(s)
6770/// and unmeta's the resulting string. No callers existed because
6771/// no caller's type fit the bogus signature.
6772///
6773/// Real use case: locale setters (c:4847, c:4867, c:4882, c:4917)
6774/// call `getsparam_u("LC_ALL")` / `getsparam_u("LANG")` to read the
6775/// param as a Meta-stripped C string suitable for `setlocale`.
6776pub fn getsparam_u(s: &str) -> Option<String> {
6777 // c:3089
6778 // c:3092 — `if ((s = getsparam(s))) return unmeta(s);`
6779 getsparam(s).map(|v| unmeta(&v))
6780}
6781
6782/// Port of `getaparam()` from `Src/params.c:3100`.
6783///
6784/// C body:
6785/// ```c
6786/// struct value vbuf;
6787/// Value v;
6788/// if (!idigit(*s) && (v = getvalue(&vbuf, &s, 0)) &&
6789/// PM_TYPE(v->pm->node.flags) == PM_ARRAY)
6790/// return v->pm->gsu.a->getfn(v->pm);
6791/// return NULL;
6792/// ```
6793///
6794/// The previous Rust port was a fabrication: signature was
6795/// `Option<&mut value> -> Option<Vec<String>>`, taking an already-
6796/// resolved Value pointer rather than the C-canonical name string.
6797/// No caller used it because the bogus signature fit nothing — and
6798/// the in-tree `savematch` at modules/zutil.rs:30 hardcoded `a = None`
6799/// because the existing API couldn't be threaded through.
6800///
6801/// Real C use: name lookup. e.g. `getaparam("match")` returns the
6802/// `$match` array from the regex-match callouts (Modules/zutil.c:45).
6803pub fn getaparam(name: &str) -> Option<Vec<String>> {
6804 // c:3101
6805 // c:3107 — `if (idigit(*s))` reject digit-first names. C
6806 // `getvalue` would also reject these later, but the explicit
6807 // check matches C's flow.
6808 if name.starts_with(|c: char| c.is_ascii_digit()) {
6809 // c:3107
6810 return None;
6811 }
6812 // c:Src/params.c:570-575 — getvalue→fetchvalue→getparamnode
6813 // resolves PM_NAMEREF before the type check.
6814 // c:570-575 getparamnode → resolve_nameref: array-read
6815 // redirection through a nameref chain, INLINE (former
6816 // nameref_aread_redirect, single-caller fold 2026-06-12).
6817 if let Some(res) = (|name: &str| -> Option<Option<Vec<String>>> {
6818 if !is_nameref(name) {
6819 return None;
6820 }
6821 match resolve_nameref_name(name, None) {
6822 nameref_resolution::NotRef => None,
6823 nameref_resolution::Placeholder(..)
6824 | nameref_resolution::SelfRef
6825 | nameref_resolution::OutOfScope => Some(None),
6826 nameref_resolution::Target {
6827 name: t,
6828 subscript,
6829 pm,
6830 level,
6831 } => {
6832 let pm = match pm {
6833 Some(p) => p,
6834 None => return Some(None),
6835 };
6836 if (pm.node.flags as u32 & PM_UNSET) != 0 {
6837 return Some(None);
6838 }
6839 if subscript.is_some() {
6840 // c:3107-3110 — getvalue with bracks 0: fetchvalue's
6841 // reference slice leaves `[sub]` unparsed (c:2264-2267),
6842 // so c:2297 `if (!bracks && *s) return NULL` and the
6843 // read is not an array one.
6844 return Some(None);
6845 }
6846 if PM_TYPE(pm.node.flags as u32) != PM_ARRAY {
6847 return Some(None);
6848 }
6849 let visible_level = paramtab()
6850 .read()
6851 .ok()
6852 .and_then(|tab| tab.get(&t).map(|p| p.level));
6853 if visible_level == Some(level) {
6854 Some(getaparam(&t))
6855 } else {
6856 Some(pm.u_arr.clone())
6857 }
6858 }
6859 }
6860 })(name)
6861 {
6862 return res;
6863 }
6864 // c:3107-3109 — `getvalue(&vbuf, &s, 0)` resolves the name to a
6865 // paramtab entry. Then PM_TYPE check + `pm->u.arr` return.
6866 //
6867 // c:3107 is `v->pm->gsu.a->getfn(v->pm)` — a VTABLE dispatch, not a
6868 // raw `u.arr` read. For an ordinary array `gsu.a` is `stdarray_gsu`
6869 // whose getfn is `arrgetfn` (c:4054 `return pm->u.arr`), which is
6870 // what the `u_arr` branch below models. But the zsh/parameter magic
6871 // arrays (`reswords`, `dis_reswords`, `patchars`, `dis_patchars`,
6872 // `funcstack`, `dirstack`, `historywords`, …) carry a MODULE getfn
6873 // instead (`reswordsgetfn` at Src/Modules/parameter.c:878) and have
6874 // no `u.arr` at all — their value is computed on demand. zshrs keeps
6875 // those rows in the separate `PARTAB_ARRAY` table
6876 // (modules/parameter.rs:4644) rather than in a gsu slot on the pm,
6877 // so the dispatch has to be spelled out. `partab_array_get`
6878 // (vm_helper.rs:4998) is the single existing entry point for it —
6879 // it applies the same `magic_special_shadowed` filter + module gate
6880 // the parameter-expansion path uses (subst.rs:20646, :20749).
6881 //
6882 // Without this, `getaparam("reswords")` returned None because the
6883 // seeded PM_SPECIAL stub has `u_arr: None` (vm_helper.rs:5139), so
6884 // `compadd -k reswords` produced ZERO matches and `which <TAB>` lost
6885 // the entire reserved-words group.
6886 let mut needs_partab_dispatch = false;
6887 // c:4010-4013 `arrgetfn` — a PM_ARRAY node whose value slot is NULL is
6888 // served as the EMPTY array (`&nullarray`, c:4006), not as "no array".
6889 // Tracked so the PARTAB dispatch below still gets its chance first.
6890 let mut declared_empty_array = false;
6891 if let Ok(tab) = paramtab().read() {
6892 if let Some(pm) = tab.get(name) {
6893 // c:Src/Modules/param_private.c:678 — the getnode hook
6894 // applies to every paramtab lookup; same canonical walk
6895 // as getsparam (SAFETY: read-only old-chain walk under
6896 // the held read guard).
6897 let visible =
6898 crate::ported::modules::param_private::getprivatenode(&**pm as *const param);
6899 if visible.is_null() {
6900 return None;
6901 }
6902 let pm: ¶m = unsafe { &*visible };
6903 if PM_TYPE(pm.node.flags as u32) == PM_ARRAY {
6904 // c:3108
6905 // c:392-393,430 — `*`, `@` and `argv` are IPDEF9 nodes on
6906 // `&pparams`; their gsu.a getfn is `arrvargetfn`
6907 // (c:4231-4236), which reads the LIVE positional vector.
6908 // zshrs keeps that vector in `builtin::PPARAMS`; the node's
6909 // `u_arr` is stale, so `shift 2 argv` saw the wrong words.
6910 if matches!(name, "argv" | "@" | "*") {
6911 return Some(PPARAMS.lock().map(|p| p.clone()).unwrap_or_default());
6912 }
6913 if let Some(arr) = pm.u_arr.as_ref() {
6914 // c:3109 — gsu.a == stdarray_gsu → arrgetfn (c:4054).
6915 return Some(arr.clone());
6916 }
6917 // c:3107 — non-stdarray gsu.a: the value lives behind
6918 // the module getfn. Dispatch AFTER dropping the read
6919 // guard: `partab_array_get` re-locks `paramtab` for the
6920 // shadow check, and the getfns themselves read the
6921 // shell's tables.
6922 needs_partab_dispatch = crate::ported::modules::parameter::PARTAB_ARRAY
6923 .iter()
6924 .any(|e| e.name == name);
6925 declared_empty_array = !needs_partab_dispatch;
6926 }
6927 }
6928 }
6929 if needs_partab_dispatch {
6930 return crate::vm_helper::partab_array_get(name); // c:3107
6931 }
6932 if declared_empty_array {
6933 // c:4012 — `return pm->u.arr ? pm->u.arr : &nullarray;`. A
6934 // declared-but-unassigned `local -a` has a NULL slot (createparam
6935 // zshcalloc's it, c:1136) with stdarray_gsu installed (c:1158), so C
6936 // reads it back as a ZERO-LENGTH array. Returning None made such a
6937 // name look scalar to every caller.
6938 return Some(Vec::new());
6939 }
6940 None // c:3110
6941}
6942
6943/// Port of `gethparam()` from `Src/params.c:3115`.
6944///
6945/// C body:
6946/// ```c
6947/// struct value vbuf;
6948/// Value v;
6949/// if (!idigit(*s) && (v = getvalue(&vbuf, &s, 0)) &&
6950/// PM_TYPE(v->pm->node.flags) == PM_HASHED)
6951/// return paramvalarr(v->pm->gsu.h->getfn(v->pm), SCANPM_WANTVALS);
6952/// return NULL;
6953/// ```
6954///
6955/// Same fabricated-port family as the prior `getaparam`/`getsparam_u`
6956/// fixes: previous Rust sig took `Option<&mut value>` instead of the
6957/// canonical name string, with no real callers. Fixed sig + body
6958/// that resolves the name through paramtab and returns the values
6959/// vector when PM_HASHED.
6960///
6961/// NOTE: zshrs's paramtab stores hash-params via `pm->u_hash` (a
6962/// `HashTable` struct that's a generic bucket-array container). The
6963/// canonical C path threads through `gsu.h->getfn(pm)` → `paramvalarr`
6964/// which extracts the value side of each key-value pair. zshrs's
6965/// canonical assoc backing lives in `paramtab_hashed_storage` (an
6966/// IndexMap<String, String> keyed by param name); read the values
6967/// directly from there as the C macro's
6968/// `paramvalarr(hashgetfn(pm), SCANPM_WANTVALS)` resolves to a
6969/// values walk over the same backing.
6970pub fn gethparam(name: &str) -> Option<Vec<String>> {
6971 // c:3117
6972 if name.starts_with(|c: char| c.is_ascii_digit()) {
6973 // c:3122
6974 return None;
6975 }
6976 // c:3118 — `paramvalarr(pm->gsu.h->getfn(pm), SCANPM_WANTVALS)`: the
6977 // hash getter runs BEFORE the scan. For `$compstate` that is
6978 // `get_compstate` (`Src/Zle/complete.c:1357`), handing back an inner
6979 // hash whose NODES carry their own per-key gsu getters
6980 // (`nmatches_gsu`, `unambig_gsu`, `unambig_curs_gsu`,
6981 // `unambig_pos_gsu`, `insert_pos_gsu`, `listlines_gsu`,
6982 // `compqstack_gsu` — c:1236-1252), which fire during the scan. Those
6983 // ten values are computed live and stored nowhere.
6984 //
6985 // zshrs reads assoc values straight out of `paramtab_hashed_storage`,
6986 // so recompute them into that map here, at the same point C invokes
6987 // the getters. Without it `${(@kv)compstate}` — and therefore
6988 // `_lastcomp` (`_main_complete` sh:407) — had no `unambiguous`,
6989 // `unambiguous_cursor`, `unambiguous_positions`, `insert_positions`,
6990 // `list_lines`, `list_max`, `all_quotes`, `ignored` or `vared` entry
6991 // at all.
6992 if name == crate::ported::zle::complete::COMPSTATENAME {
6993 for k in crate::ported::zle::compcore::LIVE_COMPSTATE_KEYS {
6994 if let Some(v) = crate::ported::zle::compcore::get_compstate_str(k) {
6995 crate::ported::zle::compcore::set_compstate_str(k, &v);
6996 }
6997 }
6998 }
6999
7000 // c:Src/params.c:570-575 — nameref deref before the type check.
7001 let resolved = match crate::ported::params::resolve_nameref_name(name, None) {
7002 crate::ported::params::nameref_resolution::Target { name: t_, .. } => t_,
7003 _ => name.to_string(),
7004 };
7005 let name: &str = &resolved;
7006 // c:Src/params.c:717 — `paramvalarr(v->pm->gsu.h->getfn(v->pm),
7007 // v->scanflags)`. For a SPECIALPMDEF magic hash (`commands`,
7008 // `functions`, `builtins`, `parameters`, `jobtexts`, …) that `getfn`
7009 // hands back a FAKE HashTable whose `scantab` is the module's
7010 // `scanpm*` fn (Src/module.c createspecialhash), so the SCAN below —
7011 // not any stored map — is the backing. zshrs keeps ordinary assoc
7012 // contents in the name-keyed `paramtab_hashed_storage` map, and the
7013 // seeded row for a magic name left an EMPTY map there which answered
7014 // first and shadowed the scanfn: `compadd -k commands`
7015 // (Completion/Unix/Type/_path_commands sh:103) returned ZERO matches
7016 // in a fresh shell, so command-name completion offered builtins only
7017 // until some other `$commands` read had gone through the scanfn
7018 // (which runs `fillcmdnamtable` under HASH_LIST_ALL,
7019 // Src/Modules/parameter.c:253). A `local -A commands` shadow makes
7020 // `magic_special_shadowed` true and the map wins again.
7021 //
7022 // Computed BEFORE the `paramtab()` read below: `magic_special_shadowed`
7023 // takes its own `paramtab().read()`, and a recursive read while a
7024 // writer is queued deadlocks on a writer-preferring RwLock.
7025 let magic_ = crate::ported::modules::parameter::PARTAB
7026 .iter()
7027 .any(|e_| e_.name == name)
7028 && !crate::vm_helper::magic_special_shadowed(name);
7029 // !!! RUST-ONLY LOCK NOTE (C has no locks here) !!!
7030 // Resolve visibility + type under `paramtab()`, then DROP the guard:
7031 // the PARTAB scan below reaches `createparam`, which wants
7032 // `paramtab().write()`, and a write request from a thread already
7033 // holding a read deadlocks. Same reasoning as `gethkparam`.
7034 //
7035 // c:Src/Modules/param_private.c:678 — getnode hook; same canonical
7036 // walk as getsparam/getaparam. (Values below still come from the
7037 // name-keyed hashed storage — a single slot per name; per-scope
7038 // assoc shadowing is a storage-model limitation predating this walk.)
7039 let (hidden_, is_hashed_) = match paramtab().read() {
7040 Ok(tab) => match tab.get(name) {
7041 Some(pm) => {
7042 let visible =
7043 crate::ported::modules::param_private::getprivatenode(&**pm as *const param);
7044 if visible.is_null() {
7045 (true, false)
7046 } else {
7047 let pm: ¶m = unsafe { &*visible };
7048 (false, PM_TYPE(pm.node.flags as u32) == PM_HASHED)
7049 }
7050 }
7051 None => (false, false),
7052 },
7053 Err(_) => (false, false),
7054 };
7055 if hidden_ {
7056 return None;
7057 }
7058 {
7059 {
7060 if is_hashed_ {
7061 // c:3123
7062 // c:3124 — `paramvalarr(hashgetfn(pm), SCANPM_WANTVALS)`.
7063 // Read values directly from the canonical hashed-storage
7064 // backing — IndexMap iteration matches C's hashtable
7065 // walk order (insertion-stable). When the storage hasn't
7066 // been populated (empty hash, fresh declaration), return
7067 // an empty Vec so the C "param exists, no entries" shape
7068 // is preserved (vs returning None which means "param
7069 // doesn't exist").
7070 if !magic_ {
7071 let store = paramtab_hashed_storage().lock().ok()?;
7072 if let Some(m) = store.get(name) {
7073 return Some(m.values().cloned().collect()); // c:3124
7074 }
7075 }
7076 // c:3124 — for SPECIALPMDEF magic hashes (jobtexts/jobstates/
7077 // jobdirs/parameters/commands/…, Src/Modules/parameter.c) the
7078 // `getfn` is the module's scan fn, not hashgetfn, so there is
7079 // no paramtab_hashed_storage backing to read. `gethkparam`
7080 // below already falls back to the partab row's scanfn for the
7081 // KEYS; without the mirror image here the VALUES came back
7082 // empty, and every caller that pairs the two got keys with
7083 // blank values. `_jobs` is the visible one: with `sleep 300 &`
7084 // running it read `jobtexts` as [("1", "")], so
7085 // `kill %<TAB>` added a bare `%` where zsh adds `%sleep` —
7086 // and `kill %s<TAB>` deleted the typed `s`.
7087 if let Some(vals) = (|| -> Option<Vec<String>> {
7088 let e_ = crate::ported::modules::parameter::PARTAB
7089 .iter()
7090 .find(|e_| e_.name == name)?;
7091 if let Some(m_) = e_.module {
7092 if !crate::ported::module::MODULESTAB
7093 .lock()
7094 .map(|t| t.is_loaded(m_))
7095 .unwrap_or(false)
7096 {
7097 return None;
7098 }
7099 }
7100 thread_local! {
7101 static VALS_: std::cell::RefCell<Vec<String>> =
7102 const { std::cell::RefCell::new(Vec::new()) };
7103 }
7104 fn cb_(pm: &crate::ported::zsh_h::param, _f: i32) {
7105 // c:665-670 — `paramvals[numparamvals] =
7106 // getstrvalue(&v)`, the same read scanparamvals does.
7107 VALS_.with(|v| v.borrow_mut().push(strgetfn(pm)));
7108 }
7109 crate::vm_helper::mark_module_param_used(name);
7110 VALS_.with(|v| v.borrow_mut().clear());
7111 (e_.scanfn)(
7112 std::ptr::null_mut(),
7113 Some(cb_),
7114 crate::ported::zsh_h::SCANPM_WANTVALS as i32,
7115 );
7116 Some(VALS_.with(|v| v.borrow().clone()))
7117 })() {
7118 return Some(vals); // c:3124
7119 }
7120 return Some(Vec::new()); // c:3124
7121 }
7122 }
7123 }
7124 None // c:3125
7125}
7126
7127/// Port of `gethkparam()` from `Src/params.c:3130`.
7128/// Same as `gethparam` but `paramvalarr(..., SCANPM_WANTKEYS)`.
7129pub fn gethkparam(name: &str) -> Option<Vec<String>> {
7130 // c:3131
7131 if name.starts_with(|c: char| c.is_ascii_digit()) {
7132 // c:3136
7133 return None;
7134 }
7135 // c:3138 — `paramvalarr(pm->gsu.h->getfn(pm), SCANPM_WANTKEYS)`: same
7136 // getfn-before-scan contract as `gethparam` above; see the note there
7137 // for why `$compstate`'s gsu-backed keys have to be recomputed here.
7138 if name == crate::ported::zle::complete::COMPSTATENAME {
7139 for k in crate::ported::zle::compcore::LIVE_COMPSTATE_KEYS {
7140 if let Some(v) = crate::ported::zle::compcore::get_compstate_str(k) {
7141 crate::ported::zle::compcore::set_compstate_str(k, &v);
7142 }
7143 }
7144 }
7145 // c:Src/params.c:717 — same magic-hash rule as `gethparam` above: the
7146 // scanfn IS the backing for a PARTAB name, so an empty
7147 // `paramtab_hashed_storage` row must not answer for it. Computed
7148 // before the `paramtab()` read for the same recursive-lock reason.
7149 let magic_ = crate::ported::modules::parameter::PARTAB
7150 .iter()
7151 .any(|e_| e_.name == name)
7152 && !crate::vm_helper::magic_special_shadowed(name);
7153 // !!! RUST-ONLY LOCK NOTE (C has no locks here) !!!
7154 // Read the TYPE under `paramtab()`, then DROP the guard before the
7155 // storage read or the PARTAB scan below. The scan's
7156 // `mark_module_param_used` → `materialize_module_param` →
7157 // `createparam` takes `paramtab().write()`, and asking for a write
7158 // while this thread still holds a read deadlocks on the
7159 // writer-preferring queue lock (`std::sys::sync::rwlock::queue`).
7160 // The scan arm was only reachable for names with no
7161 // `paramtab_hashed_storage` row before, so the hazard sat latent
7162 // until the magic-hash routing above started using it.
7163 let is_hashed_ = paramtab()
7164 .read()
7165 .ok()
7166 .and_then(|tab| {
7167 tab.get(name)
7168 .map(|pm| PM_TYPE(pm.node.flags as u32) == PM_HASHED) // c:3137
7169 })
7170 .unwrap_or(false);
7171 {
7172 {
7173 if is_hashed_ {
7174 // c:3137
7175 // c:3138 — `paramvalarr(pm->gsu.h->getfn(pm),
7176 // SCANPM_WANTKEYS)`. Same backing as gethparam —
7177 // return keys instead of values. Empty-storage
7178 // fallback identical: Some(empty Vec) for "exists,
7179 // no entries" shape.
7180 if !magic_ {
7181 let store = paramtab_hashed_storage().lock().ok()?;
7182 if let Some(m) = store.get(name) {
7183 return Some(m.keys().cloned().collect()); // c:3138
7184 }
7185 }
7186 // c:3138 — for SPECIALPMDEF magic hashes (parameters/
7187 // options/commands/…, Src/Modules/parameter.c) the
7188 // `getfn` is the module's scan fn, not hashgetfn;
7189 // there's no paramtab_hashed_storage backing. The
7190 // partab_scan_keys port (vm_helper.rs:3486) is that
7191 // scan. Without this, `${(k)parameters[PATH]}` saw an
7192 // empty key set and returned "" where zsh prints PATH.
7193 if let Some(keys) = (|| -> Option<Vec<String>> {
7194 // c:Src/Modules/parameter.c — special-hash scantab
7195 // key enumeration via the partab[] row's scanfn
7196 // (former bridge partab_scan_keys, inlined; the
7197 // captureless callback is the C ScanFunc ABI).
7198 let e_ = crate::ported::modules::parameter::PARTAB
7199 .iter()
7200 .find(|e_| e_.name == name)?;
7201 if let Some(m_) = e_.module {
7202 if !crate::ported::module::MODULESTAB
7203 .lock()
7204 .map(|t| t.is_loaded(m_))
7205 .unwrap_or(false)
7206 {
7207 return None;
7208 }
7209 }
7210 thread_local! {
7211 static KEYS_: std::cell::RefCell<Vec<String>> =
7212 const { std::cell::RefCell::new(Vec::new()) };
7213 }
7214 fn cb_(pm: &crate::ported::zsh_h::param, _f: i32) {
7215 KEYS_.with(|k| k.borrow_mut().push(pm.node.nam.clone()));
7216 }
7217 // c:Src/params.c:589-594 getparamnode → c:563-585
7218 // loadparamnode — resolving the NAME runs
7219 // ensurefeature(mn, "p:", nam), so a PM_AUTOLOAD stub stops
7220 // reading "undefined" (c:Src/Modules/parameter.c:48-50).
7221 // The non-inlined `vm_helper::partab_scan_keys` marks here;
7222 // this inlined copy dropped it, so `compadd -Qk builtins` /
7223 // `compadd -k functions` (Completion/Zsh/Type/_command_names
7224 // sh:34-35) left both names typed "undefined".
7225 crate::vm_helper::mark_module_param_used(name);
7226 KEYS_.with(|k| k.borrow_mut().clear());
7227 // c:3138 — `paramvalarr(…, SCANPM_WANTKEYS)`.
7228 (e_.scanfn)(
7229 std::ptr::null_mut(),
7230 Some(cb_),
7231 crate::ported::zsh_h::SCANPM_WANTKEYS as i32,
7232 );
7233 Some(KEYS_.with(|k| k.borrow().clone()))
7234 })() {
7235 return Some(keys); // c:3138
7236 }
7237 return Some(Vec::new()); // c:3138
7238 }
7239 }
7240 }
7241 None // c:3139
7242}
7243
7244/// Port of `check_warn_pm()` from `Src/params.c:3158`.
7245///
7246/// C body emits the WARN_CREATE_GLOBAL / WARN_NESTED_VAR
7247/// diagnostic when a function-local creates/passes a non-local
7248/// param with the matching shell options set.
7249///
7250/// The previous Rust port handled the GATE logic correctly but
7251/// SKIPPED the diagnostic emit, claiming the `funcstack` global
7252/// wasn't ported. But `FUNCSTACK`
7253/// IS ported (`Mutex<Vec<funcstack>>`). Wire the walk:
7254/// for (i = funcstack; i; i = i->prev)
7255/// if (i->tp == FS_FUNC) {
7256/// msg = created ?
7257/// "%s parameter %s created globally in function %s" :
7258/// "%s parameter %s set in enclosing scope in function %s";
7259/// zwarn(msg, pmtype, pm->node.nam, i->name);
7260/// break;
7261/// }
7262///
7263/// Without the diagnostic, `setopt WARN_CREATE_GLOBAL` had no
7264/// observable effect — the whole point of the option is the
7265/// user-visible warning.
7266pub fn check_warn_pm(pm: ¶m, pmtype: &str, created: i32, may_warn_about_nested_vars: i32) {
7267 // c:3160
7268 if may_warn_about_nested_vars == 0 && created == 0 {
7269 // c:3165
7270 return;
7271 }
7272 // `locallevel` is the canonical `pub static` above (port of
7273 // params.c:54). `forklevel` is the ported global at vm_helper
7274 // (port of exec.c:1052) set to locallevel at every entersubsh().
7275 let cur_local: i32 = locallevel.load(Ordering::Relaxed);
7276 let forklevel: i32 = FORKLEVEL.load(Ordering::Relaxed); // c:1052 (Src/exec.c)
7277 if created != 0 && isset(WARNCREATEGLOBAL) {
7278 // c:3168
7279 if cur_local <= forklevel || pm.level != 0 {
7280 // c:3169
7281 return;
7282 }
7283 } else if created == 0 && isset(WARNNESTEDVAR) {
7284 // c:3171
7285 if pm.level >= cur_local {
7286 // c:3172
7287 return;
7288 }
7289 } else {
7290 return;
7291 }
7292 if (pm.node.flags as u32 & (PM_SPECIAL | PM_NAMEREF)) != 0 {
7293 // c:3177
7294 return;
7295 }
7296 // c:3180-3190 — walk funcstack, emit zwarn at first FS_FUNC.
7297 let stack = match FUNCSTACK.lock() {
7298 Ok(s) => s,
7299 Err(_) => return,
7300 };
7301 for frame in stack.iter().rev() {
7302 // c:3180 walk most-recent-first
7303 if frame.tp == FS_FUNC {
7304 // c:3181 FS_FUNC
7305 let msg = if created != 0 {
7306 // c:3185
7307 format!(
7308 "{} parameter {} created globally in function {}",
7309 pmtype, pm.node.nam, frame.name
7310 )
7311 } else {
7312 // c:3187
7313 format!(
7314 "{} parameter {} set in enclosing scope in function {}",
7315 pmtype, pm.node.nam, frame.name
7316 )
7317 };
7318 zwarn(&msg); // c:3189
7319 break; // c:3190
7320 }
7321 }
7322}
7323
7324// intgetfn / strgetfn drift wrappers removed — replaced below with
7325// real C-shape ports `intgetfn(pm: ¶m) -> i64` (Src/params.c:3993)
7326// and `strgetfn(pm: ¶m) -> String` (Src/params.c:4029) that read
7327// directly from the union fields `pm->u.val` / `pm->u.str`.
7328
7329// ---------------------------------------------------------------------------
7330// Tests
7331// ---------------------------------------------------------------------------
7332
7333// ===========================================================
7334// Methods moved verbatim from src/ported/vm_helper because their
7335// C counterpart's source file maps 1:1 to this Rust module.
7336// Phase: params
7337// ===========================================================
7338
7339// BEGIN moved-from-exec-rs
7340// (impl ShellExecutor block moved to src/exec_shims.rs — see file marker)
7341
7342// END moved-from-exec-rs
7343
7344// ===========================================================
7345// Free ported moved verbatim from src/ported/vm_helper.
7346// ===========================================================
7347// BEGIN moved-from-exec-rs (free ported)
7348/// Subscript-argument result.
7349///
7350/// `Flags` carries the parsed flag chars and the remaining subscript
7351/// text (the pattern after `(...)`); the caller dispatches the
7352/// search itself. `Value` is the result of an in-getarg array/hash
7353/// pattern search — direct port of getarg's pprog/pattry arm at
7354/// Src/params.c:1672-1719 (array) and 1581-1660 (hash).
7355// `enum getarg_out` is a Rust extension to express the dual-mode
7356// return of `getarg`. C `getarg` (`Src/params.c:1367`) writes back
7357// via out-pointers (`int *inv`, `Value v`, `zlong *w`, ...) and
7358// returns `int`. The Rust port collapses those into one sum-typed
7359// return: `Flags` carries the parsed flag chars + remaining
7360// subscript when no search ran; `Value` carries the search result
7361// from the pprog/pattry arms at c:1581-1660 (hash) / c:1672-1719
7362// (array). Naming kept lowercase to mark this as a port-shape helper
7363// rather than a C-mirrored struct.
7364/// `getarg_out` — see variants.
7365#[allow(non_camel_case_types)]
7366pub enum getarg_out<'a> {
7367 Flags { flags: &'a str, rest: &'a str },
7368 Value(Value),
7369}
7370
7371/// Port of `assignsparam(char *s, char *val, int flags)` from `Src/params.c:3193`. C signature:
7372/// `mod_export Param assignsparam(char *s, char *val, int flags)`.
7373///
7374/// `s` may carry an embedded `[...]` subscript (matching C's
7375/// `strchr(s, '[')` parse). The function operates on the global
7376/// `paramtab` (Src/params.c:515), creating/mutating `Param`
7377/// entries in place. Branches preserved 1:1 with C:
7378/// - c:3203 `isident(s)` — reject non-identifier names.
7379/// - c:3209 `queue_signals()`.
7380/// - c:3210 subscripted path: c:3212 `getvalue` lookup,
7381/// c:3213 `createparam(t, PM_ARRAY)` on miss, c:3216
7382/// PM_READONLY guard, c:3227 ASSPM_WARN drop, c:3228 clear
7383/// PM_DEFAULTED, c:3231 `v = NULL` then re-dispatch by type.
7384/// - c:3232 non-subscripted: c:3233 `getvalue` → c:3234
7385/// `createparam(t, PM_SCALAR)`; c:3236-3250 array/hash type-flip
7386/// to PM_SCALAR (when not PM_SPECIAL|PM_TIED, not KSHARRAYS,
7387/// not ASSPM_AUGMENT) via `resetparam(v->pm, PM_SCALAR)`.
7388/// - c:3258 PM_NAMEREF → c:3259 `valid_refname(val, flags)` guard.
7389/// - c:3269 clear PM_DEFAULTED.
7390/// - c:3343 `assignstrvalue(v, val, flags)`.
7391/// - c:3344 `unqueue_signals()`; c:3345 return v->pm.
7392///
7393/// `+=` (ASSPM_AUGMENT) and `check_warn_pm` (c:3266-3268) are handled
7394/// in the branches of the body below; the parameter table is the typed
7395/// `hashtable_nodes<Param>` store (see PARAMTAB_INNER).
7396/// c:Src/params.c:395-422 — the IPDEF8 PM_TIED colon-array pairs. A
7397/// SUBSCRIPT modification of the ARRAY side (`path[N]=x`, `path[i,j]=(…)`,
7398/// `unset path[N]`) must re-derive the tied SCALAR side, exactly as C's
7399/// array setfn does after setarrvalue rebuilds the array. zshrs lacks the
7400/// per-name GSU vtable, so the subscript paths carry the tie explicitly via
7401/// this const map (the mirror of the scalar→array cascade); each site joins
7402/// the updated array with `:` and writes the scalar through assignsparam.
7403pub(crate) const TIED_COLON_ARRAYS: &[(&str, &str)] = &[
7404 ("path", "PATH"),
7405 ("fpath", "FPATH"),
7406 ("manpath", "MANPATH"),
7407 ("cdpath", "CDPATH"),
7408 ("psvar", "PSVAR"),
7409 ("module_path", "MODULE_PATH"),
7410 ("fignore", "FIGNORE"),
7411 ("mailpath", "MAILPATH"),
7412 // c:Src/Modules/watch.c:697-701 — zsh/watch's pair, PM_TIED since
7413 // upstream b2fb112ea6 ("54383: Fix WATCH/watch tying").
7414 ("watch", "WATCH"),
7415];
7416
7417/// Port of `assignsparam()` from `Src/params.c:3193`.
7418pub fn assignsparam(s: &str, val: &str, flags: i32) -> Option<Param> {
7419 // c:3203 `if (!isident(s)) { zerr; errflag |= ERRFLAG_ERROR; return NULL; }`
7420 if !isident(s) {
7421 zerr(&format!("not an identifier: {}", s)); // c:3204
7422 errflag.fetch_or(
7423 // c:3206
7424 ERRFLAG_ERROR,
7425 Ordering::Relaxed,
7426 );
7427 return None; // c:3207
7428 }
7429 // !!! RUST-ONLY: provenance tap. Records the scalar write against
7430 // the lineage ledger when `s` is a tracked name. Placed at the
7431 // funnel head rather than at one of the 30-odd exits — a write
7432 // rejected further down (read-only variable) therefore shows in the
7433 // lineage as an attempted assign, which is documented in
7434 // docs/PROVENANCE.md. Costs one relaxed atomic load otherwise.
7435 if crate::provenance::active() {
7436 let kind = if (flags & ASSPM_AUGMENT) != 0 {
7437 "append"
7438 } else {
7439 "assign"
7440 };
7441 crate::provenance::on_param_write(s, kind, val);
7442 }
7443 // !!! WARNING: RUST-ONLY ARM — ZLE GSU adapter !!!
7444 // C's makezleparams (Src/Zle/zle_params.c:194) installs LIVE GSU
7445 // setters for the editing specials; zshrs snapshots copies into
7446 // the paramtab (extensions/zle_param_sync.rs), so sequential
7447 // widget mutations read STALE peers (`LBUFFER=tpl; CURSOR=n;
7448 // RBUFFER=${RBUFFER:len}` scrambled zsh-expand's multiline
7449 // snippet). Route these writes through the live editor and let
7450 // live_write re-publish the derived family. Only fires inside an
7451 // active widget scope; live_write's guard blocks its own
7452 // re-publish writes.
7453 // ASSPM_AUGMENT must be resolved BEFORE the live write: `+=`
7454 // routes here with only the APPENDED text as `val`, and writing
7455 // that raw would ASSIGN it (`LBUFFER+=\"` in zsh-autopair's
7456 // _ap-self-insert nuked everything typed before the quote —
7457 // `echo hi "` collapsed to `""`). C never has this problem: its
7458 // augment concatenation happens before gsu.s->setfn dispatch
7459 // (Src/params.c:2742-2748). Concatenate against the LIVE editor
7460 // value (CURSOR augments arithmetically, matching PM_INTEGER).
7461 let zle_special = matches!(s, "BUFFER" | "LBUFFER" | "RBUFFER" | "CURSOR");
7462 let zle_augmented: String;
7463 let zle_val: &str = if zle_special && (flags & ASSPM_AUGMENT) != 0 {
7464 use crate::ported::zle::zle_params as zp;
7465 zle_augmented = match s {
7466 "BUFFER" => format!("{}{}", zp::get_buffer(), val),
7467 "LBUFFER" => format!("{}{}", zp::get_lbuffer(), val),
7468 "RBUFFER" => format!("{}{}", zp::get_rbuffer(), val),
7469 // c:2775-2778 — integer augment is arithmetic add. The added
7470 // value is an ARITHMETIC EXPRESSION, not a literal: `CURSOR+=n`
7471 // has to evaluate `n` the way any PM_INTEGER assignment does.
7472 _ => {
7473 let old = zp::get_cursor() as i64;
7474 let add = crate::ported::math::mathevali(val.trim()).unwrap_or(0);
7475 (old + add).to_string()
7476 }
7477 };
7478 &zle_augmented
7479 } else {
7480 val
7481 };
7482 // CURSOR is PM_INTEGER (zle_params.rs:70, c:147), so its RHS is an
7483 // ARITHMETIC EXPRESSION — `CURSOR=end` means "the value of end", not the
7484 // string "end". The three string specials above pass through untouched,
7485 // but routing CURSOR to live_write handed it the raw word, which parsed
7486 // as 0. MARK is not in this list and so kept going down the PM_INTEGER
7487 // path, which is why `MARK=found; CURSOR=end` — the last two lines of
7488 // zsh's own select-quoted — set the mark correctly and the cursor to 0,
7489 // making every quote/bracket text object select from the mark to the
7490 // start of the line.
7491 let zle_evaluated: Option<String> = if s == "CURSOR" {
7492 // A bad expression is not a silent 0: fall through to the ordinary
7493 // assignment path, which reports it the way any arithmetic error is.
7494 crate::ported::math::mathevali(zle_val.trim())
7495 .ok()
7496 .map(|n| n.to_string())
7497 } else {
7498 None
7499 };
7500 let zle_ok = s != "CURSOR" || zle_evaluated.is_some();
7501 let zle_val: &str = zle_evaluated.as_deref().unwrap_or(zle_val);
7502 if zle_special && zle_ok && crate::zle_param_sync::live_write(s, zle_val) {
7503 return getsparam(s).map(|_| {
7504 Box::new(crate::ported::zsh_h::param {
7505 node: crate::ported::zsh_h::hashnode {
7506 next: None,
7507 nam: s.to_string(),
7508 flags: 0,
7509 },
7510 u_data: 0,
7511 u_tied: None,
7512 u_arr: None,
7513 u_str: Some(zle_val.to_string()),
7514 u_val: 0,
7515 u_dval: 0.0,
7516 u_hash: None,
7517 gsu_s: None,
7518 gsu_i: None,
7519 gsu_f: None,
7520 gsu_a: None,
7521 gsu_h: None,
7522 base: 0,
7523 width: 0,
7524 env: None,
7525 ename: None,
7526 old: None,
7527 level: 0,
7528 })
7529 });
7530 }
7531 // c:3233/3252 — `getvalue(&vbuf, &s, 1)` routes through
7532 // fetchvalue which resolves PM_NAMEREF chains (c:2247-2270).
7533 // Mirror by redirecting the assignment to the resolved target.
7534 {
7535 let base = s.split('[').next().unwrap_or(s);
7536 if crate::ported::params::is_nameref(base) {
7537 match crate::ported::params::resolve_nameref_name(base, None) {
7538 crate::ported::params::nameref_resolution::SelfRef => {
7539 // zerr emitted inside the walk (c:6341-6343).
7540 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
7541 return None;
7542 }
7543 crate::ported::params::nameref_resolution::OutOfScope => {
7544 // c:1108-1118 — createparam refuses the existing
7545 // ref; assignment fails (status 1, no errflag —
7546 // c:3255 is commented out in the C source).
7547 return None;
7548 }
7549 crate::ported::params::nameref_resolution::Placeholder(last, plevel) => {
7550 // c:3233 — `fetchvalue(&vbuf, &t, 1, SCANPM_ASSIGNING)`
7551 // is handed the RESOLVED endpoint by `getparamnode`
7552 // (c:570-575) and then applies c:2262-2263
7553 // if (!pm || ((pm->node.flags & PM_UNSET) &&
7554 // !(pm->node.flags & PM_DECLARED)))
7555 // return NULL;
7556 // A chain ending on a reference that `unset -n` left
7557 // `PM_UNSET` with PM_DECLARED cleared therefore reads
7558 // as ABSENT and takes c:3234 `createparam(t,
7559 // PM_SCALAR)`, whose c:1120-1123 arm retargets
7560 // `oldpm` to that endpoint — the value becomes an
7561 // ordinary scalar ON THE ENDPOINT, not a new refname.
7562 // Re-entering through the endpoint's own name is the
7563 // name-keyed equivalent of C's pointer retarget.
7564 {
7565 let endpoint_visible = paramtab()
7566 .read()
7567 .ok()
7568 .and_then(|t| {
7569 let mut cur = t.get(&last).map(|b| &**b);
7570 while let Some(p) = cur {
7571 if p.level == plevel {
7572 let f = p.node.flags as u32;
7573 return Some((f & PM_UNSET) == 0 || (f & PM_DECLARED) != 0);
7574 }
7575 cur = p.old.as_deref();
7576 }
7577 None
7578 })
7579 .unwrap_or(true); // c:2262-2263
7580 if !endpoint_visible {
7581 // c:1062-1084 + c:1117-1120 — createparam is
7582 // called with the REFERENCE's name; it takes
7583 // the unset endpoint as `oldpm`, and a private's
7584 // PM_RO_BY_DESIGN rejects the write under that
7585 // name (Src/Modules/param_private.c:174).
7586 let endpoint_ro = paramtab().read().ok().is_some_and(|t| {
7587 let mut cur = t.get(&last).map(|b| &**b);
7588 while let Some(p) = cur {
7589 if p.level == plevel {
7590 return (p.node.flags as u32 & PM_RO_BY_DESIGN) != 0;
7591 }
7592 cur = p.old.as_deref();
7593 }
7594 false
7595 });
7596 if endpoint_ro {
7597 let refnam = s.split('[').next().unwrap_or(s);
7598 zerr(&format!("{}: can't modify read-only parameter", refnam)); // c:1118
7599 return None;
7600 }
7601 let mut new_s = last.clone();
7602 if let Some(us) = s.find('[').map(|i| &s[i..]) {
7603 new_s.push_str(us);
7604 }
7605 return assignsparam(&new_s, val, flags);
7606 }
7607 }
7608 // Chain ends at a placeholder/unset ref: the value
7609 // becomes its new refname (assignstrvalue
7610 // PM_NAMEREF arm, c:2712-2717 + c:3258).
7611 // assignstrvalue PM_NAMEREF arm, INLINE per Src/params.c:2690-2720
7612 // (former vm_helper::nameref_assign_refname,
7613 // single-caller inline 2026-06-12):
7614 // c:2696-2697 read-only; c:3258-3264 valid_refname;
7615 // c:2717 SETREFNAME; c:2845 setscope.
7616 return (|| -> Option<Param> {
7617 let last_n: &str = &last;
7618 let snap = {
7619 let tab = paramtab().read().ok()?;
7620 let mut cur = tab.get(last_n).map(|b| &**b);
7621 let mut found = None;
7622 while let Some(p) = cur {
7623 if p.level == plevel {
7624 found = Some((p.node.flags, p.node.nam.clone()));
7625 break;
7626 }
7627 cur = p.old.as_deref();
7628 }
7629 found
7630 };
7631 let (flags, nam) = snap?;
7632 if (flags as u32 & PM_READONLY) != 0 {
7633 // c:2696-2697
7634 zerr(&format!("read-only variable: {}", nam));
7635 return None;
7636 }
7637 if !val.is_empty() && !valid_refname(val, flags) {
7638 // c:3219-3226
7639 zerr(&format!("invalid variable name: {}", val));
7640 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
7641 return None;
7642 }
7643 {
7644 let mut tab = paramtab().write().ok()?;
7645 // The placeholder may be hidden under a same-name
7646 // local (upscope walked past it): write that node.
7647 let mut pm: &mut param = tab.get_mut(last_n)?.as_mut();
7648 while pm.level != plevel {
7649 pm = pm.old.as_deref_mut()?;
7650 }
7651 pm.base = 0; // c:6376 shape (rebind resets scope info)
7652 pm.width = 0;
7653 pm.u_str = Some(val.to_string()); // c:2717 SETREFNAME
7654 pm.node.flags &= !(PM_DEFAULTED as i32); // c:3269 + c:2712
7655 }
7656 // c:2845 — setscope(v->pm): self-reference detection + base.
7657 if setscope_by_name(last_n, Some(plevel)) != 0 {
7658 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
7659 return None;
7660 }
7661 let tab = paramtab().read().ok()?;
7662 let mut cur = tab.get(last_n).map(|b| &**b);
7663 while let Some(p) = cur {
7664 if p.level == plevel {
7665 return Some(Box::new(p.clone()));
7666 }
7667 cur = p.old.as_deref();
7668 }
7669 None
7670 })();
7671 }
7672 crate::ported::params::nameref_resolution::Target {
7673 name: t,
7674 subscript: rsub,
7675 pm: rpm,
7676 level,
7677 } => {
7678 // Rebuild the target expression: ref subscript
7679 // first (REFSLICE, c:2264-2268), then any caller
7680 // subscript.
7681 let user_sub = s.find('[').map(|i| &s[i..]);
7682 // Hidden (old-chain) binding — the upscope walk
7683 // (c:6455) resolved past the visible binding;
7684 // write through the chain node directly.
7685 if rsub.is_none() && user_sub.is_none() && rpm.is_some() {
7686 let visible_level = paramtab()
7687 .read()
7688 .ok()
7689 .and_then(|tb| tb.get(&t).map(|p| p.level));
7690 if visible_level != Some(level) {
7691 // upscope write (c:6455 landed on a pm->old
7692 // node), INLINE (former nameref_hidden_scalar_assign).
7693 return (|name: &str, level: i32, val: &str| -> Option<Param> {
7694 let mut tab = paramtab().write().ok()?;
7695 let mut node: &mut param = tab.get_mut(name)?.as_mut();
7696 while node.level != level {
7697 node = node.old.as_mut()?.as_mut();
7698 }
7699 if (node.node.flags as u32 & PM_READONLY) != 0 {
7700 drop(tab); // zerr redraws ZLE, which reads paramtab
7701 zerr(&format!("read-only variable: {}", name)); // c:2696
7702 return None;
7703 }
7704 let t = PM_TYPE(node.node.flags as u32);
7705 // c:3178-3184 — an array (not `+=`) or hash
7706 // target that is not special/tied is
7707 // `resetparam(v->pm, PM_SCALAR)`, and
7708 // resetparam refuses a pm that is not the
7709 // visible binding (c:3740-3746).
7710 let nf = node.node.flags as u32;
7711 if (((nf & PM_ARRAY) != 0 && (flags & ASSPM_AUGMENT) == 0)
7712 || (nf & PM_HASHED) != 0)
7713 && (nf & (PM_SPECIAL | PM_TIED)) == 0
7714 && !isset(KSHARRAYS)
7715 // c:2262-2263 — an unset, undeclared pm is
7716 // never fetched, so it takes createparam.
7717 && ((nf & PM_UNSET) == 0 || (nf & PM_DECLARED) != 0)
7718 {
7719 drop(tab); // zerr redraws ZLE, which reads paramtab
7720 zerr(&format!("can't change type of hidden variable: {}", name)); // c:3745
7721 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:3187
7722 return None;
7723 }
7724 if t == PM_INTEGER {
7725 // c:Src/params.c:4007 intsetfn via mathevali.
7726 node.u_val = crate::ported::math::mathevali(val).unwrap_or(0);
7727 } else if t == PM_EFLOAT || t == PM_FFLOAT {
7728 node.u_dval = val.trim().parse().unwrap_or(0.0);
7729 } else {
7730 // c:3236-3250 — array/hash → scalar type flip through ref.
7731 let type_mask = PM_TYPE(u32::MAX) as i32;
7732 node.node.flags =
7733 (node.node.flags & !type_mask) | PM_SCALAR as i32;
7734 node.u_arr = None;
7735 node.u_str = Some(val.to_string());
7736 }
7737 node.node.flags &= !((PM_UNSET | PM_DECLARED) as i32); // c:2712 + c:3269
7738 Some(Box::new(node.clone()))
7739 })(&t, level, val);
7740 }
7741 }
7742 let mut new_s = t.clone();
7743 if let Some(rs) = &rsub {
7744 new_s.push('[');
7745 new_s.push_str(rs);
7746 new_s.push(']');
7747 }
7748 if let Some(us) = user_sub {
7749 new_s.push_str(us);
7750 }
7751 if new_s != s {
7752 // c:2264-2287 — through a reference slice the Value's
7753 // pm is the ELEMENT (getarg's c:1584-1588 createparam
7754 // in the hash's table), so c:3270 `v->pm->node.flags
7755 // &= ~PM_DEFAULTED` never touches the base: a
7756 // TYPESET_TO_UNSET hash keeps reading as unset to
7757 // `typeset -p` while its element is stored. The direct
7758 // `name[sub]=` path below clears it on the base
7759 // (c:3229), so put the bits back.
7760 let base_defaulted = if rsub.is_some() {
7761 paramtab()
7762 .read()
7763 .ok()
7764 .and_then(|tb| tb.get(&t).map(|p| p.node.flags as u32 & PM_DEFAULTED))
7765 .unwrap_or(0)
7766 } else {
7767 0
7768 };
7769 let r = assignsparam(&new_s, val, flags);
7770 if base_defaulted == PM_DEFAULTED && r.is_some() {
7771 if let Ok(mut tb) = paramtab().write() {
7772 if let Some(p) = tb.get_mut(&t) {
7773 p.node.flags |= PM_DEFAULTED as i32;
7774 }
7775 }
7776 }
7777 return r;
7778 }
7779 }
7780 crate::ported::params::nameref_resolution::NotRef => {}
7781 }
7782 }
7783 }
7784 // c:Src/params.c randomsetfn / secondssetfn — re-assigning a
7785 // regenerator-style special clears the PM_UNSET flag so the
7786 // getfn becomes live again. zshrs's lookup_special_var uses a
7787 // side-set for this (no real pm node), so clear it here. Bug #417.
7788 if matches!(
7789 s,
7790 "RANDOM" | "SECONDS" | "EPOCHSECONDS" | "EPOCHREALTIME" | "TTYIDLE" | "ERRNO"
7791 ) {
7792 clear_unset_special(s);
7793 }
7794 queue_signals(); // c:3209
7795
7796 // c:3210 — `strchr(s, '[')`. Split the leading name from the
7797 // subscript while preserving C's `*ss = '\0'` / `*ss = '['`
7798 // restore semantics: the Rust port works on `&str` slices so
7799 // there's no in-place null-terminator dance, but the parse
7800 // shape is identical.
7801 // c:Src/params.c:3210 — `strchr(s, '[')` finds the subscript
7802 // opener; C's parse_subscript (params.c:1480+) then walks to the
7803 // matching `]` respecting backslash-escapes and nesting. zshrs's
7804 // previous `s.rfind(']')` picked the LAST `]` which works for
7805 // simple `name[key]` but mis-bounds `A[\[k\]]` (where the key
7806 // contains escaped brackets).
7807 let (name, subscript) = match s.find('[') {
7808 Some(i) => {
7809 // Walk forward from `i + 1` matching brackets with
7810 // escape-awareness per parse_subscript semantics.
7811 let mut depth = 1i32;
7812 let mut close_byte: Option<usize> = None;
7813 let mut bslash = false;
7814 let mut byte_off = i + 1;
7815 for ch in s[i + 1..].chars() {
7816 if bslash {
7817 bslash = false;
7818 byte_off += ch.len_utf8();
7819 continue;
7820 }
7821 match ch {
7822 '\\' => bslash = true,
7823 '[' => depth += 1,
7824 ']' => {
7825 depth -= 1;
7826 if depth == 0 {
7827 close_byte = Some(byte_off);
7828 break;
7829 }
7830 }
7831 _ => {}
7832 }
7833 byte_off += ch.len_utf8();
7834 }
7835 let close = close_byte.unwrap_or(s.len());
7836 let key_end = if close > i { close } else { s.len() };
7837 (&s[..i], Some(&s[i + 1..key_end]))
7838 }
7839 None => (s, None),
7840 };
7841 // c:Src/params.c:1537-1598 (getarg) — the subscript text is scanned
7842 // for `ispecial` characters (`needtok`, c:1562-1563); if there are any
7843 // it is re-lexed as a double-quote-like string (`parsestr`, c:1587)
7844 // and substituted (`singsub`, c:1592), then untokenized before the
7845 // key lookup (c:1598). So only the escapes that lexing honours go:
7846 // `\[k\]` → `[k]`, `\\` → `\`, `\$` → `$`, while `a\b` and `a\"b`
7847 // keep their backslash, and a `$name` in the key expands.
7848 //
7849 // A flag subscript (`(r)pat`, …) keeps the older blanket escape strip:
7850 // its pattern side is outside this fix.
7851 let subscript_owned: Option<String> = subscript.map(|key| {
7852 if !key.starts_with('(') {
7853 // !!! RUST-ONLY: C gets this key from getindex's in-place
7854 // `parse_subscript(s, dq, ']')` (c:2008), which lexes `\` before
7855 // a bracket, paren or brace into Bnull; getarg's loop keeps those
7856 // Bnulls (c:1540-1547) and `remnulargs` (c:1582) deletes them.
7857 // `lex::parse_subscript` returns only the end offset, so drop
7858 // exactly those backslashes here. `\\` stays a pair for parsestr.
7859 let mut k = String::with_capacity(key.len());
7860 let mut it = key.chars().peekable();
7861 while let Some(ch) = it.next() {
7862 if ch == '\\' {
7863 match it.peek() {
7864 Some('[' | ']' | '(' | ')' | '{' | '}') => continue,
7865 Some(&next) => {
7866 k.push(ch);
7867 k.push(next);
7868 it.next();
7869 continue;
7870 }
7871 None => {}
7872 }
7873 }
7874 k.push(ch);
7875 }
7876 if k.bytes().any(crate::ported::ztype_h::ispecial) {
7877 // c:1562-1563
7878 match crate::ported::lex::parsestr(&k) {
7879 Ok(t) => k = crate::ported::subst::singsub(&t), // c:1587-1592
7880 Err(_) => return key.to_string(), // c:1588 `return 0`
7881 }
7882 }
7883 return crate::ported::lex::untokenize(&k); // c:1598
7884 }
7885 let mut out = String::with_capacity(key.len());
7886 let mut bslash = false;
7887 for ch in key.chars() {
7888 if bslash {
7889 out.push(ch);
7890 bslash = false;
7891 } else if ch == '\\' {
7892 bslash = true;
7893 } else {
7894 out.push(ch);
7895 }
7896 }
7897 if bslash {
7898 out.push('\\');
7899 }
7900 out
7901 });
7902 let subscript: Option<&str> = subscript_owned.as_deref();
7903
7904 // c:Src/params.c — a bare all-digit parameter name (`2=X`,
7905 // `2+=end`) addresses the positional parameter $N (1-based). C maps
7906 // digit names onto the special `argv` value (isident accepts them,
7907 // c:1336-1340); zshrs keeps positionals in PPARAMS, so store there
7908 // directly — the same backing the `argv[N]=`/`@[N]=` path uses
7909 // below. `$0` (n==0) is the shell/script name, not a positional, so
7910 // it falls through to the normal param path. `+=` concatenates onto
7911 // the existing positional (ASSPM_AUGMENT scalar append, c:3270-3276).
7912 if subscript.is_none() && !name.is_empty() && name.bytes().all(|b| b.is_ascii_digit()) {
7913 if let Ok(n) = name.parse::<usize>() {
7914 if n >= 1 {
7915 if let Ok(mut pp) = crate::ported::builtin::PPARAMS.lock() {
7916 while pp.len() < n {
7917 pp.push(String::new());
7918 }
7919 let idx = n - 1;
7920 if (flags & ASSPM_AUGMENT) != 0 {
7921 let cur = pp[idx].clone();
7922 pp[idx] = format!("{}{}", cur, val);
7923 } else {
7924 pp[idx] = val.to_string();
7925 }
7926 }
7927 unqueue_signals();
7928 return Some(Box::new(param {
7929 node: hashnode {
7930 next: None,
7931 nam: name.to_string(),
7932 flags: PM_SCALAR as i32,
7933 },
7934 u_data: 0,
7935 u_tied: None,
7936 u_arr: None,
7937 u_str: Some(val.to_string()),
7938 u_val: 0,
7939 u_dval: 0.0,
7940 u_hash: None,
7941 gsu_s: None,
7942 gsu_i: None,
7943 gsu_f: None,
7944 gsu_a: None,
7945 gsu_h: None,
7946 base: 0,
7947 width: 0,
7948 env: None,
7949 ename: None,
7950 old: None,
7951 level: 0,
7952 }));
7953 }
7954 }
7955 }
7956
7957 // c:Src/Modules/parameter.c — magic associative-array assignment
7958 // forms: `functions[name]=body`, `aliases[name]=value`,
7959 // `dis_functions[name]=body`, `saliases[name]=value`,
7960 // `galiases[name]=value`, `dis_aliases[name]=value`, etc.
7961 // These reach assignsparam as `name[key]=value` shape and must
7962 // dispatch to the canonical setpmfunction / setpmalias hooks
7963 // BEFORE the generic paramtab subscript store. Without this
7964 // route, the assignment lands in paramtab_hashed_storage as
7965 // a normal assoc element and the corresponding function/alias
7966 // is never actually defined in shfunctab/aliastab.
7967 // c:Src/params.c:2264 — `if (!pm ...) return NULL`. `unset
7968 // functions` on a name that is still the `PM_AUTOLOAD` stub removes
7969 // the paramtab node outright (c:3874, see bin_unset), so a later
7970 // `functions[k]=v` no longer finds ANY node: `getvalue` builds a
7971 // fresh `PM_SCALAR|PM_UNSET` param (c:1609) whose `getindex` rejects
7972 // the string subscript with c:2700 "assignment to invalid subscript
7973 // range". Skipping the magic dispatch here routes the assignment
7974 // into that generic path, which already emits exactly that message.
7975 let magic_unbound = |name: &str| -> bool {
7976 crate::ported::modules::parameter::PARTAB
7977 .iter()
7978 .any(|e| e.name == name)
7979 && paramtab().read().map_or(false, |t| t.get(name).is_none())
7980 };
7981 // c:Src/params.c:1135-1144 — createparam's reuse arm: an assignment
7982 // to a name whose node is PM_UNSET but PM_SPECIAL takes the
7983 // `oldpm->node.flags &= ~PM_UNSET` branch and returns NULL, i.e. the
7984 // EXISTING special node is revived and the assignment proceeds
7985 // through it (c:1048 "If the parameter is not created because it
7986 // already exists, the PM_UNSET flag is cleared"). That is how
7987 // `unset functions; functions[gg]=body` re-activates the row after a
7988 // MATERIALIZED unset kept the node (c:3851-3852). zshrs's magic
7989 // dispatch below writes the real table directly and never runs
7990 // createparam, so clear the bit here or every later read stays gated.
7991 if crate::ported::modules::parameter::PARTAB
7992 .iter()
7993 .any(|e| e.name == name)
7994 {
7995 if let Ok(mut tab) = paramtab().write() {
7996 if let Some(pm) = tab.get_mut(name) {
7997 if (pm.node.flags as u32 & PM_SPECIAL) != 0 {
7998 pm.node.flags &= !(PM_UNSET as i32); // c:1144
7999 }
8000 }
8001 }
8002 }
8003 if let Some(key) = subscript.filter(|_| !magic_unbound(name)) {
8004 // c:1410 + c:3251 + c:3343 — a subscript FLAG GROUP has to be
8005 // resolved before any of the magic per-name writes below, which
8006 // treat the subscript as a plain key. C never gets there: the
8007 // `getvalue(&vbuf, &t, 1)` at c:3251 runs `getindex`/`getarg`
8008 // first, so `aliases[(r)bar]=Z` is a search over the association
8009 // and `assignstrvalue` rejects it (c:2701-2706 / c:2831-2839)
8010 // instead of inventing an alias literally named `(r)bar`.
8011 // Non-search groups ((e)/(w)/(n:N:)/(p)) resolve to the key with
8012 // the group consumed (c:1596-1616) and carry on below.
8013 let key_owned: String;
8014 let key: &str = if (key.starts_with('(') || key.starts_with(crate::ported::zsh_h::Inpar))
8015 && paramtab()
8016 .read()
8017 .ok()
8018 .and_then(|t| {
8019 t.get(name)
8020 .map(|pm| PM_TYPE(pm.node.flags as u32) == PM_HASHED)
8021 })
8022 .unwrap_or(false)
8023 {
8024 let pm = paramtab().read().ok().and_then(|t| t.get(name).cloned());
8025 let mut vv = value {
8026 pm,
8027 arr: Vec::new(),
8028 scanflags: SCANPM_ARRONLY as i32, // c:2274-2280
8029 valflags: 0,
8030 start: 0,
8031 end: -1, // c:2279
8032 };
8033 let bracketed = format!("[{}]", key); // c:2281
8034 let mut sp: &str = &bracketed;
8035 if getindex(&mut sp, &mut vv, 0) != 0 {
8036 unqueue_signals(); // c:3220
8037 return None;
8038 }
8039 if vv
8040 .pm
8041 .as_ref()
8042 .map_or(false, |p| PM_TYPE(p.node.flags as u32) == PM_HASHED)
8043 {
8044 // c:3343 — the search left the value on the whole
8045 // association; assignstrvalue raises the diagnostic.
8046 assignstrvalue(Some(&mut vv), Some(val.to_string()), flags);
8047 unqueue_signals();
8048 return None;
8049 }
8050 key_owned = vv
8051 .pm
8052 .as_ref()
8053 .map_or_else(|| key.to_string(), |p| p.node.nam.clone()); // c:1596-1616
8054 &key_owned
8055 } else {
8056 key
8057 };
8058 // c:Src/params.c:3153-3155 — the subscripted branch of
8059 // `assignsparam` NUL-terminates at the `[` and calls
8060 // `getvalue(&vbuf, &s, 1)` on the bare NAME. That resolves the
8061 // node through `getparamnode` → `loadparamnode` (c:563-585),
8062 // clearing PM_AUTOLOAD on the `zsh/parameter` stub. zshrs
8063 // models PM_AUTOLOAD as a side-set (vm_helper's
8064 // MATERIALIZED_MODULE_PARAMS), and the magic dispatch below
8065 // never runs `getvalue`, so the name stayed a "stub" after the
8066 // write: a later `unset "nameddirs[qq]"` /
8067 // `unset "functions[ff]"` then took bin_unset's
8068 // `module_param_is_autoload_stub` arm (builtin.rs:8483,
8069 // C's c:3896-3919 scalar arm → "assignment to invalid
8070 // subscript range") instead of the c:3891-3895 PM_HASHED arm,
8071 // leaving the entry in nameddirtab / shfunctab.
8072 if crate::ported::modules::parameter::PARTAB
8073 .iter()
8074 .any(|e| e.name == name)
8075 {
8076 crate::vm_helper::mark_module_param_used(name); // c:3155 getvalue → c:571 loadparamnode
8077 // c:Src/params.c:3159-3165 — `if (v->pm->node.flags &
8078 // PM_READONLY) { zerr("read-only variable: %s",
8079 // v->pm->node.nam); … return NULL; }`. The check runs on the
8080 // node getvalue just returned, BEFORE any element write, so
8081 // `readonly commands; commands[x]=/y` is rejected in C. The
8082 // magic dispatch below writes the real shell table directly
8083 // and never consulted the node's flags, so a user
8084 // `readonly`-ed magic assoc silently accepted element
8085 // writes. Name-keyed `is_readonly_magic` further down only
8086 // covers the PM_READONLY_SPECIAL rows, not user readonly.
8087 let ro = paramtab()
8088 .read()
8089 .ok()
8090 .and_then(|t| {
8091 t.get(name)
8092 .map(|pm| (pm.node.flags as u32 & PM_READONLY) != 0)
8093 })
8094 .unwrap_or(false);
8095 if ro {
8096 zerr(&format!("read-only variable: {}", name)); // c:3160
8097 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
8098 unqueue_signals(); // c:3163
8099 return None; // c:3164
8100 }
8101 }
8102 match name {
8103 "functions" => {
8104 use crate::ported::zsh_h::hashnode;
8105 use crate::ported::zsh_h::param as ParamStruct;
8106 // c:Src/params.c:3270-3276 — ASSPM_AUGMENT on PM_SCALAR
8107 // appends raw text to the existing value. The
8108 // `functions[name]` magic-assoc is scalar-typed under
8109 // the hood (each entry is a function-body string). For
8110 // `functions[f]+="echo b"`, fetch the existing body
8111 // from shfunctab and concatenate before calling
8112 // setpmfunction. Without this, `+=` silently no-ops.
8113 // Bug #323 in docs/BUGS.md.
8114 let final_body = if (flags & ASSPM_AUGMENT) != 0 {
8115 let existing = crate::ported::hashtable::shfunctab_lock()
8116 .read()
8117 .ok()
8118 .and_then(|tab| tab.get(key).and_then(|shf| shf.body.clone()))
8119 .unwrap_or_default();
8120 format!("{}{}", existing, val)
8121 } else {
8122 val.to_string()
8123 };
8124 let pm: Box<ParamStruct> = Box::new(ParamStruct {
8125 node: hashnode {
8126 next: None,
8127 nam: key.to_string(),
8128 flags: 0,
8129 },
8130 u_data: 0,
8131 u_tied: None,
8132 u_arr: None,
8133 u_str: None,
8134 u_val: 0,
8135 u_dval: 0.0,
8136 u_hash: None,
8137 gsu_s: None,
8138 gsu_i: None,
8139 gsu_f: None,
8140 gsu_a: None,
8141 gsu_h: None,
8142 base: 0,
8143 width: 0,
8144 env: None,
8145 ename: None,
8146 old: None,
8147 level: 0,
8148 });
8149 crate::ported::modules::parameter::setpmfunction(pm.clone(), final_body);
8150 unqueue_signals();
8151 return Some(pm);
8152 }
8153 "dis_functions" => {
8154 use crate::ported::zsh_h::hashnode;
8155 use crate::ported::zsh_h::param as ParamStruct;
8156 let pm: Box<ParamStruct> = Box::new(ParamStruct {
8157 node: hashnode {
8158 next: None,
8159 nam: key.to_string(),
8160 flags: 0,
8161 },
8162 u_data: 0,
8163 u_tied: None,
8164 u_arr: None,
8165 u_str: None,
8166 u_val: 0,
8167 u_dval: 0.0,
8168 u_hash: None,
8169 gsu_s: None,
8170 gsu_i: None,
8171 gsu_f: None,
8172 gsu_a: None,
8173 gsu_h: None,
8174 base: 0,
8175 width: 0,
8176 env: None,
8177 ename: None,
8178 old: None,
8179 level: 0,
8180 });
8181 crate::ported::modules::parameter::setpmdisfunction(pm.clone(), val.to_string());
8182 unqueue_signals();
8183 return Some(pm);
8184 }
8185 "aliases" => {
8186 // c:Src/Modules/parameter.c — install a regular
8187 // alias via canonical aliastab. Use createaliasnode
8188 // with default flags (no ALIAS_GLOBAL / ALIAS_SUFFIX).
8189 if let Ok(mut tab) = crate::ported::hashtable::aliastab_lock().write() {
8190 let node = crate::ported::hashtable::createaliasnode(key, val, 0u32);
8191 tab.add(node);
8192 }
8193 unqueue_signals();
8194 return Some(Box::new(crate::ported::zsh_h::param {
8195 node: crate::ported::zsh_h::hashnode {
8196 next: None,
8197 nam: key.to_string(),
8198 flags: 0,
8199 },
8200 u_data: 0,
8201 u_tied: None,
8202 u_arr: None,
8203 u_str: Some(val.to_string()),
8204 u_val: 0,
8205 u_dval: 0.0,
8206 u_hash: None,
8207 gsu_s: None,
8208 gsu_i: None,
8209 gsu_f: None,
8210 gsu_a: None,
8211 gsu_h: None,
8212 base: 0,
8213 width: 0,
8214 env: None,
8215 ename: None,
8216 old: None,
8217 level: 0,
8218 }));
8219 }
8220 "galiases" => {
8221 // c:Src/Modules/parameter.c — global alias.
8222 if let Ok(mut tab) = crate::ported::hashtable::aliastab_lock().write() {
8223 let node = crate::ported::hashtable::createaliasnode(
8224 key,
8225 val,
8226 crate::ported::zsh_h::ALIAS_GLOBAL as u32,
8227 );
8228 tab.add(node);
8229 }
8230 unqueue_signals();
8231 return Some(Box::new(crate::ported::zsh_h::param {
8232 node: crate::ported::zsh_h::hashnode {
8233 next: None,
8234 nam: key.to_string(),
8235 flags: 0,
8236 },
8237 u_data: 0,
8238 u_tied: None,
8239 u_arr: None,
8240 u_str: Some(val.to_string()),
8241 u_val: 0,
8242 u_dval: 0.0,
8243 u_hash: None,
8244 gsu_s: None,
8245 gsu_i: None,
8246 gsu_f: None,
8247 gsu_a: None,
8248 gsu_h: None,
8249 base: 0,
8250 width: 0,
8251 env: None,
8252 ename: None,
8253 old: None,
8254 level: 0,
8255 }));
8256 }
8257 "saliases" => {
8258 // c:Src/Modules/parameter.c — suffix alias.
8259 if let Ok(mut tab) = crate::ported::hashtable::sufaliastab_lock().write() {
8260 let node = crate::ported::hashtable::createaliasnode(
8261 key,
8262 val,
8263 crate::ported::zsh_h::ALIAS_SUFFIX as u32,
8264 );
8265 tab.add(node);
8266 }
8267 unqueue_signals();
8268 return Some(Box::new(crate::ported::zsh_h::param {
8269 node: crate::ported::zsh_h::hashnode {
8270 next: None,
8271 nam: key.to_string(),
8272 flags: 0,
8273 },
8274 u_data: 0,
8275 u_tied: None,
8276 u_arr: None,
8277 u_str: Some(val.to_string()),
8278 u_val: 0,
8279 u_dval: 0.0,
8280 u_hash: None,
8281 gsu_s: None,
8282 gsu_i: None,
8283 gsu_f: None,
8284 gsu_a: None,
8285 gsu_h: None,
8286 base: 0,
8287 width: 0,
8288 env: None,
8289 ename: None,
8290 old: None,
8291 level: 0,
8292 }));
8293 }
8294 "options" => {
8295 // c:Src/Modules/parameter.c:926 setpmoption — userspace
8296 // `options[X]=on|off` toggles option X via dosetopt, not
8297 // a generic assoc-write. Build a synthetic Param whose
8298 // node.nam carries the option name and dispatch to the
8299 // canonical setpmoption port (`src/ported/modules/
8300 // parameter.rs:1620`), which calls optlookup + dosetopt.
8301 use crate::ported::zsh_h::hashnode;
8302 use crate::ported::zsh_h::param as ParamStruct;
8303 let pm: Box<ParamStruct> = Box::new(ParamStruct {
8304 node: hashnode {
8305 next: None,
8306 nam: key.to_string(),
8307 flags: 0,
8308 },
8309 u_data: 0,
8310 u_tied: None,
8311 u_arr: None,
8312 u_str: None,
8313 u_val: 0,
8314 u_dval: 0.0,
8315 u_hash: None,
8316 gsu_s: None,
8317 gsu_i: None,
8318 gsu_f: None,
8319 gsu_a: None,
8320 gsu_h: None,
8321 base: 0,
8322 width: 0,
8323 env: None,
8324 ename: None,
8325 old: None,
8326 level: 0,
8327 });
8328 crate::ported::modules::parameter::setpmoption(pm.clone(), val.to_string());
8329 unqueue_signals();
8330 return Some(pm);
8331 }
8332 "commands" => {
8333 // c:Src/Modules/parameter.c:151-160 setpmcommand —
8334 // `commands[name]=path` installs a HASHED Cmdnam
8335 // node in cmdnamtab (`cn->node.flags = HASHED;
8336 // cn->u.cmd = ztrdup(value); cmdnamtab->addnode`),
8337 // exactly like `hash name=path`. zsh ACCEPTS the
8338 // write (verified vs zsh 5.9: rc=0, readback works,
8339 // whence/hash see the entry). Build a synthetic
8340 // Param whose node.nam carries the command name and
8341 // dispatch to the canonical setpmcommand port.
8342 // Bug #375.
8343 use crate::ported::zsh_h::hashnode;
8344 use crate::ported::zsh_h::param as ParamStruct;
8345 let pm: Box<ParamStruct> = Box::new(ParamStruct {
8346 node: hashnode {
8347 next: None,
8348 nam: key.to_string(),
8349 flags: 0,
8350 },
8351 u_data: 0,
8352 u_tied: None,
8353 u_arr: None,
8354 u_str: None,
8355 u_val: 0,
8356 u_dval: 0.0,
8357 u_hash: None,
8358 gsu_s: None,
8359 gsu_i: None,
8360 gsu_f: None,
8361 gsu_a: None,
8362 gsu_h: None,
8363 base: 0,
8364 width: 0,
8365 env: None,
8366 ename: None,
8367 old: None,
8368 level: 0,
8369 });
8370 crate::ported::modules::parameter::setpmcommand(pm.clone(), val.to_string());
8371 unqueue_signals();
8372 return Some(pm);
8373 }
8374 "nameddirs" => {
8375 // c:Src/Modules/parameter.c:1534-1545 setpmnameddir —
8376 // `nameddirs[name]=dir` installs a Nameddir node in
8377 // nameddirtab (`nd->node.flags = 0; nd->dir = value;
8378 // nameddirtab->addnode(...)`), exactly like
8379 // `hash -d name=dir`, so `~name` expands afterwards.
8380 // The SPECIALPMDEF for `nameddirs`
8381 // (Src/Modules/parameter.c:2301-2302) carries NO
8382 // PM_READONLY_SPECIAL and its element gsu is
8383 // `pmnamedir_gsu = { strgetfn, setpmnameddir,
8384 // unsetpmnameddir }` (c:1607-1608), i.e. the write is
8385 // legal in C. Verified vs zsh 5.9: `nameddirs[qq]=/tmp`
8386 // → rc=0, `print ~qq` → /tmp, `hash -d` lists qq.
8387 // Dispatch to the canonical setpmnameddir port so the
8388 // write reaches nameddirtab (a generic assoc store
8389 // would leave `~qq` silently broken).
8390 use crate::ported::zsh_h::hashnode;
8391 use crate::ported::zsh_h::param as ParamStruct;
8392 let pm: Box<ParamStruct> = Box::new(ParamStruct {
8393 node: hashnode {
8394 next: None,
8395 nam: key.to_string(),
8396 flags: 0,
8397 },
8398 u_data: 0,
8399 u_tied: None,
8400 u_arr: None,
8401 u_str: None,
8402 u_val: 0,
8403 u_dval: 0.0,
8404 u_hash: None,
8405 gsu_s: None,
8406 gsu_i: None,
8407 gsu_f: None,
8408 gsu_a: None,
8409 gsu_h: None,
8410 base: 0,
8411 width: 0,
8412 env: None,
8413 ename: None,
8414 old: None,
8415 level: 0,
8416 });
8417 crate::ported::modules::parameter::setpmnameddir(pm.clone(), val.to_string());
8418 unqueue_signals();
8419 return Some(pm);
8420 }
8421 "mapfile" => {
8422 // c:Src/Modules/mapfile.c:68 setpmmapfile —
8423 // `mapfile[fname]=value` WRITES `value` to the file
8424 // named by the key (creating it if needed). The setfn
8425 // is ported; route the element-assign to it (the
8426 // PartabHashEntry dispatch carries only get/scan).
8427 use crate::ported::zsh_h::hashnode;
8428 use crate::ported::zsh_h::param as ParamStruct;
8429 crate::ported::modules::mapfile::setpmmapfile(key, val, false);
8430 let pm: Box<ParamStruct> = Box::new(ParamStruct {
8431 node: hashnode {
8432 next: None,
8433 nam: key.to_string(),
8434 flags: 0,
8435 },
8436 u_data: 0,
8437 u_tied: None,
8438 u_arr: None,
8439 u_str: None,
8440 u_val: 0,
8441 u_dval: 0.0,
8442 u_hash: None,
8443 gsu_s: None,
8444 gsu_i: None,
8445 gsu_f: None,
8446 gsu_a: None,
8447 gsu_h: None,
8448 base: 0,
8449 width: 0,
8450 env: None,
8451 ename: None,
8452 old: None,
8453 level: 0,
8454 });
8455 unqueue_signals();
8456 return Some(pm);
8457 }
8458 _ => {}
8459 }
8460 }
8461
8462 // c:Src/params.c:3262 IPDEF9 — `argv`, `@`, `*` are aliases for
8463 // the global `pparams` (positional parameter vector). Whole-
8464 // array writes (`argv=(...)`) route through assignaparam at
8465 // params.rs:5487 which updates PPARAMS. Subscripted writes
8466 // (`argv[N]=val`) must do the same — without this, the value
8467 // landed in paramtab's u_arr but `$@`/`$1`/`$2`/... continued
8468 // reading from PPARAMS, so the update was invisible. Bug #281
8469 // in docs/BUGS.md.
8470 if let Some(key) = subscript {
8471 if matches!(name, "argv" | "@" | "*") {
8472 if let Ok(idx) = key.trim().parse::<i64>() {
8473 if let Ok(mut pp) = crate::ported::builtin::PPARAMS.lock() {
8474 let len = pp.len() as i64;
8475 let kshzero =
8476 crate::ported::zsh_h::isset(crate::ported::zsh_h::KSHZEROSUBSCRIPT);
8477 if idx == 0 && !isset(KSHARRAYS) && !kshzero {
8478 drop(pp); // zerr redraws ZLE; release the lock first
8479 zerr(&format!("{}: assignment to invalid subscript range", name));
8480 unqueue_signals();
8481 return None;
8482 }
8483 let real_idx = if idx < 0 {
8484 (len + idx).max(0) as usize
8485 } else if isset(KSHARRAYS) {
8486 idx.max(0) as usize
8487 } else {
8488 (idx - 1).max(0) as usize
8489 };
8490 while pp.len() <= real_idx {
8491 pp.push(String::new());
8492 }
8493 pp[real_idx] = val.to_string();
8494 }
8495 unqueue_signals();
8496 return Some(Box::new(param {
8497 node: hashnode {
8498 next: None,
8499 nam: name.to_string(),
8500 flags: PM_ARRAY as i32,
8501 },
8502 u_data: 0,
8503 u_tied: None,
8504 u_arr: None,
8505 u_str: None,
8506 u_val: 0,
8507 u_dval: 0.0,
8508 u_hash: None,
8509 gsu_s: None,
8510 gsu_i: None,
8511 gsu_f: None,
8512 gsu_a: None,
8513 gsu_h: None,
8514 base: 0,
8515 width: 0,
8516 env: None,
8517 ename: None,
8518 old: None,
8519 level: 0,
8520 }));
8521 }
8522 }
8523 }
8524
8525 // c:Src/Modules/parameter.c — read-only magic-assoc names. C
8526 // zsh's SPECIALPMDEF / createspecialhash sets PM_READONLY on
8527 // these so userspace assignment errors with `read-only
8528 // variable: NAME`. `vm_helper::init_partab_params` strips
8529 // PM_READONLY off the paramtab stub to allow INTERNAL writes
8530 // (function-call funcstack push, etc.) — see comment at
8531 // vm_helper.rs:2799. The trade-off is that USERSPACE writes
8532 // now slip through too. Intercept here, AFTER the writable-
8533 // magic-assoc dispatch arms above (functions, aliases,
8534 // dis_aliases, galiases, saliases, dis_functions) but BEFORE
8535 // the generic paramtab subscript store, so the userspace path
8536 // emits the canonical diagnostic and exits non-zero. Internal
8537 // table mutations that bypass `assignsparam` stay free. Bug
8538 // #242 in docs/BUGS.md.
8539 // c:Src/params.c:2688-2689 — `if (v->pm->node.flags & PM_READONLY)
8540 // zerr("read-only variable: %s", ...)` reads the flag off the NODE
8541 // `fetchvalue` returned. After `unset parameters` there is no node
8542 // (c:3874, see bin_unset), so C never reaches that test: `getindex`
8543 // rejects the string subscript on the fresh `PM_SCALAR|PM_UNSET`
8544 // param first (c:2700 "assignment to invalid subscript range").
8545 // Keep this name-keyed stand-in in step with the node's existence.
8546 if subscript.is_some() && !magic_unbound(name) {
8547 // `options` is intentionally NOT in this list — C zsh's
8548 // `setpmoption`/`setpmoptions` (Src/Modules/parameter.c:926-979)
8549 // accept "on"/"off" writes and translate them to dosetopt calls.
8550 // The `options` arm above this readonly-list check routes
8551 // `options[X]=on|off` writes through the canonical setpmoption
8552 // port, so by the time we reach here `options` has already
8553 // returned. Bug #342.
8554 // `commands` is intentionally NOT in this list — C zsh's
8555 // `setpmcommand` (Src/Modules/parameter.c:151-160) ACCEPTS
8556 // `commands[name]=path` and installs a HASHED Cmdnam node in
8557 // cmdnamtab (same effect as `hash name=path`). Verified vs
8558 // zsh 5.9: `commands[x]=/y` → rc=0, ${commands[x]} → /y,
8559 // `whence x` → /y. The `commands` arm above routes through
8560 // the canonical setpmcommand port. Bug #375.
8561 // `nameddirs` is intentionally NOT in this list — its
8562 // SPECIALPMDEF (Src/Modules/parameter.c:2301-2302) has no
8563 // PM_READONLY_SPECIAL and its element gsu (c:1607-1608
8564 // `pmnamedir_gsu = { strgetfn, setpmnameddir,
8565 // unsetpmnameddir }`) accepts the write, installing a
8566 // nameddirtab node. Verified vs zsh 5.9: `nameddirs[qq]=/tmp`
8567 // → rc=0, `print ~qq` → /tmp. The `nameddirs` arm above routes
8568 // through the canonical setpmnameddir port. `userdirs` STAYS
8569 // read-only: c:1670/1694 give it `PM_READONLY` +
8570 // `nullsetscalar_gsu`.
8571 let is_readonly_magic = matches!(
8572 name,
8573 "builtins"
8574 | "modules"
8575 | "parameters"
8576 | "dis_builtins"
8577 | "history"
8578 | "historywords"
8579 | "jobtexts"
8580 | "jobstates"
8581 | "jobdirs"
8582 | "userdirs"
8583 | "usergroups"
8584 | "widgets"
8585 | "functions_source"
8586 | "dis_functions_source"
8587 | "terminfo"
8588 | "termcap"
8589 );
8590 if is_readonly_magic {
8591 zerr(&format!("read-only variable: {}", name));
8592 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
8593 unqueue_signals();
8594 return None;
8595 }
8596 }
8597
8598 // Subscripted path (c:3210-3231).
8599 if let Some(key) = subscript {
8600 // c:params.c:1601 (getarg) — a non-hashed parameter's subscript is an
8601 // ARITHMETIC expression. Resolve it to a numeric index HERE, before
8602 // the paramtab write lock below, because mathevalarg reads parameters
8603 // and would deadlock against a held write lock. Associative arrays use
8604 // the raw string key instead, so only pre-evaluate for non-hashed.
8605 let is_hashed = {
8606 let flagged = {
8607 let tab = paramtab().read().unwrap();
8608 tab.get(name)
8609 .map(|pm| (pm.node.flags as u32 & PM_HASHED) != 0)
8610 .unwrap_or(false)
8611 };
8612 // c:Src/Zle/complete.c:1340 — `$compstate` is created PM_HASHED by
8613 // makecompparams for the duration of a completion function, so the
8614 // node itself answers this test. zshrs keeps an association's
8615 // pairs in the side store `paramtab_hashed_storage()`; a name with a
8616 // row there is an association too. A bare `compstate` name is NOT:
8617 // outside completion zsh has no such parameter and
8618 // `compstate[list]=list` takes the createparam(PM_ARRAY) path below
8619 // (c:3061) and errors "assignment to invalid subscript range".
8620 flagged
8621 || paramtab_hashed_storage()
8622 .lock()
8623 .map(|s| s.contains_key(name))
8624 .unwrap_or(false)
8625 };
8626 // c:Src/params.c:2048-2053 — getindex's FIRST test on a subscript body:
8627 // if ((s[0] == '*' || s[0] == '@') && s + 1 == tbrack) {
8628 // if ((v->scanflags || IS_UNSET_VALUE(v)) && s[0] == '@')
8629 // v->scanflags |= SCANPM_ISVAR_AT;
8630 // v->start = 0;
8631 // v->end = -1;
8632 // s += 2;
8633 // } else { … start = getarg(&s, …); … }
8634 // A bare `@` / `*` subscript names the WHOLE value, and `getarg` — the
8635 // only place the subscript is arithmetic-evaluated — is never reached.
8636 // The port had no such arm, so the subscript fell through to
8637 // `mathevalarg`, which answered `bad math expression: operand expected
8638 // at `@'` and aborted the whole expansion: `e=(); print "${e[@]:=}"`
8639 // errored where zsh assigns `('')` and prints one empty word (paramsubst
8640 // reaches this through c:Src/subst.c:3313 `setsparam(idbeg, …)`, whose
8641 // `idbeg` still carries the `[@]`).
8642 let is_whole_value = key == "@" || key == "*"; // c:2048
8643 let resolved_idx: i64 = if is_hashed || is_whole_value {
8644 0 // unused: hashed params key by name, `[@]`/`[*]` by start/end
8645 } else {
8646 key.parse::<i64>().unwrap_or_else(|_| {
8647 // c:Src/params.c:2058 getarg → c:1585-1593 — a subscript that
8648 // is not a plain literal is parameter-expanded (singsub) BEFORE
8649 // arithmetic evaluation. When assignsparam is reached with a
8650 // raw, UNEXPANDED subscript — e.g. `setsparam("pgid[$#pgid+1]",
8651 // …)` from the sysread/read builtins, whose target string the
8652 // command compiler never pre-expanded — `$#pgid` / `$n` /
8653 // `${#a}` must be resolved here, or mathevalarg sees a literal
8654 // `$` and yields 0 → "invalid subscript range" (gitstatus's
8655 // `_gitstatus_daemon_p9k_` hit this on every prompt). The
8656 // shell-level `a[$n+1]=v` form already arrives expanded, so its
8657 // numeric key takes the parse fast-path above and skips this.
8658 let expanded = crate::ported::subst::singsub(&key);
8659 crate::ported::math::mathevalarg(&expanded)
8660 })
8661 };
8662 let mut tab = paramtab().write().unwrap();
8663 let exists = tab.contains_key(name); // c:3212
8664 if !exists {
8665 // c:3213 `createparam(t, PM_ARRAY); created = 1;` — but when the
8666 // name is a hash-storage-backed associative (e.g. `$compstate`),
8667 // create it PM_HASHED so the dispatch below routes to the assoc
8668 // element store instead of the numeric-subscript array path.
8669 let (create_flags, create_arr) = if is_hashed {
8670 (PM_HASHED as i32, None)
8671 } else {
8672 (PM_ARRAY as i32, Some(Vec::new()))
8673 };
8674 let pm: Param = Box::new(param {
8675 node: hashnode {
8676 next: None,
8677 nam: name.to_string(),
8678 flags: create_flags,
8679 },
8680 u_data: 0,
8681 u_tied: None,
8682 u_arr: create_arr,
8683 u_str: None,
8684 u_val: 0,
8685 u_dval: 0.0,
8686 u_hash: None,
8687 gsu_s: None,
8688 gsu_i: None,
8689 gsu_f: None,
8690 gsu_a: None,
8691 gsu_h: None,
8692 base: 0,
8693 width: 0,
8694 env: None,
8695 ename: None,
8696 old: None,
8697 level: 0,
8698 });
8699 tab.insert(name.to_string(), pm);
8700 } else {
8701 // c:3216 `if (v->pm->node.flags & PM_READONLY)`.
8702 let pm = tab.get(name).unwrap();
8703 if (pm.node.flags as u32 & PM_READONLY) != 0 {
8704 let nam = pm.node.nam.clone();
8705 // Release the write guard before zerr: zerr -> trashzle ->
8706 // zrefresh reads paramtab, and std RwLock is not reentrant.
8707 drop(tab);
8708 zerr(&format!("read-only variable: {}", nam)); // c:3217
8709 unqueue_signals(); // c:3220
8710 return None; // c:3221
8711 }
8712 }
8713 // c:3231 `v = NULL;` — re-dispatch by storage type.
8714 let pm = tab.get_mut(name).unwrap();
8715 // c:3171 `v->pm->node.flags &= ~PM_DEFAULTED;` — see the
8716 // PM_SPECIAL note on the same clear at c:3211 below.
8717 let keep_dontimport =
8718 (pm.node.flags as u32 & (PM_SPECIAL | PM_DONTIMPORT)) == (PM_SPECIAL | PM_DONTIMPORT);
8719 pm.node.flags &= !(PM_DEFAULTED as i32); // c:3171
8720 if keep_dontimport {
8721 pm.node.flags |= PM_DONTIMPORT as i32;
8722 }
8723 if is_whole_value {
8724 // c:2051-2052 left `v->start = 0, v->end = -1`, so the write is
8725 // c:Src/params.c:2700-2706 / :2930-2946 `assignstrvalue` on the
8726 // WHOLE value rather than on one element.
8727 match PM_TYPE(pm.node.flags as u32) {
8728 // c:2700-2706 — `if (PM_TYPE(v->pm->node.flags) == PM_HASHED) {
8729 // zerr("%s: attempt to set slice of associative array", …); … }`
8730 x if x == PM_HASHED => {
8731 let nam = pm.node.nam.clone();
8732 drop(tab); // zerr redraws ZLE, which reads paramtab
8733 zerr(&format!("{}: attempt to set slice of associative array", nam)); // c:2701
8734 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
8735 unqueue_signals();
8736 return None;
8737 }
8738 // c:2721-2733 — a scalar's whole-value slice is the whole
8739 // string, so the param stays a SCALAR: `s=abc; s[@]=X` leaves
8740 // `${(t)s}` as `scalar` with the value `X`.
8741 x if x == PM_SCALAR => {
8742 pm.u_str = Some(val.to_string());
8743 pm.u_arr = None;
8744 }
8745 // c:2930-2946 `setarrvalue` with start 0 / end -1 replaces the
8746 // array with the assigned value: `e=(); e[@]=v` is `(v)`, one
8747 // element, and an UNSET name was created PM_ARRAY at c:3156.
8748 _ => {
8749 pm.u_arr = Some(vec![val.to_string()]);
8750 pm.u_str = None;
8751 }
8752 }
8753 } else if (pm.node.flags as u32 & PM_HASHED) != 0 {
8754 // c:3251 + c:3343 — `getvalue(&vbuf, &t, 1)` re-reads the
8755 // FULL `name[subscript]` text so `getindex` can resolve the
8756 // subscript, then `assignstrvalue` performs the write and
8757 // raises the association-specific diagnostics. This arm used
8758 // to skip both and store the subscript text verbatim, so
8759 // `aliases[(r)bar]=Z` silently invented a key named `(r)bar`
8760 // where zsh reports a slice assignment.
8761 //
8762 // c:1410 — `if (v->pm && (*s == '(' || *s == Inpar))` is the
8763 // only thing that needs the SOURCE spelling, and on this path
8764 // `key` still is it (callers hand assignsparam the unexpanded
8765 // subscript). Without a flag group there is nothing for
8766 // `getindex` to do beyond the c:1596-1616 exact-key rebind,
8767 // so the key goes straight to the member store.
8768 if key.starts_with('(') || key.starts_with(crate::ported::zsh_h::Inpar) {
8769 let mut vv = value {
8770 pm: Some(pm.clone()),
8771 arr: Vec::new(),
8772 // c:2274-2280 — no caller flags → SCANPM_ARRONLY.
8773 scanflags: SCANPM_ARRONLY as i32,
8774 valflags: 0,
8775 start: 0,
8776 end: -1, // c:2279
8777 };
8778 drop(tab);
8779 let bracketed = format!("[{}]", key); // c:2281
8780 let mut sp: &str = &bracketed;
8781 if getindex(&mut sp, &mut vv, 0) != 0 {
8782 unqueue_signals(); // c:3220
8783 return None;
8784 }
8785 let elem_is_hash = vv
8786 .pm
8787 .as_ref()
8788 .map_or(false, |p| PM_TYPE(p.node.flags as u32) == PM_HASHED);
8789 if elem_is_hash {
8790 // c:3343 — the search left the value on the whole
8791 // association; assignstrvalue raises c:2701 / c:2831.
8792 assignstrvalue(Some(&mut vv), Some(val.to_string()), flags);
8793 unqueue_signals();
8794 return None;
8795 }
8796 // c:1596-1616 — non-search group: the rebound element's
8797 // name is the key with the group consumed.
8798 let ekey = vv
8799 .pm
8800 .as_ref()
8801 .map_or_else(|| key.to_string(), |p| p.node.nam.clone());
8802 let mut store = paramtab_hashed_storage().lock().unwrap();
8803 store
8804 .entry(name.to_string())
8805 .or_default()
8806 .insert(ekey, val.to_string()); // c:2841
8807 unqueue_signals();
8808 return paramtab().read().ok().and_then(|t| t.get(name).cloned());
8809 }
8810 // PM_HASHED element store. `param.u_hash` is typed
8811 // `Option<HashTable>` per Src/zsh.h:1841 but the
8812 // HashTable runtime backing isn't wired; the assoc-array
8813 // values live in a parallel storage keyed on param name
8814 // (`paramtab_hashed_storage()`).
8815 let mut store = paramtab_hashed_storage().lock().unwrap();
8816 store
8817 .entry(name.to_string())
8818 .or_default()
8819 .insert(key.to_string(), val.to_string()); // c:2841
8820 } else {
8821 // Non-hashed param: subscript already arithmetic-resolved above
8822 // (c:params.c:1601). zsh never auto-creates an assoc here, so
8823 // `as[k1]=bb` on undeclared `as` uses idx 0 → invalid range.
8824 let idx = resolved_idx;
8825 // c:Src/params.c:2748-2789 — PM_SCALAR + numeric subscript
8826 // SPLICES the value into the scalar's char string
8827 // (`a=hello; a[2]=X` → `hXllo`). Only PM_ARRAY does
8828 // element-store at this subscript. Bug #589 in
8829 // docs/BUGS.md: zshrs's subscript store always treated
8830 // numeric subscript as array-store, so `a[2]=X` on a
8831 // scalar cleared `u_str` and put "X" at array slot 1,
8832 // leaving `$a` displaying as `" X"` (empty + space + X)
8833 // when joined.
8834 let pm_type = PM_TYPE(pm.node.flags as u32);
8835 if pm_type == PM_SCALAR {
8836 // c:2748+ scalar splice — replace chars at index.
8837 let s = pm.u_str.clone().unwrap_or_default();
8838 let chars: Vec<char> = s.chars().collect();
8839 let len = chars.len() as i64;
8840 let kshzero = crate::ported::zsh_h::isset(crate::ported::zsh_h::KSHZEROSUBSCRIPT);
8841 if idx == 0 && !isset(KSHARRAYS) && !kshzero {
8842 drop(tab); // zerr redraws ZLE, which reads paramtab
8843 zerr(&format!("{}: assignment to invalid subscript range", name));
8844 unqueue_signals();
8845 return None;
8846 }
8847 // 1-based forward, negative-from-end. KSHARRAYS = 0-based.
8848 // c:Src/params.c:2134 — under KSHZEROSUBSCRIPT a `[0]`
8849 // subscript "is treated as accessing the first element", so it
8850 // must land on real index 0, NOT on `0 - 1 = -1`. Falling into
8851 // the negative branch made `s[0]=X` INSERT at the front
8852 // ("Xhello") instead of replacing the first character
8853 // ("Xello"). Only the zero case shifts: `[1]` stays 1-based
8854 // (`idx - 1`), and KSHARRAYS is already 0-based above.
8855 let raw = if idx < 0 {
8856 len + idx
8857 } else if isset(KSHARRAYS) {
8858 idx
8859 } else if idx == 0 && kshzero {
8860 0
8861 } else {
8862 idx - 1
8863 };
8864 let mut out = String::with_capacity(s.len() + val.len());
8865 if raw < 0 {
8866 // c:Src/params.c:2721-2733 (assignstrvalue) — a negative
8867 // subscript that points PAST the start of the string
8868 // (`a[-4]` on "abc") clamps BOTH start and end to 0, so
8869 // the slice old[0..0] is empty and val is INSERTED at the
8870 // front (overwriting nothing): `a[-4]="x"` → "xabc". The
8871 // prior port clamped to index 0 and OVERWROTE char 0.
8872 out.push_str(val);
8873 out.extend(chars.iter());
8874 } else {
8875 // Replace one char at real_idx with val. If real_idx is
8876 // past the end, append (C clamps end to zlen and concats).
8877 let real_idx = (raw as usize).min(chars.len());
8878 out.extend(chars[..real_idx].iter());
8879 out.push_str(val);
8880 if real_idx < chars.len() {
8881 out.extend(chars[real_idx + 1..].iter());
8882 }
8883 }
8884 pm.u_str = Some(out);
8885 } else {
8886 // PM_ARRAY + numeric subscript (c:3357 `assignaparam`).
8887 // c:Src/params.c:2125-2150 + c:2911 — `a[0]=val` under
8888 // default zsh semantics (no KSHARRAYS, no KSHZEROSUBSCRIPT)
8889 // produces VALFLAG_EMPTY in getarg, which setarrvalue
8890 // then rejects with "assignment to invalid subscript
8891 // range".
8892 let kshzero = crate::ported::zsh_h::isset(crate::ported::zsh_h::KSHZEROSUBSCRIPT);
8893 if idx == 0 && !isset(KSHARRAYS) && !kshzero {
8894 drop(tab); // zerr redraws ZLE, which reads paramtab
8895 zerr(&format!("{}: assignment to invalid subscript range", name)); // c:2911 (effective)
8896 unqueue_signals();
8897 return None;
8898 }
8899 // c:2966-2967 — capture PM_UNIQUE before the mutable
8900 // borrow so the post-splice dedup below can fire.
8901 let uniq = (pm.node.flags as u32 & PM_UNIQUE) != 0;
8902 let arr = pm.u_arr.get_or_insert_with(Vec::new);
8903 let len = arr.len() as i64;
8904 // c:Src/params.c:2134 — same KSHZEROSUBSCRIPT rule as the
8905 // scalar splice above: `[0]` accesses the FIRST element, so it
8906 // maps to real index 0. Computing `idx - 1 = -1` sent it down
8907 // the negative-subscript branch, which PREPENDS: `a[0]=Z` on
8908 // (1 2 3) gave (Z 1 2 3) where zsh replaces element one and
8909 // gives (Z 2 3).
8910 let real_idx = if idx < 0 {
8911 len + idx
8912 } else if isset(KSHARRAYS) {
8913 idx
8914 } else if idx == 0 && kshzero {
8915 0
8916 } else {
8917 idx - 1
8918 };
8919 if real_idx < 0 {
8920 // c:Src/params.c:2930-2946 (setarrvalue) — a negative
8921 // subscript that points PAST the start of the array
8922 // (`a[-7]` on a 5-element array) clamps BOTH start and
8923 // end to 0, so the splice `old[start..end]` =
8924 // `old[0..0]` replaces nothing and the value is
8925 // INSERTED at the front. The prior port clamped to
8926 // index 0 and OVERWROTE element 0 (`42 2 3 4 5`);
8927 // zsh prepends keeping every element (`42 1 2 3 4 5`).
8928 arr.insert(0, val.to_string());
8929 } else {
8930 let real_idx = real_idx as usize;
8931 while arr.len() <= real_idx {
8932 arr.push(String::new());
8933 }
8934 arr[real_idx] = val.to_string();
8935 }
8936 // c:2966-2967 — `if (pm->node.flags & PM_UNIQUE)
8937 // arrunique(pm->u.arr)`. A subscript element assignment to a
8938 // PM_UNIQUE array dedupes in-place (first occurrence wins):
8939 // `typeset -aU u=(a b c); u[2]=c` → (a c). setarrvalue
8940 // already does this at its tail; the single-index element
8941 // path bypassed it.
8942 if uniq {
8943 let mut seen: std::collections::HashSet<String> =
8944 std::collections::HashSet::new();
8945 arr.retain(|s| seen.insert(s.clone())); // c:2967
8946 }
8947 pm.u_str = None;
8948 }
8949 }
8950 let cloned = pm.clone();
8951 drop(tab);
8952 unqueue_signals(); // c:3344
8953 // c:Src/params.c:2922 — `pm->gsu.a->setfn(pm, val)`. In C a
8954 // SUBSCRIPT assignment (`path[2]=x`) rebuilds the whole array
8955 // and hands it to the param's array setfn, so a tied
8956 // colon-array (path→PATH, fpath→FPATH, …) re-derives its
8957 // scalar/env side. zshrs's element path above wrote `u_arr`
8958 // directly and skipped the setfn, so `path[2]=/NEW` left
8959 // $PATH stale (`echo $PATH` showed the pre-edit value). Mirror
8960 // `arrsetfn`'s ename sync here. zshrs carries the tie via the
8961 // explicit colon-array name map (mirror of the scalar→array
8962 // cascade at params.rs:~6396); rejoin the array with `:` and
8963 // write the scalar side so `echo $PATH` sees the edit.
8964 // c:Src/params.c:2922 — a tied colon-array element assignment
8965 // re-derives the scalar side (path→PATH etc.); see
8966 // TIED_COLON_ARRAYS. The direct-u_arr element path above skipped it.
8967 let base = name.split('[').next().unwrap_or(name);
8968 if let Some((_, scalar)) = TIED_COLON_ARRAYS.iter().find(|(a, _)| *a == base) {
8969 let joined = cloned.u_arr.as_deref().unwrap_or(&[]).join(":");
8970 assignsparam(scalar, &joined, 0);
8971 }
8972 return Some(cloned); // c:3345
8973 }
8974
8975 // c:3232 non-subscripted branch.
8976 let mut tab = paramtab().write().unwrap();
8977 // c:3233 — `if (!(v = fetchvalue(&vbuf, &t, 1, SCANPM_ASSIGNING)))
8978 // { createparam(t, PM_SCALAR); created = 1; }`.
8979 // `fetchvalue` is not a plain table lookup: c:2264-2265 —
8980 // if (pm->node.flags & PM_UNSET && !(pm->node.flags & PM_DECLARED))
8981 // return NULL;
8982 // — so a node left `PM_UNSET` with `PM_DECLARED` cleared reads as
8983 // ABSENT here and takes the createparam arm, which reuses the same
8984 // struct (c:1155 `pm = oldpm`) but OVERWRITES its flags
8985 // (c:1176 `pm->node.flags = flags & ~PM_LOCAL;`). That is how
8986 // `typeset -n ref; unset -n ref; ref=str` produces an ordinary
8987 // scalar: `unsetparam_pm` clears PM_DECLARED (c:3798 "like ksh, not
8988 // like bash") and keeps the local node (c:3851), and this lookup
8989 // then drops the stale PM_NAMEREF along with the rest of the flags.
8990 //
8991 // Applied to every node type, as in C. `typeset_single` keeps its
8992 // attributes because it reaches here only after c:Src/builtin.c:2521
8993 // `createparam(pname, on & ~PM_READONLY)` re-typed the unset struct
8994 // with the full requested mask (which clears PM_UNSET); a bare
8995 // `x=val` after `integer x; unset x` gets a plain scalar, which is
8996 // also what exec.c:2619 GLOB_ASSIGN relies on (`unsetparam(name)` "to
8997 // force it to be recreated as either scalar or array").
8998 let existing = match tab.get_mut(name) {
8999 None => false,
9000 Some(pm)
9001 if (pm.node.flags as u32 & PM_RO_BY_DESIGN) != 0
9002 && crate::ported::modules::param_private::getprivatenode(&**pm as *const param)
9003 .is_null() =>
9004 {
9005 // c:2233-2235 — fetchvalue's `paramtab->getnode` is getprivatenode
9006 // (Src/Modules/param_private.c:678), which hides a private of an
9007 // enclosing scope, so c:3234 createparam runs and its c:1045-1052
9008 // RO_BY_DESIGN check (getnode2 is NULL too) rejects the write.
9009 drop(tab); // zerr redraws ZLE, which reads paramtab
9010 zerr(&format!("{}: can't modify read-only parameter", name)); // c:1049
9011 unqueue_signals(); // c:3241
9012 return None; // c:3242
9013 }
9014 Some(pm) => {
9015 let f = pm.node.flags as u32;
9016 if (f & PM_UNSET) == 0 || (f & PM_DECLARED) != 0 {
9017 true // c:2264-2265 — fetchvalue finds a set/declared node
9018 } else if isset(crate::ported::zsh_h::POSIXBUILTINS) && (f & PM_READONLY) != 0 {
9019 // c:1131-1133 — createparam's reuse arm (always taken here:
9020 // the PM_SCALAR flags carry no PM_LOCAL) rejects a read-only
9021 // node first: `setopt posixbuiltins; readonly v; v=1`.
9022 drop(tab); // zerr redraws ZLE, which reads paramtab
9023 zerr(&format!("read-only variable: {}", name)); // c:1132
9024 unqueue_signals(); // c:3241
9025 return None; // c:3242
9026 } else if (f & PM_SPECIAL) != 0
9027 || (isset(crate::ported::zsh_h::POSIXBUILTINS) && (f & PM_EXPORTED) != 0)
9028 {
9029 // c:1135-1150 — createparam declines to replace an unset
9030 // SPECIAL (or, under POSIXBUILTINS, exported) node: it
9031 // clears PM_UNSET and returns NULL, and c:3240's second
9032 // getvalue then finds the revived node.
9033 if (f & PM_RO_BY_DESIGN) != 0 {
9034 drop(tab); // zerr redraws ZLE, which reads paramtab
9035 zerr(&format!("{}: can't modify read-only parameter", name)); // c:1140
9036 unqueue_signals(); // c:3241
9037 return None; // c:3242
9038 }
9039 pm.node.flags &= !(PM_UNSET as i32); // c:1144
9040 true
9041 } else {
9042 false // c:3234 — reuse arm overwrites the flags (c:1176)
9043 }
9044 }
9045 };
9046 let created_now = !existing; // c:3232 createparam path sets `created = 1`
9047 if !existing {
9048 // c:3234 `createparam(t, PM_SCALAR); created = 1;`
9049 let mut pm_flags = PM_SCALAR as i32;
9050 if isset(ALLEXPORT) {
9051 // c:1149-1150 (ALLEXPORT path)
9052 pm_flags |= PM_EXPORTED as i32;
9053 }
9054 // c:1135-1160 — `createparam` installs gsu via the special-
9055 // params table when the name matches a PM_SPECIAL entry. C
9056 // walks `paramtab->getnode(name)` first, but for fresh
9057 // creations the gsu pointer comes from the IPDEF macro the
9058 // paramdef ships with. Mirror by looking up the name in the
9059 // special-scalar GSU table — same end-state as if
9060 // createparamtable had run.
9061 let gsu_s: Option<Box<gsu_scalar>> = match name {
9062 "0" => {
9063 pm_flags |= PM_SPECIAL as i32;
9064 Some(Box::new(ARGZERO_GSU.clone())) // c:225-226 / IPDEF2("0", argzero_gsu, 0)
9065 }
9066 "HOME" => {
9067 pm_flags |= PM_SPECIAL as i32;
9068 Some(Box::new(HOME_GSU.clone())) // c:248
9069 }
9070 "IFS" => {
9071 pm_flags |= PM_SPECIAL as i32;
9072 Some(Box::new(IFS_GSU.clone())) // c:245
9073 }
9074 "TERM" => {
9075 pm_flags |= PM_SPECIAL as i32;
9076 Some(Box::new(TERM_GSU.clone())) // c:250
9077 }
9078 "TERMINFO" => {
9079 pm_flags |= PM_SPECIAL as i32;
9080 Some(Box::new(TERMINFO_GSU.clone())) // c:251
9081 }
9082 "TERMINFO_DIRS" => {
9083 pm_flags |= PM_SPECIAL as i32;
9084 Some(Box::new(TERMINFODIRS_GSU.clone())) // c:252
9085 }
9086 "WORDCHARS" => {
9087 pm_flags |= PM_SPECIAL as i32;
9088 Some(Box::new(WORDCHARS_GSU.clone())) // c:249
9089 }
9090 "USERNAME" => {
9091 pm_flags |= PM_SPECIAL as i32;
9092 Some(Box::new(USERNAME_GSU.clone())) // c:247
9093 }
9094 "KEYBOARD_HACK" => {
9095 pm_flags |= PM_SPECIAL as i32;
9096 Some(Box::new(KEYBOARDHACK_GSU.clone())) // c:253
9097 }
9098 "HISTCHARS" | "histchars" => {
9099 pm_flags |= PM_SPECIAL as i32;
9100 Some(Box::new(HISTCHARS_GSU.clone())) // c:246
9101 }
9102 "LANG" => {
9103 pm_flags |= PM_SPECIAL as i32;
9104 Some(Box::new(LANG_GSU.clone())) // c:332
9105 }
9106 "LC_ALL" => {
9107 pm_flags |= PM_SPECIAL as i32;
9108 Some(Box::new(LC_ALL_GSU.clone())) // c:333
9109 }
9110 "LC_COLLATE" | "LC_CTYPE" | "LC_MESSAGES" | "LC_NUMERIC" | "LC_TIME" => {
9111 pm_flags |= PM_SPECIAL as i32;
9112 Some(Box::new(LC_BLAH_GSU.clone())) // c:335-346
9113 }
9114 _ => None,
9115 };
9116 // c:1152-1155 — createparam's REUSE arm:
9117 // if (oldpm && (oldpm->level == locallevel ||
9118 // !(flags & PM_LOCAL))) {
9119 // ...
9120 // pm = oldpm;
9121 // pm->base = pm->width = 0;
9122 // C keeps the SAME struct, so `pm->level` and the `pm->old`
9123 // shadow chain survive; only base/width are zeroed and the
9124 // flags are overwritten wholesale at c:1176. A node that is
9125 // still in `paramtab` but read as absent by c:2241-2243
9126 // (`PM_UNSET && !PM_DECLARED`) lands here, and rebuilding it at
9127 // `level: 0` would silently promote a LOCAL to a global —
9128 // `typeset -n ref; unset -n ref; ref=str` printed
9129 // `typeset -g ref=str` instead of `typeset ref=str`.
9130 let (reuse_level, reuse_old) = tab
9131 .get(name)
9132 .map(|old| (old.level, old.old.clone())) // c:1155
9133 .unwrap_or((0, None)); // c:1157 zshcalloc — genuinely new name
9134 let pm: Param = Box::new(param {
9135 node: hashnode {
9136 next: None,
9137 nam: name.to_string(),
9138 flags: pm_flags,
9139 },
9140 u_data: 0,
9141 u_tied: None,
9142 u_arr: None,
9143 u_str: Some(String::new()),
9144 u_val: 0,
9145 u_dval: 0.0,
9146 u_hash: None,
9147 gsu_s, // c:1149 special gsu wired
9148 gsu_i: None,
9149 gsu_f: None,
9150 gsu_a: None,
9151 gsu_h: None,
9152 base: 0, // c:1155
9153 width: 0, // c:1155
9154 env: None,
9155 ename: None,
9156 old: reuse_old, // c:1155 (same struct in C)
9157 level: reuse_level, // c:1155 (same struct in C)
9158 });
9159 tab.insert(name.to_string(), pm);
9160 } else {
9161 let pm = tab.get(name).unwrap();
9162 // c:3216 PM_READONLY guard for an existing param.
9163 if (pm.node.flags as u32 & PM_READONLY) != 0 {
9164 let nam = pm.node.nam.clone();
9165 drop(tab); // zerr redraws ZLE, which reads paramtab
9166 zerr(&format!("read-only variable: {}", nam)); // c:3217
9167 unqueue_signals(); // c:3220
9168 return None; // c:3221
9169 }
9170 // c:1135-1160 — back-fill the gsu_s vtable on existing
9171 // entries that pre-dated `createparamtable` (e.g. env-import
9172 // path inserted HOME/IFS/etc via the non-special branch
9173 // before the special-param table was populated). Without
9174 // this back-fill, those params stay None-gsu forever and
9175 // assignstrvalue falls through to the default `strsetfn`
9176 // path, bypassing homesetfn/ifssetfn — so the canonical
9177 // C globals (`home`, `ifs`, ...) drift from paramtab.
9178 let pm = tab.get_mut(name).unwrap();
9179 if pm.gsu_s.is_none() {
9180 let new_gsu: Option<Box<gsu_scalar>> = match name {
9181 "0" => Some(Box::new(ARGZERO_GSU.clone())), // c:225-226
9182 "HOME" => Some(Box::new(HOME_GSU.clone())), // c:248
9183 "IFS" => Some(Box::new(IFS_GSU.clone())), // c:245
9184 "TERM" => Some(Box::new(TERM_GSU.clone())), // c:250
9185 "TERMINFO" => Some(Box::new(TERMINFO_GSU.clone())), // c:251
9186 "TERMINFO_DIRS" => Some(Box::new(TERMINFODIRS_GSU.clone())), // c:252
9187 "WORDCHARS" => Some(Box::new(WORDCHARS_GSU.clone())), // c:249
9188 "USERNAME" => Some(Box::new(USERNAME_GSU.clone())), // c:247
9189 "KEYBOARD_HACK" => Some(Box::new(KEYBOARDHACK_GSU.clone())), // c:253
9190 "HISTCHARS" | "histchars" => Some(Box::new(HISTCHARS_GSU.clone())), // c:246
9191 "LANG" => Some(Box::new(LANG_GSU.clone())), // c:332
9192 "LC_ALL" => Some(Box::new(LC_ALL_GSU.clone())), // c:333
9193 "LC_COLLATE" | "LC_CTYPE" | "LC_MESSAGES" | "LC_NUMERIC" | "LC_TIME" => {
9194 Some(Box::new(LC_BLAH_GSU.clone())) // c:335-346
9195 }
9196 _ => None,
9197 };
9198 if new_gsu.is_some() {
9199 pm.gsu_s = new_gsu;
9200 pm.node.flags |= PM_SPECIAL as i32;
9201 }
9202 }
9203 let pm = tab.get(name).unwrap();
9204 // c:3179-3192 — an existing PM_ARRAY on a non-AUGMENT scalar
9205 // assignment, or a PM_HASHED on ANY scalar assignment (`+=`
9206 // included: `typeset -A h; h+=x` leaves h="x"), non-special,
9207 // non-tied, unset KSHARRAYS → `resetparam(v->pm, PM_SCALAR)`.
9208 let f = pm.node.flags as u32;
9209 let augment_bit = (flags & ASSPM_AUGMENT) != 0;
9210 let resets = ((f & PM_ARRAY) != 0 && !augment_bit) || (f & PM_HASHED) != 0;
9211 let is_special_or_tied = (f & (PM_SPECIAL | PM_TIED)) != 0;
9212 if resets && !is_special_or_tied && !isset(KSHARRAYS) {
9213 // c:3242 — flip type to PM_SCALAR, drop array/hash slots.
9214 let pm_mut = tab.get_mut(name).unwrap();
9215 pm_mut.node.flags =
9216 (pm_mut.node.flags & !(PM_TYPE(u32::MAX) as i32)) | PM_SCALAR as i32;
9217 pm_mut.u_arr = None;
9218 paramtab_hashed_storage().lock().unwrap().remove(name);
9219 }
9220 }
9221
9222 // c:3258-3266 `if (*val && (v->pm->node.flags & PM_NAMEREF))`.
9223 let pm = tab.get(name).unwrap();
9224 if !val.is_empty() && (pm.node.flags as u32 & PM_NAMEREF) != 0 {
9225 if !valid_refname(val, pm.node.flags) {
9226 // c:3259
9227 drop(tab); // zerr redraws ZLE, which reads paramtab
9228 zerr(&format!("invalid variable name: {}", val)); // c:3221
9229 errflag.fetch_or(
9230 // c:3263
9231 ERRFLAG_ERROR,
9232 Ordering::Relaxed,
9233 );
9234 unqueue_signals(); // c:3262
9235 return None; // c:3264
9236 }
9237 }
9238
9239 // c:3266-3268 `if (flags & ASSPM_WARN) check_warn_pm(v->pm, "scalar", created, 1);`
9240 if (flags & ASSPM_WARN) != 0 {
9241 if let Some(pm_ref) = tab.get(name) {
9242 check_warn_pm(pm_ref, "scalar", created_now as i32, 1); // c:3268
9243 }
9244 }
9245 // c:3211 `v->pm->node.flags &= ~PM_DEFAULTED;`
9246 //
9247 // c:Src/zsh.h:1926-1927 — `PM_DONTIMPORT` and `PM_DECLARED` are the
9248 // SAME bit (1<<22), and `PM_DEFAULTED` is `PM_DECLARED|PM_UNSET`
9249 // (c:1933). So this clear also wipes the do-not-import marker that
9250 // `createparamtable`'s environment-import loop reads through
9251 // `dontimport()` (c:934, c:815-828).
9252 //
9253 // In C that is harmless: the values behind the PM_SPECIAL
9254 // parameters are seeded by writing their backing globals in
9255 // `setupvals` (`ifs`, `wordchars`, `zunderscore`, `zoptind`,
9256 // `bangchar`/`hatchar`/`hashchar` — c:Src/init.c:1212-1224), which
9257 // runs BEFORE `createparamtable` (c:Src/init.c:1286) and never
9258 // touches a paramtab node, so no special ever reaches this line
9259 // before the import. zshrs keeps those values on the paramtab node
9260 // itself, so its startup seeds DO reach here — and `_`, `IFS`,
9261 // `OPTIND`, `histchars`, `path` and `fpath` lost the bit before the
9262 // import could read it (an inherited `_`/`IFS` was then imported and
9263 // exported: `typeset -p _` printed `export _=…` and `_` appeared in
9264 // `export -p` / `typeset +x -r`, where zsh shows nothing).
9265 //
9266 // Keep the bit when it can only mean PM_DONTIMPORT — i.e. on a
9267 // PM_SPECIAL node that already carries it. Every C reader of
9268 // PM_DECLARED is gated on PM_UNSET as well (c:Src/builtin.c:2038,
9269 // c:2925; c:Src/params.c:5894, c:6140; c:Src/subst.c:2813-2814) and
9270 // the value store below clears PM_UNSET (c:2704), so retaining it
9271 // changes nothing those readers can observe.
9272 let pm = tab.get_mut(name).unwrap(); // c:3211
9273 let keep_dontimport =
9274 (pm.node.flags as u32 & (PM_SPECIAL | PM_DONTIMPORT)) == (PM_SPECIAL | PM_DONTIMPORT);
9275 pm.node.flags &= !(PM_DEFAULTED as i32); // c:3211
9276 if keep_dontimport {
9277 pm.node.flags |= PM_DONTIMPORT as i32;
9278 }
9279
9280 // c:3343 `assignstrvalue(v, val, flags)`. C aliases `v->pm`
9281 // through to the param in the hash table; Rust's borrow rules
9282 // forbid holding `&mut Param` and wrapping it in `value.pm:
9283 // Option<Param>` at once. Previous port used `tab.remove(name)`
9284 // to take ownership during dispatch, then re-insert — but
9285 // assignstrvalue's PM_INTEGER arm calls `mathevali(val)` which
9286 // looks up identifiers via paramtab. With the param removed,
9287 // `X=X+1` evaluated `X` as unset (=0) and stored `0+1=1`.
9288 // Symptom: `typeset -gi X=5; X=X+1; echo $X` printed 1 instead
9289 // of 6 — broke self-referential integer arithmetic which p10k
9290 // uses for counters, frame depth, hook chain index, etc.
9291 //
9292 // Fix: clone the Param into the value struct so the original
9293 // stays in the table during mathevali's identifier lookup,
9294 // then overwrite the table entry with the mutated clone.
9295 let pm_clone = tab.get(name).unwrap().clone(); // c:3343
9296 drop(tab); // release write lock — assignstrvalue may take it
9297 let mut v = value {
9298 // c:3343
9299 pm: Some(pm_clone), // c:3343
9300 arr: Vec::new(), // c:3343
9301 scanflags: 0, // c:3343
9302 valflags: 0, // c:3343
9303 start: 0, // c:3343
9304 end: -1, // c:3343
9305 }; // c:3343
9306 assignstrvalue(Some(&mut v), Some(val.to_string()), flags); // c:3343
9307 let cloned = v.pm.as_ref().cloned(); // c:3345
9308 if let Some(pm_back) = v.pm {
9309 // c:3343
9310 paramtab()
9311 .write()
9312 .unwrap()
9313 .insert(name.to_string(), pm_back); // c:3343
9314 } // c:3343
9315 // c:Src/params.c pathsetfn / fpathsetfn / manpathsetfn /
9316 // cdpathsetfn — when the SCALAR side of a tied colon-array
9317 // pair is assigned, the canonical setfn split-rebuilds the
9318 // ARRAY side via `splitstring(value, ":", &globalarr)`. zshrs
9319 // lacks per-name GSU setfns for these, so the array stayed
9320 // stale after `PATH=/a:/b`. Mirror the split cascade
9321 // explicitly for the full IPDEF8 PM_TIED colonarr list
9322 // (c:Src/params.c:395-422): PATH↔path, FPATH↔fpath,
9323 // MANPATH↔manpath, CDPATH↔cdpath, PSVAR↔psvar,
9324 // MODULE_PATH↔module_path, FIGNORE↔fignore,
9325 // MAILPATH↔mailpath. Bug #423/#424.
9326 //
9327 // The split is the special's colonarr GSU setfn writing the internal
9328 // `path`-style array, so it only exists on the SPECIAL node:
9329 // c:Src/params.c:406 `IPDEF8("PATH", &path, "path", PM_TIED)` and, under
9330 // sh/ksh emulation, c:459 `IPDEF8("PATH", &path, NULL, 0)` both point
9331 // the special at the same internal array (zshrs keeps that array as the
9332 // `path` parameter). A `typeset -h PATH` local (c:Src/builtin.c:2083-2085
9333 // leaves newspecial NS_NONE, so createparam c:1157 gives it the plain
9334 // `stdscalar_gsu`) stores a plain string and never touches the array.
9335 // `cloned` is the node assignstrvalue just wrote.
9336 let assigned_tied = cloned
9337 .as_ref()
9338 .is_some_and(|p| (p.node.flags as u32 & PM_SPECIAL) != 0);
9339 let alt: Option<&str> = match name {
9340 "PATH" => Some("path"),
9341 "FPATH" => Some("fpath"),
9342 "MANPATH" => Some("manpath"),
9343 "CDPATH" => Some("cdpath"),
9344 "PSVAR" => Some("psvar"),
9345 "MODULE_PATH" => Some("module_path"),
9346 "FIGNORE" => Some("fignore"),
9347 "MAILPATH" => Some("mailpath"),
9348 // c:Src/Modules/watch.c:697-701 — PM_TIED since upstream b2fb112ea6.
9349 "WATCH" => Some("watch"),
9350 _ => None,
9351 };
9352 if let (Some(alt_name), true) = (alt, assigned_tied) {
9353 // c:Src/params.c colonarrsetfn splits the value the scalar now HOLDS;
9354 // under `+=` that is old + appended, not the appended text alone
9355 // (`PATH+=:/y` left `path=(/y)`).
9356 let stored = cloned.as_ref().and_then(|p| p.u_str.as_deref()).unwrap_or(val);
9357 let parts: Vec<String> = stored.split(':').map(String::from).collect();
9358 if let Ok(mut tab) = paramtab().write() {
9359 // c:Src/hashtable.c:157 — `ht->addnode(ht, ztrdup(nam), pm)`
9360 // only when the name isn't already a node; an existing node
9361 // keeps its chain position and is mutated in place.
9362 if tab.get(alt_name).is_none() {
9363 let pm = Box::new(param {
9364 node: hashnode {
9365 next: None,
9366 nam: alt_name.to_string(),
9367 flags: (PM_ARRAY | PM_SPECIAL) as i32,
9368 },
9369 u_data: 0,
9370 u_tied: None,
9371 u_arr: Some(Vec::new()),
9372 u_str: None,
9373 u_val: 0,
9374 u_dval: 0.0,
9375 u_hash: None,
9376 gsu_s: None,
9377 gsu_i: None,
9378 gsu_f: None,
9379 gsu_a: None,
9380 gsu_h: None,
9381 base: 0,
9382 width: 0,
9383 env: None,
9384 ename: None,
9385 old: None,
9386 level: 0,
9387 });
9388 tab.insert(alt_name.to_string(), pm);
9389 }
9390 let entry = tab.get_mut(alt_name).expect("inserted above");
9391 entry.u_arr = Some(parts);
9392 entry.u_str = None;
9393 }
9394 // c:Src/params.c:5291 — `if (t == path) cmdnamtab->emptytable
9395 // (cmdnamtab)`. PATH change invalidates the command-name cache.
9396 if name == "PATH" {
9397 crate::ported::hashtable::emptycmdnamtable();
9398 }
9399 }
9400 // c:Src/params.c colonarrsetfn (the REVERSE of the split-cascade above) —
9401 // a SCALAR assignment to a tied colon-ARRAY name (`path=/x`) coerces to a
9402 // 1-element array and must re-derive its tied env SCALAR (PATH). The array
9403 // assign (`path=(...)`, params.rs:4548) and element assign
9404 // (`path[2]=x`, :6230) already sync; the plain scalar-assign path did not,
9405 // so `path=/x; echo $PATH` left PATH stale while `typeset -T` ties worked.
9406 let tied_scalar: Option<&str> = TIED_COLON_ARRAYS
9407 .iter()
9408 .find(|(arr, _)| *arr == name)
9409 .map(|(_, sc)| *sc);
9410 if let Some(sc) = tied_scalar.filter(|_| assigned_tied) {
9411 // The freshly stored value joined with ':' (a scalar assign is a
9412 // 1-element array → just the value).
9413 let joined = {
9414 let tab = paramtab().read().unwrap();
9415 tab.get(name)
9416 .map(|p| match p.u_arr.as_deref() {
9417 Some(a) => a.join(":"),
9418 None => p.u_str.clone().unwrap_or_default(),
9419 })
9420 .unwrap_or_else(|| val.to_string())
9421 };
9422 if let Ok(mut tab) = paramtab().write() {
9423 // c:Src/hashtable.c:157 — add only when absent; an existing
9424 // node is mutated in place, keeping its chain position.
9425 if tab.get(sc).is_none() {
9426 let pm = Box::new(param {
9427 node: hashnode {
9428 next: None,
9429 nam: sc.to_string(),
9430 flags: (PM_SCALAR | PM_SPECIAL) as i32,
9431 },
9432 u_data: 0,
9433 u_tied: None,
9434 u_arr: None,
9435 u_str: None,
9436 u_val: 0,
9437 u_dval: 0.0,
9438 u_hash: None,
9439 gsu_s: None,
9440 gsu_i: None,
9441 gsu_f: None,
9442 gsu_a: None,
9443 gsu_h: None,
9444 base: 0,
9445 width: 0,
9446 env: None,
9447 ename: None,
9448 old: None,
9449 level: 0,
9450 });
9451 tab.insert(sc.to_string(), pm);
9452 }
9453 let entry = tab.get_mut(sc).expect("inserted above");
9454 entry.u_str = Some(joined.clone());
9455 entry.u_arr = None;
9456 }
9457 // Keep the process environment in sync so command resolution and
9458 // child processes see the new PATH/FPATH/etc.
9459 std::env::set_var(sc, &joined);
9460 if sc == "PATH" {
9461 crate::ported::hashtable::emptycmdnamtable();
9462 }
9463 }
9464 // c:Src/params.c — the prompt specials are IPDEF7 entries that share one
9465 // backing pointer: `{"PS1",…,&prompt}`, `{"PROMPT",…,&prompt}` and
9466 // `{"prompt",…,&prompt}` are the SAME storage, so a write through any one
9467 // of them is visible through all three (zshparam(1): "prompt <S> <Z>
9468 // Same as PS1"). Likewise PS2/PROMPT2 (&prompt2), PS3/PROMPT3, PS4/PROMPT4,
9469 // RPS1/RPROMPT (&rprompt) and RPS2/RPROMPT2 (&rprompt2).
9470 // zshrs lacks PM_ALIAS — mirror the scalar write to every other member of
9471 // the group. Bug #518.
9472 //
9473 // The lowercase `prompt` member is load-bearing for completion: zpwr's
9474 // `$fpath` `_parameters` lists each parameter with `${(P)name}` as its
9475 // description, so a `prompt` that reads back empty made `pr<TAB>` render
9476 // `prompt -- ` where zsh renders `prompt -- <$PS1>`.
9477 const ALIAS_GROUPS: &[&[&str]] = &[
9478 &["PS1", "PROMPT", "prompt"],
9479 &["PS2", "PROMPT2"],
9480 &["PS3", "PROMPT3"],
9481 &["PS4", "PROMPT4"],
9482 &["RPS1", "RPROMPT"],
9483 &["RPS2", "RPROMPT2"],
9484 ];
9485 let alias_group: &[&str] = ALIAS_GROUPS
9486 .iter()
9487 .copied()
9488 .find(|g| g.contains(&name))
9489 .unwrap_or(&[]);
9490 // Mirror the value the assignment actually STORED, re-read from the
9491 // table — not the incoming `val`. Under `ASSPM_AUGMENT` (`NAME+=frag`,
9492 // c:Src/params.c:3320-3332) `val` is only the appended fragment, so
9493 // mirroring it made the siblings hold the fragment alone: powerlevel10k
9494 // builds `PROMPT` chunk-by-chunk with `+=`, leaving `$PS1` — the name
9495 // `zleread` prompts with (input.rs:589) — holding just the LAST chunk,
9496 // so only the final `╰─ … ❯` line of a multi-line p10k prompt was drawn.
9497 let mirrored: Option<String> = if alias_group.is_empty() {
9498 None
9499 } else {
9500 paramtab()
9501 .read()
9502 .ok()
9503 .and_then(|tab| tab.get(name).map(|p| match p.u_arr.as_deref() {
9504 Some(a) => a.join(" "),
9505 None => p.u_str.clone().unwrap_or_default(),
9506 }))
9507 };
9508 let mirrored = mirrored.unwrap_or_else(|| val.to_string());
9509 for other in alias_group.iter().copied().filter(|n| *n != name) {
9510 if let Ok(mut tab) = paramtab().write() {
9511 // c:Src/hashtable.c:157 — add only when absent; an existing
9512 // node is mutated in place, keeping its chain position.
9513 if tab.get(other).is_none() {
9514 let pm = Box::new(param {
9515 node: hashnode {
9516 next: None,
9517 nam: other.to_string(),
9518 flags: PM_SCALAR as i32,
9519 },
9520 u_data: 0,
9521 u_tied: None,
9522 u_arr: None,
9523 u_str: Some(String::new()),
9524 u_val: 0,
9525 u_dval: 0.0,
9526 u_hash: None,
9527 gsu_s: None,
9528 gsu_i: None,
9529 gsu_f: None,
9530 gsu_a: None,
9531 gsu_h: None,
9532 base: 0,
9533 width: 0,
9534 env: None,
9535 ename: None,
9536 old: None,
9537 level: 0,
9538 });
9539 tab.insert(other.to_string(), pm);
9540 }
9541 let entry = tab.get_mut(other).expect("inserted above");
9542 entry.u_str = Some(mirrored.clone());
9543 entry.u_arr = None;
9544 }
9545 }
9546 unqueue_signals(); // c:3344
9547 cloned // c:3345
9548}
9549
9550// `VarAttr` struct + `VarKind` enum + `impl VarAttr::format_zsh`
9551// DELETED. C zsh stores typeset attributes as bare `PM_*` bit
9552// flags on `Param.node.flags` (`Src/zsh.h` PM_* + `Src/params.c`
9553// flag tests); the `${(t)var}` flag report (`typeprintparam` at
9554// `Src/builtin.c:3050+`) writes those bits to a string directly
9555// against the `Param.node.flags` int.
9556//
9557// Both types had zero external use sites — pure dead-code carryover
9558// from an earlier vm_helper refactor. The PM_* bit constants are at
9559// `zsh_h.rs:1340+` and the `(t)` formatting routes through
9560// `typeset_print_flags` (when wired) reading bare `Param.node.flags`.
9561
9562// ===========================================================
9563// Special-parameter GSU (get/set/unset) callbacks ported from
9564// Src/params.c.
9565//
9566// C zsh stores per-special-param state in file-static globals
9567// (`ifs`, `home`, `term`, `histsiz`, etc.) and dispatches getfn/
9568// setfn/unsetfn callbacks through `Param.gsu->getfn` etc. zshrs's
9569// param storage is per-evaluator HashMaps on `ShellExecutor`, so
9570// the C globals are reproduced as `OnceLock<Mutex<…>>` module
9571// statics here, with the get/set ported mutating the static.
9572//
9573// The GSU dispatch callbacks for non-special arr/hash/int/float/str
9574// params, the param-table mutators and the scope helpers operate on
9575// the `param` struct (zsh_h.rs) and its `gsu_*` function-pointer
9576// fields.
9577// ===========================================================
9578
9579// -----------------------------------------------------------
9580// Module statics — one per C global referenced by the special-
9581// param callbacks below. All initialised lazily on first read.
9582// -----------------------------------------------------------
9583
9584// `Src/params.c:515 mod_export HashTable paramtab, realparamtab;`
9585//
9586// `realparamtab` always points to the shell's global parameter
9587// table. `paramtab` normally aliases it; it is temporarily
9588// redirected during associative-array key iteration
9589// (`Src/params.c:508-513` — "paramtab is sometimes temporarily
9590// changed to point at another table").
9591//
9592// Per PORT_PLAN.md Phase 3, bucket-2 read-mostly tables use
9593// `RwLock` so parallel readers (every `$VAR` expansion, every
9594// completion lookup) don't serialize. Writers (assignments,
9595// scope pushes/pops, function-local declarations) take the
9596// exclusive write lock. `OnceLock` provides the single-static
9597// guarantee without an `Arc` allocation since the table lives
9598// for the process lifetime.
9599//
9600// Entries are keyed on `node.nam` (the canonical `param` struct
9601// lives in `zsh_h.rs`). The full `HashTable` substrate (vtable
9602// callbacks, intrusive `next` chain, scope-stacked iterators) is
9603// replaced by the typed map below, which is the operative storage.
9604//
9605// Node storage is `hashtable_nodes<Param>` — the port of the
9606// open-hashed bucket array `newparamtable(151, "paramtab")` builds
9607// (`Src/params.c:538` → `newhashtable` at `Src/hashtable.c:100`,
9608// called from `createparamtable` at `Src/params.c:854`). C's
9609// `newparamtable` differs from a plain `newhashtable` only in the
9610// callback slots it installs (`getnode = getparamnode` for the
9611// PM_AUTOLOAD stub path, `freenode = freeparamnode`,
9612// `printnode = printparamnode`, `addnode = addhashnode`,
9613// `getnode2 = gethashnode2`, `removenode = removehashnode`,
9614// c:545-556) — the node storage itself is the same bucket array,
9615// so the walk order is `hasher(nam) % 151` + front-insertion +
9616// the ×4 `expandhashtable` at `ct >= 2 * hsize`.
9617//
9618// That walk is user-visible: `${(k)parameters}` scans this table
9619// (`Src/Modules/parameter.c` `scanparamtab`), and `_parameters`
9620// feeds those matches to `compadd` in that order, which decides the
9621// `join_clines` common-prefix fold. A `FastMap` here produced an
9622// order matching zsh on zero of ~479 names.
9623static PARAMTAB_INNER: OnceLock<RwLock<hashtable_nodes<Param>>> = OnceLock::new();
9624static REALPARAMTAB_INNER: OnceLock<RwLock<hashtable_nodes<Param>>> = OnceLock::new();
9625
9626/// Array parameter assignment (no subscript).
9627///
9628/// Port of `assignaparam()` from `Src/params.c:3357`. Writes an array value into paramtab
9629/// and returns the new/updated Param.
9630///
9631/// Ported semantics:
9632/// - PM_READONLY rejection (c:3370-3381)
9633/// - PM_NAMEREF type-change reject (c:3395-3398)
9634/// - ASSPM_AUGMENT (`a+=val`) preserve-old prepend (c:3404-3412)
9635/// - PM_UNIQUE dedupe (c:3401)
9636/// - element-wise `a[k]=v` slice path pre-check (c:3373-3389)
9637/// - PM_HASHED slice rejection (c:3384-3391)
9638///
9639/// Pending (rare paths):
9640/// - resetparam from non-array (c:3415-3420) — handled implicitly
9641/// by the type-mask rewrite below; matches C observable behavior.
9642pub fn assignaparam(name: &str, val: Vec<String>, flags: i32) -> Option<Param> {
9643 // c:3357
9644 // c:3366-3370 — `if (!isident(s)) { zerr; return NULL }`.
9645 if !isident(name) {
9646 zerr(&format!("not an identifier: {}", name));
9647 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
9648 return None;
9649 }
9650 // !!! RUST-ONLY: provenance tap (see assignsparam). An array write
9651 // records the joined element list as the value summary — the same
9652 // shape `${arr}` would produce — so a lineage that flows through
9653 // `arr=( $(cmd) )` stays readable.
9654 if crate::provenance::active() {
9655 let kind = if (flags & ASSPM_AUGMENT) != 0 {
9656 "append"
9657 } else {
9658 "array"
9659 };
9660 crate::provenance::on_param_write(name, kind, &val.join(" "));
9661 }
9662 // c:Src/params.c:2922-2923 — `pm->gsu.a->setfn(pm, val)`: a special
9663 // PM_ARRAY param (e.g. `dirstack`) routes a whole-array assignment
9664 // through its GSU setter instead of plain paramtab storage. zshrs
9665 // keeps these magic arrays in PARTAB_ARRAY (not paramtab), so a
9666 // bare `dirstack=(...)` would otherwise land in the executor's
9667 // generic array store and never reach `dirssetfn` (so the dir stack
9668 // stayed empty). Dispatch writable PARTAB_ARRAY specials here. Only
9669 // whole-name (non-subscripted) assignment; `dirstack[1]=x` keeps the
9670 // existing subscript path.
9671 // c:Src/params.c:2264 — `if (!pm ...) return NULL`. Once `unset
9672 // dirstack` has removed the node (c:3874, see bin_unset), the name
9673 // has no special binding left, so `dirstack=(a b)` must create an
9674 // ORDINARY array via createparam below instead of calling
9675 // `dirssetfn`. Same gate as the subscript arm in assignsparam.
9676 if !name.contains('[') {
9677 if let Some(entry) = crate::ported::modules::parameter::PARTAB_ARRAY
9678 .iter()
9679 .find(|e| e.name == name)
9680 .filter(|_| paramtab().read().map_or(true, |t| t.get(name).is_some()))
9681 {
9682 if let Some(setfn) = entry.setfn {
9683 // c:Src/params.c:1135-1144 — see the PM_HASHED twin
9684 // below: createparam's reuse arm clears PM_UNSET on a
9685 // PM_SPECIAL node so a write after a materialized
9686 // `unset` revives the row.
9687 if let Ok(mut tab) = paramtab().write() {
9688 if let Some(pm) = tab.get_mut(name) {
9689 pm.node.flags &= !(PM_UNSET as i32); // c:1144
9690 }
9691 }
9692 setfn(std::ptr::null_mut(), val.clone()); // c:2922 gsu.a->setfn
9693 let mut pm = param::default();
9694 pm.node.nam = name.to_string();
9695 pm.node.flags = (PM_ARRAY | PM_SPECIAL) as i32;
9696 pm.u_arr = Some(val);
9697 return Some(Box::new(pm));
9698 }
9699 }
9700 }
9701 // c:3392 — `fetchvalue(&vbuf, &s, 1, SCANPM_ASSIGNING)` resolves
9702 // PM_NAMEREF chains; c:3395-3398 rejects an unresolvable ref.
9703 {
9704 let base = name.split('[').next().unwrap_or(name);
9705 if crate::ported::params::is_nameref(base) {
9706 match crate::ported::params::resolve_nameref_name(base, None) {
9707 crate::ported::params::nameref_resolution::SelfRef => {
9708 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
9709 return None;
9710 }
9711 crate::ported::params::nameref_resolution::OutOfScope => {
9712 // see assignsparam — silent failure, status 1.
9713 return None;
9714 }
9715 crate::ported::params::nameref_resolution::Placeholder(last, _) => {
9716 // c:3334 — the reject at c:3337 only fires when
9717 // `fetchvalue` RETURNED a value. `getparamnode`
9718 // (c:570-575) hands fetchvalue the RESOLVED endpoint,
9719 // and fetchvalue then applies c:2262-2263
9720 // if (!pm || ((pm->node.flags & PM_UNSET) &&
9721 // !(pm->node.flags & PM_DECLARED)))
9722 // return NULL;
9723 // so a chain ending on a reference that `unset -n`
9724 // left `PM_UNSET` with PM_DECLARED cleared reads as
9725 // ABSENT: c:3335 `createparam(t, PM_ARRAY)` runs and
9726 // c:1120-1123 retargets `oldpm` to that endpoint, so
9727 // the array lands on it as an ordinary local array.
9728 // Reaching the same end state through the endpoint's
9729 // own name is the name-keyed equivalent of C's
9730 // pointer retarget.
9731 let endpoint_visible = paramtab()
9732 .read()
9733 .ok()
9734 .and_then(|t| {
9735 t.get(&last).map(|p| {
9736 let f = p.node.flags as u32;
9737 (f & PM_UNSET) == 0 || (f & PM_DECLARED) != 0
9738 })
9739 })
9740 .unwrap_or(true); // c:2262-2263
9741 if !endpoint_visible {
9742 let mut new_s = last.clone();
9743 if let Some(us) = name.find('[').map(|i| &name[i..]) {
9744 new_s.push_str(us);
9745 }
9746 return assignaparam(&new_s, val, flags);
9747 }
9748 // c:3396 — message uses the ORIGINAL assigned name
9749 // (`t = s` saved at c:3361).
9750 zwarn(&format!("{}: can't change type of a named reference", base));
9751 return None;
9752 }
9753 crate::ported::params::nameref_resolution::Target {
9754 name: t,
9755 subscript: rsub,
9756 pm: rpm,
9757 level,
9758 } => {
9759 let user_sub = name.find('[').map(|i| &name[i..]);
9760 // Hidden (old-chain) binding — upscope write.
9761 if rsub.is_none() && user_sub.is_none() && rpm.is_some() {
9762 let visible_level = paramtab()
9763 .read()
9764 .ok()
9765 .and_then(|tb| tb.get(&t).map(|p| p.level));
9766 if visible_level != Some(level) {
9767 // upscope write, INLINE (former
9768 // nameref_hidden_array_assign).
9769 return (|name: &str, level: i32, val: Vec<String>| -> Option<Param> {
9770 let mut tab = paramtab().write().ok()?;
9771 let mut node: &mut param = tab.get_mut(name)?.as_mut();
9772 while node.level != level {
9773 node = node.old.as_mut()?.as_mut();
9774 }
9775 if (node.node.flags as u32 & PM_READONLY) != 0 {
9776 drop(tab); // zerr redraws ZLE, which reads paramtab
9777 zerr(&format!("read-only variable: {}", name)); // c:3370-3381
9778 return None;
9779 }
9780 // c:3341-3355 — a non-array, non-hash target
9781 // that is not special/tied is
9782 // `resetparam(v->pm, PM_ARRAY)`, which
9783 // refuses a pm that is not the visible
9784 // binding (c:3740-3746).
9785 let nf = node.node.flags as u32;
9786 if (PM_TYPE(nf) & (PM_ARRAY | PM_HASHED)) == 0
9787 && (nf & (PM_SPECIAL | PM_TIED)) == 0
9788 // c:2262-2263 — an unset, undeclared pm is
9789 // never fetched, so it takes createparam.
9790 && ((nf & PM_UNSET) == 0 || (nf & PM_DECLARED) != 0)
9791 {
9792 drop(tab); // zerr redraws ZLE, which reads paramtab
9793 zerr(&format!("can't change type of hidden variable: {}", name)); // c:3745
9794 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed); // c:3357
9795 return None;
9796 }
9797 let type_mask = PM_TYPE(u32::MAX) as i32;
9798 node.node.flags = (node.node.flags & !type_mask) | PM_ARRAY as i32;
9799 node.u_str = None;
9800 node.u_arr = Some(val);
9801 node.node.flags &= !((PM_UNSET | PM_DECLARED) as i32);
9802 Some(Box::new(node.clone()))
9803 })(&t, level, val);
9804 }
9805 }
9806 let mut new_s = t.clone();
9807 if let Some(rs) = &rsub {
9808 new_s.push('[');
9809 new_s.push_str(rs);
9810 new_s.push(']');
9811 }
9812 if let Some(us) = user_sub {
9813 new_s.push_str(us);
9814 }
9815 if new_s != name {
9816 return assignaparam(&new_s, val, flags);
9817 }
9818 }
9819 crate::ported::params::nameref_resolution::NotRef => {}
9820 }
9821 }
9822 }
9823
9824 // c:3375 — `if ((ss = strchr(s, '['))) { ... slice path ... }`
9825 // Extract the base name when there's a subscript and
9826 // dispatch to the slice-rejection / slice-write path.
9827 if let Some(_bracket) = name.find('[') {
9828 let base = name.split('[').next().unwrap_or(name);
9829 // c:3384-3391 — slice into a HASHED → zerr.
9830 let is_hashed = {
9831 let tab = paramtab().read().unwrap();
9832 tab.get(base)
9833 .map(|pm| (pm.node.flags as u32 & PM_HASHED) != 0)
9834 .unwrap_or(false)
9835 };
9836 if is_hashed {
9837 zerr(&format!(
9838 "{}: attempt to set slice of associative array",
9839 base
9840 ));
9841 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
9842 return None;
9843 }
9844 // Slice into a non-existent param → create as PM_ARRAY (c:3377-3382).
9845 let exists = paramtab().read().unwrap().contains_key(base);
9846 if !exists {
9847 createparam(base, PM_ARRAY as i32)?;
9848 }
9849 // c:3364-3370 — `if (!v) if (!(v = fetchvalue(&vbuf, &t, 1,
9850 // SCANPM_ASSIGNING))) { … return NULL; }` then c:3434
9851 // `setarrvalue(v, val)`: the subscript resolves to a start/end
9852 // range (getindex) and the slice is spliced. The old early return
9853 // wrote nothing, so `typeset a[2,4]=(x y)` — whose only route here is
9854 // typeset_single c:2486-2491 — left the array untouched.
9855 // !!! WARNING: RUST-ONLY WRITE-BACK — fetchvalue hands out a clone of
9856 // the paramtab node (C mutates `v->pm` in place), so the spliced node
9857 // is stored back under the base name.
9858 let mut vbuf = value {
9859 pm: None,
9860 arr: Vec::new(),
9861 scanflags: 0,
9862 valflags: 0,
9863 start: 0,
9864 end: -1,
9865 };
9866 let mut cursor: &str = name;
9867 let v = fetchvalue(Some(&mut vbuf), &mut cursor, 1, SCANPM_ASSIGNING as i32)?; // c:3365
9868 // !!! RUST-ONLY INDEX CONVENTION — getindex leaves C's 0-based start
9869 // (c:2145), but `setarrvalue` here keeps the 1-based start its fusevm
9870 // splice caller passes (see its c:2944 WARNING). Lift a non-negative
9871 // start by one; VALFLAG_INV starts are 1-based already (c:2938).
9872 if v.start >= 0 && (v.valflags & VALFLAG_INV) == 0 {
9873 v.start += 1;
9874 }
9875 setarrvalue(v, val); // c:3434
9876 let spliced = v.pm.take();
9877 if let Some(pm) = spliced {
9878 paramtab().write().unwrap().insert(base.to_string(), pm);
9879 }
9880 return paramtab().read().unwrap().get(base).cloned();
9881 }
9882
9883 // c:3391-3394 — fetchvalue / createparam(PM_ARRAY) if missing.
9884 let (existed, prior_scalar, prior_flags) = {
9885 let tab = paramtab().read().unwrap();
9886 match tab.get(name) {
9887 Some(pm) => {
9888 // c:3350 — the ASSPM_AUGMENT prepend uses
9889 // `ztrdup(getstrvalue(v))`, the type-aware string form of
9890 // the old value (PM_INTEGER → base-formatted, PM_FFLOAT/
9891 // PM_EFLOAT → convfloat, PM_SCALAR → strgetfn). Reading
9892 // `u_str` alone loses it for numeric params — u_str is
9893 // None for `integer`/`float`, so `integer i=1; i+=(2 3)`
9894 // dropped the "1". Route scalar-type targets through
9895 // getstrvalue so the conversion-to-array keeps the value.
9896 let f = pm.node.flags as u32;
9897 let ps = if f & (PM_ARRAY | PM_HASHED | PM_SPECIAL) == 0 {
9898 let mut pv = value {
9899 pm: Some(pm.clone()),
9900 arr: Vec::new(),
9901 scanflags: 0,
9902 valflags: 0,
9903 start: 0,
9904 end: -1,
9905 };
9906 Some(getstrvalue(Some(&mut pv))) // c:3350
9907 } else {
9908 pm.u_str.clone()
9909 };
9910 // c:3334 — `fetchvalue(&vbuf, &s, 1, SCANPM_ASSIGNING)`,
9911 // whose c:2241-2243 guard
9912 // if (pm->node.flags & PM_UNSET &&
9913 // !(pm->node.flags & PM_DECLARED))
9914 // return NULL;
9915 // reports a node left PM_UNSET with PM_DECLARED cleared as
9916 // ABSENT, so c:3335 `createparam(t, PM_ARRAY)` runs and
9917 // c:1176 `pm->node.flags = flags & ~PM_LOCAL;` wipes the
9918 // stale PM_NAMEREF instead of c:3337 rejecting the
9919 // assignment. `typeset -n ref; unset -n ref; ref=(a b)`
9920 // is the case that reaches this.
9921 // Applied to every type, like the twin gate in `assignsparam`.
9922 // An unset SPECIAL (or POSIXBUILTINS-exported) node is the
9923 // one createparam does not replace (c:1135-1150): it clears
9924 // PM_UNSET and the c:3368 second fetchvalue finds it again;
9925 // that revival is applied just below, outside this read lock.
9926 let fetch_visible = (f & PM_UNSET) == 0 || (f & PM_DECLARED) != 0; // c:2264-2265
9927 let revive = !fetch_visible
9928 && ((f & PM_SPECIAL) != 0
9929 || (isset(crate::ported::zsh_h::POSIXBUILTINS) && (f & PM_EXPORTED) != 0));
9930 if fetch_visible || revive {
9931 (true, ps, pm.node.flags)
9932 } else {
9933 (false, None, 0)
9934 }
9935 }
9936 None => (false, None, 0),
9937 }
9938 };
9939 if existed
9940 && (prior_flags as u32 & PM_UNSET) != 0
9941 && (prior_flags as u32 & PM_DECLARED) == 0
9942 {
9943 // c:1139-1142 — createparam's reuse arm on a RO_BY_DESIGN node (an
9944 // outer-scope private hidden by scopeprivate) errors, and the c:3368
9945 // re-fetch then finds nothing: return NULL.
9946 if (prior_flags as u32 & PM_RO_BY_DESIGN) != 0 {
9947 zerr(&format!("{}: can't modify read-only parameter", name));
9948 return None;
9949 }
9950 if let Ok(mut tab) = paramtab().write() {
9951 let mut alt: Option<String> = None;
9952 if let Some(pm) = tab.get_mut(name) {
9953 pm.node.flags &= !(PM_UNSET as i32); // c:1144
9954 if (pm.node.flags as u32 & PM_SPECIAL) != 0 {
9955 alt = pm.ename.clone(); // c:1145
9956 }
9957 }
9958 if let Some(altpm) = alt.as_deref().and_then(|a| tab.get_mut(a)) {
9959 altpm.node.flags &= !(PM_UNSET as i32); // c:1146-1149
9960 }
9961 }
9962 }
9963 // c:3397-3400 — PM_NAMEREF: can't change type of a named reference.
9964 if existed && (prior_flags as u32 & PM_NAMEREF) != 0 {
9965 zwarn(&format!("{}: can't change type of a named reference", name));
9966 return None;
9967 }
9968 let created_now = !existed; // c:3393 createparam path sets `created = 1`
9969 if !existed {
9970 createparam(name, PM_ARRAY as i32)?;
9971 }
9972
9973 // c:3370-3381 PM_READONLY rejection — C routes through setarrvalue
9974 // → arrsetfn which emits "read-only variable: X" and returns NULL.
9975 // Rust write path bypasses gsu setfn, so mirror the check here.
9976 if existed && (prior_flags as u32 & PM_READONLY) != 0 {
9977 zerr(&format!("read-only variable: {}", name));
9978 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
9979 return None;
9980 }
9981 // c:3395-3401 — the type-reset arm only fires for params that are
9982 // NOT (PM_ARRAY|PM_HASHED) and NOT PM_SPECIAL|PM_TIED. A special
9983 // param falls THROUGH the reset: `v` stays set and the assignment
9984 // reaches setarrvalue (c:3585), which dispatches:
9985 // - PM_HASHED whole-assign (v->start==0, v->end==-1) →
9986 // arrhashsetfn (c:2918-2920) → pm->gsu.h->setfn(pm, ht) — for
9987 // the zsh/parameter specials that's setpmoptions /
9988 // setpmcommands / setaliases / setfunctions / setpmnameddirs
9989 // (Src/Modules/parameter.c).
9990 // - non-array, non-hash special (e.g. $SECONDS) → zerr
9991 // "%s: attempt to assign array value to non-array" (c:2905).
9992 // The previous Rust arm rejected EVERY non-array special with
9993 // "can't change type of a special parameter" — a message C's
9994 // assignaparam never emits. Gap #2 2026-06-12.
9995 if existed {
9996 let pm_type = prior_flags as u32 & PM_TYPE(u32::MAX);
9997 if pm_type != PM_ARRAY && (prior_flags as u32 & PM_SPECIAL) != 0 {
9998 if pm_type == PM_HASHED {
9999 // c:3544-3560 — under ASSPM_KEY_VALUE, associative
10000 // arrays strictly enforce `[key]=value` syntax: walk
10001 // in strides of 3; every stride must start with a
10002 // Marker (a Marker can only introduce a
10003 // Marker/key/value triad, never appear by accident).
10004 if (flags & ASSPM_KEY_VALUE) != 0 {
10005 let mut i = 0usize;
10006 while i < val.len() {
10007 if !val[i].starts_with(Marker) {
10008 zerr("bad [key]=value syntax for associative array");
10009 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
10010 return None;
10011 }
10012 i += 3;
10013 }
10014 }
10015 // c:4124-4131 (arrhashsetfn) — count non-Marker
10016 // entries; odd → zerr + abort. zsh 5.9 truth:
10017 // `options=(noglob)` → "bad set of key/value pairs
10018 // for associative array", rc=1, script aborts.
10019 let alen = val
10020 .iter()
10021 .filter(|s| !s.starts_with(Marker as char))
10022 .count();
10023 if alen % 2 != 0 {
10024 zerr("bad set of key/value pairs for associative array");
10025 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
10026 return None;
10027 }
10028 // c:4141-4166 (arrhashsetfn pair walk) — flatten the
10029 // value list to (key, value) pairs; `[k]=v` triples
10030 // arrive as [Marker, k, v].
10031 let mut pairs: Vec<(String, String)> = Vec::with_capacity(alen / 2);
10032 let mut it = val.iter();
10033 while let Some(first) = it.next() {
10034 let key = if first.starts_with(Marker as char) {
10035 match it.next() {
10036 Some(k) => k.clone(),
10037 None => break,
10038 }
10039 } else {
10040 first.clone()
10041 };
10042 let v = it.next().cloned().unwrap_or_default();
10043 pairs.push((key, v));
10044 }
10045 // c:Src/params.c:1135-1144 — createparam's reuse arm
10046 // revives a PM_UNSET|PM_SPECIAL node
10047 // (`oldpm->node.flags &= ~PM_UNSET`) instead of making a
10048 // new param, which is how `unset functions;
10049 // functions=(k v)` re-activates the row after a
10050 // MATERIALIZED unset kept the node (c:3851-3852). The
10051 // gsu dispatch below writes the real table directly and
10052 // never runs createparam, so clear the bit here.
10053 if let Ok(mut tab) = paramtab().write() {
10054 if let Some(pm) = tab.get_mut(name) {
10055 pm.node.flags &= !(PM_UNSET as i32); // c:1144
10056 }
10057 }
10058 // pm->gsu.h->setfn(pm, ht) — per-name dispatch to the
10059 // canonical Src/Modules/parameter.c setfn ports. The
10060 // synthetic Param mirrors the established per-element
10061 // pattern (assignsparam "options"/"commands" arms).
10062 let synth: Param = Box::new(param {
10063 node: hashnode {
10064 next: None,
10065 nam: name.to_string(),
10066 flags: prior_flags,
10067 },
10068 u_data: 0,
10069 u_tied: None,
10070 u_arr: None,
10071 u_str: None,
10072 u_val: 0,
10073 u_dval: 0.0,
10074 u_hash: None,
10075 gsu_s: None,
10076 gsu_i: None,
10077 gsu_f: None,
10078 gsu_a: None,
10079 gsu_h: None,
10080 base: 0,
10081 width: 0,
10082 env: None,
10083 ename: None,
10084 old: None,
10085 level: 0,
10086 });
10087 use crate::ported::modules::parameter as pmod;
10088 match name {
10089 // SPECIALPMDEF entries with writable hash gsu
10090 // (Src/Modules/parameter.c:2235-2298, no
10091 // PM_READONLY_SPECIAL):
10092 "options" => pmod::setpmoptions(synth.clone(), &pairs), // c:2285
10093 "commands" => pmod::setpmcommands(synth.clone(), &pairs), // c:2238
10094 "functions" => pmod::setpmfunctions(synth.clone(), &pairs), // c:2263
10095 "dis_functions" => pmod::setpmdisfunctions(synth.clone(), &pairs), // c:2245
10096 "aliases" => pmod::setpmraliases(synth.clone(), &pairs), // c:2235
10097 "dis_aliases" => pmod::setpmdisraliases(synth.clone(), &pairs), // c:2241
10098 "galiases" => pmod::setpmgaliases(synth.clone(), &pairs), // c:2269
10099 "dis_galiases" => pmod::setpmdisgaliases(synth.clone(), &pairs), // c:2249
10100 "saliases" => pmod::setpmsaliases(synth.clone(), &pairs), // c:2293
10101 "dis_saliases" => pmod::setpmdissaliases(synth.clone(), &pairs), // c:2255
10102 "nameddirs" => pmod::setpmnameddirs(synth.clone(), &pairs), // c:2283
10103 _ => {
10104 // PM_READONLY_SPECIAL hashed specials (builtins,
10105 // modules, parameters, history, jobtexts, ...) —
10106 // C's PM_READONLY check (setarrvalue c:2900-2903)
10107 // rejects before any setfn. zsh 5.9 truth:
10108 // `modules=(a b)` → "read-only variable: modules",
10109 // rc=1. zshrs strips PM_READONLY off the paramtab
10110 // stubs (vm_helper init), so match by family.
10111 zerr(&format!("read-only variable: {}", name));
10112 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
10113 return None;
10114 }
10115 }
10116 return Some(synth);
10117 }
10118 // c:2905-2909 (setarrvalue) — non-hash special target.
10119 // zsh 5.9 truth: `SECONDS=(1 2)` → "SECONDS: attempt to
10120 // assign array value to non-array", rc=1.
10121 zerr(&format!(
10122 "{}: attempt to assign array value to non-array",
10123 name
10124 ));
10125 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
10126 return None;
10127 }
10128 }
10129
10130 // c:3436-3541 — ASSPM_KEY_VALUE on an ordinary array: the value
10131 // list contains Marker / index / value triads (from
10132 // keyvalpairelement, Src/subst.c:49) interleaved with plain
10133 // elements. Resolve them into a dense array:
10134 // - First pass (c:3465-3486): matheval each index; reject < 0,
10135 // and 0 unless KSH_ARRAYS (zerr "bad subscript for direct
10136 // array assignment"); KSH_ARRAYS keeps 0-based, otherwise
10137 // 1-based → decrement; track `nextind` (a plain element lands
10138 // just after the previously placed one) and `maxlen`.
10139 // - Allocate `fullval` of maxlen (c:3487-3495), pre-filled with
10140 // the existing elements under ASSPM_AUGMENT (c:3496-3502).
10141 // - Second pass (c:3503-3525): place each value; a `Marker +`
10142 // triad concatenates onto the slot's current value (c:3510-
10143 // 3515 bicat); plain elements advance nextind.
10144 // - Unset slots become "" — no sparse arrays (c:3530-3537).
10145 // C returns straight after `setarrvalue(v, fullval)` (c:3538-
10146 // 3540): the resolved vec REPLACES the whole array, so the tail
10147 // AUGMENT prepend below must not run again — clear the flag.
10148 let mut val = val;
10149 let mut flags = flags;
10150 // !!! WARNING: RUST-ONLY ARM — ZLE GSU adapter (array twin of the
10151 // assignsparam arm for BUFFER/LBUFFER/RBUFFER/CURSOR) !!!
10152 // C installs `region_highlight` with a live GSU
10153 // (Src/Zle/zle_params.c:132-133,169 `region_highlight_gsu`): a widget's assignment runs
10154 // set_region_highlight (Src/Zle/zle_refresh.c:488), which parses each
10155 // entry, and every read runs get_region_highlight (c:430), which
10156 // re-renders it — so `memo=someplugin,futureattribute=x` reads back as
10157 // `memo=someplugin`. zshrs stores the published copy in paramtab, so
10158 // inside an active widget scope route the write through the setter and
10159 // store what the getter renders. `+=` is resolved against the current
10160 // value first, as C's setarrvalue does before calling the setfn.
10161 if name == "region_highlight"
10162 && (flags & ASSPM_KEY_VALUE) == 0
10163 && crate::zle_param_sync::active()
10164 && !crate::zle_param_sync::in_live_write()
10165 {
10166 let full: Vec<String> = if (flags & ASSPM_AUGMENT) != 0 {
10167 let mut cur = getaparam(name).unwrap_or_default();
10168 cur.extend(std::mem::take(&mut val));
10169 cur
10170 } else {
10171 std::mem::take(&mut val)
10172 };
10173 crate::ported::zle::zle_refresh::set_region_highlight(Some(&full)); // c:zle_refresh.c:488
10174 val = crate::ported::zle::zle_refresh::get_region_highlight(¶m::default()); // c:zle_refresh.c:430
10175 flags &= !ASSPM_AUGMENT;
10176 }
10177 if (flags & ASSPM_KEY_VALUE) != 0 {
10178 let ksh = crate::ported::zsh_h::isset(crate::ported::zsh_h::KSHARRAYS);
10179 // c:3399 — `origptr = v->pm->gsu.a->getfn(v->pm)`: the prior
10180 // elements come from the parameter's ARRAY GETTER, not from the
10181 // node's own storage. For the IPDEF9 positional alias
10182 // `argv`/`@`/`*` (c:392-393, c:430 `IPDEF9("argv", &pparams,
10183 // NULL, 0)`) that getter is `arrvargetfn` (c:4231-4235,
10184 // `*((char ***)pm->u.data)`), which dereferences `pparams` —
10185 // the LIVE positional-parameter vector. zshrs keeps `pparams`
10186 // in `builtin::PPARAMS` and leaves this node's `u_arr` empty,
10187 // so a plain `u_arr` read here returns a stale global vector.
10188 let orig: Vec<String> = if (flags & ASSPM_AUGMENT) == 0 {
10189 Vec::new()
10190 } else if matches!(name, "argv" | "@" | "*") {
10191 // c:3399 via c:4231 arrvargetfn — read `pparams`.
10192 PPARAMS.lock().map(|p| p.clone()).unwrap_or_default()
10193 } else if existed {
10194 let tab = paramtab().read().unwrap();
10195 tab.get(name)
10196 .and_then(|pm| pm.u_arr.clone())
10197 .unwrap_or_default()
10198 } else {
10199 Vec::new()
10200 };
10201 let mut maxlen = orig.len(); // c:3460-3462
10202 let mut nextind: i64 = 0; // c:3464
10203 let mut subscripts: Vec<i64> = Vec::new(); // c:3455-3456
10204 let mut i = 0usize;
10205 while i < val.len() {
10206 // c:3466-3481
10207 if val[i].starts_with(Marker) {
10208 let key_str = val.get(i + 1).cloned().unwrap_or_default();
10209 let idx = match crate::ported::math::mathevali(&key_str) {
10210 Ok(n) => n,
10211 Err(e) => {
10212 // C mathevali zerr's internally (Src/math.c);
10213 // surface the message and abort the assign.
10214 zerr(&e);
10215 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
10216 return None;
10217 }
10218 };
10219 if idx < 0 || (!ksh && idx == 0) {
10220 // c:3468-3474
10221 zerr(&format!(
10222 "bad subscript for direct array assignment: {}",
10223 key_str
10224 ));
10225 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
10226 return None;
10227 }
10228 let ix = if ksh { idx } else { idx - 1 }; // c:3475-3476
10229 subscripts.push(ix);
10230 nextind = ix + 1; // c:3477
10231 i += 3; // c:3479
10232 } else {
10233 nextind += 1; // c:3481-3482
10234 i += 1;
10235 }
10236 if nextind > maxlen as i64 {
10237 // c:3484-3485
10238 maxlen = nextind as usize;
10239 }
10240 }
10241 // c:3487-3495 fullval = zshcalloc((maxlen+1) * sizeof(char*)).
10242 let mut fullval: Vec<Option<String>> = vec![None; maxlen];
10243 // c:3496-3502 — AUGMENT: copy the original elements in first.
10244 for (j, o) in orig.iter().enumerate() {
10245 fullval[j] = Some(o.clone());
10246 }
10247 // Second pass — c:3503-3525.
10248 let mut si = 0usize;
10249 let mut nextind = 0usize;
10250 let mut i = 0usize;
10251 while i < val.len() {
10252 if val[i].starts_with(Marker) {
10253 let augment_elt = val[i].len() > Marker.len_utf8(); // c:3507 `(*aptr)[1] == '+'`
10254 let ix = subscripts[si] as usize;
10255 si += 1;
10256 let old = fullval[ix].take();
10257 fullval[ix] = Some(match old {
10258 // c:3510-3512 bicat(old, value)
10259 Some(o) if augment_elt => format!("{}{}", o, val[i + 2]),
10260 _ => val[i + 2].clone(),
10261 });
10262 nextind = ix + 1; // c:3516
10263 i += 3;
10264 } else {
10265 fullval[nextind] = Some(val[i].clone()); // c:3519-3520
10266 nextind += 1;
10267 i += 1;
10268 }
10269 }
10270 // c:3530-3537 — unfilled slots become "".
10271 val = fullval.into_iter().map(|o| o.unwrap_or_default()).collect();
10272 // c:3538-3540 — setarrvalue(v, fullval); return. The orig
10273 // elements are already merged in; don't prepend them again.
10274 flags &= !ASSPM_AUGMENT;
10275 }
10276
10277 // c:3402-3412 — ASSPM_AUGMENT preserve-old prepend. When the
10278 // previous value was a scalar (not array/hashed) and we're
10279 // augmenting (`a+=val`), prepend that scalar's string form as
10280 // val[0]. Only fires when the existing param is not PM_UNSET.
10281 let was_scalar_array_target = existed
10282 && prior_flags & (PM_ARRAY | PM_HASHED) as i32 == 0
10283 && prior_flags & PM_SPECIAL as i32 == 0;
10284 if (flags & ASSPM_AUGMENT) != 0 && was_scalar_array_target && prior_flags & PM_UNSET as i32 == 0
10285 {
10286 if let Some(old_scalar) = prior_scalar {
10287 val.insert(0, old_scalar); // c:3408-3411
10288 }
10289 }
10290
10291 // c:3511-3515 — ASSPM_AUGMENT on the IPDEF9 positional alias
10292 // `argv` / `@` / `*` (c:392-393, c:430 `IPDEF9("argv", &pparams,
10293 // NULL, 0)`).
10294 //
10295 // if (v->start == 0 && v->end == -1) {
10296 // if (PM_TYPE(v->pm->node.flags) & PM_ARRAY) {
10297 // v->start = arrlen(v->pm->gsu.a->getfn(v->pm));
10298 // v->end = v->start + 1;
10299 // }
10300 //
10301 // The append offset comes from the parameter's ARRAY GETTER. For
10302 // these three names the getter is `arrvargetfn` (c:4231-4235,
10303 // `char **arrptr = *((char ***)pm->u.data);` over `&pparams`) and
10304 // the setter is `arrvarsetfn` (c:4246-4262, `*dptr = x;`) — the
10305 // node has NO storage of its own, `pparams` (c:59, `char **pparams;
10306 // /* $argv */`) IS the storage, and `doshfunc` swaps that vector
10307 // per call so the value is function-local.
10308 //
10309 // zshrs keeps `pparams` in `builtin::PPARAMS` and leaves this
10310 // node's `u_arr` empty, so the generic `u_arr` read below returned
10311 // an empty prior array: `f() { argv+=(X) }; f a b` stored just
10312 // `(X)`, discarding the positionals, and — because `u_arr` is a
10313 // process-global slot with no per-call swap — each later call
10314 // appended to the PREVIOUS call's leftovers (`$#` counted
10315 // 1, 2, 3 … across three identical calls where zsh counts 4 every
10316 // time). Read the live vector instead; the node is PM_SPECIAL |
10317 // PM_ARRAY and is never unset (c:430), so the append always fires
10318 // and needs none of the `existed` / PM_UNSET gates below.
10319 if (flags & ASSPM_AUGMENT) != 0 && matches!(name, "argv" | "@" | "*") {
10320 // c:3514 `arrlen(v->pm->gsu.a->getfn(v->pm))` → c:4231 arrvargetfn.
10321 let mut appended: Vec<String> = PPARAMS.lock().map(|p| p.clone()).unwrap_or_default();
10322 appended.extend(val); // c:3528 setarrvalue writes past the tail
10323 val = appended;
10324 }
10325 // c:3570-3585 — ASSPM_AUGMENT on an existing PM_ARRAY target:
10326 // append rather than replace. C bumps v->start to arrlen(existing)
10327 // and v->end to start+1 so setarrvalue writes past the tail.
10328 // zshrs writes through pm.u_arr without the value struct, so do
10329 // the equivalent here: prepend the existing array elements to the
10330 // new val so the final stored vec is [old..., new...].
10331 else if (flags & ASSPM_AUGMENT) != 0
10332 && existed
10333 && (prior_flags as u32 & PM_ARRAY) != 0
10334 && (prior_flags as u32 & PM_UNSET) == 0
10335 {
10336 // Fast in-place append. `arr+=(x)` in a loop is the hot path.
10337 // The general branch below clones the prior array, re-collects
10338 // [old…, new…], re-stores it, and then clones the whole `param`
10339 // for the return value — four O(n) copies per call, so N appends
10340 // are O(N²) (this is the "terrible slowness for array append"
10341 // the profiler pinned on `Vec<String>::clone` under
10342 // `assignaparam`). For a plain array — no special setfn, no env
10343 // tie (`ename`), no PM_UNIQUE dedupe, no colon-tie, not the
10344 // `argv`/`@`/`*` positional alias — push the new elements
10345 // straight onto `pm.u_arr` under the write lock (amortized O(k))
10346 // and return a cheap non-None marker. Every non-test caller uses
10347 // the return only via `.is_none()` for the error path.
10348 let is_special = (prior_flags as u32 & (PM_SPECIAL | PM_UNIQUE)) != 0;
10349 let is_positional = name == "argv" || name == "@" || name == "*";
10350 let is_colon_tie = TIED_COLON_ARRAYS.iter().any(|(a, _)| *a == name);
10351 if !is_special && !is_positional && !is_colon_tie {
10352 let mut tab = paramtab().write().unwrap();
10353 if let Some(pm) = tab.get_mut(name) {
10354 if pm.ename.is_none() && (pm.node.flags as u32 & (PM_SPECIAL | PM_UNIQUE)) == 0 {
10355 pm.u_arr.get_or_insert_with(Vec::new).extend(val);
10356 pm.u_str = None;
10357 pm.u_hash = None;
10358 // c:2712 setarrvalue head — clears UNSET on first write.
10359 pm.node.flags &= !((PM_UNSET | PM_DECLARED) as i32);
10360 let mut marker = param::default();
10361 marker.node.nam = name.to_string();
10362 marker.node.flags = pm.node.flags;
10363 return Some(Box::new(marker));
10364 }
10365 }
10366 // Param vanished or grew a tie under the lock — fall back to
10367 // the general read-modify-write path below.
10368 drop(tab);
10369 }
10370 let prior_arr = {
10371 let tab = paramtab().read().unwrap();
10372 tab.get(name)
10373 .and_then(|pm| pm.u_arr.clone())
10374 .unwrap_or_default()
10375 };
10376 let appended: Vec<String> = prior_arr.into_iter().chain(val.into_iter()).collect();
10377 val = appended;
10378 }
10379
10380 // c:3432 `if (flags & ASSPM_WARN) check_warn_pm(v->pm, "array", created, may_warn_about_nested_vars);`
10381 // c:3372 — `may_warn_about_nested_vars = !(flags & ASSPM_AUGMENT)`.
10382 if (flags & ASSPM_WARN) != 0 {
10383 let may_nested = if (flags & ASSPM_AUGMENT) != 0 { 0 } else { 1 };
10384 if let Some(pm_ref) = paramtab().read().unwrap().get(name) {
10385 check_warn_pm(pm_ref, "array", created_now as i32, may_nested); // c:3432
10386 }
10387 }
10388
10389 // c:3434 — setarrvalue(v, val): store array in pm.u_arr.
10390 let mut tab = paramtab().write().unwrap();
10391 let pm = tab.get_mut(name)?;
10392 let uniq = pm.node.flags & PM_UNIQUE as i32 != 0; // c:3401
10393 if pm.node.flags & PM_SPECIAL as i32 == 0 {
10394 let type_mask = PM_ARRAY | PM_INTEGER | PM_EFLOAT | PM_FFLOAT | PM_HASHED | PM_NAMEREF;
10395 pm.node.flags = (pm.node.flags & !type_mask as i32) | PM_ARRAY as i32;
10396 }
10397 // c:3401 — preserve PM_UNIQUE through the type change, then let
10398 // arrsetfn dedupe via the actual write.
10399 if uniq {
10400 pm.node.flags |= PM_UNIQUE as i32;
10401 }
10402 let val_final = if uniq { simple_arrayuniq(val) } else { val };
10403 // c:3434 — `setarrvalue(v, val);` → `gsu.a->setfn(pm, val)` for
10404 // PM_SPECIAL params (PATH/path → pathsetfn rebuilds $PATH cstring;
10405 // FPATH/fpath → fpathsetfn rebuilds; MANPATH etc.). Without this
10406 // dispatch, env var pairs (PATH vs path) drift apart on array
10407 // assignment. Fall back to direct u_arr write when no gsu_a is
10408 // wired (regular non-special arrays).
10409 let setfn_ptr = pm.gsu_a.as_ref().map(|g| g.setfn);
10410 // c:Src/params.c:3434 — `setarrvalue(v, val)` calls `gsu.a->setfn(pm, val)`.
10411 // The canonical arrsetfn (params.rs:6584) writes `pm->u_arr` then,
10412 // if `pm->ename` is set, calls `arrfixenv` which re-acquires the
10413 // paramtab lock. We're holding the WRITE lock here — that would
10414 // deadlock the RWLock. Inline the storage write under the held
10415 // lock and defer arrfixenv to AFTER drop(tab). Bug #600.
10416 pm.u_arr = Some(val_final.clone());
10417 pm.u_str = None;
10418 pm.u_hash = None;
10419 // c:3374 `v->pm->node.flags &= ~PM_DEFAULTED;` — a declared-but-unset
10420 // (TYPESET_TO_UNSET) array becomes set on its first assignment.
10421 // PM_DEFAULTED is `PM_DECLARED|PM_UNSET` (c:Src/zsh.h:1933), hence
10422 // the two-bit mask here.
10423 //
10424 // PM_DECLARED is the same bit as PM_DONTIMPORT (c:Src/zsh.h:1926-1927);
10425 // keep it on a PM_SPECIAL node that carries it, where it can only be
10426 // the do-not-import marker the environment-import loop reads
10427 // (c:934). Full rationale on the scalar twin of this clear in
10428 // `assignsparam` (c:3211). Without it the IPDEF9 arrays `path` /
10429 // `fpath` (c:425-432 — `PM_ARRAY|PM_SPECIAL|PM_DONTIMPORT`) lost the
10430 // bit while being seeded at startup.
10431 let keep_dontimport =
10432 (pm.node.flags as u32 & (PM_SPECIAL | PM_DONTIMPORT)) == (PM_SPECIAL | PM_DONTIMPORT);
10433 pm.node.flags &= !((PM_UNSET | PM_DECLARED) as i32); // c:3374
10434 if keep_dontimport {
10435 pm.node.flags |= PM_DONTIMPORT as i32;
10436 }
10437 // c:Src/params.c:4027-4029 — `if (pm->ename && x) arrfixenv(pm->ename, x);`
10438 // arrsetfn runs this UNCONDITIONALLY; it does not consult the gsu. Gating
10439 // it on a wired `gsu_a` setfn meant a USER tie — whose array half
10440 // bin_typeset builds with no gsu at all (builtin.rs, `apm`) — never
10441 // republished to the environment: `local -xT OUTER outer; outer=(i n n e r)`
10442 // read back as `i:n:n:e:r` inside the shell while every child saw `$OUTER`
10443 // empty. arrfixenv itself returns early unless the scalar half is
10444 // PM_EXPORTED (c:5311-5312), so this is a no-op for unexported ties.
10445 let _ = setfn_ptr;
10446 let ename_for_envsync: Option<String> = pm.ename.clone();
10447 let cloned = pm.clone();
10448 drop(tab);
10449 // c:Src/params.c:5285 arrfixenv — deferred from inside the lock
10450 // (see Bug #600 above). Acquires its own paramtab read+write
10451 // locks; safe to call now that we've dropped the write lock.
10452 if let Some(ename) = ename_for_envsync {
10453 arrfixenv(&ename, Some(&val_final));
10454 }
10455 // c:Src/params.c:3434 — `setarrvalue(v, val)` runs the array's setfn, and
10456 // for a colon-tied special that setfn republishes the joined value to its
10457 // paired SCALAR. zshrs routes that through `ename` + `arrfixenv` above, but
10458 // only when a `gsu_a` setfn exists — and path/fpath/cdpath/… are registered
10459 // from the `special_paramdef` DATA table (params.rs:1027) with no gsu
10460 // pointers at all, so `ename_for_envsync` was None and the scalar never
10461 // moved. A GLOBAL `path=(…)` hides the bug because it compiles to
10462 // BUILTIN_SET_ARRAY in the bridge, which carries the tie itself; a
10463 // `local path=(…)` is a typeset DECLARATION and lands here instead, so
10464 // `$PATH` — and therefore command lookup inside the function — kept the
10465 // outer value. docs/BUGS.md #1039 A.
10466 //
10467 // Writes the scalar straight into paramtab rather than calling
10468 // `assignsparam`, matching the existing sync sites: assignsparam carries
10469 // the scalar→array half of the same tie, so routing through it would
10470 // recurse.
10471 if let Some((_, sc)) = TIED_COLON_ARRAYS.iter().find(|(a, _)| *a == name) {
10472 let joined = val_final.join(":");
10473 if let Ok(mut tab) = paramtab().write() {
10474 if let Some(entry) = tab.get_mut(*sc) {
10475 entry.u_str = Some(joined.clone());
10476 entry.u_arr = None;
10477 }
10478 }
10479 // Keep the process environment and the command hash in step, exactly as
10480 // the scalar-side sync does — without the rehash, `command -v` inside
10481 // the function still resolves against the stale PATH.
10482 std::env::set_var(sc, &joined);
10483 if *sc == "PATH" {
10484 crate::ported::hashtable::emptycmdnamtable();
10485 }
10486 }
10487 // c:Src/params.c:3262 IPDEF9 — \`argv\`/\`@\`/\`*\` are aliases for
10488 // the C global \`pparams\` (the positional parameter vector).
10489 // assignaparam("argv", [...]) in C writes through the array's
10490 // setfn which mutates \`pparams\` directly. zshrs's pparams lives
10491 // in builtin::PPARAMS; mirror the write so \`argv=(...)\` updates
10492 // \$1/\$2/.../\$# correctly.
10493 if name == "argv" || name == "@" || name == "*" {
10494 if let Ok(mut pp) = crate::ported::builtin::PPARAMS.lock() {
10495 *pp = val_final.clone();
10496 }
10497 }
10498 let _ = val_final;
10499 Some(cloned)
10500}
10501
10502/// Set array parameter.
10503/// Port of `setaparam()` from `Src/params.c:3595` — single-line wrapper
10504/// around `assignaparam(s, val, ASSPM_WARN)`. C body:
10505/// ```c
10506/// mod_export Param setaparam(char *s, char **val) {
10507/// return assignaparam(s, val, ASSPM_WARN);
10508/// }
10509/// ```
10510///
10511/// `ASSPM_WARN` (params.c:104) drives the WARN_CREATE_GLOBAL /
10512/// WARN_NESTED_VAR diagnostics inside `assignaparam` →
10513/// `check_warn_pm` (params.rs:4428).
10514/// WARNING: param names don't match C — Rust=() vs C=(s, val)
10515pub fn setaparam(name: &str, val: Vec<String>) -> Option<Param> {
10516 // !!! RUST-ONLY PLACEMENT !!! C's assignaparam keeps an existing ordinary
10517 // PM_HASHED hashed — c:3341-3342 `else if (!(PM_TYPE(v->pm->node.flags) &
10518 // (PM_ARRAY|PM_HASHED)) && ...)` excludes it from the reset — and c:3434
10519 // `setarrvalue` hands the words to `arrhashsetfn` (c:2918-2920) as
10520 // key/value pairs; `sethparam` is that chain (c:3651). The port's
10521 // assignaparam instead rewrites the type to PM_ARRAY, and `typeset -a h`
10522 // on an existing assoc relies on that rewrite (C converts in
10523 // typeset_single before assigning), so the C behaviour is applied at this
10524 // entry point, which typeset does not use. Without it
10525 // `typeset -A h; read -A h <<< 'k v'` and `zstat -A h -n` made `h` a
10526 // plain array (D07multibyte.ztst, workers/50150).
10527 let hashed = paramtab()
10528 .read()
10529 .ok()
10530 .and_then(|tab| tab.get(name).map(|pm| pm.node.flags as u32))
10531 .map(|f| PM_TYPE(f) == PM_HASHED && f & (PM_SPECIAL | PM_UNSET) == 0)
10532 .unwrap_or(false);
10533 if hashed {
10534 return sethparam(name, val); // c:3434 → c:2920 arrhashsetfn
10535 }
10536 // c:3766 — `return assignaparam(s, val, ASSPM_WARN)`.
10537 assignaparam(name, val, ASSPM_WARN)
10538}
10539
10540/// Port of `sethparam()` from `Src/params.c:3602`. Writes an associative array (flat
10541/// alternating key,value list) into paramtab + the parallel
10542/// `paramtab_hashed_storage` table; returns the new Param.
10543///
10544/// Ported C semantics:
10545/// - PM_READONLY rejection (c:3625 via setarrvalue chain in C; here inline)
10546/// - PM_SPECIAL type-change reject (c:3637)
10547/// Pending:
10548/// - resetparam(PM_HASHED) for non-special type-change (rare)
10549pub fn sethparam(name: &str, val: Vec<String>) -> Option<Param> {
10550 // c:3611-3615 — `if (!isident(s)) { zerr; return NULL }`.
10551 if !isident(name) {
10552 zerr(&format!("not an identifier: {}", name));
10553 return None;
10554 }
10555 // !!! RUST-ONLY: provenance tap (see assignsparam).
10556 if crate::provenance::active() {
10557 crate::provenance::on_param_write(name, "assoc", &val.join(" "));
10558 }
10559 // c:3630 — `fetchvalue(&vbuf, &s, 1, SCANPM_ASSIGNING)` resolves
10560 // PM_NAMEREF chains (same shape as assignaparam c:3392-3398).
10561 if crate::ported::params::is_nameref(name) {
10562 match crate::ported::params::resolve_nameref_name(name, None) {
10563 crate::ported::params::nameref_resolution::SelfRef => {
10564 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
10565 return None;
10566 }
10567 crate::ported::params::nameref_resolution::OutOfScope => {
10568 return None;
10569 }
10570 crate::ported::params::nameref_resolution::Placeholder(..) => {
10571 zwarn(&format!("{}: can't change type of a named reference", name));
10572 return None;
10573 }
10574 crate::ported::params::nameref_resolution::Target {
10575 name: t,
10576 subscript: None,
10577 ..
10578 } => {
10579 if t != name {
10580 return sethparam(&t, val);
10581 }
10582 }
10583 _ => {}
10584 }
10585 }
10586 // c:3617-3621 — `if (strchr(s, '[')) { zerr; return NULL }`.
10587 if name.contains('[') {
10588 zerr("nested associative arrays not yet supported");
10589 return None;
10590 }
10591
10592 // c:3625 — PM_READONLY rejection. C routes through gsu.h->setfn
10593 // which checks readonly inside hashsetfn / arrhashsetfn; Rust
10594 // bypasses that path, so check explicitly here.
10595 {
10596 let tab = paramtab().read().unwrap();
10597 if let Some(pm) = tab.get(name) {
10598 if (pm.node.flags as u32 & PM_READONLY) != 0 {
10599 drop(tab); // zerr redraws ZLE, which reads paramtab
10600 zerr(&format!("read-only variable: {}", name));
10601 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
10602 return None;
10603 }
10604 // c:3637 — `if (PM_TYPE(pm->node.flags) != PM_HASHED &&
10605 // (pm->node.flags & PM_SPECIAL)) { zerr; return; }`
10606 // Can't change type of a PM_SPECIAL non-hashed param.
10607 let pm_type = pm.node.flags as u32 & PM_TYPE(u32::MAX);
10608 if pm_type != PM_HASHED && (pm.node.flags as u32 & PM_SPECIAL) != 0 {
10609 drop(tab); // zerr redraws ZLE, which reads paramtab
10610 zerr(&format!(
10611 "{}: can't change type of a special parameter",
10612 name
10613 ));
10614 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
10615 return None;
10616 }
10617 }
10618 }
10619
10620 // c:3625 — fetchvalue / createparam(PM_HASHED) if missing.
10621 let exists = paramtab().read().unwrap().contains_key(name);
10622 let checkcreate = !exists; // c:3626 `checkcreate = 1;`
10623 if !exists {
10624 createparam(name, PM_HASHED as i32)?;
10625 }
10626
10627 // c:3649 `check_warn_pm(v->pm, "associative array", checkcreate, 1);`
10628 // — sethparam always warns (no ASSPM_WARN gate in C).
10629 if let Some(pm_ref) = paramtab().read().unwrap().get(name) {
10630 check_warn_pm(pm_ref, "associative array", checkcreate as i32, 1); // c:3649
10631 }
10632
10633 // c:3651 — `setarrvalue(v, val);` — full-replace dispatch for a
10634 // PM_HASHED param (setarrvalue c:2919-2920) collapses to
10635 // `arrhashsetfn(v->pm, val, 0)`, which owns the odd-count gate
10636 // (c:4128-4131, zerr + ERRFLAG_ERROR) and the pair walk.
10637 // !!! LOCK DISCIPLINE — NO GSU CALLBACK MAY RUN UNDER THE GUARD !!!
10638 // Same rule, and the same reasoning, as the long note in `assignnparam`:
10639 // C holds no lock, so `arrhashsetfn` (c:4113) is free to reach anything,
10640 // and it does — its odd-pair rejection at c:4129-4131 calls `zerr`, which
10641 // is not a leaf (zwarning → zleentry(ZLE_CMD_TRASH) → the ZLE repaint,
10642 // which reads parameters and takes this very RwLock). `typeset -A h; h=(a
10643 // b c)` in an interactive shell was therefore one `zerr` away from parking
10644 // the shell on itself. Commit the flags under the guard, detach, release,
10645 // then dispatch — the hash contents go to `paramtab_hashed_storage`, a
10646 // separate map, so there is nothing to publish back.
10647 let mut staged = {
10648 let mut tab = paramtab().write().unwrap();
10649 let pm = tab.get_mut(name)?;
10650 if pm.node.flags & PM_SPECIAL as i32 == 0 {
10651 let type_mask = PM_ARRAY | PM_INTEGER | PM_EFLOAT | PM_FFLOAT | PM_HASHED | PM_NAMEREF;
10652 pm.node.flags = (pm.node.flags & !type_mask as i32) | PM_HASHED as i32;
10653 }
10654 // c:3590 — `v->pm->node.flags &= ~PM_DEFAULTED;` before the
10655 // setarrvalue: an assignment clears the PM_UNSET|PM_DECLARED state
10656 // a TYPESET_TO_UNSET declaration (`typeset -A h`) left behind, so a
10657 // later `typeset -p h` prints the value instead of the bare
10658 // declaration.
10659 pm.node.flags &= !(PM_DEFAULTED as i32); // c:3590
10660 pm.u_arr = None;
10661 pm.u_str = None;
10662 (**pm).clone()
10663 };
10664 // ---- guard released ----
10665 arrhashsetfn(&mut staged, val, 0); // c:3651 via setarrvalue c:2920
10666
10667 // c:3652-3653 — `unqueue_signals(); return v->pm;` — C returns
10668 // the param even when arrhashsetfn errored; the failure travels
10669 // via errflag (callers like the SET_ARRAY bridge check it).
10670 Some(Box::new(staged))
10671}
10672
10673// -----------------------------------------------------------
10674// Param-table mutators / scope / nameref helpers.
10675// `Src/params.c` calls these against the global `paramtab`; here they
10676// operate on the typed `paramtab()` store.
10677// -----------------------------------------------------------
10678
10679/// Port of `assignnparam()` from `Src/params.c:3664`. C body
10680/// looks up the param via `gethashnode2(realparamtab, s)`,
10681/// dispatches on PM_TYPE: PM_INTEGER → `intsetfn(pm, val.u.l)`;
10682/// PM_FFLOAT/EFLOAT → `floatsetfn(pm, val.u.d)`; otherwise
10683/// `assignstrvalue(&v, conv_to_string(val), flags)`. Signature
10684/// mirrors C `mnumber val`.
10685/// flow: isident guard → unset(EXECOPT) bail → `getvalue(&vbuf,&s,1)`
10686/// → if existing array/hashed (non-special, non-tied, non-KSHARRAYS,
10687/// no subscript) → unsetparam_pm + recreate → else if no value →
10688/// `createparam(t, type)` (POSIXIDENTIFIERS gates SCALAR vs
10689/// MN_INTEGER→PM_INTEGER else PM_FFLOAT) → second `getvalue` →
10690/// `check_warn_pm` if ASSPM_WARN → clear PM_DEFAULTED → `setnumvalue`
10691/// → return pm. The create-new-param branch calls `createparam`
10692/// and returns None only when the parameter cannot be found or created.
10693pub fn assignnparam(s: &str, val: mnumber, flags: i32) -> Option<Box<param>> {
10694 // c:3666 `if (!isident(s)) { zerr; errflag |= ERRFLAG_ERROR; return NULL; }`
10695 if !isident(s) {
10696 zerr(&format!("not an identifier: {}", s)); // c:3667
10697 errflag.fetch_or(
10698 // c:3669
10699 ERRFLAG_ERROR,
10700 Ordering::Relaxed,
10701 );
10702 return None; // c:3670
10703 }
10704 if unset(EXECOPT) {
10705 return None;
10706 }
10707 let mut vbuf = value {
10708 pm: None,
10709 arr: Vec::new(),
10710 scanflags: 0,
10711 valflags: 0,
10712 start: 0,
10713 end: -1,
10714 };
10715 let mut cursor: &str = s;
10716 let has_sub = s.contains('[');
10717 let mut was_unset = false;
10718 let v = getvalue(Some(&mut vbuf), &mut cursor, 1);
10719 let need_create = match v {
10720 Some(ref vv) => {
10721 if let Some(pm) = vv.pm.as_ref() {
10722 let f = pm.node.flags as u32;
10723 if (f & (PM_ARRAY | PM_HASHED)) != 0
10724 && (f & (PM_SPECIAL | PM_TIED)) == 0
10725 && unset(KSHARRAYS)
10726 && !has_sub
10727 {
10728 // unsetparam_pm(vv.pm, 0, 1);
10729 was_unset = true;
10730 true
10731 } else {
10732 false
10733 }
10734 } else {
10735 true
10736 }
10737 }
10738 None => true,
10739 };
10740 if need_create {
10741 // c:3686-3716 — create the parameter (or revive the existing node
10742 // createparam declines), then store the number through the same
10743 // setnumvalue path a reassignment takes.
10744 // c:Src/params.c assignnparam — createparam picks the type as
10745 // ss ? PM_ARRAY : isset(POSIXIDENTIFIERS) ? PM_SCALAR
10746 // : (val.type & MN_INTEGER) ? PM_INTEGER : PM_FFLOAT
10747 // So `let` / `(( … ))` do NOT auto-type a NEW variable as integer when
10748 // POSIXIDENTIFIERS is set — the var is a plain scalar holding the
10749 // decimal string, so a later `y=abc` keeps the string (plain zsh, with
10750 // POSIXIDENTIFIERS off, arith-coerces it to 0). `typeset -i` is
10751 // unaffected: it pre-creates the integer param, so `need_create` is
10752 // false there. POSIXIDENTIFIERS is set by BOTH `emulate sh` and
10753 // `emulate ksh` (and their `--sh`/`--ksh` drop-ins), off in zsh — so
10754 // this correctly covers every non-zsh leg. Verified: `let 'y=5';
10755 // y=abc; print $y` → `abc` in ksh93/mksh AND zsh `emulate sh`/`ksh`;
10756 // only plain zsh → `0`. Also fixes `(( x=010 ))` → scalar `8` (was the
10757 // integer `8#10`) under emulate sh.
10758 if isset(crate::ported::zsh_h::POSIXIDENTIFIERS) {
10759 let sval = if val.type_ == MN_FLOAT {
10760 convfloat_underscore(val.d, 0)
10761 } else {
10762 convbase_underscore(val.l, 10, 0)
10763 };
10764 return assignsparam(s, &sval, flags);
10765 }
10766 // c:3678-3685 — a plain (non-special, untied) array or hash that is
10767 // assigned a number without a subscript is removed first so it can be
10768 // recreated as a number:
10769 // unsetparam_pm(v->pm, 0, 1);
10770 // was_unset = 1;
10771 if was_unset {
10772 unsetparam(s); // c:3681
10773 }
10774 let new_type = if val.type_ == MN_FLOAT {
10775 PM_FFLOAT // c:3690
10776 } else {
10777 PM_INTEGER // c:3690
10778 };
10779 // c:3686-3690 — `ss = strchr(s, '['); if (ss) *ss = '\0';
10780 // pm = createparam(t, ss ? PM_ARRAY : ... );`
10781 let base_name: &str = match s.find('[') {
10782 Some(i) => &s[..i],
10783 None => s,
10784 };
10785 let created = createparam(base_name, if has_sub { PM_ARRAY as i32 } else { new_type as i32 });
10786 // c:3691-3695 — `if (errflag) { unqueue_signals(); return NULL; }`
10787 if (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0 {
10788 return None;
10789 }
10790 // c:3696-3701 — `if (!pm && !(pm = paramtab->getnode(paramtab, t)))`:
10791 // createparam declines for a node that already exists (a special, or a
10792 // set parameter) after clearing its PM_UNSET (c:1130-1152); the
10793 // assignment then lands on that node.
10794 let found = created.is_some()
10795 || paramtab()
10796 .read()
10797 .map(|t| t.contains_key(base_name))
10798 .unwrap_or(false);
10799 if !found {
10800 zerr(&format!("{}: parameter not found", base_name)); // c:3699
10801 return None; // c:3701
10802 }
10803 if !has_sub && val.type_ != MN_FLOAT {
10804 // c:3704-3705 — `else if (val.type & MN_INTEGER) pm->base = outputradix;`
10805 if let Ok(mut tab) = paramtab().write() {
10806 if let Some(pm) = tab.get_mut(base_name) {
10807 pm.base = crate::ported::math::outputradix();
10808 }
10809 }
10810 }
10811 // c:3706-3711 — the second `getvalue(&vbuf, &t, 1)`; the value write
10812 // (c:3716 `setnumvalue(v, val)`) is the reassign path below.
10813 // c:3712-3713 — `if (flags & ASSPM_WARN) check_warn_pm(v->pm,
10814 // "numeric", !was_unset, 1);`. The new parameter was CREATED, so
10815 // WARN_CREATE_GLOBAL reports `(( n=8 ))` in a function exactly as
10816 // it reports the scalar `n=8`.
10817 if (flags & ASSPM_WARN) != 0 {
10818 let snapshot = paramtab().read().ok().and_then(|t| t.get(base_name).cloned());
10819 if let Some(pm) = snapshot {
10820 check_warn_pm(&pm, "numeric", (!was_unset) as i32, 1);
10821 }
10822 }
10823 } else if (flags & ASSPM_WARN) != 0 {
10824 // c:3714-3715
10825 if let Some(ref vv) = v {
10826 if let Some(ref pm) = vv.pm {
10827 check_warn_pm(pm, "numeric", 0, 1);
10828 }
10829 }
10830 }
10831 // The reassign path: getvalue gave us a cloned pm inside the value
10832 // buffer. setnumvalue mutates that clone but the write doesn't
10833 // propagate back to paramtab. Write through paramtab directly so
10834 // reassignments stick — same shape as `assignsparam`'s c:3343
10835 // `assignstrvalue(v, val, flags)` path which mutates paramtab in
10836 // place.
10837 //
10838 // !!! LOCK DISCIPLINE — NO GSU CALLBACK MAY RUN UNDER THE GUARD !!!
10839 //
10840 // C holds no lock here at all, so `Src/params.c` gives no hint that the
10841 // dispatch is dangerous. `setnumvalue` (c:2846) hands the setfn a pointer
10842 // to the LIVE node — `v->pm->gsu.i->setfn(v->pm, ...)` (c:2870) — and the
10843 // setfn is free to read or write any OTHER parameter while it runs.
10844 // Several do: `zlevarsetfn` (c:4226) re-enters `adjustwinsize`,
10845 // `histsizesetfn` (c:4974) resizes the history, and every arm that can
10846 // fail reaches `zerr`, which is not a leaf either (zwarning →
10847 // zleentry(ZLE_CMD_TRASH) → the ZLE repaint, which reads parameters).
10848 // `paramtab()` is a `std::sync::RwLock` and is NOT reentrant, so any of
10849 // those under the write guard parks the shell on itself — blocked in
10850 // `semaphore_wait_trap`, not spinning. That shipped twice already: once as
10851 // the `$LINES` startup hang that silenced every `zsh/zpty`-spawned shell
10852 // (fixed in 5929e82b81 by removing a Rust-only return value from
10853 // `adjustwinsize` — the callee, not the pattern), and once as the
10854 // `read-only variable` report below.
10855 //
10856 // So the guard is released BEFORE the dispatch, and the order C commits in
10857 // is preserved exactly:
10858 //
10859 // 1. under the guard, commit everything C has already written to the
10860 // node by the time `setnumvalue` is entered — the `~PM_DEFAULTED`
10861 // clear (c:3671) and the `pm->base` inheritance (c:2801) — and take
10862 // the readonly rejection (c:2852), which C also takes before any
10863 // dispatch;
10864 // 2. release the guard;
10865 // 3. run the setfn (c:2870 / c:2876) against a detached node, which is
10866 // what C's pointer-to-the-live-node amounts to once no lock stands in
10867 // the way. The setfn may now take the paramtab lock itself, in either
10868 // mode, as deeply as it likes;
10869 // 4. re-take the guard only to publish the union slot the setfn wrote.
10870 //
10871 // Step 4 is the one place this port can still differ from C: a setfn that
10872 // reassigned ITS OWN parameter through the table would have that write
10873 // overwritten by the publish. No setfn reachable from here does (every arm
10874 // of `intsetfn` either writes `pm` directly or writes a global), and a
10875 // future one must not. There is deliberately no lock-ordering rule to
10876 // violate instead — after step 2 no paramtab guard is held at all, so a
10877 // setfn cannot deadlock against this caller no matter what it touches.
10878 //
10879 // `staged` carries the node between the three phases.
10880 let mut staged: param;
10881 let t: u32;
10882 {
10883 let Ok(mut tab) = paramtab().write() else {
10884 return None;
10885 };
10886 if let Some(pm) = tab.get_mut(s) {
10887 // c:Src/params.c — setnumvalue (the C function this
10888 // reassign-path mirrors) eventually calls setfn which
10889 // goes through assignstrvalue (params.c:2899-2904) where
10890 // PM_READONLY rejection lives. The Rust port writes
10891 // u_val / u_dval / u_str directly, bypassing that path,
10892 // so the readonly check has to happen here. Without it,
10893 // `(( x++ ))` / `let "x = ..."` could mutate readonly
10894 // params silently. Bug #154 in docs/BUGS.md.
10895 if (pm.node.flags as u32 & PM_READONLY) != 0 {
10896 // zerr internally sets ERRFLAG_ERROR via the
10897 // c:194 path in Src/utils.c. Match the existing
10898 // readonly check at params.rs:3951 (assignstrvalue
10899 // arm) — same call shape, same behavior.
10900 //
10901 // The name is copied and the paramtab guard DROPPED before
10902 // reporting. `zerr` is not a leaf: it reaches zwarning ->
10903 // zleentry -> zle_main_entry -> trashzle -> zrefresh, and the
10904 // refresh reads a parameter (getaparam -> is_nameref), which
10905 // takes this same RwLock. std's RwLock is not reentrant, so
10906 // reporting under the write guard deadlocked the shell against
10907 // itself on one thread — a live interactive session was caught
10908 // parked in lock_contended with exactly that 98-frame stack.
10909 // Reached whenever a widget publishes a read-only ZLE
10910 // parameter (makezleparams -> setiparam -> here), which is why
10911 // it presented as "the prompt froze".
10912 let nam = pm.node.nam.clone();
10913 drop(tab);
10914 zerr(&format!("read-only variable: {}", nam));
10915 return None;
10916 }
10917 // c:3671 `v->pm->node.flags &= ~PM_DEFAULTED;` — PM_DECLARED
10918 // shares its bit with PM_DONTIMPORT (c:Src/zsh.h:1926-1927);
10919 // keep it on a PM_SPECIAL node that carries it. Full
10920 // rationale on the same clear in `assignsparam` (c:3211).
10921 let keep_dontimport = (pm.node.flags as u32 & (PM_SPECIAL | PM_DONTIMPORT))
10922 == (PM_SPECIAL | PM_DONTIMPORT);
10923 pm.node.flags &= !(PM_DEFAULTED as i32); // c:3671
10924 if keep_dontimport {
10925 pm.node.flags |= PM_DONTIMPORT as i32;
10926 }
10927 t = PM_TYPE(pm.node.flags as u32);
10928 if t == PM_INTEGER {
10929 // c:Src/params.c:2801 — `if (!v->pm->base && lastbase
10930 // != -1) v->pm->base = lastbase;`. After setfn the C
10931 // path falls through `setstrvalue(v, NULL)` which
10932 // inherits the source numeric base from `lastbase`
10933 // when the param doesn't yet have an explicit base.
10934 // Mirror that here so `(( x = 0xFF ))` on an existing
10935 // integer param updates pm.base to 16, displaying as
10936 // `16#FF` instead of decimal `255`. Bug #175 in
10937 // docs/BUGS.md. The create path already inherits at
10938 // params.rs:5759-5768; this is the reassign-path
10939 // equivalent.
10940 // Posix-faithful: no per-var output base (see the create-path
10941 // gate above) — bash/ksh/POSIX integers print decimal.
10942 //
10943 // This stays INSIDE the guard because C has already applied it
10944 // to the live node by the time the setfn runs, and a setfn is
10945 // entitled to read `pm->base`.
10946 if pm.base == 0 && !crate::dash_mode::posix_faithful() {
10947 let lb = crate::ported::math::lastbase();
10948 if lb > 0 {
10949 pm.base = lb;
10950 }
10951 }
10952 }
10953 // c:2856-2880 — `setnumvalue`'s type switch. The STORAGE half of
10954 // each arm lands here, under the guard, because that is where C
10955 // lands it: C's setfn writes through the live node pointer, so the
10956 // value is visible to everything that runs afterwards without any
10957 // handoff. Doing it here also means the common case needs no second
10958 // guard at all — see the publish below.
10959 match t {
10960 // c:2870-2871 — `(val.type & MN_INTEGER) ? val.u.l : (zlong) val.u.d`.
10961 // This is `intsetfn`'s own body (c:4007 `pm->u.val = x`) for
10962 // every name it does not special-case; the queued dispatch
10963 // below adds the side effects only the special names have.
10964 PM_INTEGER => {
10965 pm.u_val = if val.type_ == MN_FLOAT {
10966 crate::zlong_cast::zlong_from_double(val.d)
10967 } else {
10968 val.l
10969 }
10970 }
10971 // c:2876-2877 — MN_INTEGER input promotes to f64.
10972 PM_EFLOAT | PM_FFLOAT => {
10973 pm.u_dval = if val.type_ == MN_FLOAT {
10974 val.d
10975 } else {
10976 val.l as f64
10977 }
10978 }
10979 // c:2860-2867 — convbase/convfloat → `setstrvalue(v, ztrdup(p))`.
10980 PM_SCALAR | PM_NAMEREF | PM_ARRAY => {
10981 pm.u_str = Some(if val.type_ == MN_FLOAT {
10982 convfloat_underscore(val.d, pm.width)
10983 } else {
10984 convbase_underscore(val.l, if pm.base > 0 { pm.base } else { 10 }, pm.width)
10985 })
10986 }
10987 _ => {}
10988 }
10989 // Phase 1 ends: everything C had written to the node before
10990 // entering `setnumvalue` (c:2846) is committed. Detach and release.
10991 staged = (**pm).clone();
10992 } else {
10993 return None;
10994 }
10995 }
10996 // ---- guard released; no paramtab lock is held from here on ----
10997 //
10998 // Phase 2 — the setfn dispatch this whole restructure exists for. Only the
10999 // integer arm has one: c:2870 `v->pm->gsu.i->setfn(v->pm, ...)`. It is now
11000 // free to take the paramtab lock in either mode, as deeply as it likes.
11001 //
11002 // For `$COLUMNS` / `$LINES` it lands in `zlevarsetfn` (c:362-363
11003 // `IPDEF5(..., zlevar_gsu)`), whose c:4230 `*p = x` and c:4232
11004 // `adjustwinsize(2 + (p == &zterm_columns))` now BOTH run, in C's order,
11005 // from C's one call site. `assignnparam` used to open-code the `*p = x`
11006 // half by name and call `adjustwinsize` itself after `drop(tab)` — a
11007 // name-keyed special case covering only the two names somebody had already
11008 // been bitten by, which left every other parameter-reading setfn
11009 // (`intsecondssetfn`'s `zwarn`, the `uidsetfn` / `euidsetfn` / `gidsetfn` /
11010 // `egidsetfn` failure reports) still dispatching under the guard.
11011 let committed = staged.u_val;
11012 if t == PM_INTEGER {
11013 intsetfn(&mut staged, committed); // c:2870
11014 }
11015 // Phase 3 — publish, IF the setfn wrote a value phase 1 had not already
11016 // committed. C needs no equivalent at all: its setfn wrote through the live
11017 // node pointer. No setfn reachable from here produces a different value —
11018 // each either stores exactly what it was handed or leaves `pm` alone and
11019 // writes a global — so this costs no second guard on any path the shell
11020 // takes today, which is what keeps a `(( i++ ))` loop at the acquisition
11021 // count it had before. The comparison, not a name list, is what decides:
11022 // a future setfn that DOES compute a value is published correctly without
11023 // anyone having to remember to add it here.
11024 //
11025 // Only the value slot moves, never the flags — the setfn may have unset or
11026 // retyped the node through the table (`unsetparam`, `assignsparam`), and
11027 // stamping a stale flag word back over that would undo it. A node the setfn
11028 // removed outright is left removed; the detached copy goes back to the
11029 // caller, which is the pointer C would still be holding.
11030 if t == PM_INTEGER && staged.u_val != committed {
11031 if let Ok(mut tab) = paramtab().write() {
11032 if let Some(pm) = tab.get_mut(s) {
11033 pm.u_val = staged.u_val;
11034 }
11035 }
11036 }
11037 Some(Box::new(staged)) // c:3674 `return v->pm;`
11038}
11039
11040/// Port of `setnparam()` from `Src/params.c:3744`.
11041///
11042/// C body (c:3747-3748):
11043/// ```c
11044/// return assignnparam(s, val, ASSPM_WARN);
11045/// ```
11046///
11047/// Single-line wrapper around `assignnparam` with ASSPM_WARN flags.
11048///
11049/// The previous Rust port took `(s: &str, val: f64) -> ()` — losing
11050/// the integer branch (callers couldn't set integer params via
11051/// `setnparam`) AND the Param return. No real callers existed because
11052/// the fabricated sig fit nothing. Match C exactly: `(s, val)` where
11053/// `val` is the canonical `mnumber` tagged union, returning the
11054/// resulting Param.
11055pub fn setnparam(s: &str, val: mnumber) -> Option<Param> {
11056 assignnparam(s, val, ASSPM_WARN as i32) // c:3748
11057}
11058
11059/// Port of `assigniparam()` from `Src/params.c:3753`.
11060///
11061/// C body (c:3757-3760):
11062/// ```c
11063/// mnumber mnval;
11064/// mnval.type = MN_INTEGER;
11065/// mnval.u.l = val;
11066/// return assignnparam(s, mnval, flags);
11067/// ```
11068///
11069/// Two divergences in the previous Rust port:
11070/// 1. Dropped the `flags` arg — caller-supplied flags (e.g.
11071/// ASSPM_AUGMENT for `+= int`) couldn't be threaded through;
11072/// every call hardcoded ASSPM_WARN regardless.
11073/// 2. Returned void instead of Param — losing the new param
11074/// pointer the caller may want to read back.
11075pub fn assigniparam(s: &str, val: i64, flags: i32) -> Option<Param> {
11076 // c:3757-3759 — `mnumber{ .type = MN_INTEGER, .u.l = val }`.
11077 let mnval = mnumber {
11078 l: val,
11079 d: 0.0,
11080 type_: MN_INTEGER,
11081 };
11082 // c:3760 — `return assignnparam(s, mnval, flags);`
11083 assignnparam(s, mnval, flags) // c:3760
11084}
11085
11086/// Port of `setiparam()` from `Src/params.c:3765`.
11087///
11088/// C body (c:3769-3772):
11089/// ```c
11090/// mnumber mnval;
11091/// mnval.type = MN_INTEGER;
11092/// mnval.u.l = val;
11093/// return assignnparam(s, mnval, ASSPM_WARN);
11094/// ```
11095///
11096/// The previous Rust port stringified to decimal and routed through
11097/// `assignsparam` — which CREATES THE PARAM AS PM_SCALAR. C creates
11098/// as PM_INTEGER. `setiparam("x", 5)` followed by `typeset -p x`:
11099/// - C: \`typeset -i x=5\`
11100/// - Old Rust: \`typeset x=5\`
11101///
11102/// `assignnparam` IS now ported (params.rs:4403). Route through it
11103/// matching C exactly so integer-typed params get created with the
11104/// right PM_INTEGER flag.
11105pub fn setiparam(s: &str, val: i64) -> Option<Param> {
11106 // c:3770-3771 — `mnumber{ .type = MN_INTEGER, .u.l = val }`.
11107 let mnval = mnumber {
11108 l: val,
11109 d: 0.0,
11110 type_: MN_INTEGER,
11111 };
11112 // c:3772 — `return assignnparam(s, mnval, ASSPM_WARN);`
11113 assignnparam(s, mnval, ASSPM_WARN as i32) // c:3772
11114}
11115
11116/// Port of `setiparam_no_convert()` from `Src/params.c:3781`. C
11117/// source comment: "If the target is already an integer, this
11118/// gets converted back. Low technology rules." It uses convbase
11119/// to render decimal then calls assignsparam.
11120/// WARNING: param names don't match C — Rust=() vs C=(s, val)
11121pub fn setiparam_no_convert(s: &str, val: i64) -> Option<Param> {
11122 assignsparam(s, &val.to_string(), ASSPM_WARN as i32)
11123}
11124
11125/// Port of `resetparam()` from `Src/params.c:3796`. C body:
11126/// ```c
11127/// char *s = pm->node.nam;
11128/// queue_signals();
11129/// if (pm != (Param)(paramtab == realparamtab ?
11130/// paramtab->getnode2(paramtab, s) :
11131/// paramtab->getnode(paramtab, s))) {
11132/// unqueue_signals();
11133/// zerr("can't change type of hidden variable: %s", s);
11134/// return 1;
11135/// }
11136/// s = dupstring(s);
11137/// unsetparam_pm(pm, 0, 1);
11138/// unqueue_signals();
11139/// createparam(s, flags);
11140/// return 0;
11141/// ```
11142/// Tears `pm` down + recreates it with `flags` so the next
11143/// assignment lands in a fresh slot of the requested type. Used
11144/// by `assignsparam` when the type-flag of an existing param
11145/// changes (e.g. `typeset -i x; x="abc"` resets x back to scalar).
11146///
11147/// The `paramtab->getnode` reachability check at c:3800 catches
11148/// the hidden-shadow case (a local var hiding the global `pm` we
11149/// were handed) — without the paramtab vtable we skip the check
11150/// and proceed to unset+create.
11151pub fn resetparam(pm: &mut param, flags: i32) -> i32 {
11152 // c:3796
11153 let s = pm.node.nam.clone(); // c:3796
11154 queue_signals(); // c:3799
11155 // c:3800-3807 — paramtab->getnode2 / getnode reachability check.
11156 // Without paramtab vtable wired we cannot detect the hidden-
11157 // variable case, so we proceed; a future port of paramtab
11158 // adds the check at this site.
11159 unsetparam_pm(pm, 0, 1); // c:3819
11160 unqueue_signals(); // c:3819
11161 let _ = createparam(&s, flags); // c:3819
11162 0 // c:3819
11163}
11164
11165/// Port of `unsetparam()` from `Src/params.c:3819`.
11166///
11167/// C body:
11168/// ```c
11169/// Param pm;
11170/// queue_signals();
11171/// if ((pm = (Param)(paramtab == realparamtab ?
11172/// paramtab->getnode2(paramtab, s) :
11173/// paramtab->getnode(paramtab, s))))
11174/// unsetparam_pm(pm, 0, 1);
11175/// unqueue_signals();
11176/// ```
11177///
11178/// The previous Rust port took `(variables, arrays, assoc_arrays,
11179/// name)` operating on EXTERNAL HashMap storage — a SubstState-
11180/// era stale signature. C operates on the canonical `paramtab`
11181/// global. No live callers used the old 4-arg form (all use
11182/// `paramtab().write().remove(...)` directly), so renaming is
11183/// safe.
11184pub fn unsetparam(name: &str) -> i32 {
11185 // !!! RUST-ONLY: provenance tap — the unset is the last event in a
11186 // tracked parameter's lineage; the chain itself is kept so
11187 // `provenance NAME` still explains what the value was.
11188 if crate::provenance::active() {
11189 crate::provenance::on_param_unset(name);
11190 }
11191 // c:3819 — C's unsetparam is void and discards unsetparam_pm's
11192 // status; bin_unset (c:Src/builtin.c:3952-3953) does the
11193 // paramtab lookup itself and calls `if (unsetparam_pm(pm, 0, 1))
11194 // returnval = 1;`. The Rust bin_unset routes through this
11195 // wrapper for the bridge plumbing (tied names, hashed-storage
11196 // shadow, special regenerators), so the rejection status is
11197 // surfaced here instead: 1 = readonly rejection, 0 = unset ok.
11198 // c:Src/params.c:3853-3935 — unsetparam_pm's tied-alt-name
11199 // removal block. zsh's PATH/path, FPATH/fpath, MANPATH/manpath,
11200 // CDPATH/cdpath, PSVAR/psvar pairs are tied (`pm->ename` points
11201 // to the alt name) — unsetting one must clear the other or
11202 // command lookup keeps finding binaries via the surviving `path`
11203 // array even after `unset PATH`. The generic `ename` cascade lives in
11204 // unsetparam_pm (it covers user `typeset -T` ties). The canonical
11205 // special pairs keep derived state outside the node, so they are
11206 // mirrored explicitly here, making `unset PATH` a security boundary.
11207 // Bug #416.
11208 let tied_alt: Option<&str> = match name {
11209 "PATH" => Some("path"),
11210 "path" => Some("PATH"),
11211 "FPATH" => Some("fpath"),
11212 "fpath" => Some("FPATH"),
11213 "MANPATH" => Some("manpath"),
11214 "manpath" => Some("MANPATH"),
11215 "CDPATH" => Some("cdpath"),
11216 "cdpath" => Some("CDPATH"),
11217 "PSVAR" => Some("psvar"),
11218 "psvar" => Some("PSVAR"),
11219 "MODULE_PATH" => Some("module_path"),
11220 "module_path" => Some("MODULE_PATH"),
11221 "FIGNORE" => Some("fignore"),
11222 "fignore" => Some("FIGNORE"),
11223 "MAILPATH" => Some("mailpath"),
11224 "mailpath" => Some("MAILPATH"),
11225 // c:Src/Modules/watch.c:697-701 — PM_TIED since upstream b2fb112ea6.
11226 "WATCH" => Some("watch"),
11227 "watch" => Some("WATCH"),
11228 _ => None,
11229 };
11230 queue_signals(); // c:3825
11231 // c:3826-3831 — `if ((pm = ... getnode2 ...) && !(pm->node.flags
11232 // & PM_NAMEREF)) unsetparam_pm(pm, 0, 1);`.
11233 //
11234 // Two divergences in the previous Rust port:
11235 // 1. Missing PM_NAMEREF check — `unsetparam("ref")` where `ref`
11236 // is a nameref would remove the ref alias itself. C explicitly
11237 // skips nameref params here (they're cleared via the
11238 // ref-specific path, not the value-side unset).
11239 // 2. Bypassed `unsetparam_pm` — removed the entry directly from
11240 // paramtab without running the readonly-guard at c:3850, the
11241 // stdunsetfn dispatch at c:3870, or the `pm->old` scope
11242 // restore. `typeset -r x=foo; unset x` would silently succeed
11243 // in Rust where C rejects with `read-only variable: x`.
11244 // c:Src/params.c:3853 — flag regenerator-style specials as unset
11245 // so subsequent reads via lookup_special_var skip the getfn.
11246 // RANDOM/SECONDS/EPOCH*/TTYIDLE/ERRNO have no paramtab pm node in
11247 // zshrs (they're lookup_special_var libc shims), so the standard
11248 // unsetparam_pm path below doesn't catch them. Bug #417/#418.
11249 if matches!(
11250 name,
11251 "RANDOM" | "SECONDS" | "EPOCHSECONDS" | "EPOCHREALTIME" | "TTYIDLE" | "ERRNO"
11252 ) {
11253 mark_unset_special(name);
11254 }
11255 // c:Src/params.c:3850 — `if (pm->node.flags & PM_READONLY) { zerr;
11256 // return 1; }`. Read-only specials (LINENO, HISTCMD, PPID, etc.)
11257 // have PM_READONLY in their special_paramdef entry but no paramtab
11258 // pm node by default, so the standard PM_READONLY check inside
11259 // unsetparam_pm never fires for them. Walk the special_params
11260 // table directly to catch these. Bug #419.
11261 // …but ONLY when the name has no live node. C tests the FLAG ON THE NODE
11262 // (`pm->node.flags & PM_READONLY`), so a shadow that hides the special —
11263 // `typeset -h +g LINENO` builds a plain `scalar-local-hide` per
11264 // Src/builtin.c:2083-2085 — is unsettable. The by-name table walk ignored
11265 // the shadow and rejected it, which is what made B02typeset's
11266 // `typeset -h +g -m [[:alpha:]_]*; unset -m [[:alpha:]_]*` abort.
11267 let live_readonly: Option<bool> = paramtab().read().ok().and_then(|t| {
11268 t.get(name)
11269 .map(|pm| (pm.node.flags as u32 & PM_READONLY) != 0)
11270 });
11271 let is_readonly_special = match live_readonly {
11272 Some(ro) => ro, // c:3850 — the node governs itself
11273 None => special_params
11274 .iter()
11275 .any(|ip| ip.name == name && (ip.pm_flags & PM_READONLY) != 0),
11276 };
11277 if is_readonly_special {
11278 // !!! BASH-MODE (no C counterpart) !!! bash: `unset: NAME: cannot unset:
11279 // readonly variable`, status 1, script continues (zsh zerr aborts the line).
11280 if crate::dash_mode::bash_mode() || crate::dash_mode::korn_mode() {
11281 crate::ported::utils::zwarnnam("unset", &format!("{name}: cannot unset: readonly variable"));
11282 unqueue_signals();
11283 return 1;
11284 }
11285 zerr(&format!("read-only variable: {}", name));
11286 unqueue_signals();
11287 return 1; // c:3854 — unsetparam_pm's readonly rejection status
11288 }
11289 let mut retval = 0i32;
11290 // c:Src/params.c:3855 — `if (pm->ename && !altflag) altremove =
11291 // ztrdup(pm->ename)`. User ties created by `typeset -T FOO foo`
11292 // cross-link the two params via `pm.ename` (each side names the
11293 // other); the static `tied_alt` map above only covers the
11294 // canonical PM_SPECIAL pairs (PATH/path …). Capture the unset
11295 // param's ename here so the cascade below can clear the tied
11296 // partner for user ties too.
11297 // c:3855 — `if (pm->ename && !altflag)`: the cascade is driven by the
11298 // LIVE node's `ename`, which only a genuinely tied param carries. Capture
11299 // PM_TIED alongside so the static `tied_alt` map below can be gated the
11300 // same way: a shadow that HIDES a tied special (`typeset -h +g PATH`
11301 // builds a plain `scalar-local-hide`, Src/builtin.c:2083-2085) has no tie
11302 // and must not drag its partner down with it.
11303 let (found, is_nameref, param_ename, param_tied, param_special) = {
11304 let tab = paramtab().read().unwrap();
11305 match tab.get(name) {
11306 Some(pm) => (
11307 true,
11308 (pm.node.flags as u32 & PM_NAMEREF) != 0,
11309 pm.ename.clone(),
11310 (pm.node.flags as u32 & PM_TIED) != 0,
11311 (pm.node.flags as u32 & PM_SPECIAL) != 0,
11312 ),
11313 None => (false, false, None, false, false),
11314 }
11315 };
11316 if found && !is_nameref {
11317 // c:3826-3830
11318 // c:3831 — `unsetparam_pm(pm, 0, 1)`. Take an owned copy out
11319 // of paramtab so we can mutate it (unsetparam_pm wants
11320 // &mut), run the readonly-guard + env teardown, then re-insert
11321 // or fully remove based on the readonly path.
11322 // The node stays in the table while `unsetparam_pm` runs, as in C —
11323 // c:3874 `removenode` is the LAST step — so the tied-partner cascade
11324 // (c:3805-3836) and the partner's setfn still see this name bound.
11325 let mut pm_owned = paramtab().read().unwrap().get(name).cloned().unwrap();
11326 let rejected = unsetparam_pm(&mut pm_owned, 0, 1); // c:3831
11327 if rejected != 0 {
11328 retval = 1; // c:Src/builtin.c:3952-3953 surfaced to bin_unset
11329 // Readonly rejection — restore the entry so the state
11330 // is unchanged.
11331 paramtab()
11332 .write()
11333 .unwrap()
11334 .insert(name.to_string(), pm_owned);
11335 } else if pm_owned.old.is_some()
11336 || (pm_owned.level > 0 && locallevel.load(Ordering::Relaxed) as i32 >= pm_owned.level)
11337 {
11338 // c:Src/params.c:3892-3925 — when the unset'd pm is a
11339 // local that shadowed an outer binding (chained via
11340 // pm.old by `addparam` at c:1137), the local pm STAYS
11341 // in paramtab with PM_UNSET set so the current scope's
11342 // reads see "unset" (empty). The pm.old chain is
11343 // preserved so endparamscope can uncover the outer
11344 // when the local scope ends. Without this re-insert,
11345 // either:
11346 // - the outer would be uncovered immediately (wrong:
11347 // zsh hides the outer until scope end), or
11348 // - pm.old would be dropped (wrong: outer never
11349 // comes back).
11350 // c:3911-3913 — `if ((pm->level && locallevel >= pm->level)
11351 // ...) return 0;` — locals are kept in the table marked
11352 // PM_UNSET even WITHOUT an outer binding ("foo() { local
11353 // bar; unset bar; } makes the global bar available? The
11354 // following makes the answer no"), and `typeset -p bar`
11355 // still finds the node (prints nothing, status 0).
11356 paramtab()
11357 .write()
11358 .unwrap()
11359 .insert(name.to_string(), pm_owned);
11360 } else if (pm_owned.node.flags as u32 & PM_SPECIAL) != 0
11361 && (pm_owned.node.flags as u32 & PM_REMOVABLE) == 0
11362 {
11363 // c:Src/params.c:3911-3913 — `if ((pm->flags &
11364 // (PM_SPECIAL|PM_REMOVABLE)) == PM_SPECIAL) return 0;`.
11365 // PM_SPECIAL params (SECONDS, RANDOM, HOME, IFS, ...)
11366 // stay in paramtab with PM_UNSET set after unset. A
11367 // subsequent re-assign (`SECONDS=100`) finds the same
11368 // pm via createparam's `oldpm` lookup, hits the reuse
11369 // arm (c:1132), preserves the PM_INTEGER|PM_SPECIAL
11370 // flags + gsu vtable, so intsetfn's name-dispatch
11371 // fires and routes through intsecondssetfn. Without
11372 // this, the special pm was dropped, a fresh PM_SCALAR
11373 // pm was created for the re-assignment, the value
11374 // landed in pm.u_str, and lookup_special_var kept
11375 // reading the time-since-shtimer delta. Bug #418 in
11376 // docs/BUGS.md.
11377 //
11378 // PM_UNSET is already stamped by unsetparam_pm at
11379 // line 6491; keep it on the re-inserted pm so reads
11380 // still see "unset" until re-assignment clears it
11381 // (clear_unset_special at line 4756 handles the
11382 // regenerator-style unset_specials set; the pm flag
11383 // is cleared inside assignsparam's value-write arm
11384 // via stdunsetfn's symmetric set/clear convention).
11385 paramtab()
11386 .write()
11387 .unwrap()
11388 .insert(name.to_string(), pm_owned);
11389 }
11390 else {
11391 // No pm.old + no rejection + not PM_SPECIAL → drop entirely
11392 // (c:3874 `paramtab->removenode(paramtab, pm->node.nam)`).
11393 paramtab().write().unwrap().remove(name);
11394 }
11395 }
11396 // c:Src/params.c:3905-3935 — tied-alt removal. Cascade the
11397 // unset to the paired name (PATH↔path etc.). Also clear the OS
11398 // env mirror since command lookup at the syscall level reads
11399 // the inherited libc environ.
11400 // Static canonical pair (PATH↔path …) takes precedence; otherwise
11401 // fall back to the unset param's `ename` (user `typeset -T` ties).
11402 // c:3855 — gate on the LIVE tie. `found && !param_tied` is a node that
11403 // exists but is not tied (typically a `-h` shadow of PATH/path), and C
11404 // would leave the partner alone; only fall back to the static pair map
11405 // when the name has no node at all (the pre-createparamtable `-fc` path).
11406 // A live node carrying `ename` was already cascaded by `unsetparam_pm`
11407 // itself (c:3805-3836); only the static-pair fallback for a name with no
11408 // node, or a tie with no `ename`, is left to the block below.
11409 let cascaded = found && param_ename.is_some() && !param_special;
11410 let effective_alt: Option<String> = if found && !param_tied && param_ename.is_none() {
11411 None
11412 } else {
11413 tied_alt.map(|s| s.to_string()).or(param_ename)
11414 };
11415 if let Some(alt) = effective_alt.as_deref() {
11416 let alt_present = !cascaded
11417 && paramtab()
11418 .read()
11419 .map(|t| t.contains_key(alt))
11420 .unwrap_or(false);
11421 if alt_present {
11422 if let Some(mut alt_pm) = paramtab().write().ok().and_then(|mut t| t.remove(alt)) {
11423 let _ = unsetparam_pm(&mut alt_pm, 1, 1); // c:3828
11424 // c:Src/params.c:3892-3925 — the partner obeys the same
11425 // keep-the-node rules as the primary: a local that shadows an
11426 // outer binding stays in the table marked PM_UNSET so
11427 // endparamscope can uncover the outer. Dropping it outright
11428 // took the whole `pm.old` chain with it, so unsetting a
11429 // shadowed `PATH` erased the GLOBAL `path` for good.
11430 // c:3911-3913 — `(pm->node.flags & (PM_SPECIAL|PM_REMOVABLE))
11431 // == PM_SPECIAL` also keeps the node: a tied special stays in
11432 // the table, PM_UNSET, with its tie and gsu intact, so a later
11433 // `manpath=(…)` revives the SPECIAL pair and a following
11434 // `unset MANPATH` cascades to `manpath` again. Dropping it
11435 // made the revival a plain untied array that survived the
11436 // partner's unset. `$+manpath` reads the PM_UNSET flag
11437 // (subst.rs, c:Src/subst.c:2805).
11438 let keep = alt_pm.old.is_some()
11439 || (alt_pm.level > 0
11440 && locallevel.load(Ordering::Relaxed) as i32 >= alt_pm.level)
11441 || (alt_pm.node.flags as u32 & (PM_SPECIAL | PM_REMOVABLE)) == PM_SPECIAL; // c:3911-3913
11442 if keep {
11443 // The executor-side array/assoc bag (see the else arm)
11444 // holds the value outside the node; the unset empties it.
11445 crate::ported::exec::unset_array(alt);
11446 crate::ported::exec::unset_assoc(alt);
11447 paramtab().write().unwrap().insert(alt.to_string(), alt_pm);
11448 } else {
11449 // c:3874 `paramtab->removenode(paramtab, pm->node.nam)` —
11450 // in C that is the only store the name lives in. zshrs
11451 // keeps a parallel executor-side array/assoc bag that
11452 // `exec::array` consults BEFORE getaparam, so dropping the
11453 // paramtab node alone left `unset PATH; print $+path`
11454 // reading 1 (a plain `array`, not the special) where zsh
11455 // reads 0.
11456 crate::ported::exec::unset_array(alt);
11457 crate::ported::exec::unset_assoc(alt);
11458 }
11459 }
11460 }
11461 env::remove_var(alt);
11462 env::remove_var(name);
11463 // c:Src/params.c:5291 — `if (t == path) cmdnamtab->emptytable
11464 // (cmdnamtab)`. The hashed-cmdnam cache holds absolute paths
11465 // resolved via the prior PATH search; without clearing,
11466 // `unset PATH; ls` still hits the cached `/bin/ls` entry and
11467 // exec succeeds — defeating the security boundary the unset
11468 // is supposed to establish. Bug #416.
11469 if matches!(name, "PATH" | "path") {
11470 crate::ported::hashtable::emptycmdnamtable();
11471 }
11472 }
11473 unqueue_signals(); // c:3832
11474 retval
11475}
11476
11477/// Unset parameter (from params.c unsetparam_pm)
11478/// Port of `unsetparam_pm()` from `Src/params.c:3841`. Runs the
11479/// readonly guard, `pm->gsu.s->unsetfn(pm, exp)`, `delenv`, and the
11480/// tied-alternate (`pm->ename`) teardown when `!altflag`. The
11481/// c:3851-3890 `removenode`/`addnode` postlude and `pm->old`
11482/// resurrection for `pm` itself is done by the callers, which own the
11483/// node (see `unsetparam`).
11484#[allow(unused_variables)]
11485pub fn unsetparam_pm(pm: &mut param, altflag: i32, exp: i32) -> i32 {
11486 // c:3850 — `if ((pm->node.flags & PM_READONLY) && pm->level <= locallevel)`.
11487 let cur_ll = locallevel.load(Ordering::Relaxed) as i32; // c:3850 locallevel
11488 if (pm.node.flags as u32 & PM_READONLY) != 0 && pm.level <= cur_ll {
11489 // c:3850
11490 // c:3852 — `zerr("read-only %s: %s", ...)`. Emit diagnostic
11491 // so users see why the unset failed.
11492 let kind = if (pm.node.flags as u32 & PM_NAMEREF) != 0 {
11493 // c:3852
11494 "reference"
11495 } else {
11496 "variable"
11497 };
11498 // !!! BASH/KSH-MODE (no C counterpart) !!! bash reports `unset: NAME:
11499 // cannot unset: readonly variable` and carries on (status 1); zsh's
11500 // zerr aborts the rest of the command line.
11501 if crate::dash_mode::bash_mode() || crate::dash_mode::korn_mode() {
11502 crate::ported::utils::zwarnnam(
11503 "unset",
11504 &format!("{}: cannot unset: readonly {}", pm.node.nam, kind),
11505 );
11506 return 1;
11507 }
11508 zerr(&format!("read-only {}: {}", kind, pm.node.nam));
11509 return 1; // c:3854
11510 }
11511 // c:3793-3796 — `if (pm->ename && !altflag) altremove = ztrdup(pm->ename);
11512 // else altremove = NULL;`. Captured before the unsetfn, which clears a
11513 // tied param's `ename` (stdunsetfn c:3932-3938).
11514 //
11515 // !!! DEVIATION — the cascade below runs only for a NON-special pm !!!
11516 // The tied specials (PATH/path, FPATH/fpath, MANPATH/manpath, …) keep
11517 // derived state outside the node (the executor array bag, cmdnamtab, the
11518 // environ mirror); `unsetparam`'s static-pair block below owns their
11519 // partner teardown, and running this generic cascade for them left
11520 // `unset PATH; print $+path` reading 1 where zsh reads 0. User
11521 // `typeset -T` ties have no such state and take C's path.
11522 let altremove: Option<String> =
11523 if altflag == 0 && (pm.node.flags as u32 & PM_SPECIAL) == 0 {
11524 pm.ename.clone()
11525 } else {
11526 None
11527 };
11528 pm.node.flags &= !(PM_DECLARED as i32); // c:3868
11529 // c:3870 — WHICH unsetfn `pm->gsu.s->unsetfn` is comes from
11530 // createspecialhash (c:1227-1228): a SPECIALPMDEF hash whose
11531 // `partab[]` row supplies NO gsu keeps the default
11532 // `nullsethash_gsu` unless it is PM_READONLY (which takes
11533 // `stdhash_gsu`), and `nullsethash_gsu`'s unsetfn is `nullunsetfn`
11534 // (c:196-197 → c:4143-4145), whose body is EMPTY. So `unset` on
11535 // such a row does nothing at all: no PM_UNSET, node untouched,
11536 // value still readable — `zmodload zsh/langinfo; unset langinfo`
11537 // leaves `${#langinfo}` at 55 in zsh. Rows that DO carry a gsu get
11538 // it stamped over the default by addparamdef
11539 // (c:Src/module.c:1080-1110) and so unset through `stdunsetfn`.
11540 // `langinfo` (c:Src/Modules/langinfo.c:447 `SPECIALPMDEF("langinfo",
11541 // 0, NULL, getlanginfo, scanlanginfo)`) is the only `partab[]` row
11542 // that is both gsu-less and non-readonly; every other gsu-less row
11543 // is PM_READONLY_SPECIAL and is rejected at c:3786 above before
11544 // reaching here.
11545 let nullsethash_gsu = pm.node.nam == "langinfo"; // c:1227-1228
11546 if nullsethash_gsu {
11547 return 0; // c:3870 nullunsetfn — empty body
11548 }
11549 // c:3870 — a `private` parameter carries the zsh/param/private GSU
11550 // closure, whose unsetfn (pps_unsetfn and its siblings,
11551 // c:Src/Modules/param_private.c:311-322) only runs the wrapped unsetfn
11552 // when `locallevel <= pm->level` and, on an EXPLICIT unset, sets
11553 // PM_DECLARED again: `() { private x=1; unset x; x=2 }` must find the
11554 // node through fetchvalue (c:2264-2265) instead of re-creating it over
11555 // a PM_RO_BY_DESIGN struct.
11556 let private_closure =
11557 crate::ported::modules::param_private::is_private(pm as *const param) != 0;
11558 if (pm.node.flags as u32 & PM_UNSET) == 0 || (pm.node.flags as u32 & PM_REMOVABLE) != 0 {
11559 if private_closure {
11560 if cur_ll <= pm.level {
11561 stdunsetfn(pm, exp); // c:param_private.c:317-318 gsu->unsetfn
11562 }
11563 if exp != 0 {
11564 pm.node.flags |= PM_DECLARED as i32; // c:param_private.c:320
11565 }
11566 } else {
11567 // c:3870 — `pm->gsu.s->unsetfn(pm, exp)` — open-coded to stdunsetfn.
11568 stdunsetfn(pm, exp);
11569 }
11570 }
11571 if pm.env.is_some() {
11572 delenv(&pm.node.nam); // c:3872 delenv(pm)
11573 pm.env = None;
11574 }
11575 // c:3804-3836 — "remove it under its alternate name if necessary".
11576 // Every caller hands this fn a node it has already copied out of (or
11577 // removed from) paramtab and holds no guard, so the partner is taken
11578 // out of the table, unset, and put back (or not) here.
11579 if let Some(alt) = altremove.as_deref() {
11580 // c:3806 — `altpm = (Param) paramtab->getnode(paramtab, altremove);`
11581 let top = paramtab().write().ok().and_then(|mut t| t.remove(alt));
11582 if let Some(mut top) = top {
11583 // c:3815-3821 — `if (altpm && !(altpm->node.flags & PM_SPECIAL))
11584 // while (altpm && altpm->level > pm->level) { oldpm = altpm;
11585 // altpm = altpm->old; }` — tied parameters are at the same local
11586 // level as each other, so skip locals hidden above that level.
11587 let mut depth = 0usize;
11588 let mut reached = true;
11589 if (top.node.flags as u32 & PM_SPECIAL) == 0 {
11590 let mut cur: ¶m = ⊤
11591 while cur.level > pm.level {
11592 match cur.old.as_deref() {
11593 Some(o) => {
11594 cur = o;
11595 depth += 1;
11596 }
11597 None => {
11598 reached = false; // c:3819 altpm = NULL
11599 break;
11600 }
11601 }
11602 }
11603 }
11604 if reached {
11605 if depth == 0 {
11606 // c:3828 — `unsetparam_pm(altpm, 1, exp);` on the visible node.
11607 let _ = unsetparam_pm(&mut top, 1, exp);
11608 // c:3851-3890 postlude for the partner, which this port
11609 // leaves to whoever took the node out of paramtab: the
11610 // same keep-or-drop rule `unsetparam` applies to its
11611 // static-pair fallback below (see the note there on why
11612 // a global tied special is dropped, not kept).
11613 let keep = top.old.is_some()
11614 || (top.level > 0
11615 && locallevel.load(Ordering::Relaxed) as i32 >= top.level);
11616 if keep {
11617 paramtab().write().unwrap().insert(alt.to_string(), top);
11618 } else {
11619 crate::ported::exec::unset_array(alt); // c:3874 removenode
11620 crate::ported::exec::unset_assoc(alt);
11621 }
11622 } else {
11623 let mut oldpm: &mut param = &mut top;
11624 for _ in 1..depth {
11625 oldpm = oldpm.old.as_deref_mut().unwrap();
11626 }
11627 if oldpm.old.as_ref().is_some_and(|a| a.level == 0) {
11628 // c:3823-3827 — `if (oldpm && !altpm->level) {
11629 // oldpm->old = NULL; altpm->level = 1; }` — detach the
11630 // global from the shadow chain so removenode is not
11631 // called; the detached node is then unset and freed.
11632 let mut altpm = oldpm.old.take().unwrap();
11633 altpm.level = 1; // c:3826
11634 let _ = unsetparam_pm(&mut altpm, 1, exp); // c:3828
11635 } else if let Some(altpm) = oldpm.old.as_deref_mut() {
11636 // c:3828 — a hidden local at pm's level stays in the
11637 // chain marked PM_UNSET (c:3851-3853 keeps locals).
11638 let _ = unsetparam_pm(altpm, 1, exp);
11639 }
11640 paramtab().write().unwrap().insert(alt.to_string(), top);
11641 }
11642 pm.node.flags |= PM_UNSET as i32; // c:3829 "so we must repeat this"
11643 } else {
11644 paramtab().write().unwrap().insert(alt.to_string(), top);
11645 }
11646 }
11647 // c:3833-3836 — `if (!(pm->node.flags & PM_SPECIAL)) {
11648 // pm->gsu.s = &stdscalar_gsu; pm->node.flags &= ~PM_ARRAY; }`
11649 if (pm.node.flags as u32 & PM_SPECIAL) == 0 {
11650 pm.gsu_s = Some(Box::new(gsu_scalar {
11651 getfn: strgetfn,
11652 setfn: strsetfn,
11653 unsetfn: stdunsetfn,
11654 }));
11655 pm.node.flags &= !(PM_ARRAY as i32);
11656 }
11657 }
11658 // The c:3851-3890 removenode/addnode postlude for `pm` itself stays with
11659 // the callers, which own the node (see `unsetparam`).
11660 pm.node.flags |= PM_UNSET as i32;
11661 0
11662}
11663
11664
11665// -----------------------------------------------------------
11666// GSU dispatch callbacks — direct ports against `param.u_*`
11667// fields. C source in Src/params.c:4002.
11668// -----------------------------------------------------------
11669
11670/// Port of `intgetfn()` from `Src/params.c:3993`. C body:
11671/// `return pm->u.val;`
11672pub fn intgetfn(pm: ¶m) -> i64 {
11673 pm.u_val
11674}
11675
11676/// Port of `intsetfn()` from `Src/params.c:4002`. C body:
11677/// `pm->u.val = x;`
11678pub fn intsetfn(pm: &mut param, x: i64) {
11679 // c:Src/params.c:4575 — PM_SPECIAL integers have per-name gsu_i->setfn
11680 // hooks: SECONDS routes through intsecondssetfn (shtimer math),
11681 // RANDOM seeds the PRNG, etc. The default intsetfn is the fallback
11682 // for non-special integers (pm->u.val write).
11683 //
11684 // Rust port lookup_special_var dispatches GETTERS by name; the
11685 // setters need symmetric dispatch so `SECONDS=N` actually moves
11686 // shtimer instead of writing u.val which intsecondsgetfn never
11687 // reads. Without this, `$SECONDS` always read the time-since-shtimer
11688 // delta regardless of writes.
11689 // Name-based dispatch (not flag-based): some assignment paths
11690 // construct a fresh param shell for tc and lose PM_SPECIAL.
11691 match pm.node.nam.as_str() {
11692 "SECONDS" => {
11693 intsecondssetfn(x);
11694 return;
11695 }
11696 // c:Src/params.c:364-365 —
11697 // IPDEF6("TRY_BLOCK_ERROR", &try_errflag, varinteger_gsu)
11698 // IPDEF6("TRY_BLOCK_INTERRUPT", &try_interrupt, varinteger_gsu)
11699 // varinteger_gsu's setfn is `intvarsetfn` (c:4213), whose body is
11700 // `*pm->u.valptr = x;` — i.e. the assignment writes the loop.c
11701 // GLOBAL itself, not just `pm->u.val`. That is the entire
11702 // mechanism behind `TRY_BLOCK_ERROR=0` inside an `always` block:
11703 // `exectry` re-reads the global after the always-list runs
11704 // (c:Src/loop.c:774 `if (try_errflag) errflag |= ERRFLAG_ERROR;`)
11705 // and a user-zeroed global swallows the try-block's error.
11706 //
11707 // The Rust port cannot carry C's `u.valptr` raw pointer, so the
11708 // bound global is reached by name — the same asymmetry the
11709 // SECONDS / RANDOM arms below already resolve this way. Without
11710 // it, `TRY_BLOCK_ERROR=0` wrote `pm.u_val` while every reader
11711 // (params.rs's special-var getter, exectry) kept consulting the
11712 // atomic, so the assignment was a no-op.
11713 "TRY_BLOCK_ERROR" => {
11714 crate::ported::r#loop::try_errflag.store(x, Ordering::Relaxed); // c:4213
11715 intvarsetfn(pm, x); // c:4213 `*pm->u.valptr = x;`
11716 return;
11717 }
11718 "TRY_BLOCK_INTERRUPT" => {
11719 crate::ported::r#loop::try_interrupt.store(x, Ordering::Relaxed); // c:4213
11720 intvarsetfn(pm, x); // c:4213
11721 return;
11722 }
11723 // c:Src/params.c:362-363 —
11724 // IPDEF5("COLUMNS", &zterm_columns, zlevar_gsu),
11725 // IPDEF5("LINES", &zterm_lines, zlevar_gsu),
11726 // whose setfn is `zlevarsetfn` (c:4176): `*p = x` writes the
11727 // `zterm_columns` / `zterm_lines` GLOBAL through the same pointer the
11728 // getter reads, then `adjustwinsize(2 + (p == &zterm_columns))`
11729 // re-derives the terminal state. Exactly the TRY_BLOCK_* shape above:
11730 // the port cannot carry `u.valptr`, so the bound global is reached by
11731 // name. `zlevarsetfn` existed with no caller, so `COLUMNS=40` moved
11732 // only `pm.u_val` while every geometry reader kept the old width.
11733 "COLUMNS" | "LINES" => {
11734 zlevarsetfn(pm, x); // c:4176
11735 return;
11736 }
11737 // c:Src/params.c:4552 randomsetfn — `RANDOM=N` calls
11738 // srand(N). Without this dispatch, $RANDOM writes only u.val
11739 // and the next read returns rand()'s next value from the
11740 // PROCESS-START seed, not the user-requested seed. Same
11741 // name-based dispatch shape as SECONDS above.
11742 "RANDOM" => {
11743 randomsetfn(x);
11744 return;
11745 }
11746 // c:Src/params.c:4698 uidsetfn / c:4719 euidsetfn / c:4740
11747 // gidsetfn / c:4761 egidsetfn — `UID=N` / `EUID=N` /
11748 // `GID=N` / `EGID=N` attempt the corresponding setuid /
11749 // seteuid / setgid / setegid syscall and emit
11750 // `failed to change [effective ]{user,group} ID: ERRNO`
11751 // on failure. Bug #254 in docs/BUGS.md. Same name-based
11752 // dispatch shape as SECONDS/RANDOM above.
11753 "UID" => {
11754 uidsetfn(x);
11755 return;
11756 }
11757 "EUID" => {
11758 euidsetfn(x);
11759 return;
11760 }
11761 "GID" => {
11762 gidsetfn(x);
11763 return;
11764 }
11765 "EGID" => {
11766 egidsetfn(x);
11767 return;
11768 }
11769 // c:Src/params.c:4974 histsizesetfn / c:4998 savehistsizesetfn —
11770 // `HISTSIZE=N` / `SAVEHIST=N` must update the canonical
11771 // `histsiz` / `savehistsiz` globals AND clamp to >= 1 +
11772 // call `resizehistents()` so the in-memory history buffer
11773 // shrinks/grows. Without this dispatch the assignment
11774 // lands in `pm.u_val` and `histsizegetfn` (which reads the
11775 // global) keeps returning the un-touched default. Bug #520.
11776 "HISTSIZE" => {
11777 histsizesetfn(x);
11778 return;
11779 }
11780 "SAVEHIST" => {
11781 savehistsizesetfn(x);
11782 return;
11783 }
11784 _ => {}
11785 }
11786 pm.u_val = x;
11787}
11788
11789/// Port of `floatgetfn()` from `Src/params.c:4011`. C body:
11790/// `return pm->u.dval;`
11791pub fn floatgetfn(pm: ¶m) -> f64 {
11792 pm.u_dval
11793}
11794
11795/// Port of `floatsetfn()` from `Src/params.c:4020`. C body:
11796/// `pm->u.dval = x;`
11797pub fn floatsetfn(pm: &mut param, x: f64) {
11798 // c:Src/params.c:4603 floatsecondssetfn — PM_SPECIAL float SECONDS
11799 // routes through shtimer math. Symmetric with intsetfn's SECONDS
11800 // dispatch above and lookup_special_var's getter.
11801 // c:Src/params.c:4603 floatsecondssetfn — PM_SPECIAL float SECONDS
11802 // routes through shtimer math. Symmetric with intsetfn's SECONDS
11803 // dispatch above and lookup_special_var's getter. Some assignment
11804 // paths construct a fresh `param` shell for tc (type-conversion)
11805 // and lose PM_SPECIAL, so name-only dispatch is the safe fallback.
11806 if pm.node.nam == "SECONDS" {
11807 floatsecondssetfn(x);
11808 return;
11809 }
11810 pm.u_dval = x;
11811}
11812
11813/// Port of `strgetfn()` from `Src/params.c:4029`. C body:
11814/// `return pm->u.str ? pm->u.str : (char *) hcalloc(1);`
11815pub fn strgetfn(pm: ¶m) -> String {
11816 pm.u_str.clone().unwrap_or_default()
11817}
11818
11819/// Port of `strsetfn()` from `Src/params.c:4038`.
11820///
11821/// C body (c:4043-4051):
11822/// ```c
11823/// zsfree(pm->u.str); pm->u.str = x;
11824/// if (!(pm->node.flags & PM_HASHELEM) &&
11825/// ((pm->node.flags & PM_NAMEDDIR) || isset(AUTONAMEDIRS))) {
11826/// pm->node.flags |= PM_NAMEDDIR;
11827/// adduserdir(pm->node.nam, x, 0, 0);
11828/// }
11829/// ```
11830///
11831/// The C body fires the `adduserdir` path when EITHER `PM_NAMEDDIR`
11832/// is already set OR the `AUTONAMEDIRS` option is on. The previous
11833/// Rust port only fired when PM_NAMEDDIR was already set, missing
11834/// the AUTONAMEDIRS auto-create branch entirely. With `setopt
11835/// AUTONAMEDIRS`, every scalar assignment to a path-shaped value
11836/// should register a named-directory entry for `~name` expansion;
11837/// the Rust port silently dropped that behavior.
11838pub fn strsetfn(pm: &mut param, x: String) {
11839 pm.u_str = Some(x.clone()); // c:3995 pm->u.str = x
11840 // c:3997-3998 — `if (!(pm->node.flags & PM_HASHELEM) && !pm->level &&
11841 // ((pm->node.flags & PM_NAMEDDIR) || isset(AUTONAMEDIRS)))`. A local
11842 // (level > 0) never names a directory (workers/54309).
11843 if (pm.node.flags as u32 & PM_HASHELEM) == 0
11844 && pm.level == 0
11845 && ((pm.node.flags as u32 & PM_NAMEDDIR) != 0 || isset(AUTONAMEDIRS))
11846 // c:3998
11847 {
11848 pm.node.flags |= PM_NAMEDDIR as i32; // c:3999
11849 adduserdir(&pm.node.nam, &x, 0, false); // c:4000
11850 }
11851}
11852
11853/// Port of `arrgetfn()` from `Src/params.c:4057`. C body:
11854/// `return pm->u.arr ? pm->u.arr : &nullarray;`
11855pub fn arrgetfn(pm: ¶m) -> Vec<String> {
11856 pm.u_arr.clone().unwrap_or_default()
11857}
11858
11859/// Port of `arrsetfn()` from `Src/params.c:4066`. C body frees
11860/// the old array, applies PM_UNIQUE filter via `uniqarray()`, then
11861/// stores. Calls `arrfixenv(ename, x)` for tied colon-arrays.
11862pub fn arrsetfn(pm: &mut param, x: Vec<String>) {
11863 let val = if (pm.node.flags as u32 & PM_UNIQUE) != 0 {
11864 simple_arrayuniq(x)
11865 } else {
11866 x
11867 };
11868 pm.u_arr = Some(val.clone());
11869 if let Some(ename) = pm.ename.clone() {
11870 arrfixenv(&ename, Some(&val));
11871 }
11872}
11873
11874/// Port of `hashgetfn()` from `Src/params.c:4084`. C body:
11875/// `return pm->u.hash;`
11876pub fn hashgetfn(pm: ¶m) -> Option<&HashTable> {
11877 pm.u_hash.as_ref()
11878}
11879
11880/// Port of `hashsetfn()` from `Src/params.c:4093`. C body:
11881/// `if (pm->u.hash && pm->u.hash != x) deleteparamtable(pm->u.hash);
11882/// pm->u.hash = x;`
11883pub fn hashsetfn(pm: &mut param, x: HashTable) {
11884 pm.u_hash = Some(x);
11885}
11886
11887/// Port of `arrhashsetfn()` from `Src/params.c:4113`. Set callback for
11888/// assoc arrays: takes a flat `[k1, v1, k2, v2, ...]` value list
11889/// and turns it into a hash.
11890///
11891/// C body:
11892/// 1. Count non-Marker entries; if odd, error c:4128-4131.
11893/// 2. Under ASSPM_AUGMENT, fetch existing hash via getfn
11894/// (c:4134-4137); otherwise allocate fresh via
11895/// newparamtable(17, name).
11896/// 3. Walk pairs: each value (k, v) becomes a PM_SCALAR|PM_UNSET
11897/// child param `createparam(k)`, then `assignstrvalue(v->pm,
11898/// val, eltflags)` (c:4140-4166).
11899/// 4. `pm->gsu.h->setfn(pm, ht)` to install (c:4168).
11900///
11901/// Storage model: C's per-pair `createparam(k)` + `assignstrvalue`
11902/// builds child Params inside a fresh `newparamtable`; zshrs's assoc
11903/// values live in the `paramtab_hashed_storage` IndexMap keyed by the
11904/// owning param's name, so the pair walk writes there. `pm.u_hash`
11905/// stays untouched — the IndexMap is the authoritative store (same
11906/// contract sethparam/gethparam already use).
11907pub fn arrhashsetfn(
11908 // c:4113
11909 pm: &mut param,
11910 val: Vec<String>,
11911 flags: i32,
11912) {
11913 // c:4124-4127 — count non-Marker entries.
11914 let alen: usize = val
11915 .iter()
11916 .filter(|s| !s.starts_with(Marker as char))
11917 .count();
11918
11919 // c:4129-4131 — odd count → error.
11920 if alen % 2 != 0 {
11921 zerr("bad set of key/value pairs for associative array");
11922 return;
11923 }
11924
11925 // c:4135-4139 — ASSPM_AUGMENT starts from the existing hash
11926 // (`ht = paramtab = pm->gsu.h->getfn(pm)`); otherwise a fresh
11927 // table (`newparamtable(17, pm->node.nam)`).
11928 let mut map: IndexMap<String, String> = if (flags & ASSPM_AUGMENT) != 0 {
11929 paramtab_hashed_storage()
11930 .lock()
11931 .unwrap()
11932 .get(&pm.node.nam)
11933 .cloned()
11934 .unwrap_or_default() // c:4136
11935 } else {
11936 IndexMap::new() // c:4138-4139
11937 };
11938
11939 // c:4141-4166 — pair walk. keyvalpairelement (Src/subst.c:49)
11940 // emits `[Marker, key, value]` triples for `[k]=v` / `[k]+=v`
11941 // forms ("Either all elements have Marker or none. Checked in
11942 // caller." c:4144); plain input is flat `[k, v, ...]` pairs.
11943 let mut it = val.into_iter();
11944 while let Some(first) = it.next() {
11945 let (elt_augment, key) = if first.starts_with(Marker as char) {
11946 // c:4145-4151 — `(*aptr)[1] == '+'` → per-element append
11947 // (ASSPM_AUGMENT via the setsparam INT_MAX trick).
11948 let aug = first[Marker.len_utf8()..].starts_with('+');
11949 match it.next() {
11950 Some(k) => (aug, k),
11951 None => break,
11952 }
11953 } else {
11954 (false, first)
11955 };
11956 let v = it.next().unwrap_or_default(); // c:4166 assignstrvalue value
11957 if elt_augment {
11958 // c:4147-4150 — `[k]+=v` appends to the existing element.
11959 map.entry(key).or_default().push_str(&v);
11960 } else {
11961 // c:4156-4166 — createparam(k, PM_SCALAR|PM_UNSET) +
11962 // assignstrvalue: plain insert in the IndexMap model.
11963 map.insert(key, v);
11964 }
11965 }
11966
11967 // c:4168-4169 — `pm->gsu.h->setfn(pm, ht)` installs the table.
11968 paramtab_hashed_storage()
11969 .lock()
11970 .unwrap()
11971 .insert(pm.node.nam.clone(), map);
11972 // c:4170 — free(val). Rust drops automatically.
11973}
11974
11975/// Port of `nullstrsetfn()` from `Src/params.c:4180`. C body:
11976/// `zsfree(x);` — frees but doesn't store. Rust drop handles free.
11977#[allow(unused_variables)]
11978pub fn nullstrsetfn(pm: &mut param, x: String) {}
11979
11980/// Port of `nullunsetfn()` from `Src/params.c:4192`. C body: empty.
11981#[allow(unused_variables)]
11982pub fn nullunsetfn(pm: &mut param, exp: i32) {}
11983
11984/// Port of `stdunsetfn()` from `Src/params.c:3955`. C body:
11985/// dispatches `pm->gsu->setfn(pm, NULL)` per `PM_TYPE`, clears
11986/// `PM_TIED`/frees ename for tied params, sets PM_UNSET.
11987///
11988/// Rust port mirrors C semantics: clears the union slot and sets
11989/// PM_UNSET. The GSU vtable callbacks are stored on `param` as
11990/// `Option<Gsu*>` (zsh_h:760-764) but the dispatch uses callback
11991/// fn-ptrs that aren't generally registered yet, so we open-code
11992/// the "setfn(pm, NULL)" effect by zeroing the matching union
11993/// member instead of calling through the vtable.
11994#[allow(unused_variables)]
11995pub fn stdunsetfn(pm: &mut param, exp: i32) {
11996 match PM_TYPE(pm.node.flags as u32) {
11997 PM_SCALAR | PM_NAMEREF => {
11998 pm.u_str = None;
11999 // c:3913-3914 — `if (pm->gsu.s->setfn) pm->gsu.s->setfn(pm, NULL);`.
12000 // !!! RUST-ONLY DISPATCH !!! The port's `gsu_scalar::setfn` takes a
12001 // `String` and cannot receive NULL, so the NULL call is spelled out
12002 // for the one special whose setfn gives NULL a meaning: IFS's
12003 // `ifssetfn` (c:4745-4750) stores `ifs = NULL`, and `inittyptab`
12004 // then splits on the default separators (c:Src/utils.c:4207/4216).
12005 // Without it `IFS=:; unset IFS; s="p q"; print -rl -- ${=s}` kept
12006 // splitting on `:` and printed `p q` (zsh: `p` `q`).
12007 if pm.node.nam == "IFS" && pm.node.flags as u32 & PM_SPECIAL != 0 {
12008 *ifs_lock().lock().expect("ifs poisoned") = None; // c:4748
12009 inittyptab(); // c:4749
12010 }
12011 // The locale setfns are the other specials that give NULL a
12012 // meaning: `lc_allsetfn` (c:4831 `if (!x || !*x)`) falls back to
12013 // `setlang($LANG)`, `lcsetfn` (c:4866) to `$LANG` for its category,
12014 // and `langsetfn` re-runs `setlang`. All three already treat "" as
12015 // NULL, so "" stands in for C's NULL here. Without this,
12016 // `LC_ALL=C; unset LC_ALL` left the process in the C locale.
12017 if pm.node.flags as u32 & PM_SPECIAL != 0 {
12018 match pm.node.nam.as_str() {
12019 "LC_ALL" => {
12020 // c:4873 `strsetfn(pm, x)` empties the ONE node that
12021 // setlang's c:4847 `getsparam_u("LC_ALL")` then reads.
12022 // The port's `pm` is a detached copy of the table row,
12023 // so clear the row too or setlang still sees "C" and
12024 // returns without restoring $LANG's locale.
12025 if let Ok(mut tab) = paramtab().write() {
12026 if let Some(p) = tab.get_mut("LC_ALL") {
12027 p.u_str = None;
12028 }
12029 }
12030 lc_allsetfn(pm, String::new()) // c:3914 → c:4871
12031 }
12032 "LANG" => langsetfn(pm, String::new()), // c:3914 → c:4896
12033 n if LC_NAMES.iter().any(|(ln, _)| *ln == n) => {
12034 lcsetfn(pm, String::new()) // c:3914 → c:4904
12035 }
12036 _ => {}
12037 }
12038 pm.u_str = None;
12039 }
12040 }
12041 PM_ARRAY => {
12042 pm.u_arr = None;
12043 // c:3917-3920 — the PM_ARRAY arm is `pm->gsu.a->setfn(pm, NULL)`,
12044 // and for the three `IPDEF9(…, &pparams, …)` rows
12045 // (c:Src/params.c:392,393,430 — `*`, `@`, `argv`) that setfn is
12046 // `arrvarsetfn` (c:189-190 `vararray_gsu`). Its body does NOT
12047 // touch `pm->u.arr`: c:4248 `char ***dptr = (char ***)pm->u.data;`
12048 // then c:4259-4260 `if ((pm->node.flags & PM_SPECIAL) && !x)
12049 // *dptr = mkarray(NULL);` — so the GLOBAL positional vector is
12050 // replaced by an EMPTY array (not a NULL pointer; the row carries
12051 // PM_SPECIAL from IPDEF9).
12052 //
12053 // zshrs keeps that vector in `builtin::PPARAMS` rather than on the
12054 // node — the same split `assignaparam` mirrors for the write side
12055 // (params.rs:9548) and `printparamnode` for the print side
12056 // (params.rs:13874). Without the mirror here `unset argv` stamped
12057 // PM_UNSET on a node nothing reads and left `$@` / `$#` holding
12058 // the old positionals.
12059 //
12060 // `*` and `@` are unreachable through this path — both carry
12061 // PM_READONLY_SPECIAL (c:392-393) so `unsetparam_pm` rejects them
12062 // at c:3850, and `unset '*'` never gets that far anyway
12063 // (`isident` fails, c:Src/builtin.c:3878). Only c:430's `argv`,
12064 // whose IPDEF9 flag word is 0, is unsettable. They are matched
12065 // together because they are one parameter.
12066 if matches!(pm.node.nam.as_str(), "argv" | "*" | "@") {
12067 if let Ok(mut pp) = crate::ported::builtin::PPARAMS.lock() {
12068 pp.clear(); // c:4260 mkarray(NULL)
12069 }
12070 }
12071 }
12072 PM_HASHED => {
12073 // c:3922-3925 — `pm->gsu.h->setfn(pm, NULL)`, which for the
12074 // standard hash gsu (c:194 `{ hashgetfn, hashsetfn, stdunsetfn }`)
12075 // is `hashsetfn` (c:4045-4050): `if (pm->u.hash && pm->u.hash != x)
12076 // deleteparamtable(pm->u.hash); pm->u.hash = x;` — with `x` NULL,
12077 // the association's pairs are DESTROYED and the slot cleared.
12078 //
12079 // !!! WARNING: RUST-ONLY HAZARD (the structures do NOT match) !!!
12080 // In C the pairs hang off the Param itself, so clearing `u.hash`
12081 // IS the teardown. zshrs keeps them in `paramtab_hashed_storage`,
12082 // a side map keyed by NAME, and `arrhashsetfn` — the only writer
12083 // of whole associations (c:4065, reached from `sethparam` →
12084 // `setarrvalue` c:2908) — never populates `pm.u_hash` at all. So
12085 // clearing `u_hash` here freed a slot nothing reads, and this arm
12086 // was inert: the pairs outlived the unset and every subscripted
12087 // read still found them.
12088 //
12089 // Callers papered over that one at a time. `bin_typeset`'s `+a`/
12090 // `+A` arm said so outright — "unsetparam alone can leave the
12091 // paramtab_hashed_storage entry, so a later scalar deref still saw
12092 // the joined values" — and `bin_unset`'s ordinary `unset NAME` arm
12093 // carried its own copy. Doing it here instead is what C does, and
12094 // it puts the teardown on the same node the rest of this function
12095 // is tearing down.
12096 //
12097 // Keyed by name, because that is how the store is keyed; this is
12098 // the same dimension mismatch recorded on the store's own
12099 // declaration. It is safe against the one place where name is not
12100 // enough — a `local -A` shadow — because that path does not come
12101 // through here at all: `endparamscope` unlinks the popped node
12102 // directly and restores the outer scope's row from
12103 // `PARAMTAB_HASHED_SHADOW_STACK`, never calling any unsetfn.
12104 pm.u_hash = None;
12105 if let Ok(mut m) = paramtab_hashed_storage().lock() {
12106 m.remove(pm.node.nam.as_str()); // c:4047 deleteparamtable
12107 }
12108 }
12109 _ => {
12110 if (pm.node.flags as u32 & PM_SPECIAL) == 0 {
12111 pm.u_str = None;
12112 }
12113 }
12114 }
12115 if (pm.node.flags as u32 & (PM_SPECIAL | PM_TIED)) == PM_TIED {
12116 pm.ename = None;
12117 pm.node.flags &= !(PM_TIED as i32);
12118 }
12119 pm.node.flags |= PM_UNSET as i32;
12120}
12121
12122// -----------------------------------------------------------
12123// "Null" callbacks — no-op getfn/setfn/unsetfn slots used for
12124// read-only or write-only special params.
12125// -----------------------------------------------------------
12126
12127/// Port of `nullintsetfn()` from `Src/params.c:4187`. C body:
12128/// empty (no-op setter for read-only int params).
12129#[allow(unused_variables)]
12130pub fn nullintsetfn(pm: &mut param, x: i64) {}
12131
12132/// Port of `nullsethashfn()` from `Src/params.c:4104`. C body:
12133/// `deleteparamtable(x);` — frees the supplied table, doesn't store.
12134#[allow(unused_variables)]
12135pub fn nullsethashfn(pm: &mut param, x: HashTable) {
12136 // Rust drop semantics free `x` when this scope ends.
12137}
12138
12139// -----------------------------------------------------------
12140// Generic special-param GSU callbacks (`u.valptr` / `u.data`).
12141// C source uses raw pointer indirection through `pm->u.data`/
12142// `pm->u.valptr` — Rust port stores the global's name in `u_str`
12143// (lookup key) since we can't carry raw pointers across an FFI
12144// boundary safely. The lookup-table integration ships with the
12145// special-params init code (Src/params.c:4213 createparamtable).
12146// -----------------------------------------------------------
12147
12148/// Port of `intvargetfn()` from `Src/params.c:4202`. C body:
12149/// `return *pm->u.valptr;`
12150pub fn intvargetfn(pm: ¶m) -> i64 {
12151 // c:4156 — `return *pm->u.valptr;`. For most IPDEF4/IPDEF5 specials the
12152 // pointer aims at a global that zshrs models as an atomic rather than as
12153 // storage on the node, so the read has to name that global. `$COLUMNS`
12154 // and `$LINES` are `IPDEF5("COLUMNS", &zterm_columns, zlevar_gsu)` /
12155 // `IPDEF5("LINES", &zterm_lines, zlevar_gsu)` (c:362-363).
12156 //
12157 // The alias runs both ways in C and nothing synchronises the halves:
12158 // `zlevarsetfn` (c:4176 `*p = x`) writes the global through the same
12159 // pointer, and `adjustwinsize`'s `setiparam("COLUMNS", zterm_columns)`
12160 // (c:1934) exists only to re-export an environment COLUMNS — it is
12161 // guarded by `zgetenv("COLUMNS")` precisely because the parameter itself
12162 // needs no update.
12163 //
12164 // zshrs ported only the SET half (`zlevarsetfn` mirrors into
12165 // `ZTERM_COLUMNS`), so a read answered from the node's `u_val` — a
12166 // snapshot, and a snapshot can be rolled back. `adjustwinsize` runs from
12167 // the SIGWINCH handler, which zshrs can enter while the shell is inside a
12168 // command substitution's saved state, so the handler's `setiparam` write
12169 // was discarded when that state was restored while the `ZTERM_*` globals
12170 // kept the new size. A completer that forked a helper across a resize
12171 // then read `$COLUMNS` as the OLD width while its matches were counted
12172 // against the NEW one: `_git_commands` pads every display string to
12173 // `${(r.COLUMNS-4.)…}` (Functions/Completion/Unix/_git:6890), so
12174 // `git <TAB>` across a 24x80 -> 24x60 resize asked to see "all 152
12175 // possibilities (236 lines)" for 152 one-line matches. The same gap made
12176 // `local COLUMNS` read 0 where zsh reads the live width.
12177 match pm.node.nam.as_str() {
12178 "COLUMNS" => crate::ported::utils::ZTERM_COLUMNS.load(Ordering::SeqCst) as i64, // c:362
12179 "LINES" => crate::ported::utils::ZTERM_LINES.load(Ordering::SeqCst) as i64, // c:363
12180 _ => pm.u_val,
12181 }
12182}
12183
12184/// Port of `intvarsetfn()` from `Src/params.c:4213`. C body:
12185/// `*pm->u.valptr = x;`
12186pub fn intvarsetfn(pm: &mut param, x: i64) {
12187 pm.u_val = x;
12188}
12189
12190/// Port of `zlevarsetfn()` from `Src/params.c:4224`. C body sets
12191/// the int and triggers `adjustwinsize` for LINES/COLUMNS.
12192/// Port of `zlevarsetfn(Param pm, zlong x)` from `Src/params.c:4226`.
12193/// C body: `*p = x; if (p == &zterm_lines || p == &zterm_columns)
12194/// adjustwinsize(2 + (p == &zterm_columns));`
12195///
12196/// The `from` argument to `adjustwinsize` is documented at
12197/// `Src/utils.c:1883-1887`: 0=signal, 1=manual, 2=LINES callback,
12198/// 3=COLUMNS callback. Each value selects a different code path
12199/// inside `adjustwinsize` — for example, `from=2` skips the
12200/// COLUMNS-specific ioctl, and `from=3` skips the LINES path.
12201///
12202/// The previous Rust port passed `0` for both LINES and COLUMNS,
12203/// which triggered the FULL `getwinsz` ioctl + both adjustlines
12204/// AND adjustcolumns calls AND the potential setiparam recursion
12205/// — diverging from C's narrow "just adjust the one axis we
12206/// changed" semantics. Effect: setting `LINES=80` would re-issue
12207/// `setiparam("COLUMNS", ...)` recursively, churning the
12208/// paramtab for no reason.
12209pub fn zlevarsetfn(pm: &mut param, x: i64) {
12210 // c:4226
12211 pm.u_val = x; // c:4230 *p = x;
12212 // c:4230 — `p` IS `&zterm_lines` / `&zterm_columns`
12213 // (Src/params.c:362-363 `IPDEF5("COLUMNS", &zterm_columns,
12214 // zlevar_gsu)`), so C's single `*p = x` publishes the value
12215 // to the parameter AND to the global every other C reader
12216 // consults. zshrs's param carries its own `u_val` copy, so
12217 // the same write has to be made twice. It belongs HERE and
12218 // not inside `adjustwinsize`, because `adjustwinsize`
12219 // early-returns at c:1900-1901 when `SHTTY == -1` — a
12220 // non-interactive `COLUMNS=40` would otherwise never reach
12221 // the global, while C honours it.
12222 // c:4231-4232 — `2 + (p == &zterm_columns)` selects 2 for LINES
12223 // (zterm_lines) and 3 for COLUMNS (zterm_columns).
12224 if pm.node.nam == "LINES" {
12225 crate::ported::utils::ZTERM_LINES.store(x as i32, Ordering::SeqCst); // c:4230
12226 let _ = adjustwinsize(2); // c:4232 LINES path
12227 } else if pm.node.nam == "COLUMNS" {
12228 crate::ported::utils::ZTERM_COLUMNS.store(x as i32, Ordering::SeqCst); // c:4230
12229 let _ = adjustwinsize(3); // c:4232 COLUMNS path
12230 }
12231}
12232
12233/// Port of `strvarsetfn()` from `Src/params.c:4249`. C body:
12234/// `zsfree(*q); *q = x;` where `q = (char **)pm->u.data`.
12235pub fn strvarsetfn(pm: &mut param, x: Option<String>) {
12236 pm.u_str = x;
12237}
12238
12239/// Port of `strvargetfn()` from `Src/params.c:4263`. C body:
12240/// `s = *((char **)pm->u.data); return s ? s : hcalloc(1);`
12241pub fn strvargetfn(pm: ¶m) -> String {
12242 pm.u_str.clone().unwrap_or_default()
12243}
12244
12245/// Port of `arrvargetfn()` from `Src/params.c:4279`. C body:
12246/// `arrptr = *((char ***)pm->u.data); return arrptr ?: &nullarray;`
12247pub fn arrvargetfn(pm: ¶m) -> Vec<String> {
12248 pm.u_arr.clone().unwrap_or_default()
12249}
12250
12251/// Port of `arrvarsetfn()` from `Src/params.c:4294`. The previous body skipped three of
12252/// the four canonical C arms:
12253/// 1. PM_UNIQUE → uniqarray (was ported via simple_arrayuniq).
12254/// 2. PM_SPECIAL + null x → `*dptr = mkarray(NULL)` so a tied
12255/// array set to NULL becomes a writable empty array, not a
12256/// dangling null (was missing).
12257/// 3. `pm->ename` set → `arrfixenv(ename, x)` syncs the colon-
12258/// joined env var partner (was missing — breaks PATH/path,
12259/// FPATH/fpath, etc. when set via the array form).
12260/// 4. `pm->ename` set + null x + `*dptr == path` → invalidate
12261/// pathchecked so the next path resolution re-walks (was
12262/// missing).
12263pub fn arrvarsetfn(pm: &mut param, x: Option<Vec<String>>) {
12264 // c:4296 `char ***dptr = (char ***)pm->u.data;`
12265 // c:4298-4299 — `if (*dptr != x) freearray(*dptr);` Rust Vec drop
12266 // on reassignment handles freeing automatically.
12267 // c:4300-4301 — `if (pm->node.flags & PM_UNIQUE) uniqarray(x);`
12268 let uniq_applied: Option<Vec<String>> = match x {
12269 Some(v) if (pm.node.flags as u32 & PM_UNIQUE) != 0 => Some(simple_arrayuniq(v)),
12270 other => other,
12271 };
12272 // c:4302-4310 — PM_SPECIAL + NULL → mkarray(NULL); else assign.
12273 let final_val: Vec<String> = match uniq_applied {
12274 Some(v) => v, // c:4310 `*dptr = x;`
12275 None => {
12276 if (pm.node.flags as u32 & PM_SPECIAL) != 0 {
12277 crate::ported::utils::mkarray(None) // c:4308 `mkarray(NULL)`
12278 } else {
12279 Vec::new() // c:4310 — null case for non-special: empty.
12280 }
12281 }
12282 };
12283 // c:4311-4316 — ename sync.
12284 if let Some(ename) = pm.ename.clone() {
12285 // c:4311 `if (pm->ename)`
12286 if !final_val.is_empty() || pm.u_arr.is_some() {
12287 // c:4312-4313 — `if (x) arrfixenv(pm->ename, x);`
12288 arrfixenv(&ename, Some(&final_val));
12289 } else if pm.node.nam == "path" {
12290 // c:4314-4315 — `else if (*dptr == path) pathchecked = path;`
12291 // — invalidate the path-resolver cache. Rust port uses an
12292 // AtomicUsize sentinel; storing 0 marks "must re-walk".
12293 crate::ported::hashtable::pathchecked.store(0, Ordering::SeqCst);
12294 }
12295 }
12296 pm.u_arr = Some(final_val);
12297}
12298
12299/// Array to colon-separated path — inverse of `colonsplit`.
12300/// Port of `colonarrgetfn()` from `Src/params.c:4321` (joins the array
12301/// stored in `pm->u.colon` back into the `:`-form for env).
12302/// WARNING: param names don't match C — Rust=(arr) vs C=(pm)
12303pub fn colonarrgetfn(arr: &[String]) -> String {
12304 arr.join(":")
12305}
12306
12307/// Port of `colonarrsetfn()` from `Src/params.c:4329`. C body
12308/// splits the colon-string into an array and stores via the
12309/// generic arrvarsetfn.
12310pub fn colonarrsetfn(pm: &mut param, x: Option<String>) {
12311 let uniq = (pm.node.flags as u32 & PM_UNIQUE) != 0; // c:4339
12312 // c:4339-4341 — `arrvarsetfn(pm, x ? colonsplit(...) : NULL);`
12313 // The None branch must pass `None` (not `Some(Vec::new())`) so the
12314 // PM_SPECIAL + NULL → mkarray(NULL) arm in arrvarsetfn fires.
12315 let arr = x.map(|s| colonsplit(&s, uniq)); // c:4339
12316 arrvarsetfn(pm, arr);
12317}
12318
12319/// Port of `tiedarrgetfn()` from `Src/params.c:4348`. C body:
12320/// `struct tieddata *dptr = (struct tieddata *)pm->u.data;`
12321/// `return *dptr->arrptr ? zjoin(*dptr->arrptr, …) : "";`
12322///
12323/// C's `pm->u.data->arrptr` is a raw pointer into the partner array
12324/// param's storage. The Rust port can't hold a pointer into another
12325/// paramtab entry's heap, so the partner lookup goes via
12326/// `pm.ename` → paramtab → `apm.u_arr`. For backwards compatibility
12327/// with callers that set `pm.u_arr` directly (the pre-fix code path
12328/// at c:4348 that's still in some Rust call sites), fall back to
12329/// the scalar's own `u_arr` when `ename` is None or the partner is
12330/// missing. Bug #24 in docs/BUGS.md.
12331pub fn tiedarrgetfn(pm: ¶m) -> Vec<String> {
12332 if let Some(ename) = pm.ename.as_deref() {
12333 if let Ok(tab) = paramtab().read() {
12334 if let Some(apm) = tab.get(ename) {
12335 // The partner is found by NAME, so the lookup lands on whatever
12336 // binding is currently visible — including a LOCAL shadow that
12337 // is no part of the tie. C links the pair by struct pointer, so
12338 // `local -a v=(a b)` inside a function cannot change what `$V`
12339 // reads; zshrs published the local through the scalar half
12340 // (`$V` became `a:b` where zsh keeps `g1:g2`). Note the local
12341 // is genuinely untied — `${(t)v}` is `array-local`, matching
12342 // zsh — so the flags were already right and only this read was
12343 // wrong. Walk the shadow chain (`old`) to the binding that
12344 // actually carries PM_TIED. docs/BUGS.md #1039 B.
12345 //
12346 // A local SPECIAL is unaffected: it keeps PM_TIED through the
12347 // newspecial inheritance (`${(t)path}` is
12348 // `array-local-tied-special`), so `local path=(…)` still
12349 // publishes to `$PATH` as #1039 A requires.
12350 let mut cand: Option<¶m> = Some(apm);
12351 while let Some(p) = cand {
12352 if (p.node.flags as u32 & PM_TIED) != 0 {
12353 if let Some(arr) = p.u_arr.as_ref() {
12354 return arr.clone();
12355 }
12356 break;
12357 }
12358 cand = p.old.as_deref();
12359 }
12360 }
12361 }
12362 }
12363 pm.u_arr.clone().unwrap_or_default()
12364}
12365
12366/// Port of `tiedarrsetfn()` from `Src/params.c:4357`. Setter for a colon-array-tied
12367/// scalar (PATH/CDPATH/MAILPATH/etc.).
12368///
12369/// C body:
12370/// 1. Free the existing tied array (`*dptr->arrptr`) at c:4363.
12371/// 2. If no array but an `ename` exists, clear PM_DEFAULTED on
12372/// the tied array param (c:4365-4368).
12373/// 3. If `x` is non-null: build a 1-or-2-byte separator from
12374/// `dptr->joinchar` (Meta-quoting if needed, c:4371-4380),
12375/// `sepsplit(x, sepbuf, 0, 0)` into the array (c:4381), and
12376/// uniqarray() if PM_UNIQUE (c:4382-4383). Free `x` (c:4384).
12377/// 4. Else: `*dptr->arrptr = NULL` (c:4385-4386).
12378/// 5. If `pm->ename` is set, call `arrfixenv(pm->name, arrptr)`
12379/// to sync env (c:4387-4388).
12380///
12381/// The Rust port treats `u_arr` as the tied array storage and
12382/// uses `':'` as the joinchar default (matches PATH/CDPATH/FPATH
12383/// /MAILPATH/PSVAR/MODULE_PATH which all use colon separators —
12384/// the joinchar field on the C-side tieddata wasn't ported to the
12385/// Rust Param struct yet).
12386pub fn tiedarrsetfn(pm: &mut param, x: Option<String>) {
12387 // c:4357
12388
12389 // c:4361-4368 — free old / clear PM_DEFAULTED on tied counterpart.
12390 if pm.u_arr.is_none() {
12391 if let Some(ename) = pm.ename.clone() {
12392 // c:4365
12393 let mut tab = paramtab().write().unwrap();
12394 if let Some(altpm) = tab.get_mut(&ename) {
12395 // c:4366
12396 altpm.node.flags &= !(PM_DEFAULTED as i32); // c:4367
12397 }
12398 }
12399 }
12400
12401 // c:4369-4386 — split + assign. C writes through `*dptr->arrptr`
12402 // which is a pointer INTO THE PARTNER ARRAY param's storage. The
12403 // Rust port can't hold raw pointers across paramtab entries, so
12404 // when `pm.ename` is set, write the split result to the partner's
12405 // `u_arr` via paramtab (bug #24). When `pm.ename` is None (older
12406 // call sites or non-tied use), keep the scalar's own `u_arr`
12407 // up-to-date for legacy callers.
12408 // c:4370-4380 — single-byte separator built from `dptr->joinchar`
12409 // on the tieddata riding `pm->u.data` (Rust: typed `u_tied` view);
12410 // joinchar==0 → empty sepbuf; no tieddata → `:` default (the
12411 // PM_SPECIAL colon-tied params, c:5314-5315).
12412 let sepbuf: String = match pm.u_tied.as_deref() {
12413 // c:4320-4332 — `if (imeta(dptr->joinchar)) { sepbuf[0] = Meta;
12414 // sepbuf[1] = dptr->joinchar ^ 32; } else sepbuf[0] = dptr->joinchar;`
12415 // !!! WARNING: RUST-ONLY ARM — zshrs keeps NUL unmetafied inside values,
12416 // so a NUL join character is the raw `\0`, not C's `Meta ' '` pair.
12417 Some(td) => match td.joinchar as u8 {
12418 0 => "\0".to_string(),
12419 b if crate::ported::utils::imeta_byte(b) => [char::from(Meta), char::from(b ^ 32)].iter().collect(),
12420 b => char::from(b).to_string(), // c:4330
12421 },
12422 None => ":".to_string(), // c:5314-5315
12423 };
12424 let arr_opt: Option<Vec<String>> = if let Some(s) = x {
12425 // c:4369
12426 // c:4333 — `*dptr->arrptr = sepsplit(x, sepbuf, 0, 0);`. sepbuf holds
12427 // the join character itself, NUL included, so a NUL-tied scalar splits
12428 // on NUL; a value with no NUL stays one element.
12429 let split: Vec<String> = crate::ported::utils::sepsplit(&s, Some(&sepbuf), true);
12430 // c:4382-4383 — uniqarray if PM_UNIQUE.
12431 let split = if pm.node.flags & PM_UNIQUE as i32 != 0 {
12432 // c:4382
12433 uniqarray(split) // c:4383
12434 } else {
12435 split
12436 };
12437 Some(split)
12438 } else {
12439 None
12440 };
12441 if let Some(ename) = pm.ename.clone() {
12442 if let Ok(mut tab) = paramtab().write() {
12443 if let Some(apm) = tab.get_mut(&ename) {
12444 apm.u_arr = arr_opt.clone(); // c:4381
12445 }
12446 }
12447 // c:4352 — zjoin writes the raw joinchar byte; joinchar==0
12448 // joins with NUL (measured on 5.9.1: `s=(x y); print -rn
12449 // "$S"` → `x\0y`), not with the empty split-sepbuf.
12450 let joinsep = if sepbuf.is_empty() {
12451 "\0"
12452 } else {
12453 sepbuf.as_str()
12454 };
12455 pm.u_str = arr_opt.as_ref().map(|a| a.join(joinsep));
12456 } else {
12457 pm.u_arr = arr_opt;
12458 }
12459
12460 // c:4387-4388 — `if (pm->ename) arrfixenv(pm->name, *dptr->arrptr)`.
12461 if pm.ename.is_some() {
12462 let nam = pm.node.nam.clone();
12463 // Pull the live array out of the partner for env sync.
12464 let snap = paramtab().read().ok().and_then(|t| {
12465 t.get(pm.ename.as_deref().unwrap())
12466 .and_then(|p| p.u_arr.clone())
12467 });
12468 arrfixenv(&nam, snap.as_deref());
12469 }
12470}
12471
12472/// Port of `tiedarrunsetfn()` from `Src/params.c:4393`. C body
12473/// frees the tied storage and calls stdunsetfn.
12474/// Direct port of `void tiedarrunsetfn(Param pm, UNUSED(int exp))`
12475/// from `Src/params.c:4393`. Special unset for tied arrays:
12476/// frees tieddata, ename, clears PM_TIED, sets PM_UNSET.
12477///
12478/// C body:
12479/// pm->gsu.s->setfn(pm, NULL); // c:4393
12480/// zfree(pm->u.data, sizeof(tieddata)); // c:4393
12481/// pm->u.data = NULL; // c:4393
12482/// zsfree(pm->ename); // c:4393
12483/// pm->ename = NULL; // c:4393
12484/// pm->flags &= ~PM_TIED; // c:4393
12485/// pm->flags |= PM_UNSET; // c:4393
12486pub fn tiedarrunsetfn(pm: &mut param, _exp: i32) {
12487 // c:4393
12488 // c:4400 — invoke the scalar setfn with NULL (frees backing array).
12489 tiedarrsetfn(pm, None);
12490 // c:4401-4403 — drop tieddata.
12491 pm.u_data = 0;
12492 pm.u_arr = None;
12493 // c:4404-4405 — `zsfree(pm->ename); pm->ename = NULL`.
12494 pm.ename = None;
12495 // c:4406-4407 — flag toggles.
12496 pm.node.flags &= !(PM_TIED as i32);
12497 pm.node.flags |= PM_UNSET as i32;
12498}
12499
12500// -----------------------------------------------------------
12501// Array uniq helpers.
12502// -----------------------------------------------------------
12503
12504/// Port of `simple_arrayuniq()` from `Src/params.c:4412`. C body:
12505/// O(n^2) dedupe in place — first occurrence wins.
12506/// WARNING: param names don't match C — Rust=(x) vs C=(x, freeok)
12507pub fn simple_arrayuniq(x: Vec<String>) -> Vec<String> {
12508 let mut seen: HashSet<String> = HashSet::new();
12509 let mut out = Vec::with_capacity(x.len());
12510 for s in x {
12511 if seen.insert(s.clone()) {
12512 out.push(s);
12513 }
12514 }
12515 out
12516}
12517
12518/// Port of `arrayuniq_freenode()` from `Src/params.c:4443`. C
12519/// body: `zsfree(((Pathnode)hn)->name); zfree(hn, sizeof…);` —
12520/// the freenode callback for the temporary HashTable `arrayuniq`
12521/// builds. Rust drop semantics handle this; no-op shim.
12522/// is `(void)hn;` — intentional no-op; passed as freenode callback
12523/// to scratch hashtable used by `arrayuniq` so existing entries
12524/// aren't freed when the table is torn down.
12525/// WARNING: param names don't match C — Rust=() vs C=(hn)
12526/// WARNING: param names don't match C — Rust=() vs C=(pm, x)
12527pub fn arrayuniq_freenode() {}
12528
12529/// Port of `newuniqtable()` from `Src/params.c:4450`. C body allocates a `HashTable`
12530/// named "arrayuniq" with the standard hasher/cmpnodes/
12531/// add/get/remove/disable/enable function pointers plus
12532/// `arrayuniq_freenode` as the freenode callback (which is a
12533/// no-op — see c:4443). Rust returns a `HashSet<String>` with
12534/// the size hint pre-allocated; the freenode-callback role is
12535/// implicit (Drop runs on HashSet teardown without freeing
12536/// borrowed strings).
12537pub fn newuniqtable(size: i64) -> HashSet<String> {
12538 // c:4450
12539 HashSet::with_capacity(size.max(0) as usize) // c:4450 newhashtable(size, ...)
12540}
12541
12542/// Port of `arrayuniq()` from `Src/params.c:4473`. First-wins dedupe of `x`,
12543/// in-place. C uses simple O(n²) scan for arrays under 10
12544/// entries, switching to a HashTable for larger arrays. `freeok`
12545/// controls whether to `zsfree()` duplicates (only safe when
12546/// caller owns the strings — Rust drop semantics handle it).
12547///
12548/// Signature note: C takes `char **x` + in-place mutation; Rust
12549/// takes owned `Vec<String>` and returns the deduped result.
12550/// `freeok` is preserved but is a no-op in Rust (drops free
12551/// automatically). The hashtable / simple-loop tiering follows
12552/// the same threshold (10) as C.
12553pub fn arrayuniq(x: Vec<String>, freeok: i32) -> Vec<String> {
12554 // c:4473
12555 let _ = freeok;
12556 let array_size = x.len();
12557 if array_size == 0 {
12558 // c:4481
12559 return x;
12560 }
12561 // c:4482-4486 — small-array fallback to simple_arrayuniq.
12562 if array_size < 10 {
12563 // c:4482
12564 return simple_arrayuniq(x); // c:4484
12565 }
12566 // c:4483 — `if (!(ht = newuniqtable(array_size + 1)))` — Rust
12567 // newuniqtable never fails, but mirror the C order of allocation.
12568 let mut ht = newuniqtable(array_size as i64 + 1);
12569 // c:4487-4507 — walk + first-wins.
12570 let mut out: Vec<String> = Vec::with_capacity(array_size);
12571 for s in x {
12572 // c:4487 walk
12573 if ht.insert(s.clone()) {
12574 // c:4488 gethashnode2 + addhashnode2
12575 out.push(s); // c:4495 *write_it = *it
12576 }
12577 // else: dup — drop the value (c:4502 zsfree if freeok).
12578 }
12579 drop(ht); // c:4523 deletehashtable
12580 out
12581}
12582
12583/// Remove duplicate elements from array while preserving order.
12584/// Port of `uniqarray()` from `Src/params.c:4514`.
12585/// WARNING: param names don't match C — Rust=(arr) vs C=(x)
12586pub fn uniqarray(arr: Vec<String>) -> Vec<String> {
12587 let mut seen = HashSet::new();
12588 arr.into_iter().filter(|s| seen.insert(s.clone())).collect()
12589}
12590
12591/// Port of `zhuniqarray()` from `Src/params.c:4523`. Wraps `arrayuniq` with `freeok=0`.
12592/// (C body is literally `arrayuniq(x, 0);`.)
12593pub fn zhuniqarray(x: Vec<String>) -> Vec<String> {
12594 // c:4523
12595 arrayuniq(x, 0) // c:4523
12596}
12597
12598/// Port of `poundgetfn()` from `Src/params.c:4534`. C body:
12599/// `return arrlen(pparams);`
12600/// WARNING: param names don't match C — Rust=() vs C=(pm)
12601pub fn poundgetfn() -> i64 {
12602 pparams_lock().lock().expect("pparams poisoned").len() as i64
12603}
12604
12605/// Port of `randomgetfn()` from `Src/params.c:4543`. C body:
12606/// `return rand() & 0x7fff;`
12607/// WARNING: param names don't match C — Rust=() vs C=(pm)
12608pub fn randomgetfn() -> i64 {
12609 (unsafe { libc::rand() } & 0x7fff) as i64
12610}
12611
12612/// Port of `randomsetfn()` from `Src/params.c:4552`. C body:
12613/// `srand((unsigned int)v);`
12614/// WARNING: param names don't match C — Rust=(v) vs C=(pm, v)
12615pub fn randomsetfn(v: i64) {
12616 unsafe { libc::srand(v as libc::c_uint) };
12617}
12618
12619// -----------------------------------------------------------
12620// SECONDS / EPOCHSECONDS family — backed by SHTIMER static.
12621// -----------------------------------------------------------
12622
12623/// Port of `intsecondsgetfn()` from `Src/params.c:4561`. C body:
12624/// `return (zlong)(now.tv_sec - shtimer.tv_sec - …);`
12625/// WARNING: param names don't match C — Rust=() vs C=(pm)
12626pub fn intsecondsgetfn() -> i64 {
12627 // c:4563 — `shtimer` is initialized at shell startup (zsh.h
12628 // mod_export). Force shtimer init BEFORE reading `now` so the
12629 // lazy-init race doesn't make `now < shtimer` on first call
12630 // (which produced -1 from the nsec borrow-from-sec adjustment).
12631 let timer = *shtimer_lock().lock().expect("shtimer poisoned");
12632 let now = SystemTime::now()
12633 .duration_since(UNIX_EPOCH)
12634 .unwrap_or_default();
12635 let now_sec = now.as_secs() as i64;
12636 let timer_sec = timer.as_secs() as i64;
12637 let now_nsec = now.subsec_nanos() as i64;
12638 let timer_nsec = timer.subsec_nanos() as i64;
12639 let diff = now_sec - timer_sec - i64::from(now_nsec < timer_nsec);
12640 // c:4565 — clamp negative-diff (lazy-init or clock skew) to 0
12641 // so \$SECONDS reads as a non-negative count of elapsed seconds
12642 // from shell start. zsh's shtimer is set in main() before any
12643 // user code runs, guaranteeing now >= shtimer; the Rust lazy
12644 // init makes this stricter via .max(0).
12645 diff.max(0)
12646}
12647
12648/// Port of `intsecondssetfn()` from `Src/params.c:4575`. C body:
12649/// ```c
12650/// diff = (zlong)now.tv_sec - x;
12651/// shtimer.tv_sec = diff;
12652/// if ((zlong)shtimer.tv_sec != diff)
12653/// zwarn("SECONDS truncated on assignment");
12654/// shtimer.tv_nsec = now.tv_nsec;
12655/// ```
12656/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
12657pub fn intsecondssetfn(x: i64) {
12658 let now = SystemTime::now()
12659 .duration_since(UNIX_EPOCH)
12660 .unwrap_or_default();
12661 let now_sec = now.as_secs() as i64;
12662 let new_sec = now_sec - x;
12663 // c:4587 — C uses `zwarn` (informational), NOT `zerr` (fatal).
12664 // The C body STORES `diff` unconditionally then emits the warning
12665 // if truncation lost information. Rust port previously used `zerr`
12666 // and early-returned (skipping the store) — divergent from C.
12667 if new_sec < 0 {
12668 zwarn("SECONDS truncated on assignment");
12669 // c:4585 — C still stores; Rust represents shtimer as Duration
12670 // which is non-negative. We clamp to zero to preserve the
12671 // "store-anyway" semantic for the time-display path, even
12672 // though the negative-time case is unrepresentable.
12673 *shtimer_lock().lock().expect("shtimer poisoned") = Duration::new(0, now.subsec_nanos());
12674 return;
12675 }
12676 *shtimer_lock().lock().expect("shtimer poisoned") =
12677 Duration::new(new_sec as u64, now.subsec_nanos());
12678}
12679
12680/// Port of `floatsecondsgetfn()` from `Src/params.c:4591`. C body:
12681/// `return (double)(now-tv_sec - shtimer.tv_sec) + nsec/1e9;`
12682/// WARNING: param names don't match C — Rust=() vs C=(pm)
12683pub fn floatsecondsgetfn() -> f64 {
12684 // Read shtimer BEFORE `now`, forcing its lazy init first — intsecondsgetfn
12685 // (above) documents the same ordering requirement: with the opposite order
12686 // a first call can observe `now < shtimer` and go backwards.
12687 let timer = *shtimer_lock().lock().expect("shtimer poisoned");
12688 let now = SystemTime::now()
12689 .duration_since(UNIX_EPOCH)
12690 .unwrap_or_default();
12691 // c:4595-4596 — `(double)(now.tv_sec - shtimer.tv_sec) +
12692 // (double)(now.tv_nsec - shtimer.tv_nsec) / 1000000000.0`
12693 // C subtracts the two fields SEPARATELY and SIGNED, so a `now` behind
12694 // `shtimer` simply yields a negative reading. This was
12695 // `(now - timer).as_secs_f64()`, and Duration - Duration PANICS on
12696 // underflow rather than going negative — `overflow when subtracting
12697 // durations`. It had never fired because the float type was unreachable:
12698 // `typeset -F SECONDS` was silently ignored (bin_typeset never called
12699 // setsecondstype), so nothing ever selected this getter. Wiring that up
12700 // turned a silent no-op into a crash, which is how this surfaced.
12701 (now.as_secs() as f64 - timer.as_secs() as f64)
12702 + (now.subsec_nanos() as f64 - timer.subsec_nanos() as f64) / 1_000_000_000.0
12703}
12704
12705/// Port of `floatsecondssetfn()` from `Src/params.c:4603`. C body:
12706/// `shtimer.tv_sec = now.tv_sec - (zlong)x; shtimer.tv_nsec = now.tv_nsec - (x-int)*1e9;`
12707/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
12708pub fn floatsecondssetfn(x: f64) {
12709 let now = SystemTime::now()
12710 .duration_since(UNIX_EPOCH)
12711 .unwrap_or_default();
12712 let new = now
12713 .checked_sub(Duration::from_secs_f64(x))
12714 .unwrap_or_default();
12715 *shtimer_lock().lock().expect("shtimer poisoned") = new;
12716}
12717
12718/// Port of `getrawseconds()` from `Src/params.c:4615`. C body:
12719/// `return (double)shtimer.tv_sec + (double)shtimer.tv_nsec / 1e9;`
12720pub fn getrawseconds() -> f64 {
12721 shtimer_lock()
12722 .lock()
12723 .expect("shtimer poisoned")
12724 .as_secs_f64()
12725}
12726
12727/// Port of `setrawseconds()` from `Src/params.c:4622`. C body:
12728/// `shtimer.tv_sec = (zlong)x; shtimer.tv_nsec = (x-int)*1e9;`
12729pub fn setrawseconds(x: f64) {
12730 *shtimer_lock().lock().expect("shtimer poisoned") = Duration::from_secs_f64(x);
12731}
12732
12733/// Port of `setsecondstype()` from `Src/params.c:4630`. C body
12734/// flips the `gsu.f`/`gsu.i` callback pointer based on the new
12735/// param-flag bitset.
12736///
12737/// WARNING: zshrs has no Param/GSU dispatch table yet — the
12738/// "promotion between integer/float seconds" logic happens via
12739/// pm->gsu pointer swaps in C. Returns 0 to signal success;
12740/// callers can assume the type change is recorded by the caller's
12741/// own bookkeeping until the GSU table lands.
12742/// WARNING: param names don't match C — Rust=(on, off) vs C=(pm, on, off)
12743pub fn setsecondstype(
12744 // c:4630
12745 pm: &mut param,
12746 on: i32,
12747 off: i32,
12748) -> i32 {
12749 // c:4632 — `int newflags = (pm->flags | on) & ~off`.
12750 let newflags = (pm.node.flags | on) & !off;
12751 // c:4633 — `int tp = PM_TYPE(newflags)`.
12752 let tp = PM_TYPE(newflags as u32);
12753 // c:4635-4638 / 4639-4642 — float vs integer GSU pointer swap.
12754 if tp == PM_EFLOAT || tp == PM_FFLOAT {
12755 // c:4635
12756 // C: `pm->gsu.f = &floatseconds_gsu`. GSU table not yet
12757 // wired in the Rust port; record the type by clearing
12758 // any integer GSU.
12759 pm.gsu_i = None;
12760 // pm.gsu_f = Some(floatseconds_gsu) — pending GSU port.
12761 } else if tp == PM_INTEGER {
12762 // c:4639
12763 // C: `pm->gsu.i = &intseconds_gsu`.
12764 pm.gsu_f = None;
12765 // pm.gsu_i = Some(intseconds_gsu) — pending GSU port.
12766 } else {
12767 return 1; // c:4644
12768 }
12769 pm.node.flags = newflags; // c:4645
12770 0 // c:4646
12771}
12772
12773// -----------------------------------------------------------
12774// $USERNAME
12775// -----------------------------------------------------------
12776
12777/// Port of `usernamegetfn()` from `Src/params.c:4653`. C body:
12778/// Port of `usernamegetfn(UNUSED(Param pm))` from Src/params.c:4655.
12779/// C body: `return get_username();`. C's `get_username()`
12780/// (Src/utils.c:1075) walks `getuid() != cached_uid` and
12781/// refreshes the cache via `getpwuid()` on mismatch — so a
12782/// USERNAME read AFTER an `setuid()` call sees the NEW
12783/// username, not the stale cache.
12784///
12785/// The previous Rust port returned `cached_username_lock()`
12786/// directly without the refresh, so a script that called
12787/// setuid(3) (or USER changed externally via setuid binary)
12788/// would keep returning the old username.
12789///
12790pub fn usernamegetfn(_pm: ¶m) -> String {
12791 // c:4655
12792 // c:4658 — `return get_username();`. Route through the
12793 // canonical refresh-on-uid-change accessor at utils.rs.
12794 get_username() // c:4658
12795}
12796
12797/// Port of `usernamesetfn()` from `Src/params.c:4662`. C body:
12798/// `getpwnam(x); setgid; setuid; cached_uid = pswd->pw_uid;`
12799///
12800/// WARNING: the SUID-changing path requires getpwnam(3) which
12801/// crosses an unsafe FFI boundary not yet wrapped here. The
12802/// cached-name update is performed; uid/gid changes still need
12803/// porting of the `pwd.h` getpwnam wrapper.
12804pub fn usernamesetfn(_pm: &mut param, x: String) {
12805 // c:4662
12806 // c:4662 — `if (x && (pswd = getpwnam(x)) && pswd->pw_uid != cached_uid)`.
12807 let target = std::ffi::CString::new(x.as_bytes()).ok();
12808 if let Some(cstr) = target {
12809 unsafe {
12810 let pwd = libc::getpwnam(cstr.as_ptr()); // c:4666
12811 if !pwd.is_null() {
12812 // c:4666 — C reads `cached_uid` (a global initialized
12813 // to `getuid()` at init.c:1219 — the REAL uid, NOT
12814 // the effective one). The previous Rust port used
12815 // `geteuid()` which diverges when running setuid
12816 // (geteuid != getuid) — the shell would erroneously
12817 // try to change to a uid it's already at, or skip
12818 // a needed change. Match C exactly: use `getuid()`.
12819 let cached_uid = libc::getuid(); // c:4666 cached_uid = getuid()
12820 if (*pwd).pw_uid != cached_uid {
12821 // c:4666
12822 // c:4670-4672 — initgroups(x, pswd->pw_gid).
12823 let _ = libc::initgroups(cstr.as_ptr(), (*pwd).pw_gid as _);
12824 // c:4671 — setgid(pswd->pw_gid).
12825 if libc::setgid((*pwd).pw_gid) != 0 {
12826 // c:4673
12827 zwarn(&format!(
12828 "failed to change group ID: {}",
12829 crate::ported::utils::zsh_errno_msg(
12830 std::io::Error::last_os_error().raw_os_error().unwrap_or(0),
12831 )
12832 ));
12833 } else if libc::setuid((*pwd).pw_uid) != 0 {
12834 // c:4675
12835 // c:4675-4676 — setuid failed.
12836 zwarn(&format!(
12837 "failed to change user ID: {}",
12838 crate::ported::utils::zsh_errno_msg(
12839 std::io::Error::last_os_error().raw_os_error().unwrap_or(0),
12840 )
12841 ));
12842 } else {
12843 // c:4677-4681 — cache update.
12844 let name_cstr = std::ffi::CStr::from_ptr((*pwd).pw_name);
12845 let name_str = name_cstr.to_string_lossy().to_string();
12846 *cached_username_lock().lock().expect("username poisoned") =
12847 ztrdup_metafy(&name_str);
12848 }
12849 }
12850 }
12851 }
12852 }
12853 // c:4683 — `zsfree(x)`; Rust drop handles it.
12854 drop(x);
12855}
12856
12857// -----------------------------------------------------------
12858// libc-backed callbacks (UID/GID/EUID/EGID/errno/RANDOM/TTYIDLE).
12859// -----------------------------------------------------------
12860
12861/// Port of `uidgetfn()` from `Src/params.c:4689`. C body:
12862/// `return getuid();`
12863/// WARNING: param names don't match C — Rust=() vs C=(pm)
12864pub fn uidgetfn() -> i64 {
12865 unsafe { libc::getuid() as i64 }
12866}
12867
12868// `termflags` from Src/init.c — bitmap of terminal-state flags. Set
12869// from term_reinit_from_pm and consulted by ZLE before first paint.
12870/// `TERMFLAGS` static.
12871/// Starts as TERM_UNKNOWN (0x02) — c:Src/init.c:1103 `termflags =
12872/// TERM_UNKNOWN;` in init_setup. Cleared by init_term() on success
12873/// (c:Src/init.c:802-803); promptexpand/zleread lazily call
12874/// init_term() when the bit is still set (c:Src/prompt.c:189-190,
12875/// c:Src/Zle/zle_main.c:1260-1261).
12876pub static TERMFLAGS: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0x02);
12877// `TERM_UNKNOWN` re-exported from canonical zsh_h.rs (port of
12878// `Src/zsh.h:1986`). The local declaration here had the value
12879// `1 << 0 = 0x01` — which is C's TERM_BAD (Src/zsh.h:1985), NOT
12880// TERM_UNKNOWN. The canonical TERM_UNKNOWN value is 0x02.
12881//
12882// Callers reading `crate::ported::params::TERM_UNKNOWN` got the
12883// TERM_BAD bit; the params.rs term-init path fired
12884// `TERMFLAGS.fetch_or(TERM_UNKNOWN)` which actually set TERM_BAD,
12885// while the prompt.rs guard at line 441 imported the correct
12886// (0x02) value from zsh_h.rs — so the two paths disagreed silently
12887// about which bit means "unknown terminal".
12888
12889/// Port of `uidsetfn()` from `Src/params.c:4698`. C body:
12890/// `if (setuid((uid_t)x)) zerr("failed to change user ID: %e", errno);`
12891/// C body (2 lines):
12892/// `if (setuid((uid_t)x)) zerr("failed to change user ID: %e", errno);`
12893/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
12894pub fn uidsetfn(x: i64) {
12895 // c:4698
12896 if unsafe { libc::setuid(x as libc::uid_t) } != 0 {
12897 // c:4701 — `zerr("failed to change user ID: %e", errno)`.
12898 // C's `%e` formatter consumes errno and prints
12899 // `strerror(errno)` with the system's casing (typically
12900 // lowercase on macOS/Linux). Rust's
12901 // `std::io::Error::last_os_error()` displays the same
12902 // text but with capital first letter + `(os error N)`
12903 // suffix, diverging from zsh. Mirror the C format by
12904 // calling strerror via libc directly. Bug #254 in
12905 // docs/BUGS.md.
12906 let errno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
12907 let msg = unsafe {
12908 let p = libc::strerror(errno);
12909 std::ffi::CStr::from_ptr(p).to_string_lossy().into_owned()
12910 };
12911 zerr(&format!("failed to change user ID: {}", msg.to_lowercase())); // c:4702
12912 }
12913}
12914
12915/// Port of `euidgetfn()` from `Src/params.c:4710`. C body:
12916/// `return geteuid();`
12917/// WARNING: param names don't match C — Rust=() vs C=(pm)
12918pub fn euidgetfn() -> i64 {
12919 unsafe { libc::geteuid() as i64 }
12920}
12921
12922/// Port of `euidsetfn()` from `Src/params.c:4719`. C body:
12923/// `if (seteuid((uid_t)x)) zerr("failed to change effective user ID: %e", errno);`
12924/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
12925pub fn euidsetfn(x: i64) {
12926 // c:4719
12927 if unsafe { libc::seteuid(x as libc::uid_t) } != 0 {
12928 // c:4722 — strerror format to match C zerr's `%e`. See
12929 // uidsetfn above.
12930 let errno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
12931 let msg = unsafe {
12932 let p = libc::strerror(errno);
12933 std::ffi::CStr::from_ptr(p).to_string_lossy().into_owned()
12934 };
12935 zerr(&format!(
12936 "failed to change effective user ID: {}",
12937 msg.to_lowercase()
12938 )); // c:4723
12939 }
12940}
12941
12942/// Port of `gidgetfn()` from `Src/params.c:4731`. C body: `return getgid();`
12943/// WARNING: param names don't match C — Rust=() vs C=(pm)
12944pub fn gidgetfn() -> i64 {
12945 unsafe { libc::getgid() as i64 }
12946}
12947
12948/// Port of `gidsetfn()` from `Src/params.c:4740`. C body:
12949/// `if (setgid((gid_t)x)) zerr("failed to change group ID: %e", errno);`
12950/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
12951pub fn gidsetfn(x: i64) {
12952 // c:4740
12953 if unsafe { libc::setgid(x as libc::gid_t) } != 0 {
12954 // c:4743 — strerror format to match C zerr's `%e`. See
12955 // uidsetfn above.
12956 let errno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
12957 let msg = unsafe {
12958 let p = libc::strerror(errno);
12959 std::ffi::CStr::from_ptr(p).to_string_lossy().into_owned()
12960 };
12961 zerr(&format!(
12962 "failed to change group ID: {}",
12963 msg.to_lowercase()
12964 )); // c:4744
12965 }
12966}
12967
12968/// Port of `egidgetfn()` from `Src/params.c:4752`. C body: `return getegid();`
12969/// WARNING: param names don't match C — Rust=() vs C=(pm)
12970pub fn egidgetfn() -> i64 {
12971 unsafe { libc::getegid() as i64 }
12972}
12973
12974/// Port of `egidsetfn()` from `Src/params.c:4761`. C body:
12975/// `if (setegid((gid_t)x)) zerr("failed to change effective group ID: %e", errno);`
12976/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
12977pub fn egidsetfn(x: i64) {
12978 // c:4761
12979 if unsafe { libc::setegid(x as libc::gid_t) } != 0 {
12980 // c:4764 — strerror format to match C zerr's `%e`. See
12981 // uidsetfn above.
12982 let errno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
12983 let msg = unsafe {
12984 let p = libc::strerror(errno);
12985 std::ffi::CStr::from_ptr(p).to_string_lossy().into_owned()
12986 };
12987 zerr(&format!(
12988 "failed to change effective group ID: {}",
12989 msg.to_lowercase()
12990 )); // c:4765
12991 }
12992}
12993
12994/// Port of `ttyidlegetfn()` from `Src/params.c:4771`. C body:
12995/// ```c
12996/// struct stat ttystat;
12997/// if (SHTTY == -1 || fstat(SHTTY, &ttystat)) return -1;
12998/// return time(NULL) - ttystat.st_atime;
12999/// ```
13000/// Rust port reads stdin (fd 0) — closest match to `SHTTY` the
13001/// shell tracks as the controlling-tty fd. Returns -1 if stdin is
13002/// not a tty.
13003/// WARNING: param names don't match C — Rust=() vs C=(pm)
13004pub fn ttyidlegetfn() -> i64 {
13005 // c:4776 — `if (SHTTY == -1 || fstat(SHTTY, &ttystat)) return -1;`
13006 // The previous Rust port hardcoded fd 0 (stdin) which is wrong
13007 // when SHTTY was opened on a non-stdin file descriptor (e.g.
13008 // `zsh < script` where stdin is a file but the controlling tty
13009 // was opened separately). C tracks the actual SHTTY fd.
13010 let shtty = SHTTY.load(Ordering::SeqCst);
13011 if shtty == -1 {
13012 // c:4776
13013 return -1;
13014 }
13015 let mut st: libc::stat = unsafe { std::mem::zeroed() };
13016 if unsafe { libc::fstat(shtty, &mut st) } != 0 {
13017 // c:4776
13018 return -1;
13019 }
13020 let now = SystemTime::now()
13021 .duration_since(UNIX_EPOCH)
13022 .unwrap_or_default()
13023 .as_secs() as i64;
13024 now - st.st_atime as i64 // c:4779
13025}
13026
13027// -----------------------------------------------------------
13028// $IFS / $HOME / $TERM / $WORDCHARS / $TERMINFO / $TERMINFO_DIRS
13029// $KEYBOARD_HACK / $HISTCHARS / $_ — string-state callbacks.
13030// -----------------------------------------------------------
13031
13032/// Port of `ifsgetfn()` from `Src/params.c:4784`. C body: `return ifs;`
13033pub fn ifsgetfn(_pm: ¶m) -> String {
13034 // c:4787 `return ifs;` — NULL (unset) reads as the empty string.
13035 ifs_lock().lock().expect("ifs poisoned").clone().unwrap_or_default()
13036}
13037
13038/// Port of `ifssetfn()` from `Src/params.c:4793`. C body:
13039/// `zsfree(ifs); ifs = x; inittyptab();`
13040pub fn ifssetfn(_pm: &mut param, x: String) {
13041 *ifs_lock().lock().expect("ifs poisoned") = Some(x);
13042 // c:4795 — `inittyptab()` rebuilds the typtab[] ISEP/IWSEP bits
13043 // from the new IFS. Without this, every word-split path stays
13044 // pinned to the old separator set and silently mis-splits.
13045 inittyptab();
13046}
13047
13048// -----------------------------------------------------------
13049// Locale callbacks: $LANG, $LC_*, setlang
13050// -----------------------------------------------------------
13051
13052/// Port of `clear_mbstate()` from `Src/params.c:4831`. C body:
13053/// `mb_charinit(); clear_shiftstate();`
13054///
13055/// WARNING: zshrs uses Rust's UTF-8 native handling so multibyte
13056/// state machines aren't kept; this is a no-op pinned to the
13057/// C name for parity.
13058/// (under `MULTIBYTE_SUPPORT`):
13059/// ```c
13060/// mb_charinit(); /* utils.c */
13061/// clear_shiftstate(); /* pattern.c */
13062/// ```
13063/// Resets the mbstate_t globals after LC_CTYPE changes (NetBSD-9
13064/// requires this). Rust port forwards to the matching helpers.
13065pub fn clear_mbstate() {
13066 // c:Src/params.c:4732+ — `#ifdef MULTIBYTE_SUPPORT
13067 // mb_charinit(); /* utils.c */
13068 // clear_shiftstate(); /* pattern.c */
13069 // #endif`
13070 // Both helpers are ported (utils.rs:526 and pattern.rs:190). The
13071 // pattern.rs version is currently a no-op (shiftstate machine
13072 // not stored) and utils.rs::mb_charinit resets the mbstate_t
13073 // tracking. Wire them through so a future locale-change hook
13074 // routes through this one entry point per c:Src/params.c
13075 // setlang(c:4842) which calls clear_mbstate() between setlocale
13076 // and the per-LC_* re-apply loop.
13077 crate::ported::utils::mb_charinit(); // c:utils.c mb_charinit
13078 crate::ported::pattern::clear_shiftstate(); // c:pattern.c:327 clear_shiftstate
13079}
13080
13081/// Port of `static struct localename lc_names[]` from `Src/params.c:4805-4825`.
13082/// C body:
13083/// ```c
13084/// static struct localename {
13085/// char *name;
13086/// int category;
13087/// } lc_names[] = {
13088/// {"LC_COLLATE", LC_COLLATE},
13089/// {"LC_CTYPE", LC_CTYPE},
13090/// {"LC_MESSAGES", LC_MESSAGES},
13091/// {"LC_NUMERIC", LC_NUMERIC},
13092/// {"LC_TIME", LC_TIME},
13093/// {NULL, 0}
13094/// };
13095/// ```
13096///
13097/// The C source guards each entry under `#ifdef LC_*`; libc on
13098/// macOS/Linux defines all five so the Rust port simply lists them.
13099const LC_NAMES: &[(&str, libc::c_int)] = &[
13100 ("LC_COLLATE", libc::LC_COLLATE), // c:4810
13101 ("LC_CTYPE", libc::LC_CTYPE), // c:4813
13102 ("LC_MESSAGES", libc::LC_MESSAGES), // c:4816
13103 ("LC_NUMERIC", libc::LC_NUMERIC), // c:4819
13104 ("LC_TIME", libc::LC_TIME), // c:4822
13105];
13106
13107/// Port of `setlang()` from `Src/params.c:4840`.
13108///
13109/// C body (c:4842-4869):
13110/// ```c
13111/// if ((x2 = getsparam_u("LC_ALL")) && *x2) return;
13112/// setlocale(LC_ALL, x ? unmeta(x) : "");
13113/// clear_mbstate();
13114/// queue_signals();
13115/// for (ln = lc_names; ln->name; ln++)
13116/// if ((x = getsparam_u(ln->name)) && *x)
13117/// setlocale(ln->category, x);
13118/// unqueue_signals();
13119/// inittyptab();
13120/// ```
13121///
13122/// The previous Rust port skipped the actual `setlocale(LC_ALL, ...)`
13123/// libc call and just set the LANG env var. C invokes libc
13124/// setlocale to actually change the program's locale state —
13125/// required so any libc calls during shell execution (e.g.,
13126/// `iswctype`, `mbrtowc`) use the new locale's classification.
13127///
13128/// Also skipped: the per-LC_* override loop (c:4866-4868) which
13129/// re-applies category-specific settings after the global
13130/// LC_ALL set. The Rust port doesn't yet have the lc_names
13131/// table, but we can at least respect the canonical sequence.
13132pub fn setlang(x: Option<&str>) {
13133 // c:4842
13134 // c:4847 — `if ((x2 = getsparam_u("LC_ALL")) && *x2) return;`
13135 if let Some(lc_all) = getsparam_u("LC_ALL") {
13136 // c:4847
13137 if !lc_all.is_empty() {
13138 return;
13139 }
13140 }
13141 // c:4860 — `setlocale(LC_ALL, x ? unmeta(x) : "");`
13142 let locale_arg = match x {
13143 Some(s) => unmeta(s),
13144 None => String::new(),
13145 };
13146 // The previous Rust port skipped the libc setlocale call.
13147 // Without it, libc's locale state (used by iswctype, mbrtowc,
13148 // etc.) stays pinned to whatever the shell inherited from
13149 // its parent — diverging from C which actively changes the
13150 // running program's locale.
13151 let cstr = std::ffi::CString::new(locale_arg.as_bytes()).unwrap_or_default();
13152 unsafe {
13153 libc::setlocale(libc::LC_ALL, cstr.as_ptr()); // c:4860
13154 }
13155 // Mirror to env so subsequent `getsparam("LANG")` reads agree.
13156 if let Some(s) = x {
13157 // c:Src/params.c:5354 — setenv via the C-named zputenv
13158 let _ = zputenv(&format!("{}={}", "LANG", s));
13159 }
13160 clear_mbstate(); // c:4861
13161 // c:4863-4867 — `for (ln = lc_names; ln->name; ln++) if ((x =
13162 // getsparam_u(ln->name)) && *x) setlocale(ln->category, x);`
13163 // After the global LC_ALL setlocale, any explicitly-set LC_*
13164 // category overrides its slot. The previous Rust port skipped
13165 // this loop, so `LC_NUMERIC=tr_TR.UTF-8 LANG=C` would leave
13166 // numeric formatting on C rather than tr_TR.
13167 for (name, category) in LC_NAMES {
13168 // c:4863
13169 if let Some(val) = getsparam_u(name) {
13170 // c:4866 getsparam_u
13171 if !val.is_empty() {
13172 let cat_cstr = std::ffi::CString::new(val.as_bytes()).unwrap_or_default();
13173 unsafe {
13174 libc::setlocale(*category, cat_cstr.as_ptr()); // c:4867
13175 }
13176 }
13177 }
13178 }
13179 // c:4868 — `inittyptab();`. The locale change may shift which
13180 // bytes are isalpha/isalnum/etc under the typtab init, so the
13181 // table must be rebuilt.
13182 inittyptab();
13183}
13184
13185/// Port of `lc_allsetfn()` from `Src/params.c:4871`.
13186///
13187/// C body (c:4873-4894):
13188/// ```c
13189/// strsetfn(pm, x);
13190/// if (!x || !*x) {
13191/// x = getsparam_u("LANG");
13192/// if (x && *x) {
13193/// queue_signals();
13194/// setlang(x);
13195/// unqueue_signals();
13196/// }
13197/// } else {
13198/// setlocale(LC_ALL, unmeta(x));
13199/// clear_mbstate();
13200/// inittyptab();
13201/// }
13202/// ```
13203///
13204/// The previous Rust port for the non-empty case set the env
13205/// var via `env::set_var("LC_ALL", &s)` but skipped THREE
13206/// pieces:
13207/// 1. `setlocale(LC_ALL, unmeta(x))` — actively changes the
13208/// program's locale per c:4890.
13209/// 2. `unmeta(x)` — strips Meta-encoded bytes before passing
13210/// to libc setlocale per c:4890.
13211/// 3. `inittyptab()` — rebuilds the typtab for the new
13212/// LC_CTYPE per c:4892.
13213///
13214pub fn lc_allsetfn(pm: &mut param, x: String) {
13215 // c:4823
13216 // c:4825 — `strsetfn(pm, x);`. `lc_all_gsu`'s getfn is the plain
13217 // `strgetfn` (c:261-262), so the param slot is the only storage this
13218 // value has; skipping the store would make `$LC_ALL` read back empty.
13219 strsetfn(pm, x.clone()); // c:4825
13220 // c:4831 — `if (!x || !*x)`: C's `char *x` is NULL for an unset and ""
13221 // for an empty assignment, and both take the LANG fallback. A GSU setfn
13222 // always receives a String, so the two collapse into `is_empty()`.
13223 match x {
13224 s if s.is_empty() => {
13225 // c:4831
13226 // c:4831-4838 — empty x falls back to setlang(getsparam_u("LANG")).
13227 setlang(getsparam_u("LANG").as_deref()); // c:4832
13228 }
13229 s => {
13230 // c:4889 — `setlocale(LC_ALL, unmeta(x));`
13231 let unmeta = unmeta(&s); // c:4889 unmeta(x)
13232 let cstr = std::ffi::CString::new(unmeta.as_bytes()).unwrap_or_default();
13233 unsafe {
13234 libc::setlocale(libc::LC_ALL, cstr.as_ptr()); // c:4890
13235 }
13236 // c:Src/params.c:5354 — setenv via the C-named zputenv
13237 let _ = zputenv(&format!("{}={}", "LC_ALL", &s));
13238 clear_mbstate(); // c:4891
13239 // c:4892 — `inittyptab();` rebuild typtab for new LC_CTYPE.
13240 inittyptab(); // c:4892
13241 }
13242 }
13243}
13244
13245/// Port of `langsetfn()` from `Src/params.c:4896`. C body:
13246/// `strsetfn(pm, x); setlang(unmeta(x));`
13247///
13248/// `unmeta(x)` strips Meta-encoding before passing to libc
13249/// `setlocale` — locale names are normally ASCII but Meta bytes
13250/// in the assigned value (from a `LANG="$value"` round-trip
13251/// through metafied param storage) would otherwise reach
13252/// setlocale literally. The previous Rust port passed raw `x`
13253/// without unmeta'ing — divergent.
13254///
13255/// `strsetfn(pm, x)` stores the value in the param slot — `lang_gsu`
13256/// pairs this setter with the plain `strgetfn` (c:259-260), so the slot
13257/// is the only storage `$LANG` has.
13258pub fn langsetfn(pm: &mut param, x: String) {
13259 // c:4848
13260 strsetfn(pm, x.clone()); // c:4850
13261 // c:4851 — `setlang(unmeta(x));`. Strip Meta bytes before
13262 // passing to libc setlocale.
13263 let unmeta_x = unmeta(&x); // c:4851 unmeta(x)
13264 setlang(Some(&unmeta_x));
13265}
13266
13267/// Port of `lcsetfn()` from `Src/params.c:4904`. C body
13268/// (c:4912-4931):
13269/// ```c
13270/// strsetfn(pm, x);
13271/// if ((x2 = getsparam("LC_ALL")) && *x2) return;
13272/// queue_signals();
13273/// if (!x || !*x) x = getsparam("LANG");
13274/// if (x && *x) {
13275/// for (ln = lc_names; ln->name; ln++)
13276/// if (!strcmp(ln->name, pm->node.nam))
13277/// setlocale(ln->category, unmeta(x));
13278/// }
13279/// unqueue_signals();
13280/// clear_mbstate();
13281/// inittyptab();
13282/// ```
13283///
13284/// Two divergences in the previous Rust port:
13285/// 1. Missed `inittyptab()` call at c:4932 — LC_CTYPE changes
13286/// shift which bytes are isalpha/iblank/isep, but the
13287/// typtab stayed pinned to the prior locale's classes.
13288/// `setopt POSIX_BUILTINS; LC_NUMERIC=tr_TR.UTF-8; ...`
13289/// would still classify with the old C locale's tables.
13290/// 2. The Meta-unmeta'ing on the value passed to setlocale
13291/// wasn't applied. C uses `setlocale(cat, unmeta(x))`.
13292pub fn lcsetfn(pm: &mut param, x: String) {
13293 // c:4856
13294 // c:4861 — `strsetfn(pm, x);` runs BEFORE the `$LC_ALL` early return, so
13295 // the value is stored even when the locale is not applied.
13296 strsetfn(pm, x.clone()); // c:4861
13297 // c:4876 — `!strcmp(ln->name, pm->node.nam)` matches the CATEGORY by the
13298 // parameter's own name; read it here, after the store's borrow ends.
13299 let pm_name = pm.node.nam.clone(); // c:4876 pm->node.nam
13300 // c:4862-4863 — `if ((x2 = getsparam("LC_ALL")) && *x2) return;`.
13301 if let Some(lc_all) = getsparam("LC_ALL") {
13302 // c:4862
13303 if !lc_all.is_empty() {
13304 return;
13305 }
13306 }
13307 // c:4866-4867 — `if (!x || !*x) x = getsparam("LANG");`.
13308 let val = Some(x)
13309 .filter(|s| !s.is_empty())
13310 .or_else(|| getsparam("LANG").filter(|s| !s.is_empty())); // c:4867
13311 // c:4924-4928 — apply `setlocale(category, unmeta(x))` for the
13312 // matching LC_* category. The previous Rust port skipped the
13313 // actual libc setlocale call and only wrote the env var, so
13314 // assigning `LC_NUMERIC=tr_TR.UTF-8` never flipped libc's
13315 // numeric-formatting category.
13316 if let Some(v) = val {
13317 let unmeta = unmeta(&v); // c:4928 unmeta(x)
13318 // c:Src/params.c:5354 — setenv via the C-named zputenv
13319 let _ = zputenv(&format!("{}={}", pm_name, &unmeta));
13320 for (name, category) in LC_NAMES {
13321 // c:4875
13322 if *name == pm_name {
13323 // c:4876 strcmp
13324 let cstr = std::ffi::CString::new(unmeta.as_bytes()).unwrap_or_default();
13325 unsafe {
13326 libc::setlocale(*category, cstr.as_ptr()); // c:4927
13327 }
13328 break;
13329 }
13330 }
13331 }
13332 // c:4930 — `clear_mbstate();` — LC_CTYPE may have changed.
13333 clear_mbstate();
13334 // c:4931 — `inittyptab();` — rebuild typtab classifications.
13335 // The previous Rust port skipped this; char-classification
13336 // predicates would stay pinned to the prior locale's class
13337 // set even after `LC_CTYPE=` was assigned.
13338 inittyptab(); // c:4931
13339}
13340
13341/// Port of `argzerosetfn()` from `Src/params.c:4937`. Setter for `$0` —
13342/// POSIX mode rejects assignment (read-only), zsh mode replaces
13343/// `argzero`.
13344///
13345/// C body:
13346/// if (x) {
13347/// if (isset(POSIXARGZERO))
13348/// zerr("read-only variable: 0");
13349/// else {
13350/// zsfree(argzero);
13351/// argzero = ztrdup(x);
13352/// }
13353/// zsfree(x);
13354/// }
13355/// Port of `argzerosetfn(UNUSED(Param pm), char *x)` from `Src/params.c:4937`.
13356/// `pm` is UNUSED in C (the `argzero` global is updated regardless of
13357/// the Param the assignment hit), but the parameter is preserved here
13358/// to match the C signature so this fn can wire into the GsuScalar.setfn
13359/// slot directly (see ARGZERO_GSU at line ~8307).
13360pub fn argzerosetfn(_pm: &mut param, x: String) {
13361 // c:4937
13362 // c:4937 — if (x).
13363 if !x.is_empty() {
13364 // c:4940 — isset(POSIXARGZERO) reject.
13365 if isset(POSIXARGZERO) {
13366 zerr("read-only variable: 0"); // c:4941
13367 } else {
13368 // c:4943-4944 — zsfree(argzero); argzero = ztrdup(x).
13369 set_argzero(Some(ztrdup(&x)));
13370 }
13371 // c:4946 — `zsfree(x)`. Rust drop handles via move.
13372 }
13373}
13374
13375// -----------------------------------------------------------
13376// $0 / $#
13377// -----------------------------------------------------------
13378
13379/// Port of `argzerogetfn()` from `Src/params.c:4954`. C body:
13380/// `return isset(POSIXARGZERO) ? posixzero : argzero;`
13381///
13382/// Both `argzero` and `posixzero` live in `utils.rs` (OnceLock storage).
13383/// After `exec -a foo` or function-call argv-rewrite, `$0` under
13384/// POSIXARGZERO reports the ORIGINAL startup `argv[0]`, not the
13385/// rewritten name. `pm` is UNUSED in C; signature preserved for the
13386/// GsuScalar.getfn slot at ARGZERO_GSU (line ~8307).
13387pub fn argzerogetfn(_pm: ¶m) -> String {
13388 if isset(POSIXARGZERO) {
13389 // c:4958
13390 posixzero().unwrap_or_default() // c:4959
13391 } else {
13392 argzero().unwrap_or_default() // c:4960
13393 }
13394}
13395
13396// -----------------------------------------------------------
13397// $HISTSIZE / $SAVEHIST
13398// -----------------------------------------------------------
13399
13400/// Port of `histsizegetfn()` from `Src/params.c:4965`. C body: `return histsiz;`
13401/// WARNING: param names don't match C — Rust=() vs C=(pm)
13402pub fn histsizegetfn() -> i64 {
13403 *histsiz_lock().lock().expect("histsiz poisoned")
13404}
13405
13406/// Port of `histsizesetfn()` from `Src/params.c:4974`. C body:
13407/// `if ((histsiz = v) < 1) histsiz = 1; resizehistents();`
13408///
13409/// The previous Rust port noted `resizehistents()` as "pending the
13410/// history-table port", but `resizehistents`
13411/// IS available — was a stale comment. Without the resize call,
13412/// setting HISTSIZE to a smaller value left the in-memory ring
13413/// over-sized until the next implicit prune (next entry added).
13414/// Wired the call now per c:4977.
13415/// WARNING: param names don't match C — Rust=(v) vs C=(pm, v)
13416pub fn histsizesetfn(v: i64) {
13417 *histsiz_lock().lock().expect("histsiz poisoned") = v.max(1);
13418 // c:4977 — mirror into the hist.rs atomic so resizehistents()
13419 // sees the new size, then trigger the prune.
13420 histsiz.store(v.max(1), Ordering::SeqCst);
13421 resizehistents(); // c:4977
13422}
13423
13424/// Port of `savehistsizegetfn()` from `Src/params.c:4985`. C body:
13425/// `return savehistsiz;`
13426/// WARNING: param names don't match C — Rust=() vs C=(pm)
13427pub fn savehistsizegetfn() -> i64 {
13428 *savehistsiz_lock().lock().expect("savehistsiz poisoned")
13429}
13430
13431/// Port of `savehistsizesetfn()` from `Src/params.c:4994`. C body:
13432/// `if ((savehistsiz = v) < 0) savehistsiz = 0;`
13433///
13434/// The Rust port has TWO mirrors of `savehistsiz`: a `Mutex<i64>`
13435/// in params.rs (read by `savehistsizegetfn`) AND an AtomicI64
13436/// in hist.rs (read by the history-file writer at
13437/// `Src/hist.c:savehistfile` per c:3878). The previous Rust port
13438/// only wrote to the params.rs lock; `hist.rs::savehistsiz`
13439/// stayed pinned to its initial 0 value, so `SAVEHIST=10000`
13440/// would store the limit in `savehistsiz_lock` (visible to
13441/// `$SAVEHIST` reads) but the history-file writer would still
13442/// cap at the original AtomicI64 value (effectively saving zero
13443/// lines). Sync both storages so reads + writes agree.
13444///
13445/// WARNING: param names don't match C — Rust=(v) vs C=(pm, v)
13446pub fn savehistsizesetfn(v: i64) {
13447 // c:4994
13448 let clamped = v.max(0); // c:4998
13449 *savehistsiz_lock().lock().expect("savehistsiz poisoned") = clamped;
13450 // Mirror to hist.rs::savehistsiz so the writer-side cap
13451 // matches the just-assigned value. C uses a single global;
13452 // the Rust port's twin-storage requires sync writes.
13453 savehistsiz.store(clamped, Ordering::SeqCst);
13454 // c:4994
13455}
13456
13457/// Port of `errnosetfn()` from `Src/params.c:5004`. C body:
13458/// `errno = (int)x; if ((zlong)errno != x) zwarn("errno truncated on assignment");`
13459///
13460/// Rust note: `errno` is a libc thread-local; Rust uses `std::io::Error`
13461/// which captures the *last* call. To set errno for subsequent
13462/// `last_os_error()` reads on macOS / Linux, write through the libc
13463/// `__error()`/`__errno_location()` accessor.
13464/// C body (Src/params.c:5004):
13465/// `errno = (int)x;
13466/// if ((zlong)errno != x) zwarn("errno truncated on assignment");`
13467/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
13468pub fn errnosetfn(x: i64) {
13469 // c:5004
13470 let truncated = x as i32;
13471 unsafe {
13472 *errno_ptr() = truncated;
13473 } // c:5006 errno = (int)x
13474 // c:5009-5010 — C uses `zwarn` (informational), NOT `zerr`. The
13475 // store happens unconditionally; the warning fires only on
13476 // truncation. Previously used `zerr` — divergent.
13477 if truncated as i64 != x {
13478 // c:5008
13479 zwarn("errno truncated on assignment"); // c:5009
13480 }
13481}
13482
13483/// !!! WARNING: RUST-ONLY HELPER !!!
13484/// No C counterpart: C reads `errno` through the `<errno.h>` macro, which the
13485/// compiler expands to the per-platform getter. Rust's libc exposes both spellings
13486/// (`__error()` on macOS, `__errno_location()` on Linux) as raw FFI, so this shim
13487/// picks the right one per target and keeps `errnogetfn`/`errnosetfn` one-liners.
13488#[inline]
13489unsafe fn errno_ptr() -> *mut libc::c_int {
13490 #[cfg(target_os = "macos")]
13491 {
13492 libc::__error()
13493 }
13494 #[cfg(target_os = "linux")]
13495 {
13496 libc::__errno_location()
13497 }
13498 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
13499 {
13500 std::ptr::null_mut()
13501 }
13502}
13503
13504/// Port of `errnogetfn()` from `Src/params.c:5015`. C body: `return errno;`
13505///
13506/// Reads the libc errno directly through the per-platform accessor
13507/// (matching C's `return errno;` semantics).
13508/// WARNING: param names don't match C — Rust=() vs C=(pm)
13509pub fn errnogetfn() -> i64 {
13510 let p = unsafe { errno_ptr() }; // c:5017 return errno
13511 if p.is_null() {
13512 // Non-Linux/macOS fallback: best-effort via std API.
13513 std::io::Error::last_os_error().raw_os_error().unwrap_or(0) as i64
13514 } else {
13515 unsafe { *p as i64 }
13516 }
13517}
13518
13519/// Port of `keyboardhackgetfn()` from `Src/params.c:5024`. C body:
13520/// `static char buf[2]; buf[0] = keyboardhackchar; return buf;`
13521pub fn keyboardhackgetfn(_pm: ¶m) -> String {
13522 let c = *keyboardhack_lock().lock().expect("keyboardhack poisoned");
13523 if c == 0 {
13524 String::new()
13525 } else {
13526 (c as char).to_string()
13527 }
13528}
13529
13530/// Port of `keyboardhacksetfn()` from `Src/params.c:5038`. C body:
13531/// ```c
13532/// if (x) {
13533/// unmetafy(x, &len);
13534/// if (len > 1) { len = 1; zwarn("Only one KEYBOARD_HACK character can be defined"); }
13535/// for (i = 0; i < len; i++)
13536/// if (!isascii((unsigned char) x[i])) {
13537/// zwarn("KEYBOARD_HACK can only contain ASCII characters");
13538/// return;
13539/// }
13540/// keyboardhackchar = len ? (unsigned char) x[0] : '\0';
13541/// } else
13542/// keyboardhackchar = '\0';
13543/// ```
13544///
13545/// The C source `unmetafy(x, &len)` strips Meta-encoded prefix
13546/// bytes (collapsing every `Meta + (b^32)` pair to the original
13547/// byte) BEFORE the length and ASCII checks. The previous Rust
13548/// port skipped unmetafy, so:
13549/// - `len > 1` warning fired on every assignment of a Meta-
13550/// encoded single byte (the byte-length was 2 pre-unmetafy).
13551/// - ASCII check ran against the raw Meta byte (0x83) instead
13552/// of the demetafied result, falsely rejecting valid ASCII
13553/// characters that happened to round-trip through Meta
13554/// encoding in the assignment pipeline.
13555///
13556pub fn keyboardhacksetfn(_pm: &mut param, x: String) {
13557 // c:5040
13558 // c:5044 — `unmetafy(x, &len)` — strip Meta-encoded pairs.
13559 // Run on the byte buffer so the protocol matches C's pointer
13560 // walk; the Rust `unmeta()` helper does the same fold.
13561 let unmeta = unmeta(&x); // c:5044 unmetafy(x)
13562 let bytes = unmeta.as_bytes();
13563 // c:5046-5049 — `if (len > 1) { len = 1; zwarn(...); }`. The
13564 // length check happens AFTER unmetafy so a 2-byte Meta pair
13565 // representing a single byte doesn't trigger the warning.
13566 if bytes.len() > 1 {
13567 zwarn("Only one KEYBOARD_HACK character can be defined");
13568 }
13569 let c = bytes.first().copied().unwrap_or(0);
13570 // c:5050-5054 — ASCII check runs on the unmetafied byte, NOT
13571 // the raw Meta byte. With unmetafy now in place this works as
13572 // C intended.
13573 if c >= 0x80 {
13574 // c:5051 !isascii(...)
13575 zwarn("KEYBOARD_HACK can only contain ASCII characters");
13576 return;
13577 }
13578 // c:5056 — `keyboardhackchar = len ? (unsigned char) x[0] : '\0';`
13579 *keyboardhack_lock().lock().expect("keyboardhack poisoned") = c;
13580}
13581
13582/// Port of `histcharsgetfn()` from `Src/params.c:5064`. C body:
13583/// ```c
13584/// static char buf[4];
13585/// buf[0] = bangchar; buf[1] = hatchar; buf[2] = hashchar; buf[3] = '\0';
13586/// return buf;
13587/// ```
13588/// Reads from the three canonical atomic globals
13589/// (`crate::ported::hist::{bangchar, hatchar, hashchar}`) to mirror C
13590/// which reads from three separate `unsigned char` globals.
13591pub fn histcharsgetfn(_pm: ¶m) -> String {
13592 let b = bangchar.load(Ordering::SeqCst) as u8;
13593 let h = hatchar.load(Ordering::SeqCst) as u8;
13594 let p = hashchar.load(Ordering::SeqCst) as u8;
13595 // c:5068-5073 — terminal NUL trims unset chars (default-`!^#` is
13596 // 3 non-NUL bytes); explicit NULs are skipped to match C `buf[3]
13597 // = '\0'` C-string truncation semantics.
13598 let mut s = String::new();
13599 for &byte in &[b, h, p] {
13600 if byte != 0 {
13601 s.push(byte as char);
13602 }
13603 }
13604 s
13605}
13606
13607/// Port of `histcharssetfn()` from `Src/params.c:5079`. C body
13608/// validates ASCII, takes up to 3 chars; defaults `!^#` if NULL.
13609///
13610/// C `unmetafy(x, &len)` (c:5086) strips Meta-encoded pairs BEFORE
13611/// the length truncation and ASCII guard. The previous Rust port
13612/// skipped unmetafy entirely:
13613/// - `len > 3` truncation ran on raw byte length, so a Meta-pair
13614/// would inflate the byte count and skip valid chars.
13615/// - ASCII check ran against raw Meta bytes (0x83), falsely
13616/// rejecting valid round-tripped values.
13617///
13618pub fn histcharssetfn(_pm: &mut param, x: String) {
13619 // c:5081
13620 // C signature is `histcharssetfn(Param pm, char *x)`. The `x == NULL`
13621 // arm (c:5100-5103, `bangchar = '!'; hashchar = '#'; hatchar = '^'`)
13622 // has no caller: the gsu is `{ histcharsgetfn, histcharssetfn,
13623 // stdunsetfn }` (c:238-239), so `unset histchars` never reaches it, and
13624 // every write, including scanendscope's restore, hands over a string.
13625 // An EMPTY string is not NULL: `len` is 0, so all three characters
13626 // become '\0' (c:5095-5097) and `$histchars` reads empty, which is what
13627 // `histchars=` and `local +h histchars` (c:Src/builtin.c:2617
13628 // `setfn(pm, ztrdup(""))`) produce in zsh. Treating "" as NULL restored
13629 // `!^#` instead.
13630 let new_chars: [u8; 3] = {
13631 let s = x;
13632 {
13633 // c:5086 — `unmetafy(x, &len)`. Strip Meta pairs first.
13634 let unmeta = unmeta(&s); // c:5086 unmetafy(x)
13635 let bytes = unmeta.as_bytes();
13636 // c:5087-5088 — `if (len > 3) len = 3;`. Truncation
13637 // applies AFTER unmetafy.
13638 let bytes = if bytes.len() > 3 { &bytes[..3] } else { bytes };
13639 for &b in bytes.iter() {
13640 if b >= 0x80 {
13641 // c:5090-5093
13642 // c:5091 — C uses `zwarn` (informational), NOT
13643 // `zerr` (fatal). Function returns early without
13644 // updating any globals.
13645 zwarn("HISTCHARS can only contain ASCII characters");
13646 return;
13647 }
13648 }
13649 // c:5095-5097 — `bangchar = x[0]; hatchar = x[1]; hashchar = x[2]`.
13650 // C uses `len ? x[0] : '\0'` etc — for short strings the
13651 // unset bytes are NUL.
13652 let mut chars = [0u8; 3];
13653 for (i, &b) in bytes.iter().enumerate() {
13654 chars[i] = b;
13655 }
13656 chars
13657 }
13658 };
13659 // c:5079 — set histchars table.
13660 *histchars_lock().lock().expect("histchars poisoned") = new_chars;
13661 // c:5095-5097 — `bangchar = x[0]; hatchar = x[1]; hashchar = x[2]`.
13662 // Sync all three per-char atomic globals so lex/hist callers
13663 // see the new HISTCHARS. (Previously hashchar was a `const char`
13664 // in lex.rs — promoted to atomic this iteration.)
13665 bangchar.store(new_chars[0] as i32, Ordering::SeqCst);
13666 hatchar.store(new_chars[1] as i32, Ordering::SeqCst);
13667 hashchar.store(new_chars[2] as i32, Ordering::SeqCst);
13668 // c:5104 — `inittyptab();`. The bangchar special bit in typtab
13669 // depends on the current `bangchar` global; reseed.
13670 inittyptab();
13671}
13672
13673// ---------------------------------------------------------------------
13674// Special-scalar GSU vtables — port of `Src/params.c:217-256` `stdscalar_gsu`,
13675// `home_gsu`, `ifs_gsu`, etc. Each entry pairs the canonical
13676// getfn/setfn/unsetfn for a PM_SPECIAL scalar. `createparamtable`
13677// copies the matching gsu into each special param's `gsu_s` slot so
13678// `assignstrvalue` dispatches via `pm->gsu.s->setfn(pm, val)`
13679// (params.c:2748) exactly like C.
13680// ---------------------------------------------------------------------
13681
13682/// Port of `static const struct gsu_scalar argzero_gsu` from `Src/params.c:225-226`.
13683/// `{ argzerogetfn, argzerosetfn, nullunsetfn }`. Both functions' Rust
13684/// signatures match C exactly (with `pm` UNUSED), so they wire into
13685/// the GSU vtable directly — no Rust-only wrappers. The $0 Param picks
13686/// this up at `add_special` (line ~1555) and `assignsparam` routes
13687/// `0=value` through the canonical setter via the gsu_s.setfn dispatch
13688/// at params.rs:3767.
13689pub const ARGZERO_GSU: gsu_scalar = gsu_scalar {
13690 // c:225-226
13691 getfn: argzerogetfn,
13692 setfn: argzerosetfn,
13693 unsetfn: nullunsetfn,
13694};
13695
13696/// Port of `static const struct gsu_scalar home_gsu` from `Src/params.c:248`.
13697pub const HOME_GSU: gsu_scalar = gsu_scalar {
13698 // c:248
13699 getfn: homegetfn,
13700 setfn: homesetfn,
13701 unsetfn: stdunsetfn,
13702};
13703
13704/// Port of `static const struct gsu_scalar ifs_gsu` from `Src/params.c:245`.
13705pub const IFS_GSU: gsu_scalar = gsu_scalar {
13706 // c:245
13707 getfn: ifsgetfn,
13708 setfn: ifssetfn,
13709 unsetfn: stdunsetfn,
13710};
13711
13712/// Port of `static const struct gsu_scalar term_gsu` from `Src/params.c:250`.
13713pub const TERM_GSU: gsu_scalar = gsu_scalar {
13714 // c:250
13715 getfn: termgetfn,
13716 setfn: termsetfn,
13717 unsetfn: stdunsetfn,
13718};
13719
13720/// Port of `static const struct gsu_scalar terminfo_gsu` from `Src/params.c:251`.
13721pub const TERMINFO_GSU: gsu_scalar = gsu_scalar {
13722 // c:251
13723 getfn: terminfogetfn,
13724 setfn: terminfosetfn,
13725 unsetfn: stdunsetfn,
13726};
13727
13728/// Port of `static const struct gsu_scalar terminfodirs_gsu`
13729/// from `Src/params.c:252`.
13730pub const TERMINFODIRS_GSU: gsu_scalar = gsu_scalar {
13731 // c:252
13732 getfn: terminfodirsgetfn,
13733 setfn: terminfodirssetfn,
13734 unsetfn: stdunsetfn,
13735};
13736
13737/// Port of `static const struct gsu_scalar wordchars_gsu`
13738/// from `Src/params.c:249`.
13739pub const WORDCHARS_GSU: gsu_scalar = gsu_scalar {
13740 // c:249
13741 getfn: wordcharsgetfn,
13742 setfn: wordcharssetfn,
13743 unsetfn: stdunsetfn,
13744};
13745
13746/// Port of `static const struct gsu_scalar username_gsu`
13747/// from `Src/params.c:247`.
13748pub const USERNAME_GSU: gsu_scalar = gsu_scalar {
13749 // c:247
13750 getfn: usernamegetfn,
13751 setfn: usernamesetfn,
13752 unsetfn: stdunsetfn,
13753};
13754
13755/// Port of `static const struct gsu_scalar keyboardhack_gsu`
13756/// from `Src/params.c:253`.
13757pub const KEYBOARDHACK_GSU: gsu_scalar = gsu_scalar {
13758 // c:253
13759 getfn: keyboardhackgetfn,
13760 setfn: keyboardhacksetfn,
13761 unsetfn: stdunsetfn,
13762};
13763
13764/// Port of `static const struct gsu_scalar lang_gsu` from `Src/params.c:259-260`.
13765/// `{ strgetfn, langsetfn, stdunsetfn }` — registered by
13766/// `IPDEF2("LANG", lang_gsu, PM_UNSET)` at c:332.
13767pub const LANG_GSU: gsu_scalar = gsu_scalar {
13768 // c:259-260
13769 getfn: strgetfn,
13770 setfn: langsetfn,
13771 unsetfn: stdunsetfn,
13772};
13773
13774/// Port of `static const struct gsu_scalar lc_all_gsu` from `Src/params.c:261-262`.
13775/// `{ strgetfn, lc_allsetfn, stdunsetfn }` — registered by
13776/// `IPDEF2("LC_ALL", lc_all_gsu, PM_UNSET)` at c:333.
13777pub const LC_ALL_GSU: gsu_scalar = gsu_scalar {
13778 // c:261-262
13779 getfn: strgetfn,
13780 setfn: lc_allsetfn,
13781 unsetfn: stdunsetfn,
13782};
13783
13784/// Port of `static const struct gsu_scalar lc_blah_gsu` from `Src/params.c:257-258`.
13785/// `{ strgetfn, lcsetfn, stdunsetfn }` — the vtable every per-category
13786/// `LCIPDEF(name)` entry gets (c:331, c:334-347): LC_COLLATE, LC_CTYPE,
13787/// LC_MESSAGES, LC_NUMERIC, LC_TIME.
13788pub const LC_BLAH_GSU: gsu_scalar = gsu_scalar {
13789 // c:257-258
13790 getfn: strgetfn,
13791 setfn: lcsetfn,
13792 unsetfn: stdunsetfn,
13793};
13794
13795/// Port of `static const struct gsu_scalar histchars_gsu`
13796/// from `Src/params.c:246`.
13797pub const HISTCHARS_GSU: gsu_scalar = gsu_scalar {
13798 // c:246
13799 getfn: histcharsgetfn,
13800 setfn: histcharssetfn,
13801 unsetfn: stdunsetfn,
13802};
13803
13804/// Port of `homegetfn()` from `Src/params.c:5109`. C body: `return home;`
13805pub fn homegetfn(_pm: ¶m) -> String {
13806 home_lock().lock().expect("home poisoned").clone()
13807}
13808
13809/// Port of `homesetfn()` from `Src/params.c:5118`. C body:
13810/// ```c
13811/// zsfree(home);
13812/// if (x && isset(CHASELINKS) && (home = xsymlink(x, 0)))
13813/// zsfree(x);
13814/// else
13815/// home = x ? x : ztrdup("");
13816/// finddir(NULL);
13817/// ```
13818pub fn homesetfn(_pm: &mut param, x: String) {
13819 // c:5121-5126 — CHASELINKS path resolves symlinks before storing.
13820 // Falls through to the plain `x` store when CHASELINKS is off or
13821 // xsymlink fails.
13822 let resolved = if !x.is_empty() && isset(CHASELINKS) {
13823 xsymlink(&x).unwrap_or(x)
13824 } else {
13825 x
13826 };
13827 *home_lock().lock().expect("home poisoned") = resolved;
13828 // c:5127 — `finddir(NULL)` invalidates zsh's cached named-directory
13829 // lookups. zshrs's finddir port has no cache (per hashnameddir.rs
13830 // createnameddirtable note); the call is a no-op here.
13831}
13832
13833/// Port of `wordcharsgetfn()` from `Src/params.c:5132`. C body:
13834/// `return wordchars;`
13835pub fn wordcharsgetfn(_pm: ¶m) -> String {
13836 wordchars_lock().lock().expect("wordchars poisoned").clone()
13837}
13838
13839/// Port of `wordcharssetfn()` from `Src/params.c:5141`. C body:
13840/// `zsfree(wordchars); wordchars = x; inittyptab();`
13841pub fn wordcharssetfn(_pm: &mut param, x: String) {
13842 *wordchars_lock().lock().expect("wordchars poisoned") = x;
13843 // c:5143 — `inittyptab()` rebuilds typtab IWORD bits from the
13844 // new WORDCHARS. Without this, every IWORD lookup stays pinned
13845 // to the old set and silently mis-classifies word boundaries.
13846 inittyptab();
13847}
13848
13849/// Port of `underscoregetfn()` from `Src/params.c:5152`. C body:
13850/// `char *u = dupstring(zunderscore); untokenize(u); return u;`
13851///
13852/// C runs `untokenize(u)` on the cloned string before returning, so
13853/// ITOK bytes (Pound..Nularg per `Src/zsh.h:159-194`) in `$_` get
13854/// replaced/dropped via the canonical `ztokens[]` table. The previous
13855/// Rust port skipped untokenize entirely — every `$_` read that
13856/// included a lexer-injected token byte exposed the raw token in user
13857/// output (e.g. `$_` containing `$cmd` would surface as raw Stringg
13858/// instead of the literal `$`).
13859/// WARNING: param names don't match C — Rust=() vs C=(pm)
13860pub fn underscoregetfn() -> String {
13861 let u = zunderscore_lock()
13862 .lock()
13863 .expect("zunderscore poisoned")
13864 .clone();
13865 untokenize(&u) // c:5156 untokenize(u)
13866}
13867
13868/// Port of `term_reinit_from_pm()` from `Src/params.c:5163`.
13869/// C: `static void term_reinit_from_pm(void)` →
13870/// `if (unset(INTERACTIVE) || !*term) termflags |= TERM_UNKNOWN;
13871/// else init_term();`
13872pub fn term_reinit_from_pm() {
13873 // c:5163
13874 // c:5167 — `if (unset(INTERACTIVE) || !*term) termflags |= TERM_UNKNOWN;`
13875 let interactive = isset(INTERACTIVE);
13876 let term = term_lock().lock().map(|s| s.clone()).unwrap_or_default();
13877 if !interactive || term.is_empty() {
13878 // c:5167
13879 TERMFLAGS.fetch_or(TERM_UNKNOWN, Ordering::Relaxed); // c:5168
13880 } else {
13881 crate::ported::init::init_term(); // c:5170
13882 }
13883}
13884
13885/// Port of `termgetfn()` from `Src/params.c:5176`. C body: `return term;`
13886pub fn termgetfn(_pm: ¶m) -> String {
13887 term_lock().lock().expect("term poisoned").clone()
13888}
13889
13890/// Port of `termsetfn()` from `Src/params.c:5185`. C body:
13891/// `zsfree(term); term = x ? x : ""; term_reinit_from_pm();`
13892pub fn termsetfn(_pm: &mut param, x: String) {
13893 *term_lock().lock().expect("term poisoned") = x;
13894 term_reinit_from_pm();
13895}
13896
13897/// Port of `terminfogetfn()` from `Src/params.c:5196`. C body:
13898/// `return zsh_terminfo ? zsh_terminfo : "";`
13899pub fn terminfogetfn(_pm: ¶m) -> String {
13900 zsh_terminfo_lock()
13901 .lock()
13902 .expect("zsh_terminfo poisoned")
13903 .clone()
13904}
13905
13906/// Port of `int rprompt_indent` from `Src/init.c`. Set to 1 by
13907/// `init_term()` and reset by `rprompt_indent_unsetfn` when the
13908/// `RPROMPT_INDENT` parameter is unset.
13909pub static RPROMPT_INDENT: Mutex<i32> = Mutex::new(1);
13910
13911/// Port of `terminfosetfn()` from `Src/params.c:5205`.
13912///
13913/// Frees the old `zsh_terminfo`, stores `x`, and — only when the
13914/// parameter is exported — pushes `TERMINFO=x` into the environment
13915/// (terminfo reads the value from the env before the terminal is
13916/// reinitialised), then reinitialises the terminal.
13917pub fn terminfosetfn(pm: &mut param, x: String) {
13918 // c:5205
13919 // c:5207-5208 — `zsfree(zsh_terminfo); zsh_terminfo = x;`
13920 *zsh_terminfo_lock().lock().expect("zsh_terminfo poisoned") = x.clone();
13921 // c:5214-5215 — `if ((pm->node.flags & PM_EXPORTED) && x) addenv(pm, x);`
13922 // Only leak into the environment when $TERMINFO is actually
13923 // exported; a bare `TERMINFO=x` (no export) must not reach children.
13924 if pm.node.flags & PM_EXPORTED as i32 != 0 {
13925 let _ = zputenv(&format!("TERMINFO={}", &x)); // c:5215 addenv(pm, x)
13926 }
13927 term_reinit_from_pm(); // c:5217
13928}
13929
13930/// Port of `terminfodirsgetfn()` from `Src/params.c:5224`. C body:
13931/// `return zsh_terminfodirs ? zsh_terminfodirs : "";`
13932pub fn terminfodirsgetfn(_pm: ¶m) -> String {
13933 zsh_terminfodirs_lock()
13934 .lock()
13935 .expect("zsh_terminfodirs poisoned")
13936 .clone()
13937}
13938
13939/// Port of `terminfodirssetfn()` from `Src/params.c:5233`. C body
13940/// mirrors `terminfosetfn` for the TERMINFO_DIRS env var.
13941pub fn terminfodirssetfn(pm: &mut param, x: String) {
13942 // c:5233
13943 // c:5235-5236 — `zsfree(zsh_terminfodirs); zsh_terminfodirs = x;`
13944 *zsh_terminfodirs_lock()
13945 .lock()
13946 .expect("zsh_terminfodirs poisoned") = x.clone();
13947 // c:5242-5243 — `if ((pm->node.flags & PM_EXPORTED) && x) addenv(pm, x);`
13948 // Only export when $TERMINFO_DIRS is exported; a bare assignment
13949 // must not reach child processes.
13950 if pm.node.flags & PM_EXPORTED as i32 != 0 {
13951 let _ = zputenv(&format!("TERMINFO_DIRS={}", &x)); // c:5243 addenv(pm, x)
13952 }
13953 term_reinit_from_pm(); // c:5245
13954}
13955
13956// -----------------------------------------------------------
13957// $pipestatus
13958// -----------------------------------------------------------
13959
13960/// Port of `pipestatgetfn()` from `Src/params.c:5251`. C body
13961/// snapshots the `pipestats[]` C array as a heap-allocated
13962/// `char **`. Rust port returns the cloned snapshot.
13963/// WARNING: param names don't match C — Rust=() vs C=(pm)
13964pub fn pipestatgetfn() -> Vec<String> {
13965 pipestats_lock()
13966 .lock()
13967 .expect("pipestats poisoned")
13968 .iter()
13969 .map(|n| n.to_string())
13970 .collect()
13971}
13972
13973/// Port of `pipestatsetfn()` from `Src/params.c:5270`. C body:
13974/// `for (i=0; *x && i<MAX_PIPESTATS; i++) pipestats[i] = atoi(*x++); numpipestats = i;`
13975/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
13976pub fn pipestatsetfn(x: Option<Vec<String>>) {
13977 const MAX_PIPESTATS: usize = 256;
13978 let mut guard = pipestats_lock().lock().expect("pipestats poisoned");
13979 guard.clear();
13980 if let Some(v) = x {
13981 for s in v.iter().take(MAX_PIPESTATS) {
13982 guard.push(s.parse::<i32>().unwrap_or(0));
13983 }
13984 }
13985}
13986
13987/// Port of `arrfixenv()` from `Src/params.c:5285`. C body re-syncs
13988/// the env entry for an array param after mutation, joining with
13989/// the param's `joinchar`. Rust port joins with ':' (the default
13990/// for PATH-style arrays) and updates the env var.
13991/// Direct port of `void arrfixenv(char *s, char **t)` from
13992/// `Src/params.c:5285`. Re-syncs the env-side entry for an
13993/// array parameter after mutation. Order of operations (C body):
13994/// 1. If `t == path`, flush the command-name cache (c:5291).
13995/// 2. Look up the param node by name (c:5294); skip if
13996/// PM_HASHELEM is set (c:5300-5301).
13997/// 3. Under ALLEXPORT, mark PM_EXPORTED (c:5304); always clear
13998/// PM_DEFAULTED (c:5305).
13999/// 4. Skip if not PM_EXPORTED (c:5311-5312).
14000/// 5. joinchar = ':' for PM_SPECIAL else
14001/// `((struct tieddata *)pm->u.data)->joinchar` (c:5314-5318).
14002/// 6. `addenv(pm, t ? zjoin(t, joinchar, 1) : "")` (c:5319).
14003pub fn arrfixenv(s: &str, t: Option<&[String]>) {
14004 // c:5285
14005
14006 // c:5291 — `if (t == path) cmdnamtab->emptytable(cmdnamtab)`.
14007 // PATH change invalidates the command-name cache.
14008 if s == "PATH" || s == "path" {
14009 emptycmdnamtable();
14010 }
14011
14012 // c:5294 — `pm = paramtab->getnode(paramtab, s)`.
14013 let pm_arc_data = {
14014 let tab = paramtab().read().unwrap();
14015 tab.get(s).map(|pm| (pm.node.flags, pm.gsu_a.is_some()))
14016 };
14017 let (flags, _has_gsu_a) = match pm_arc_data {
14018 Some(x) => x,
14019 None => {
14020 // No param yet — just sync via env::set_var as fallback.
14021 let val = t.map(|v| v.join(":")).unwrap_or_default();
14022 // c:Src/params.c:5354 — setenv via the C-named zputenv
14023 let _ = zputenv(&format!("{}={}", s, &val));
14024 return;
14025 }
14026 };
14027
14028 // c:5300-5301 — `if (pm->flags & PM_HASHELEM) return`.
14029 if flags & PM_HASHELEM as i32 != 0 {
14030 return;
14031 }
14032
14033 // c:5304 — `if (isset(ALLEXPORT)) pm->flags |= PM_EXPORTED`.
14034 let allexport = isset(ALLEXPORT);
14035 // c:5305 — `pm->flags &= ~PM_DEFAULTED` always.
14036 {
14037 let mut tab = paramtab().write().unwrap();
14038 if let Some(pm) = tab.get_mut(s) {
14039 if allexport {
14040 pm.node.flags |= PM_EXPORTED as i32;
14041 }
14042 pm.node.flags &= !(PM_DEFAULTED as i32);
14043 }
14044 }
14045
14046 // c:5311-5312 — `if (!(pm->flags & PM_EXPORTED)) return`.
14047 let new_flags = {
14048 let tab = paramtab().read().unwrap();
14049 tab.get(s).map(|pm| pm.node.flags).unwrap_or(0)
14050 };
14051 if new_flags & PM_EXPORTED as i32 == 0 {
14052 return;
14053 }
14054
14055 // c:5314-5317 — joinchar selection.
14056 let joinchar: char = if new_flags & PM_SPECIAL as i32 != 0 {
14057 ':' // c:5315
14058 } else {
14059 // c:5317 — `joinchar = (unsigned char)
14060 // ((struct tieddata *)pm->u.data)->joinchar;`
14061 // The tieddata rides the SCALAR half of the pair (builtin.c:2982-2989),
14062 // which is exactly the `s` this function was handed. Hardcoding ':'
14063 // exported `typeset -T MORESTUFF morestuff -` as `here:we:go` instead
14064 // of `here-we-go`.
14065 paramtab()
14066 .read()
14067 .ok()
14068 .and_then(|t| {
14069 t.get(s)
14070 .and_then(|pm| pm.u_tied.as_deref().map(|td| td.joinchar))
14071 })
14072 .map(|jc| jc as u8 as char)
14073 .unwrap_or(':')
14074 };
14075
14076 // c:5319 — `addenv(pm, t ? zjoin(t, joinchar, 1) : "")`.
14077 let joined = match t {
14078 Some(arr) => arr.join(&joinchar.to_string()),
14079 None => String::new(),
14080 };
14081 addenv(s, &joined);
14082}
14083
14084/// Port of `zputenv()` from `Src/params.c:5325` (USE_SET_UNSET_ENV branch). Splits
14085/// `str` at the first `=`, validates the name is in the portable
14086/// character set (rejects any byte >= 128), and calls
14087/// `setenv(name, value, 1)`.
14088///
14089/// C body walks `str` byte-by-byte looking for either a high-byte
14090/// (reject) or `=` (split). On a clean ASCII `name=value`, it
14091/// temporarily writes `\0` at the `=` to splice off the name,
14092/// calls setenv, then restores the `=`. On `=`-less input, it
14093/// flags via DPUTS and still calls setenv with the whole string
14094/// as the name (with value pointing at the trailing `\0`). Rust
14095/// equivalent: split, set_var; the in-place mutation isn't
14096/// observable since we copy.
14097/// Port of `zputenv(char *str)` from `Src/params.c:5325`.
14098pub fn zputenv(str: &str) -> i32 {
14099 // c:5325
14100 // c:5327 — DPUTS(!str, "Attempt to put null string into environment.")
14101 DPUTS!(
14102 str.is_empty(),
14103 "Attempt to put null string into environment."
14104 ); // c:5327
14105 if str.is_empty() {
14106 // c:5328 (after DPUTS, defensive return)
14107 return 0;
14108 }
14109 let bytes = str.as_bytes();
14110 // c:5339-5341 — walk until `=` or high byte; reject high bytes.
14111 let mut ptr = 0;
14112 while ptr < bytes.len() && bytes[ptr] != b'=' && bytes[ptr] < 128 {
14113 // c:5339
14114 ptr += 1;
14115 }
14116 if ptr < bytes.len() && bytes[ptr] >= 128 {
14117 // c:5342
14118 // c:5351 — `return 1` to reject non-portable name.
14119 return 1;
14120 }
14121 if ptr < bytes.len() {
14122 // c:5352 `else if (*ptr)`
14123 // c:5353-5355 — write `\0` at `=`, setenv(name, value), restore.
14124 let name = &str[..ptr];
14125 let value = &str[ptr + 1..];
14126 // c:Src/params.c:5354 — `setenv(name, value, 1)` uses libc
14127 // setenv which treats the value as a NUL-terminated C string.
14128 // An embedded NUL byte ends the value there. Rust's
14129 // std::env::set_var PANICS on NUL in value; emulate C's
14130 // truncate-at-NUL semantics so shell params holding raw NUL
14131 // bytes (e.g. `X=$'a\0b\0c'` for `${(ps:\0:)X}` splits) can
14132 // still propagate the leading portion to env. The full
14133 // raw value lives in the canonical param table; this is
14134 // just the libc-env mirror.
14135 let safe_value: &str = match value.find('\0') {
14136 Some(n) => &value[..n],
14137 None => value,
14138 };
14139 if name.as_bytes().contains(&b'\0') {
14140 // c: setenv on a name with NUL is malformed — C's libc
14141 // rejects, we silently drop the env mirror.
14142 return 1;
14143 }
14144 env::set_var(name, safe_value);
14145 0
14146 } else {
14147 // c:5355 else
14148 // c:5356 — DPUTS(1, "bad environment string").
14149 // With no `=`, treat `str` as a bare name with empty value.
14150 DPUTS!(true, "bad environment string"); // c:5356
14151 if str.as_bytes().contains(&b'\0') {
14152 return 1;
14153 }
14154 env::set_var(str, ""); // c:5357
14155 0
14156 }
14157}
14158
14159// NUL-safe env-mirror helper lives in src/vm_helper.rs
14160// (zputenv, Src/params.c:5325) — the C-named env writer; the
14161// src/ported/ build gate forbids non-C-named fns here.
14162
14163/// Port of `findenv()` from `Src/params.c:5391`. Walks `environ` looking for an
14164/// entry whose name component (bytes up to `=`) matches `name`.
14165/// Returns Some(index) on a match; the C source writes the
14166/// index into `*pos` and returns 1.
14167///
14168/// Rust signature differs (no out-param; returns `Option<usize>`)
14169/// — the C int-with-out-param idiom maps to `Option<index>` here.
14170/// Walks std::env::vars_os() which preserves the same ordering
14171/// as the underlying libc environ array.
14172pub fn findenv(name: &str) -> Option<usize> {
14173 // c:5391
14174 // c:5391 — `eq = strchr(name, '=')`. Strip any trailing `=value`.
14175 let nlen = name.find('=').unwrap_or(name.len()); // c:5397
14176 let bare = &name[..nlen];
14177
14178 // c:5398-5404 — walk environ until match. Use std::env::vars()
14179 // which preserves the same ordering as the underlying libc
14180 // environ.
14181 for (i, (k, _)) in env::vars_os().enumerate() {
14182 if let Some(s) = k.to_str() {
14183 if s == bare {
14184 return Some(i); // c:5401-5403
14185 }
14186 }
14187 }
14188 None // c:5406
14189}
14190
14191// -----------------------------------------------------------
14192// env management (zsh's wrapper around setenv/unsetenv).
14193// -----------------------------------------------------------
14194
14195/// Port of `zgetenv()` from `Src/params.c:5416`. C body walks
14196/// `environ` byte-by-byte. Rust port uses `std::env::var`.
14197pub fn zgetenv(name: &str) -> Option<String> {
14198 env::var(name).ok()
14199}
14200
14201/// Port of `copyenvstr()` from `Src/params.c:5434`. Unmetafies `value`
14202/// into `s` (Meta NEXT pairs collapse to NEXT^32) and applies
14203/// PM_LOWER / PM_UPPER case folding.
14204///
14205/// Two representation-driven deviations, both deliberate:
14206///
14207/// 1. **No Meta un-escaping.** C param values are metafied byte
14208/// strings, so `Meta` (0x83) always introduces an escaped byte.
14209/// zshrs stores plain UTF-8 `String`s, where 0x83 is an ordinary
14210/// UTF-8 continuation byte — U+2603 SNOWMAN is `e2 98 83`. Treating
14211/// it as Meta ate the following byte and truncated the value.
14212///
14213/// 2. **Fold per char, not per byte.** C's `tulower` (c:Src/utils.c:2302)
14214/// is `c &= 0xff; isupper(c) ? tolower(c) : c` — a single-byte,
14215/// locale-table fold that mangles UTF-8 (real zsh turns `É` = `c3 89`
14216/// into `e3 89` on export). Folding ASCII only keeps every byte of a
14217/// multibyte character intact. The two agree for every ASCII value;
14218/// they differ only where real zsh corrupts the encoding, which is
14219/// recorded in docs/BUGS.md rather than reproduced.
14220pub fn copyenvstr(buf: &mut String, value: &str, flags: i32) {
14221 // c:5434
14222 let flags_u = flags as u32;
14223 for ch in value.chars() {
14224 // c:5436
14225 // `char::to_ascii_*` is the exact ASCII-domain restriction of
14226 // `tulower`/`tuupper`: it rewrites A-Z / a-z and returns every
14227 // other scalar unchanged.
14228 let ch = if flags_u & PM_LOWER != 0 {
14229 ch.to_ascii_lowercase() // c:5439-5440
14230 } else if flags_u & PM_UPPER != 0 {
14231 ch.to_ascii_uppercase() // c:5441-5442
14232 } else {
14233 ch
14234 };
14235 buf.push(ch);
14236 }
14237}
14238
14239/// Port of `addenv()` from `Src/params.c:5448` (USE_SET_UNSET_ENV branch — the
14240/// portable one). C body:
14241/// 1. `newenv = mkenvstr(pm->nam, value, pm->flags)` (c:5463)
14242/// 2. `if (zputenv(newenv)) { free; pm->env=NULL; return }` (c:5464-5468)
14243/// 3. Otherwise: `if (pm->env) free(pm->env); pm->env = newenv;
14244/// pm->flags |= PM_EXPORTED` (c:5482-5484)
14245///
14246/// Rust takes `name` instead of `Param pm` and looks up the
14247/// `pm` node internally — the C body's only reads of `pm` are
14248/// `pm->nam`, `pm->flags`, `pm->env`, all available from
14249/// paramtab. The return type changes from `void` to `i32` so
14250/// callers can chain it; 0 = success, 1 = zputenv failed.
14251pub fn addenv(name: &str, value: &str) -> i32 {
14252 // c:5448
14253
14254 // c:5463 — `newenv = mkenvstr(pm->nam, value, pm->flags)`.
14255 let flags = {
14256 let tab = paramtab().read().unwrap();
14257 tab.get(name).map(|pm| pm.node.flags).unwrap_or(0)
14258 };
14259 let newenv = mkenvstr(name, value, flags);
14260 // c:5464-5468 — `if (zputenv(newenv)) { free; pm->env=NULL; return }`.
14261 if zputenv(&newenv) != 0 {
14262 let mut tab = paramtab().write().unwrap();
14263 if let Some(pm) = tab.get_mut(name) {
14264 pm.env = None;
14265 }
14266 return 1;
14267 }
14268 // c:5482-5484 — `pm->env = newenv; pm->flags |= PM_EXPORTED`.
14269 let mut tab = paramtab().write().unwrap();
14270 if let Some(pm) = tab.get_mut(name) {
14271 pm.env = Some(newenv);
14272 pm.node.flags |= PM_EXPORTED as i32;
14273 }
14274 0
14275}
14276
14277/// Port of `mkenvstr()` from `Src/params.c:5513`. Builds `name=value`
14278/// in a fresh heap-string, where `value` is unmetafied and
14279/// case-folded according to `flags` (PM_LOWER → lower, PM_UPPER →
14280/// upper). The C source computes the unmetafied length first via
14281/// the `while (*s && (*s++ != Meta || *s++ != 32))` loop, then
14282/// allocates and writes via copyenvstr; the Rust port appends to
14283/// a `String` so the length pre-scan is implicit.
14284pub fn mkenvstr(name: &str, value: &str, flags: i32) -> String {
14285 // c:5513
14286 let mut buf = String::with_capacity(name.len() + value.len() + 2);
14287 buf.push_str(name); // c:5522 strcpy(s, name)
14288 buf.push('='); // c:5524 *s = '='
14289 if !value.is_empty() {
14290 // c:5525
14291 copyenvstr(&mut buf, value, flags); // c:5526
14292 }
14293 buf // c:5530
14294}
14295
14296/// Port of `delenvvalue()` from `Src/params.c:5542`. Removes `x` from environ by walking
14297/// to its pointer and shifting subsequent entries down one slot.
14298///
14299/// C body operates on the environ array directly. The Rust port
14300/// uses `env::remove_var(name)` since Rust's env is mediated by
14301/// libc::unsetenv internally — same shift semantics.
14302pub fn delenvvalue(name: &str) {
14303 // c:5542
14304 env::remove_var(name); // c:5542 equivalent
14305}
14306
14307/// Port of `delenv()` from `Src/params.c:5563`. Removes the param's env entry and
14308/// clears `pm->env`. Under USE_SET_UNSET_ENV (the portable
14309/// branch) the C body is:
14310/// unsetenv(pm->node.nam);
14311/// zsfree(pm->env);
14312/// pm->env = NULL;
14313///
14314/// "Note we don't remove PM_EXPORT from the flags. This may be
14315/// asking for trouble but we need to know later if we restore
14316/// this parameter to its old value." (c:5575-5577)
14317///
14318/// Rust signature drift: takes `&str` (the param name) instead
14319/// of `&mut Param`. The pm.env field is cleared via the paramtab
14320/// lookup; PM_EXPORTED is intentionally preserved per the C
14321/// comment.
14322pub fn delenv(name: &str) {
14323 // c:5563
14324 // c:5563 — `unsetenv(pm->node.nam)`.
14325 env::remove_var(name);
14326 // c:5568 / c:5572 — `pm->env = NULL`. PM_EXPORTED stays set.
14327 let mut tab = paramtab().write().unwrap();
14328 if let Some(pm) = tab.get_mut(name) {
14329 pm.env = None;
14330 }
14331}
14332
14333/// The process environment as it stood on entry to an in-process
14334/// subshell, for the subshell's exit to put back.
14335///
14336/// !!! WARNING: RUST-ONLY TYPE — C forks, and the child's `environ` dies
14337/// with it !!!
14338/// Every export writes the live `environ` array: `zputenv` (c:5320) is
14339/// `setenv(3)`, `delenvvalue` (c:5537) / `delenv` (c:5558) are
14340/// `unsetenv(3)`. In C those writes happen in the child `getoutput`
14341/// forked (`Src/exec.c:4816`), so `x=$(export v=1); env` never shows `v`.
14342/// zshrs runs `$( … )` in process, and the paramtab snapshot cannot undo
14343/// a `setenv`: the export reached every later child of the parent.
14344///
14345/// The image is taken rather than a journal of writes because `setenv`
14346/// is reached from far more places than the three above (`cd` exporting
14347/// `PWD`, `allexport`, module code, locale handling). One contiguous copy
14348/// of the bytes costs a single allocation, sized by the environment and
14349/// not by the number of variables; `restore` does nothing further unless
14350/// the body actually changed the environment.
14351#[allow(non_camel_case_types)]
14352pub struct environ_image {
14353 /// Every `NAME=VALUE` string of `environ`, in array order, each
14354 /// followed by a NUL.
14355 bytes: Vec<u8>,
14356}
14357
14358impl environ_image {
14359 /// Visit every entry of the live `environ` array, in order — the
14360 /// same array `zexecve` hands to `execve(2)` (`crate::ported::exec`).
14361 /// The bytes are only valid until the next `setenv`/`unsetenv`, so
14362 /// they are lent to `f` rather than returned.
14363 fn walk_live(mut f: impl FnMut(&[u8])) {
14364 extern "C" {
14365 static environ: *const *const libc::c_char;
14366 }
14367 unsafe {
14368 let mut p = environ;
14369 while !p.is_null() && !(*p).is_null() {
14370 f(std::ffi::CStr::from_ptr(*p).to_bytes());
14371 p = p.add(1);
14372 }
14373 }
14374 }
14375
14376 /// Split `NAME=VALUE` at its first `=`. An entry without one, or with
14377 /// an empty name, is not a variable `setenv` could have made.
14378 fn split(entry: &[u8]) -> Option<(&[u8], &[u8])> {
14379 let eq = entry.iter().position(|b| *b == b'=')?;
14380 (eq > 0).then(|| (&entry[..eq], &entry[eq + 1..]))
14381 }
14382
14383 /// Copy the live environment: one buffer, no per-variable allocation.
14384 pub fn save() -> Self {
14385 let mut bytes = Vec::new();
14386 Self::walk_live(|entry| {
14387 bytes.extend_from_slice(entry);
14388 bytes.push(0);
14389 });
14390 environ_image { bytes }
14391 }
14392
14393 /// Make the live environment equal the image again — same entries,
14394 /// same order (`env` lists them in array order).
14395 pub fn restore(self) {
14396 use std::ffi::OsStr;
14397 use std::os::unix::ffi::OsStrExt;
14398 let saved: Vec<&[u8]> = self
14399 .bytes
14400 .split(|b| *b == 0)
14401 .filter(|e| !e.is_empty())
14402 .collect();
14403 let mut live: Vec<Vec<u8>> = Vec::new();
14404 Self::walk_live(|entry| live.push(entry.to_vec()));
14405 if live.len() == saved.len() && live.iter().zip(&saved).all(|(l, s)| l == s) {
14406 return; // the body left the environment alone
14407 }
14408 let set = |entry: &[u8]| {
14409 if let Some((name, value)) = Self::split(entry) {
14410 env::set_var(OsStr::from_bytes(name), OsStr::from_bytes(value));
14411 }
14412 };
14413 let name_of = |e: &[u8]| Self::split(e).map(|(n, _)| n.to_vec());
14414 let same_names = live.len() == saved.len()
14415 && live.iter().zip(&saved).all(|(l, s)| name_of(l) == name_of(s));
14416 if same_names {
14417 // Only values changed: rewrite those in place, order intact.
14418 for (l, s) in live.iter().zip(&saved) {
14419 if l.as_slice() != *s {
14420 set(s);
14421 }
14422 }
14423 return;
14424 }
14425 // A variable was added or removed. `setenv` appends, so the saved
14426 // order only comes back by clearing and re-adding in that order.
14427 for entry in &live {
14428 if let Some((name, _)) = Self::split(entry) {
14429 env::remove_var(OsStr::from_bytes(name));
14430 }
14431 }
14432 for entry in saved {
14433 set(entry);
14434 }
14435 }
14436}
14437
14438/// Port of `convbase_ptr()` from `Src/params.c:5586`. C body
14439/// converts `v` into base `base` (negative `base` suppresses the
14440/// "0x"/"N#" discriminator), writing the digits into `s` and
14441/// returning the digit count via `*ndigits`. Rust port returns
14442/// `(formatted_string, digit_count)` since Rust strings own
14443/// their buffer.
14444/// WARNING: param names don't match C — Rust=(v, base) vs C=(s, v, base, ndigits)
14445pub fn convbase_ptr(v: i64, base: i32) -> (String, i32) {
14446 let mut s = String::new();
14447 let mut value = v;
14448 if value < 0 {
14449 s.push('-');
14450 // c:Src/params.c — `value = -value;` on INT_MIN is UB in C
14451 // but in practice wraps back to INT_MIN. Use wrapping_neg
14452 // to avoid Rust's debug-build overflow panic. zsh prints
14453 // `-9223372036854775808` for `$((2**63))`; we mirror that.
14454 value = value.wrapping_neg();
14455 }
14456 let mut b = base;
14457 if (-1..=1).contains(&b) {
14458 b = -10;
14459 }
14460 if b > 0 {
14461 if isset(CBASES) && b == 16 {
14462 s.push_str("0x");
14463 } else if isset(CBASES) && b == 8 && isset(OCTALZEROES) {
14464 s.push('0');
14465 } else if b != 10 {
14466 s.push_str(&format!("{}#", b));
14467 }
14468 } else {
14469 b = -b;
14470 }
14471 let base_u = b as u64;
14472 let mut x = value as u64;
14473 let mut digs: i32 = 0;
14474 while x != 0 {
14475 x /= base_u;
14476 digs += 1;
14477 }
14478 if digs == 0 {
14479 digs = 1;
14480 }
14481 // Digits > 9 print UPPERCASE in zsh (`16#FF`) but LOWERCASE in the Korn
14482 // family (`16#ff`), which `--ksh`/`--mksh`/`--pdksh` are drop-ins for. zsh's
14483 // own `emulate ksh` KEEPS uppercase, so this must fire ONLY for the
14484 // real-shell-faithful ksh drop-in (`posix_faithful()`), not the zsh-STYLE
14485 // `--ksh --zsh` leg (which must match `emulate ksh`). Verified vs
14486 // `typeset -i16 v=255` (real ksh93/mksh → `16#ff`).
14487 let alpha = if crate::ported::options::emulation.load(std::sync::atomic::Ordering::Relaxed)
14488 & crate::ported::zsh_h::EMULATE_KSH
14489 != 0
14490 && crate::dash_mode::posix_faithful()
14491 {
14492 b'a'
14493 } else {
14494 b'A'
14495 };
14496 let mut digits: Vec<u8> = vec![0u8; digs as usize];
14497 let mut i = digs - 1;
14498 let mut x = value as u64;
14499 while i >= 0 {
14500 let dig = (x % base_u) as u8;
14501 digits[i as usize] = if dig < 10 {
14502 b'0' + dig
14503 } else {
14504 alpha + dig - 10
14505 };
14506 x /= base_u;
14507 i -= 1;
14508 }
14509 s.push_str(std::str::from_utf8(&digits).unwrap_or(""));
14510 (s, digs)
14511}
14512
14513// ---------------------------------------------------------------------------
14514// Integer/Float conversion (from convbase/convfloat)
14515// ---------------------------------------------------------------------------
14516
14517/// Port of `convbase()` from `Src/params.c:5632`. C body (single statement):
14518/// `convbase_ptr(s, v, base, NULL);`
14519/// Rust takes (v, base) and returns the formatted string since Rust
14520/// strings own their buffer; the discarded `ndigits` out-param of
14521/// `convbase_ptr` is `.1` of the returned tuple.
14522/// WARNING: param names don't match C — Rust=(val, base) vs C=(s, v, base)
14523pub fn convbase(val: i64, base: u32) -> String {
14524 // c:5632
14525 convbase_ptr(val, base as i32).0 // c:5634
14526}
14527
14528/// Convert integer to string with underscores for readability
14529/// Port of `convbase_underscore()` from `Src/params.c:5646`.
14530///
14531/// `base` is `i32` not `u32` because zsh uses NEGATIVE `base` values
14532/// (set by `[##N]`) to mean "emit `N`-radix digits WITHOUT the `N#`
14533/// prefix" — convbase_ptr at params.rs:7435-7451 handles the sign:
14534/// positive `b` produces `b#NNN`, negative `b` produces bare `NNN`
14535/// (absolute value of `b` is the actual radix).
14536///
14537/// Previous Rust signature `base: u32` silently dropped the sign, so
14538/// `$(([##16] 255))` (outputradix=-16) ended up as `convbase(255, 16)`
14539/// = `"16#FF"` instead of `"FF"`.
14540/// WARNING: param names don't match C — Rust=(val, base, underscore) vs C=(s, v, base, underscore)
14541pub fn convbase_underscore(val: i64, base: i32, underscore: i32) -> String {
14542 // c:5646 — `convbase_ptr(s, v, base, &ndigits)` returns BOTH the string and
14543 // the digit count EXCLUDING the base prefix (`0x`, `N#`, leading `0`).
14544 let (s, ndigits) = convbase_ptr(val, base);
14545 if underscore <= 0 {
14546 return s; // c:5648
14547 }
14548 let ndigits = ndigits.max(0) as usize;
14549
14550 // c:5654-5657 — group only the last `ndigits` chars; the prefix (`len -
14551 // ndigits` chars) is copied through untouched. The previous port re-scanned
14552 // for the first digit, which matched the `0` in `0x…` and grouped the `0x`
14553 // prefix too (`0x10000` → `0_x10_000` instead of `0x10_000`).
14554 let split = s.len().saturating_sub(ndigits);
14555 let prefix = &s[..split];
14556 let digits = &s[split..];
14557
14558 if digits.len() <= underscore as usize {
14559 return s;
14560 }
14561
14562 let u = underscore as usize;
14563 let mut result = prefix.to_string();
14564 let chars: Vec<char> = digits.chars().collect();
14565 let first_group = chars.len() % u;
14566 if first_group > 0 {
14567 result.extend(&chars[..first_group]);
14568 if first_group < chars.len() {
14569 result.push('_');
14570 }
14571 }
14572 for (i, chunk) in chars[first_group..].chunks(u).enumerate() {
14573 if i > 0 {
14574 result.push('_');
14575 }
14576 result.extend(chunk);
14577 }
14578 result
14579}
14580
14581/// Port of `convfloat()` from `Src/params.c:5690`.
14582///
14583/// C signature: `char *convfloat(double dval, int digits, int flags,
14584/// FILE *fout)` — picks `%e` / `%f` / `%g` based on PM_EFLOAT /
14585/// PM_FFLOAT (line 5705-5727), then snprintf'd with `digits` precision.
14586/// When neither E nor F flag is set, zsh uses `%.*g` with a default
14587/// of 17 significant digits (line 5712-5714). E-flag with N significant
14588/// figures decrements `digits` because `%e` counts decimal places not
14589/// significants (line 5720-5725).
14590///
14591/// Rust signature drops the `fout` parameter — every caller wanted the
14592/// returned string. IEEE specials (inf/nan) hand-formatted to `Inf`/
14593/// `-Inf`/`NaN` ahead of the snprintf, matching the C source's Inf/NaN
14594/// shortcuts at lines 5733-5736 / 5742-5744. The trailing-dot rule for
14595/// integer-valued floats (`5` -> `5.`) is added by the caller (params'
14596/// internal printing path) in C zsh; mirrored here for the no-flag case
14597/// so `MathNum::(crate::ported::math::mn_format_subst(Float(5.0)))` produces `5.` not `5`.
14598/// WARNING: param names don't match C — Rust=(dval, digits, pm_flags) vs C=(dval, digits, flags, fout)
14599pub fn convfloat(dval: f64, digits: i32, pm_flags: u32) -> String {
14600 if dval.is_infinite() {
14601 // c:5742
14602 return if dval < 0.0 {
14603 "-Inf".to_string()
14604 } else {
14605 "Inf".to_string()
14606 };
14607 }
14608 if dval.is_nan() {
14609 // c:5744
14610 return "NaN".to_string();
14611 }
14612 // !!! EMULATION-ONLY (no C counterpart) !!! ksh93 prints through `%g`:
14613 // `-E[n]` is `%.{n}g` (default 10), plain float arithmetic is `%.15g`, and
14614 // neither appends zsh's trailing dot. Measured against ksh93u+m:
14615 // `typeset -E f=1.5; print $f` -> `1.5`, `print $((2**0.5))` ->
14616 // `1.4142135623731`, `print $((5.0))` -> `5`.
14617 let ksh93 = crate::dash_mode::ksh93_mode();
14618 let (fmt_char, digits) = if ksh93 && (pm_flags & PM_EFLOAT) != 0 {
14619 ('g', if digits <= 0 { 10 } else { digits })
14620 } else if (pm_flags & PM_EFLOAT) != 0 {
14621 // c:5715
14622 let d = if digits <= 0 { 10 } else { digits }; // c:5718
14623 ('e', (d - 1).max(0)) // c:5725
14624 } else if (pm_flags & PM_FFLOAT) != 0 {
14625 // c:5716
14626 let d = if digits <= 0 { 10 } else { digits }; // c:5718
14627 ('f', d)
14628 } else {
14629 let d = if digits == 0 { if ksh93 { 15 } else { 17 } } else { digits }; // c:5713
14630 ('g', d)
14631 };
14632 // Mirror zsh's snprintf path (Src/params.c:5751) — the C source
14633 // uses `VARARR(char, buf, 512 + digits)` for %f's full integer-
14634 // part expansion. 512 + 17 = 529 covers the zsh general case;
14635 // wider buffers below for the unbounded %f.
14636 let buf_len = 512usize + digits as usize + 4;
14637 let mut buf = vec![0u8; buf_len];
14638 let fmt = match fmt_char {
14639 'e' => c"%.*e",
14640 'f' => c"%.*f",
14641 _ => c"%.*g",
14642 };
14643 // SAFETY: buf has the C-required size for any double precision; fmt
14644 // is a NUL-terminated literal; snprintf writes ASCII only.
14645 let n = unsafe {
14646 libc::snprintf(
14647 buf.as_mut_ptr() as *mut libc::c_char,
14648 buf_len,
14649 fmt.as_ptr(),
14650 digits as libc::c_int,
14651 dval,
14652 )
14653 };
14654 if n < 0 {
14655 return format!("{}", dval);
14656 }
14657 let len = (n as usize).min(buf_len - 1);
14658 buf.truncate(len);
14659 let mut s = String::from_utf8(buf).unwrap_or_else(|_| format!("{}", dval));
14660 // zsh's general-format (%g) callers (math `$(( ))` substitution)
14661 // append `.` when the output has no `e` and no `.`, so integer-
14662 // valued floats like `5` render as `5.`. PM_EFLOAT/PM_FFLOAT skip
14663 // this rule (the format spec already pins shape).
14664 if !ksh93 && fmt_char == 'g' && !s.contains('e') && !s.contains('.') {
14665 s.push('.');
14666 }
14667 s
14668}
14669
14670/// Port of `startparamscope()` from `Src/params.c:5840`.
14671/// Start a parameter scope. The C source pushes the
14672/// current scope counter so `local`-declared params disappear on function
14673/// exit. Rust port operates on the bucket-2 holder `paramtab` via a
14674/// `&mut HashTable` argument.
14675pub fn startparamscope(_table: &mut HashTable) {
14676 inc_locallevel();
14677}
14678
14679/// Port of `endparamscope()` from `Src/params.c:5857`. C signature:
14680/// `mod_export void endparamscope(void)`. Decrements `locallevel`,
14681/// pops any pushed history stack, then iterates `paramtab` calling
14682/// `scanendscope` to restore/unset every param whose `level`
14683/// exceeds the new `locallevel`. Operates on the global `paramtab`
14684/// just like C — no parameter, no fake injection wrapper.
14685pub fn endparamscope() {
14686 queue_signals();
14687 // c:5861 — `LinkList refs = locallevel < scoperefs_num ? scoperefs[locallevel] : NULL;`
14688 // Snapshot the refs at the OLD locallevel BEFORE decrementing.
14689 let old_ll = locallevel_fn();
14690 let refs_snapshot: Vec<String> = SCOPEREFS.with(|sr| {
14691 let sr = sr.borrow();
14692 if (old_ll as usize) < sr.len() {
14693 sr[old_ll as usize].clone()
14694 } else {
14695 Vec::new()
14696 }
14697 });
14698
14699 dec_locallevel(); // c:5863 locallevel--
14700 // c:5865 — `saveandpophiststack(0, HFILE_USE_OPTIONS);`. Pop
14701 // all stack entries with locallevel > current.
14702 saveandpophiststack(0, HFILE_USE_OPTIONS as i32);
14703 let ll = locallevel_fn();
14704 LC_UPDATE_NEEDED.store(0, Ordering::SeqCst); // c:5860 lc_update_needed = 0 (USE_LOCALE)
14705 // c:5869 scanhashtable(paramtab, 0, 0, 0, scanendscope, 0). Walk
14706 // the live paramtab (HashMap-backed until the hashtable.c vtable
14707 // is wired) and apply scanendscope's `pm->level > locallevel`
14708 // filter, restoring the `pm.old` chain or removing the entry.
14709 // c:Src/params.c:5867-5933 — for PM_SPECIAL scalar params the
14710 // gsu setfn must be re-fired with the restored value so global
14711 // side-effects (ifs char buffer, PATH chunks, lc_update_needed
14712 // flag, etc.) get rolled back. Bug #8 in docs/BUGS.md: `local
14713 // IFS=:` inside a function left the global ifs buffer pinned
14714 // to ":" after return.
14715 //
14716 // The setfn closures often re-enter paramtab (ifssetfn → inittyptab
14717 // → paramtab.read), so we MUST drop the write lock before calling
14718 // them. Collect (name, setfn, value) into a deferred list inside
14719 // the lock, restore the pm.old chain, drop the lock, then re-fire
14720 // setfn on each special.
14721 // setfn None → name-routed restore via setsparam (PM_TIED params
14722 // whose pm carries no scalar gsu — FPATH/PATH/CDPATH…; setsparam
14723 // dispatches to the tied setter by name, refilling the tied
14724 // array's global storage).
14725 type DeferredSetfn = (String, Option<fn(&mut param, String)>, String);
14726 let mut deferred: Vec<DeferredSetfn> = Vec::new();
14727 // Array-shaped specials/tieds restore through the name-routed
14728 // array setter after the lock drops (same deadlock rationale).
14729 let mut deferred_arrays: Vec<(String, Vec<String>)> = Vec::new();
14730 if let Ok(mut tab) = paramtab().write() {
14731 let stale: Vec<(String, bool)> = tab
14732 .iter()
14733 .filter_map(|(k, pm)| {
14734 if pm.level > ll {
14735 Some((k.clone(), (pm.node.flags as u32 & PM_HASHED) != 0))
14736 } else {
14737 None
14738 }
14739 })
14740 .collect();
14741 for (n, was_assoc) in stale {
14742 // c:5904 — `if ((pm->node.flags & (PM_SPECIAL|PM_REMOVABLE)) ==
14743 // PM_SPECIAL)`: a non-removable special is restored THROUGH the
14744 // node already in the table (c:5933-5937 assign straight into
14745 // `pm`), so its bucket position never changes. Only the
14746 // `else unsetparam_pm(pm, 0, 0)` arm (c:5966) unlinks the node
14747 // and re-adds `pm->old` (c:3916-3920), which puts the revealed
14748 // outer binding at the FRONT of its chain. Reproduce both: skip
14749 // the remove for the special arm and let the `insert` below take
14750 // `addhashnode2`'s replace-in-place path (Src/hashtable.c:189-203).
14751 let restore_in_place = tab
14752 .get(&n)
14753 .map(|pm| (pm.node.flags as u32 & (PM_SPECIAL | PM_REMOVABLE)) == PM_SPECIAL)
14754 .unwrap_or(false);
14755 // c:5919-5922 — scanendscope's special arm (USE_LOCALE):
14756 // if (!strncmp(pm->node.nam, "LC_", 3) ||
14757 // !strcmp(pm->node.nam, "LANG"))
14758 // lc_update_needed = 1;
14759 if restore_in_place && (n.starts_with("LC_") || n == "LANG") {
14760 LC_UPDATE_NEEDED.store(1, Ordering::SeqCst);
14761 }
14762 // c:scanendscope:5903 — non-special path: restore pm.old
14763 // (or remove if no outer binding existed).
14764 let popped = if restore_in_place {
14765 tab.get(&n).cloned()
14766 } else {
14767 tab.remove(&n)
14768 };
14769 if let Some(pm) = popped {
14770 let had_outer = pm.old.is_some();
14771 // The level this local was stamped with at createparam time
14772 // (c:Src/builtin.c:2576 `pm->level = locallevel`); the assoc
14773 // shadow frame pushed for it carries the same number.
14774 let popped_level = pm.level;
14775 // c:Src/params.c:3862 — scanendscope's non-special arm calls
14776 // `unsetparam_pm(pm, 0, 0)`, whose `if (pm->env) delenv(pm)`
14777 // strips the popped local's ENVIRON entry. This pop path
14778 // removes the node from the table directly instead of routing
14779 // through unsetparam_pm, so an exported local's environment
14780 // entry outlived its scope: `f(){ local -x E=1 }; f` left E=1
14781 // in the real process environment — inherited by every child —
14782 // even though paramtab itself was clean (`typeset -p E` already
14783 // reported "no such variable").
14784 let popped_exported = (pm.node.flags as u32 & PM_EXPORTED) != 0;
14785 // c:Src/params.c:3926-3934 — once the outer binding is back,
14786 // `if (oldpm->node.flags & PM_EXPORTED) export_param(oldpm)`
14787 // re-exports the OUTER value, which typeset_single had removed
14788 // from the environment (c:1143 `delenv(oldpm)`) when the local
14789 // shadowed it. Capture it before `pm.old` is moved below.
14790 let outer_export: Option<String> = pm.old.as_ref().and_then(|p| {
14791 if (p.node.flags as u32 & PM_EXPORTED) != 0 {
14792 Some(p.u_str.clone().unwrap_or_default())
14793 } else {
14794 None
14795 }
14796 });
14797 if let Some(mut prev) = pm.old {
14798 // c:Src/params.c:5928 — `if (!(tpm->node.flags &
14799 // (PM_NORESTORE|PM_READONLY)))` gates the value restore.
14800 // PM_NORESTORE is stamped by typeset_single (c:2408) when
14801 // the OTHER half of a tied path/PATH pair was already made
14802 // local at the same level: that half's own restore writes
14803 // through the tie, so re-firing this half's saved value
14804 // would clobber it with a copy that already reflects the
14805 // local state.
14806 // PM_READONLY belongs in the SAME gate — C skips the value
14807 // restore outright for a read-only special, because a
14808 // shadow never wrote through to it in the first place. Only
14809 // the scalar arm below compensated (by dropping the flag
14810 // around the name-routed setter); the ARRAY arm did not, so
14811 // unwinding a scope that shadowed a read-only special ARRAY
14812 // (`f(){ typeset -h +g dis_reswords }`, and every
14813 // `typeset -h +g -m 'PAT'` that matches one) reached
14814 // assignaparam and printed `read-only variable: NAME` at
14815 // function exit.
14816 let norestore = (prev.node.flags as u32 & (PM_NORESTORE | PM_READONLY)) != 0;
14817 // c:Src/params.c:5924 — `pm->node.flags =
14818 // (tpm->node.flags & ~PM_NORESTORE);` the bit never
14819 // survives into the re-exposed outer binding.
14820 prev.node.flags = (prev.node.flags as u32 & !PM_NORESTORE) as i32;
14821 // c:scanendscope:5933 pm->old = tpm->old
14822 // PM_TIED counts too — the tied array's GLOBAL
14823 // storage was written through by the local's
14824 // assignments; the deferred restore re-fires the
14825 // saved value through the tied setter (by gsu
14826 // setfn when present, else name-routed setsparam)
14827 // so fpath/path/cdpath roll back with the scalar.
14828 // c:5904 — a REMOVABLE special (a private, c:Src/Modules/
14829 // param_private.c:174) is "normal": the local is unset
14830 // and the old node re-added, with no value replay.
14831 let restored_is_special = (prev.node.flags as u32 & (PM_SPECIAL | PM_TIED)) != 0
14832 && (prev.node.flags as u32 & PM_REMOVABLE) == 0;
14833 let restored_is_array = (PM_TYPE(prev.node.flags as u32) & PM_ARRAY) != 0;
14834 // c:5961-5962 — `case PM_HASHED: pm->gsu.h->setfn(pm,
14835 // tpm->u.hash)`. typeset_single's copy of a magic hash
14836 // (builtin.rs, c:Src/builtin.c:2410) is carried as a
14837 // flat key/value list in `u_arr`; take it off the node so
14838 // the restored special does not keep a stale array value.
14839 let restored_is_hashed = PM_TYPE(prev.node.flags as u32) == PM_HASHED;
14840 let restored_val = prev.u_str.clone();
14841 let restored_arr = if restored_is_hashed {
14842 prev.u_arr.take()
14843 } else {
14844 prev.u_arr.clone()
14845 };
14846 let restored_setfn = prev.gsu_s.as_ref().map(|g| g.setfn);
14847 tab.insert(n.clone(), prev); // restore outer binding (Box<param>)
14848 if restored_is_special && !norestore {
14849 if restored_is_hashed {
14850 // c:5962 — replayed after the lock drops through
14851 // assignaparam's whole-hash special arm, which is
14852 // the per-row `setpm*s` dispatch (gsu.h->setfn).
14853 if let Some(pairs) = restored_arr {
14854 deferred_arrays.push((n.clone(), pairs));
14855 }
14856 } else if restored_is_array {
14857 // ARRAY side of a tied pair — re-fire the
14858 // saved element vector through the
14859 // name-routed array setter so the tied
14860 // scalar + global storage roll back too.
14861 if let Some(arr) = restored_arr {
14862 deferred_arrays.push((n.clone(), arr));
14863 }
14864 } else if let Some(val) = restored_val {
14865 deferred.push((n.clone(), restored_setfn, val));
14866 }
14867 }
14868 } else if restore_in_place {
14869 // A non-removable special with no `pm->old` cannot be
14870 // restored in place (c:5930 DPUTS asserts `tpm` exists),
14871 // so fall back to the c:5966 arm and unlink the node —
14872 // the `tab.remove` above was skipped for this name.
14873 tab.remove(&n);
14874 }
14875 // else: c:5966 unsetparam_pm — name unset entirely
14876 // RUST-ONLY: the assoc-storage shadow lives in a
14877 // parallel `paramtab_hashed_storage` map keyed by name;
14878 // endparamscope's pm removal must also clear that
14879 // shadow when no outer binding remains. Without this,
14880 // `f() { typeset -A H; H[x]=v; }; f` leaves H's data
14881 // in the shadow map even after the local pm is gone,
14882 // so `${H[x]}` outside reads "v" instead of empty.
14883 if was_assoc && !had_outer {
14884 let _ = paramtab_hashed_storage()
14885 .lock()
14886 .ok()
14887 .as_deref_mut()
14888 .map(|m| m.remove(&n));
14889 }
14890 // RUST-ONLY: assoc-storage shadow restoration — pop the
14891 // frame createparam pushed for THIS local and put the name's
14892 // `paramtab_hashed_storage` row back the way it was before the
14893 // shadow installed. In C there is nothing to do here: the
14894 // outer's pairs never moved (they hang off the `pm->old`
14895 // struct's own `u.hash`, c:Src/params.c:1157-1158) and the
14896 // local's pairs die with the local in `stdunsetfn`'s PM_HASHED
14897 // arm (c:3922-3925 → hashsetfn c:4045). zshrs's one row per
14898 // NAME has to be unwound by hand, and this is the only place
14899 // that does it. Bug #415.
14900 //
14901 // The frame is matched by the level createparam recorded, not
14902 // by re-deriving the push condition from the flags: the two
14903 // spellings had drifted into "outer is PM_HASHED" on both
14904 // sides, so a hashed local under a NON-hashed outer pushed
14905 // nothing and popped nothing, and its row outlived every
14906 // enclosing scope (see the createparam comment). A level match
14907 // also means a frame is never stolen from an enclosing scope
14908 // when this scope pushed none — which is what kept
14909 // typeset -A h=(a 1); f(){ local h; }; f; echo ${h[a]}
14910 // working (a non-hashed local over a hashed outer still pushes,
14911 // so it still restores `a 1`).
14912 {
14913 let stk_mtx =
14914 PARAMTAB_HASHED_SHADOW_STACK.get_or_init(|| Mutex::new(HashMap::new()));
14915 // Outer Option: whether a frame belonging to this scope was
14916 // popped at all. Inner: whether the name had a row before.
14917 let saved: Option<Option<IndexMap<String, String>>> =
14918 if let Ok(mut stk) = stk_mtx.lock() {
14919 match stk.get_mut(&n) {
14920 Some(frames)
14921 if frames
14922 .last()
14923 .is_some_and(|(lvl, _)| *lvl == popped_level) =>
14924 {
14925 frames.pop().map(|(_, row)| row)
14926 }
14927 _ => None,
14928 }
14929 } else {
14930 None
14931 };
14932 let frame_popped = saved.is_some();
14933 if let Some(saved) = saved {
14934 if let Ok(mut m) = paramtab_hashed_storage().lock() {
14935 match saved {
14936 Some(map) => {
14937 m.insert(n.clone(), map);
14938 }
14939 None => {
14940 m.remove(&n);
14941 }
14942 }
14943 }
14944 }
14945 // RUST-ONLY: the same unwind for a local that became hashed
14946 // AFTER it was declared, where createparam saw no hash on
14947 // either side and pushed no frame:
14948 // d0() { local -a H; d1 }
14949 // d1() { local H=s; typeset -A H; H[x]=1 }
14950 // The second declaration takes createparam's REUSE arm
14951 // (c:Src/params.c:1154-1155 `pm = oldpm`, reached because
14952 // `oldpm->level == locallevel`), which installs no shadow at
14953 // all — in C there is still nothing to unwind, since the
14954 // pairs went into the one Param that the scope pop frees.
14955 // Here they went into the name-keyed row, which nothing
14956 // owned. So enforce the invariant the push side states: a
14957 // binding that is not PM_HASHED owns no row. `tab` already
14958 // holds whatever this pop revealed, so the test reads the
14959 // binding that is about to become visible.
14960 if !frame_popped && had_outer {
14961 let visible_hashed = tab
14962 .get(&n)
14963 .map(|p| (p.node.flags as u32 & PM_HASHED) != 0)
14964 .unwrap_or(false);
14965 if !visible_hashed {
14966 if let Ok(mut m) = paramtab_hashed_storage().lock() {
14967 m.remove(&n);
14968 }
14969 }
14970 }
14971 }
14972 // Net environment effect of the two C steps above: the outer
14973 // value if it was exported, otherwise no entry at all. Uses
14974 // the raw env helpers (not export_param/unsetparam_pm) because
14975 // we still hold the paramtab write lock here and those re-enter
14976 // it; neither `env::set_var` nor `delenvvalue` touches paramtab.
14977 if popped_exported || outer_export.is_some() {
14978 match &outer_export {
14979 Some(v) => env::set_var(&n, v), // c:3933 export_param(oldpm)
14980 None => delenvvalue(&n), // c:3862 delenv(pm)
14981 }
14982 }
14983 }
14984 }
14985 }
14986 // c:Src/params.c:5915-5933 — re-fire PM_SPECIAL setfns NOW that
14987 // the paramtab write lock is released. Each setfn takes its own
14988 // paramtab read (via inittyptab / similar) so the deferred call
14989 // is the only deadlock-safe path.
14990 for (n, setfn, val) in deferred {
14991 match setfn {
14992 Some(setfn) => {
14993 let mut pm_copy: Option<param> = None;
14994 if let Ok(tab) = paramtab().read() {
14995 pm_copy = tab.get(&n).map(|p| (**p).clone());
14996 }
14997 if let Some(mut pm) = pm_copy {
14998 setfn(&mut pm, val);
14999 }
15000 }
15001 // PM_TIED without a scalar gsu — name-routed: setsparam
15002 // reaches the tied setter and refills the tied array's
15003 // global storage (fpath/path/cdpath…).
15004 None => {
15005 // c:Src/params.c:5915-5933 — C restores by CALLING THE
15006 // SPECIAL'S setfn directly (`pm->gsu.s->setfn(pm, ...)`).
15007 // The `read-only variable` guard lives one level up in
15008 // `assignsparam`/`setstrvalue` (c:3216), so C's restore
15009 // never meets it: for a PM_READONLY_SPECIAL the setfn is
15010 // simply a no-op (`varint_readonly_gsu` =
15011 // `{intvargetfn, nullintsetfn, stdunsetfn}`, c:351).
15012 //
15013 // zshrs has no GSU wired for these rows, so the restore
15014 // falls back to the name-routed `setsparam`, which DOES
15015 // re-enter `assignsparam` and would emit `read-only
15016 // variable: NAME` while unwinding a scope that shadowed
15017 // one (`f() { local ARGC=2 }`). Drop the flag for the
15018 // duration of the restore so the write behaves like C's
15019 // direct setfn call, then put it back. The bit is
15020 // re-read from the node rather than assumed, so a row
15021 // that is not read-only is untouched.
15022 let ro_bit = paramtab()
15023 .read()
15024 .ok()
15025 .and_then(|t| t.get(&n).map(|p| p.node.flags as u32 & PM_READONLY))
15026 .unwrap_or(0);
15027 if ro_bit != 0 {
15028 if let Ok(mut tab) = paramtab().write() {
15029 if let Some(pm) = tab.get_mut(&n) {
15030 pm.node.flags &= !(ro_bit as i32);
15031 }
15032 }
15033 }
15034 setsparam(&n, &val);
15035 if ro_bit != 0 {
15036 if let Ok(mut tab) = paramtab().write() {
15037 if let Some(pm) = tab.get_mut(&n) {
15038 pm.node.flags |= ro_bit as i32;
15039 }
15040 }
15041 }
15042 }
15043 }
15044 }
15045 for (n, arr) in deferred_arrays {
15046 // c:5921 `pm->gsu.a->setfn(pm, tpm->u.arr)`.
15047 let _ = assignaparam(&n, arr, 0);
15048 }
15049 // c:5864-5880 (USE_LOCALE) — "Locale changed --- ensure it is restored."
15050 // Runs after the deferred setfn replays above, which are where this
15051 // port performs scanendscope's value restores. A shadowed LC_ALL whose
15052 // outer binding is unset replays nothing, so without this block the
15053 // process locale stayed at the local's value after the function
15054 // returned (`() { local LC_ALL=C }` left LC_CTYPE at C).
15055 if LC_UPDATE_NEEDED.load(Ordering::SeqCst) != 0 {
15056 match getsparam_u("LC_ALL").filter(|v| !v.is_empty()) {
15057 Some(val) => {
15058 let c = std::ffi::CString::new(val.as_bytes()).unwrap_or_default();
15059 unsafe {
15060 libc::setlocale(libc::LC_ALL, c.as_ptr()); // c:5869
15061 }
15062 }
15063 None => {
15064 if let Some(val) = getsparam_u("LANG").filter(|v| !v.is_empty()) {
15065 setlang(Some(&val)); // c:5873
15066 }
15067 for (name, category) in LC_NAMES {
15068 // c:5874
15069 if let Some(val) = getsparam_u(name).filter(|v| !v.is_empty()) {
15070 let c = std::ffi::CString::new(val.as_bytes()).unwrap_or_default();
15071 unsafe {
15072 libc::setlocale(*category, c.as_ptr()); // c:5876
15073 }
15074 }
15075 }
15076 }
15077 }
15078 clear_mbstate(); // c:5879 LC_CTYPE may have changed
15079 }
15080
15081 // c:5890-5894 — `for (Param pm; refs && (pm = getlinknode(refs));) {
15082 // if ((pm->flags & PM_NAMEREF) && !(pm->flags & PM_UNSET) &&
15083 // !(pm->flags & PM_UPPER) && pm->base > locallevel) {
15084 // pm->base = 0; setscope(pm); } }`
15085 // Reset PM_NAMEREF refs whose base was above the popped scope.
15086 //
15087 // setscope() recomputes pm->base from the level of the binding its
15088 // refname NOW resolves to (c:6402-6410), so a ref that pointed at a
15089 // local of the popped scope rebinds to the enclosing binding of the
15090 // same name and is pushed onto that scope's list in turn.
15091 //
15092 // SCOPEREFS records names, not structs, so the pushed structs are
15093 // found by walking the name's `old` chain; each one is addressed to
15094 // `setscope_by_name` by its level (a ref can be hidden by a same-name
15095 // local and still be on this list).
15096 if !refs_snapshot.is_empty() {
15097 let mut names: Vec<&String> = Vec::new();
15098 for name in refs_snapshot.iter() {
15099 if !names.contains(&name) {
15100 names.push(name);
15101 }
15102 }
15103 for name in names {
15104 // c:5891-5893 — the qualifying structs, reset to base 0.
15105 let mut levels: Vec<i32> = Vec::new();
15106 if let Ok(mut tab) = paramtab().write() {
15107 let mut cur = tab.get_mut(name.as_str()).map(|b| b.as_mut());
15108 while let Some(pm) = cur {
15109 let f = pm.node.flags as u32;
15110 if (f & PM_NAMEREF) != 0
15111 && (f & PM_UNSET) == 0
15112 && (f & PM_UPPER) == 0
15113 && pm.base > ll
15114 {
15115 pm.base = 0; // c:5893
15116 levels.push(pm.level);
15117 }
15118 cur = pm.old.as_deref_mut();
15119 }
15120 }
15121 for lvl in levels {
15122 setscope_by_name(name, Some(lvl)); // c:5894 setscope(pm)
15123 }
15124 }
15125 }
15126 // c:5896 — clear out the now-popped scope's refs list.
15127 SCOPEREFS.with(|sr| {
15128 let mut sr = sr.borrow_mut();
15129 if (old_ll as usize) < sr.len() {
15130 sr[old_ll as usize].clear();
15131 }
15132 });
15133 // c:5896-5902 — `/* Delete unset global variables that were hidden at
15134 // unset time */ if ((refs = scoperefs ? scoperefs[0] : NULL)) {
15135 // scoperefs[0] = NULL; for (...) if ((pm->node.flags & PM_UNSET) &&
15136 // !(pm->node.flags & PM_DECLARED)) unsetparam_pm(pm, 1, 0); }`
15137 let hidden_globals: Vec<String> =
15138 SCOPEREFS.with(|sr| sr.borrow_mut().first_mut().map(std::mem::take).unwrap_or_default());
15139 for name in hidden_globals {
15140 let mut tab = paramtab().write().unwrap();
15141 let Some(top) = tab.get(&name) else { continue };
15142 let dead = |p: ¶m| {
15143 let f = p.node.flags as u32;
15144 p.level == 0 && (f & PM_UNSET) != 0 && (f & PM_DECLARED) == 0
15145 };
15146 if dead(top) {
15147 // Uncovered: unsetparam_pm's removenode (c:3874).
15148 tab.remove(&name);
15149 drop(tab);
15150 let _ = paramtab_hashed_storage()
15151 .lock()
15152 .ok()
15153 .as_deref_mut()
15154 .map(|m| m.remove(&name));
15155 } else {
15156 let mut cur = top.old.as_deref();
15157 let mut still_hidden = false;
15158 while let Some(p) = cur {
15159 if dead(p) {
15160 still_hidden = true;
15161 break;
15162 }
15163 cur = p.old.as_deref();
15164 }
15165 drop(tab);
15166 if still_hidden {
15167 // c:3864-3868 — still hidden: back on scoperefs[0].
15168 SCOPEREFS.with(|sr| {
15169 let mut sr = sr.borrow_mut();
15170 if sr.is_empty() {
15171 sr.resize(8, Vec::new());
15172 }
15173 sr[0].insert(0, name.clone());
15174 });
15175 }
15176 }
15177 }
15178 unqueue_signals();
15179}
15180
15181/// Port of `scanendscope()` from `Src/params.c:5900`. Per-node
15182/// callback used by `endparamscope` (params.c:5867 calls
15183/// `scanhashtable(paramtab, 0, 0, 0, scanendscope, 0)`) when a
15184/// function returns. C body:
15185/// ```c
15186/// Param pm = (Param)hn;
15187/// if (pm->level > locallevel) {
15188/// if ((pm->node.flags & (PM_SPECIAL|PM_REMOVABLE)) == PM_SPECIAL) {
15189/// /* Non-removable special — restore from pm->old in-place. */
15190/// Param tpm = pm->old;
15191/// #ifdef USE_LOCALE
15192/// if (!strncmp(pm->node.nam, "LC_", 3) ||
15193/// !strcmp(pm->node.nam, "LANG"))
15194/// lc_update_needed = 1;
15195/// #endif
15196/// if (!strcmp(pm->node.nam, "SECONDS")) {
15197/// setsecondstype(pm, PM_TYPE(tpm->node.flags),
15198/// PM_TYPE(pm->node.flags));
15199/// setrawseconds(tpm->u.dval);
15200/// tpm->node.flags |= PM_NORESTORE;
15201/// }
15202/// pm->old = tpm->old;
15203/// pm->node.flags = (tpm->node.flags & ~PM_NORESTORE);
15204/// pm->level = tpm->level;
15205/// pm->base = tpm->base;
15206/// pm->width = tpm->width;
15207/// if (pm->env) delenv(pm);
15208/// if (!(tpm->node.flags & (PM_NORESTORE|PM_READONLY)))
15209/// switch (PM_TYPE(pm->node.flags)) {
15210/// case PM_SCALAR: case PM_NAMEREF:
15211/// pm->gsu.s->setfn(pm, tpm->u.str); break;
15212/// case PM_INTEGER:
15213/// pm->gsu.i->setfn(pm, tpm->u.val); break;
15214/// case PM_EFLOAT: case PM_FFLOAT:
15215/// pm->gsu.f->setfn(pm, tpm->u.dval); break;
15216/// case PM_ARRAY:
15217/// pm->gsu.a->setfn(pm, tpm->u.arr); break;
15218/// case PM_HASHED:
15219/// pm->gsu.h->setfn(pm, tpm->u.hash); break;
15220/// }
15221/// zfree(tpm, sizeof(*tpm));
15222/// if (pm->node.flags & PM_EXPORTED) export_param(pm);
15223/// } else
15224/// unsetparam_pm(pm, 0, 0);
15225/// }
15226/// ```
15227/// Rust port mirrors the structure 1:1. `locallevel` is read via
15228/// the ported global `crate::ported::params::locallevel` (atomic).
15229/// `setsecondstype` (params.rs:6183), `setrawseconds` (params.rs:6169),
15230/// and `delenv` (params.rs:7591) are all ported.
15231pub fn scanendscope(pm: &mut param, _flags: i32) {
15232 // c:5900
15233 let cur_local = locallevel.load(Ordering::Relaxed);
15234 if pm.level <= cur_local {
15235 // c:5903
15236 return;
15237 }
15238 let pmflags = pm.node.flags as u32;
15239 if (pmflags & (PM_SPECIAL | PM_REMOVABLE)) == PM_SPECIAL {
15240 // Take ownership of the saved old param.
15241 let mut tpm = match pm.old.take() {
15242 Some(t) => t,
15243 None => {
15244 // C uses DPUTS — fatal in debug, silent in release.
15245 return;
15246 }
15247 };
15248
15249 // USE_LOCALE branch: LC_*/LANG bumps LC_UPDATE_NEEDED.
15250 if pm.node.nam.starts_with("LC_") || pm.node.nam == "LANG" {
15251 LC_UPDATE_NEEDED.store(1, Ordering::SeqCst);
15252 }
15253
15254 if pm.node.nam == "SECONDS" {
15255 // setsecondstype(pm, PM_TYPE(tpm.flags), PM_TYPE(pm.flags));
15256 // setrawseconds(tpm.u_dval);
15257 tpm.node.flags |= PM_NORESTORE as i32;
15258 }
15259
15260 // pm->old = tpm->old;
15261 pm.old = tpm.old.take();
15262 // pm->node.flags = tpm->node.flags & ~PM_NORESTORE;
15263 pm.node.flags = (tpm.node.flags as u32 & !PM_NORESTORE) as i32;
15264 pm.level = tpm.level;
15265 pm.base = tpm.base;
15266 pm.width = tpm.width;
15267
15268 if pm.env.is_some() {
15269 delenv(&pm.node.nam);
15270 pm.env = None;
15271 }
15272
15273 let restore = (tpm.node.flags as u32 & (PM_NORESTORE | PM_READONLY)) == 0;
15274 if restore {
15275 match PM_TYPE(pm.node.flags as u32) {
15276 t if t == PM_SCALAR || t == PM_NAMEREF => {
15277 // pm->gsu.s->setfn(pm, tpm->u.str)
15278 pm.u_str = tpm.u_str.clone();
15279 }
15280 t if t == PM_INTEGER => {
15281 pm.u_val = tpm.u_val;
15282 }
15283 t if t == PM_EFLOAT || t == PM_FFLOAT => {
15284 pm.u_dval = tpm.u_dval;
15285 }
15286 t if t == PM_ARRAY => {
15287 pm.u_arr = tpm.u_arr.clone();
15288 }
15289 t if t == PM_HASHED => {
15290 pm.u_hash = tpm.u_hash.take();
15291 }
15292 _ => {}
15293 }
15294 }
15295 // zfree(tpm) — Rust drops the Box at end of scope.
15296 drop(tpm);
15297
15298 if (pm.node.flags as u32 & PM_EXPORTED) != 0 {
15299 export_param(pm);
15300 }
15301 } else {
15302 unsetparam_pm(pm, 0, 0);
15303 }
15304}
15305
15306/// Port of `freeparamnode()` from `Src/params.c:5977`. Frees a Param node, including
15307/// running its unsetfn callback when the global `delunset` flag
15308/// is set.
15309///
15310/// C body:
15311/// if (delunset)
15312/// pm->gsu.s->unsetfn(pm, 1); // c:5977
15313/// zsfree(pm->node.nam); // c:5977
15314/// if (!(pm->flags & PM_SPECIAL)) // c:5977
15315/// zsfree(pm->ename); // c:5977
15316/// zfree(pm, sizeof(struct param)); // c:5977
15317///
15318/// Rust's Drop handles every zsfree/zfree above; the explicit
15319/// step here is the optional unsetfn dispatch when `DELUNSET` is
15320/// non-zero. The remaining drop cascade fires when `_hn`
15321/// (`Box<param>`) leaves scope.
15322pub fn freeparamnode(mut _hn: Param) {
15323 // c:5977
15324 // c:5977-5987 — `if (delunset) pm->gsu.s->unsetfn(pm, 1);`.
15325 if DELUNSET.load(Ordering::Relaxed) != 0 {
15326 // The Rust port's stdunsetfn writes the unset state back to
15327 // paramtab; calling it on the about-to-drop param re-marks
15328 // its slot in the table so consumers that read the table
15329 // see PM_UNSET on the next lookup.
15330 stdunsetfn(_hn.as_mut(), 1); // c:5987
15331 }
15332 // c:5988-5992 — drop cascade frees nam / ename (non-PM_SPECIAL)
15333 // / struct itself when _hn goes out of scope.
15334}
15335
15336/// Port of `printparamvalue()` from `Src/params.c:6035`. C body
15337/// dispatches on `PM_TYPE(p->node.flags)` and writes the value
15338/// (no `name=` prefix unless `!PRINT_KV_PAIR`, which prints `=`
15339/// first). PM_SCALAR/PM_NAMEREF: `quotedzputs(t)`; PM_INTEGER:
15340/// `printf("%ld")`; PM_EFLOAT/PM_FFLOAT: `convfloat(...)`;
15341/// PM_ARRAY: `( v1 v2 ... )` with `\n ` separators on
15342/// PRINT_LINE; PM_HASHED: same shape via scan callback.
15343pub fn printparamvalue(p: &mut param, printflags: i32) {
15344 if (printflags & PRINT_KV_PAIR) == 0 {
15345 print!("=");
15346 }
15347 let t = PM_TYPE(p.node.flags as u32);
15348 if t == PM_SCALAR || t == PM_NAMEREF {
15349 // c:Src/params.c:6052 — `t = pm->gsu.s->getfn(pm)` then
15350 // quotedzputs. For SPECIAL params, the gsu_s vtable returns
15351 // the live value (HOME/PATH/IFS from globals or env). Direct
15352 // dispatch avoids the deadlock that getsparam would hit if
15353 // the caller holds paramtab write lock. Fallback chain:
15354 // gsu_s.getfn(pm) → pm.u_str → env::var(name)
15355 // The env fallback covers exported scalars whose gsu_s wasn't
15356 // wired (vm_helper bootstrap path doesn't run createparam for
15357 // every special).
15358 let mut s = if let Some(gsu) = &p.gsu_s {
15359 (gsu.getfn)(p)
15360 } else {
15361 strgetfn(p)
15362 };
15363 if s.is_empty() && (p.node.flags as u32 & PM_EXPORTED) != 0 {
15364 if let Ok(v) = std::env::var(&p.node.nam) {
15365 s = v;
15366 }
15367 }
15368 // c:Src/params.c::printparamvalue — for scalar SPECIALS like `-`
15369 // (shell flags via dashgetfn, `${(t)-}` is `scalar-readonly-special`)
15370 // that have no gsu_s wired but do have a live getter routed through
15371 // lookup_special_var, fall back to getsparam if both gsu_s/strgetfn
15372 // returned empty. Mirrors the same dispatch the PM_INTEGER arm uses.
15373 // Bug #516.
15374 //
15375 // The PM_SPECIAL gate is required and was missing. getsparam applies
15376 // the PM_LEFT/PM_RIGHT_B/PM_RIGHT_Z padding (that is C's VALFLAG_SUBST
15377 // behaviour — the width attributes transform the value at EXPANSION),
15378 // so an ungated fallback padded any ORDINARY parameter that happened to
15379 // be empty:
15380 // typeset -L5 v; typeset -p v → zsh `v=''`, was `v=' '`
15381 // while `typeset -L5 v=abc` printed `abc` correctly — the fallback only
15382 // fires when the value is empty, which is exactly why the bug looked
15383 // like a storage problem. It is not: the store holds "" in both shells
15384 // (`typeset -L5 v; typeset +L v; typeset -p v` gives `v=''` in each);
15385 // only this read padded it. C's printparamvalue prints the raw value
15386 // and never re-enters the substitution path.
15387 //
15388 // Gated the same way as the PM_EXPORTED env fallback just above.
15389 if s.is_empty() && (p.node.flags as u32 & PM_SPECIAL) != 0 {
15390 if let Some(v) = crate::ported::params::getsparam(&p.node.nam) {
15391 s = v;
15392 }
15393 }
15394 let _ = std::io::Write::write_all(&mut std::io::stdout(), &crate::ported::utils::unmetafy_str("edzputs(&s))); // c:6053
15395 } else if t == PM_INTEGER {
15396 // c:Src/params.c:6051-6057 PM_INTEGER arm — C calls
15397 // `printf("%ld", p->gsu.i->getfn(p))` (or output64 on 64-bit
15398 // builds). Always emits DECIMAL — `typeset -i 16 n=255` prints
15399 // `n=255`, NOT `n=16#FF`. The base formatting only applies to
15400 // `$n` expansion + `print -- $n`, not `typeset -p n`.
15401 //
15402 // The previous Rust port routed through `getsparam` first,
15403 // which calls `convbase` and formats as `16#FF`. That polluted
15404 // typeset -p output. Bug #608.
15405 //
15406 // Use the custom gsu_i.getfn when present (special integers
15407 // like PPID/EUID/SECONDS dispatch through their own getfn);
15408 // fall back to intgetfn (which reads pm.u_val). For lookup-
15409 // special-var integers without a wired gsu_i, fall through to
15410 // getsparam ONLY when u_val is 0 (the "no stored value"
15411 // sentinel) so live PPID etc. still surface.
15412 let getfn_ptr = p.gsu_i.as_ref().map(|g| g.getfn);
15413 let raw = if let Some(getfn) = getfn_ptr {
15414 getfn(p)
15415 } else {
15416 intgetfn(p)
15417 };
15418 if raw == 0 {
15419 if let Some(s) = crate::ported::params::getsparam(&p.node.nam) {
15420 if !s.is_empty() {
15421 // Strip any base#prefix added by convbase so the
15422 // typeset -p output stays decimal-only per C source.
15423 let dec = if let Some(idx) = s.find('#') {
15424 // Try parsing the part after # in the base.
15425 let (base_str, rest) = s.split_at(idx);
15426 let rest = &rest[1..];
15427 if let Ok(b) = base_str.parse::<u32>() {
15428 if (2..=36).contains(&b) {
15429 i64::from_str_radix(rest, b)
15430 .map(|n| n.to_string())
15431 .unwrap_or(s.clone())
15432 } else {
15433 s.clone()
15434 }
15435 } else {
15436 s.clone()
15437 }
15438 } else {
15439 s.clone()
15440 };
15441 print!("{}", dec);
15442 return;
15443 }
15444 }
15445 }
15446 print!("{}", raw);
15447 } else if t == PM_EFLOAT || t == PM_FFLOAT {
15448 // c:6063 — `convfloat(p->gsu.f->getfn(p), p->base, p->node.flags,
15449 // stdout)`. Honors pm.base for precision and
15450 // pm.flags for PM_EFLOAT/PM_FFLOAT format selection. The
15451 // previous Rust port used `print!("{}", floatgetfn(p))`
15452 // which always renders in Rust's default float format
15453 // (which differs from C's printf %g / %e formats).
15454 print!("{}", convfloat(floatgetfn(p), p.base, p.node.flags as u32)); // c:6063
15455 } else if t == PM_ARRAY {
15456 if (printflags & PRINT_KV_PAIR) == 0 {
15457 print!("(");
15458 if (printflags & PRINT_LINE) == 0 {
15459 print!(" ");
15460 }
15461 }
15462 let arr = arrgetfn(p);
15463 if !arr.is_empty() {
15464 if (printflags & PRINT_LINE) != 0 {
15465 if (printflags & PRINT_KV_PAIR) != 0 {
15466 print!(" ");
15467 } else {
15468 print!("\n ");
15469 }
15470 }
15471 // c:Src/params.c:6166-6171 — each array element goes
15472 // through `quotedzputs` so elements containing spaces,
15473 // quotes, or other shell-meta chars round-trip through
15474 // `eval`. zshrs previously emitted bare elements; output
15475 // like `( red blue green yellow )` for `(red "blue green"
15476 // yellow)` was 4 words when re-parsed, not 3. Bug #181
15477 // in docs/BUGS.md.
15478 let _ = std::io::Write::write_all(&mut std::io::stdout(), &crate::ported::utils::unmetafy_str("edzputs(&arr[0])));
15479 for el in &arr[1..] {
15480 if (printflags & PRINT_LINE) != 0 {
15481 print!("\n ");
15482 } else {
15483 print!(" ");
15484 }
15485 let _ = std::io::Write::write_all(&mut std::io::stdout(), &crate::ported::utils::unmetafy_str("edzputs(el)));
15486 }
15487 if (printflags & (PRINT_LINE | PRINT_KV_PAIR)) == PRINT_LINE {
15488 println!();
15489 }
15490 }
15491 if (printflags & PRINT_KV_PAIR) == 0 {
15492 if (printflags & PRINT_LINE) == 0 {
15493 print!(" ");
15494 }
15495 print!(")");
15496 }
15497 } else if t == PM_HASHED {
15498 if (printflags & PRINT_KV_PAIR) == 0 {
15499 print!("(");
15500 }
15501 // c:Src/params.c:6108-6110 — `scanhashtable(ht, 1, 0,
15502 // PM_UNSET, ht->printnode, PRINT_KV_PAIR | (printflags &
15503 // PRINT_LINE))`. C source ALWAYS passes PRINT_KV_PAIR when
15504 // scanning hash entries, regardless of the incoming
15505 // printflags. This guarantees the per-entry format is
15506 // `[key]=value` (which is the only syntactically-valid form
15507 // that round-trips through `eval`). The previous Rust port
15508 // only used `[k]=v` when PRINT_TYPESET / PRINT_KV_PAIR was
15509 // already set on the outer call, falling back to bare
15510 // `k v` form otherwise — so `typeset h` for an assoc
15511 // printed `h=''` instead of `h=( [a]=1 [b]=2 )`. Bug #218
15512 // in docs/BUGS.md.
15513 let mut had_entries = false;
15514 if let Ok(stor) = paramtab_hashed_storage().lock() {
15515 if let Some(map) = stor.get(&p.node.nam) {
15516 let mut entries: Vec<(&String, &String)> = map.iter().collect();
15517 entries.sort_by(|(a, _), (b, _)| a.cmp(b));
15518 let mut first = true;
15519 for (k, v) in entries {
15520 if first {
15521 // Leading space before first entry
15522 // (only when not in PRINT_KV_PAIR mode).
15523 if (printflags & PRINT_KV_PAIR) == 0 && (printflags & PRINT_LINE) == 0 {
15524 print!(" ");
15525 }
15526 if (printflags & PRINT_LINE) != 0 {
15527 print!("\n ");
15528 }
15529 first = false;
15530 had_entries = true;
15531 } else if (printflags & PRINT_LINE) != 0 {
15532 print!("\n ");
15533 } else {
15534 print!(" ");
15535 }
15536 // c:6299-6303 — `[key]=value` form per the
15537 // unconditional PRINT_KV_PAIR pass at c:6109. Both the
15538 // KEY and the VALUE go through quotedzputs: C emits
15539 // `putchar('['); quotedzputs(node.nam); printf("]=");
15540 // printparamvalue(...)`. The key MUST be quoted so
15541 // special-character keys (`[#]`, `[$]`, `[*]`, `[a b]`)
15542 // round-trip through `eval "$(typeset -p h)"` — p10k's
15543 // `_p9k_must_init` rebuilds parameter signatures exactly
15544 // this way, so an unquoted `[#]=…` reparsed wrong.
15545 let _ = std::io::Write::write_all(&mut std::io::stdout(), &crate::ported::utils::unmetafy_str(&format!("[{}]={}", quotedzputs(k), quotedzputs(v))));
15546 }
15547 }
15548 }
15549 if (printflags & PRINT_KV_PAIR) == 0 {
15550 // c:Src/params.c — empty assoc prints `( )` (single
15551 // space between parens). Non-empty: `( [k]=v )` (space
15552 // before first + space before close paren). Verified vs
15553 // /opt/homebrew/bin/zsh: `declare -Ax h; declare -p h`
15554 // → `typeset -Ax h=( )`.
15555 if (printflags & PRINT_LINE) == 0 {
15556 print!(" ");
15557 } else if had_entries {
15558 // c:Src/params.c — under PRINT_LINE (`typeset -p1`) the
15559 // closing paren sits on its OWN line after the last
15560 // `[k]=v` entry: `(\n [k]=v\n)`. Without this newline the
15561 // `)` was glued to the last entry (`[k]=v)`).
15562 print!("\n");
15563 }
15564 print!(")");
15565 }
15566 let _ = had_entries;
15567 }
15568}
15569
15570/// Port of `printparamnode()` from `Src/params.c:6123`. Real C
15571/// body is ~200 lines emitting the typeset/declare-style listing
15572/// for one param honouring PRINT_NAMEONLY / PRINT_TYPESET /
15573/// PRINT_KV_PAIR / PRINT_LINE / PRINT_INCLUDEVALUE /
15574/// PRINT_POSIX_READONLY / PRINT_POSIX_EXPORT / PRINT_WITH_NAMESPACE
15575/// and the per-paramtypes attribute table. Faithful direct port
15576/// of the common path: skip-on-`.`-prefix without WITH_NAMESPACE,
15577/// skip-on-PM_UNSET (with the POSIX preserve), AUTOLOAD gating,
15578/// then `nam` + `=value` via `printparamvalue`.
15579pub fn printparamnode(hn: &mut param, mut printflags: i32) {
15580 use crate::ported::zsh_h::PRINT_WITH_NAMESPACE; // c:Src/zsh.h:2187 (1<<9)
15581 // c:Src/params.c — the `argv`/`*`/`@` special array IS the positional
15582 // parameter list (stored in PPARAMS), not this paramtab entry's u_arr
15583 // (which stays empty). Refresh u_arr from PPARAMS so `typeset -p argv`
15584 // shows the positionals instead of `( )`. The all-params `set`/typeset
15585 // listing path already special-cases these (builtin.rs:1315); mirror
15586 // it here for the explicit-name `typeset -p NAME` print.
15587 if matches!(hn.node.nam.as_str(), "argv" | "*" | "@") {
15588 let pp: Vec<String> = PPARAMS
15589 .lock()
15590 .ok()
15591 .map(|p| p.iter().cloned().collect())
15592 .unwrap_or_default();
15593 hn.u_arr = Some(pp);
15594 }
15595 // !!! BASH-MODE GATE (no C counterpart) !!! `declare -p` (PRINT_TYPESET)
15596 // uses bash's reusable format under --bash: `declare -FLAGS name="value"`
15597 // for a scalar, `declare -a name=([i]="v" …)` / `declare -A name=([k]="v"
15598 // …)` for arrays/assocs — not zsh's `typeset` form. Sparse indexed arrays
15599 // list only their live indices. --zsh keeps zsh output (gate). Covers both
15600 // the explicit-name and the all-params listing print paths.
15601 // `export -p` / `readonly -p` use the SAME `declare` form under bash:
15602 // `declare -x FOO="bar"` / `declare -r R="1"`, not POSIX's
15603 // `export FOO=bar`. Each still filters to the parameters it is about,
15604 // which the POSIX branch below does via PRINT_POSIX_EXPORT /
15605 // PRINT_POSIX_READONLY.
15606 let bash_posix_listing = crate::dash_mode::bash_mode()
15607 && (printflags & (PRINT_POSIX_EXPORT | PRINT_POSIX_READONLY)) != 0;
15608 if bash_posix_listing {
15609 let f = hn.node.flags as u32;
15610 if (printflags & PRINT_POSIX_EXPORT) != 0 && (f & PM_EXPORTED) == 0 {
15611 return;
15612 }
15613 if (printflags & PRINT_POSIX_READONLY) != 0 && (f & PM_READONLY) == 0 {
15614 return;
15615 }
15616 }
15617 // dash quotes the value in its `export -p` / `readonly -p` listings
15618 // (`export FOO='bar'`); ksh and mksh print it bare.
15619 if crate::extensions::emulation_output::alias_always_quotes()
15620 && (printflags & (PRINT_POSIX_EXPORT | PRINT_POSIX_READONLY)) != 0
15621 {
15622 let f = hn.node.flags as u32;
15623 let (want, kw) = if (printflags & PRINT_POSIX_EXPORT) != 0 {
15624 (PM_EXPORTED, "export")
15625 } else {
15626 (PM_READONLY, "readonly")
15627 };
15628 if (f & want) == 0 {
15629 return;
15630 }
15631 // !!! DASH-FAITHFUL GATE (no C counterpart) !!! dash lists a declared
15632 // but unset name bare (`export NAME`) and escapes a quote inside the
15633 // value as `'"'"'` (var.c `single_quote`), not the alias form `'\''`.
15634 if crate::dash_mode::dash_faithful() {
15635 if (f & PM_UNSET) != 0 {
15636 println!("{kw} {}", hn.node.nam);
15637 } else {
15638 let val = hn.u_str.clone().unwrap_or_default();
15639 println!("{kw} {}='{}'", hn.node.nam, val.replace('\'', "'\"'\"'"));
15640 }
15641 return;
15642 }
15643 let val = hn.u_str.clone().unwrap_or_default();
15644 println!(
15645 "{kw} {}={}",
15646 hn.node.nam,
15647 crate::extensions::emulation_startup::single_quote(&val)
15648 );
15649 return;
15650 }
15651 if crate::dash_mode::bash_mode() && ((printflags & PRINT_TYPESET) != 0 || bash_posix_listing) {
15652 let fl = hn.node.flags as u32;
15653 let nm = hn.node.nam.clone();
15654 let mut fstr = String::new();
15655 if fl & PM_HASHED != 0 {
15656 fstr.push('A');
15657 } else if fl & PM_ARRAY != 0 {
15658 fstr.push('a');
15659 }
15660 if fl & PM_INTEGER != 0 {
15661 fstr.push('i');
15662 }
15663 if fl & PM_NAMEREF != 0 {
15664 fstr.push('n');
15665 }
15666 if fl & PM_READONLY != 0 {
15667 fstr.push('r');
15668 }
15669 if fl & PM_EXPORTED != 0 {
15670 fstr.push('x');
15671 }
15672 if fl & PM_UPPER != 0 {
15673 fstr.push('u');
15674 }
15675 if fl & PM_LOWER != 0 {
15676 fstr.push('l');
15677 }
15678 let flag_disp = if fstr.is_empty() {
15679 "--".to_string()
15680 } else {
15681 format!("-{}", fstr)
15682 };
15683 // Backslash-escape the chars that are special inside `"…"`.
15684 let esc = |s: &str| -> String {
15685 let mut o = String::with_capacity(s.len());
15686 for c in s.chars() {
15687 if matches!(c, '"' | '\\' | '$' | '`') {
15688 o.push('\\');
15689 }
15690 o.push(c);
15691 }
15692 o
15693 };
15694 if fl & PM_HASHED != 0 {
15695 let m = crate::ported::exec::assoc(&nm).unwrap_or_default();
15696 let body: String = m
15697 .iter()
15698 .map(|(k, v)| {
15699 format!("[{}]={} ", esc(k), crate::dash_mode::bash_quote_element(v))
15700 })
15701 .collect();
15702 println!("declare {} {}=({})", flag_disp, nm, body);
15703 } else if fl & PM_ARRAY != 0 {
15704 let a = hn
15705 .u_arr
15706 .clone()
15707 .or_else(|| crate::ported::exec::array(&nm))
15708 .unwrap_or_default();
15709 let body: String = crate::bash_arrays::live_indices(&nm, a.len())
15710 .iter()
15711 .filter_map(|&i| {
15712 a.get(i)
15713 .map(|v| format!("[{}]={}", i, crate::dash_mode::bash_quote_element(v)))
15714 })
15715 .collect::<Vec<_>>()
15716 .join(" ");
15717 println!("declare {} {}=({})", flag_disp, nm, body);
15718 } else {
15719 // A nameref lists its TARGET NAME, not the value it points at.
15720 let v = if fl & PM_NAMEREF != 0 {
15721 hn.u_str.clone().unwrap_or_default()
15722 } else {
15723 getsparam(&nm).unwrap_or_default()
15724 };
15725 println!("declare {} {}={}", flag_disp, nm, crate::dash_mode::bash_quote_element(&v));
15726 }
15727 return;
15728 }
15729 // !!! KSH93-MODE GATE (no C counterpart) !!! ksh93's `typeset -p` listing:
15730 // a plain scalar prints as bare `name=value`; otherwise `typeset` plus one
15731 // `-X` per attribute (`-x`, `-r`, `-t` first, then the type flags; a width
15732 // or base follows its flag as a separate word, `-Z 3` also lists `-R 3`),
15733 // and array / associative bodies are `(v1 v2)` / `([k]=v …)` with no inner
15734 // padding. Measured against ksh93u+m 1.0.10. Unset, special and autoload
15735 // entries keep the zsh path.
15736 if crate::dash_mode::ksh93_mode()
15737 && (printflags & PRINT_TYPESET) != 0
15738 && (printflags & (PRINT_POSIX_EXPORT | PRINT_POSIX_READONLY)) == 0
15739 && (hn.node.flags as u32 & (PM_UNSET | PM_AUTOLOAD | PM_RO_BY_DESIGN | PM_SPECIAL)) == 0
15740 && !hn.node.nam.starts_with('.')
15741 {
15742 let fl = hn.node.flags as u32;
15743 let nm = hn.node.nam.clone();
15744 let q = crate::dash_mode::ksh93_printf_q;
15745 let mut attrs: Vec<String> = Vec::new();
15746 for (flag, letter) in [(PM_EXPORTED, "x"), (PM_READONLY, "r"), (PM_TAGGED, "t")] {
15747 if fl & flag != 0 {
15748 attrs.push(format!("-{letter}"));
15749 }
15750 }
15751 if fl & PM_HASHED != 0 {
15752 attrs.push("-A".to_string());
15753 } else if fl & PM_ARRAY != 0 {
15754 attrs.push("-a".to_string());
15755 }
15756 let with_num = |letter: &str, n: i32| -> String {
15757 if n != 0 {
15758 format!("-{letter} {n}")
15759 } else {
15760 format!("-{letter}")
15761 }
15762 };
15763 if fl & PM_INTEGER != 0 {
15764 attrs.push(with_num("i", hn.base));
15765 }
15766 if fl & PM_FFLOAT != 0 {
15767 attrs.push(with_num("F", hn.base));
15768 }
15769 if fl & PM_EFLOAT != 0 {
15770 attrs.push(with_num("E", hn.base));
15771 }
15772 if fl & PM_LOWER != 0 {
15773 attrs.push("-l".to_string());
15774 }
15775 if fl & PM_UPPER != 0 {
15776 attrs.push("-u".to_string());
15777 }
15778 if fl & PM_LEFT != 0 {
15779 attrs.push(with_num("L", hn.width));
15780 }
15781 if fl & PM_RIGHT_Z != 0 {
15782 attrs.push(with_num("Z", hn.width));
15783 attrs.push(with_num("R", hn.width));
15784 }
15785 if fl & PM_RIGHT_B != 0 {
15786 attrs.push(with_num("R", hn.width));
15787 }
15788 let head = if attrs.is_empty() {
15789 String::new()
15790 } else {
15791 format!("typeset {} ", attrs.join(" "))
15792 };
15793 if fl & PM_HASHED != 0 {
15794 let mut m: Vec<(String, String)> = crate::ported::exec::assoc(&nm)
15795 .unwrap_or_default()
15796 .into_iter()
15797 .collect();
15798 m.sort();
15799 let body: Vec<String> = m.iter().map(|(k, v)| format!("[{k}]={}", q(v))).collect();
15800 println!("{head}{nm}=({})", body.join(" "));
15801 } else if fl & PM_ARRAY != 0 {
15802 let a = hn
15803 .u_arr
15804 .clone()
15805 .or_else(|| crate::ported::exec::array(&nm))
15806 .unwrap_or_default();
15807 let body: Vec<String> = a.iter().map(|v| q(v)).collect();
15808 println!("{head}{nm}=({})", body.join(" "));
15809 } else {
15810 let v = getsparam(&nm).unwrap_or_default();
15811 let shown = if fl & PM_INTEGER != 0 && hn.base > 1 && hn.base != 10 {
15812 match v.parse::<i64>() {
15813 Ok(n) => convbase(n, hn.base as u32).to_lowercase(),
15814 Err(_) => v,
15815 }
15816 } else {
15817 q(&v)
15818 };
15819 println!("{head}{nm}={shown}");
15820 }
15821 return;
15822 }
15823 // !!! PDKSH-MODE GATE (no C counterpart) !!! mksh's `typeset -p` listing
15824 // (see `pdksh_typeset_p`); special, autoload and associative entries keep the zsh path.
15825 if crate::dash_mode::pdksh_family()
15826 && crate::dash_mode::korn_mode()
15827 && (printflags & PRINT_TYPESET) != 0
15828 && (printflags & (PRINT_POSIX_EXPORT | PRINT_POSIX_READONLY)) == 0
15829 && (hn.node.flags as u32 & (PM_AUTOLOAD | PM_RO_BY_DESIGN | PM_SPECIAL | PM_HASHED)) == 0
15830 && !hn.node.nam.starts_with('.')
15831 {
15832 let fl = hn.node.flags as u32;
15833 // mksh lists nothing for a plain unset name, and `NAME=''` for an unset
15834 // one that carries attributes (`typeset -r k` -> `typeset -r k=''`).
15835 let attr_mask = PM_INTEGER | PM_EXPORTED | PM_READONLY | PM_TAGGED | PM_LEFT | PM_RIGHT_B
15836 | PM_RIGHT_Z | PM_LOWER | PM_UPPER | PM_UNIQUE;
15837 if fl & PM_UNSET != 0 && fl & attr_mask == 0 {
15838 return;
15839 }
15840 let nm = hn.node.nam.clone();
15841 let elems = (fl & PM_ARRAY != 0).then(|| {
15842 let a = hn.u_arr.clone().or_else(|| crate::ported::exec::array(&nm)).unwrap_or_default();
15843 let live = crate::bash_arrays::live_indices(&nm, a.len());
15844 live.into_iter().filter_map(|i| a.get(i).map(|v| (i, v.clone()))).collect()
15845 });
15846 let mut v = getsparam(&nm).unwrap_or_default();
15847 if fl & PM_INTEGER != 0 && hn.base > 1 && hn.base != 10 {
15848 if let Ok(n) = v.parse::<i64>() {
15849 v = convbase(n, hn.base as u32).to_lowercase();
15850 }
15851 }
15852 for line in crate::dash_mode::pdksh_typeset_p(&nm, fl, hn.width, &v, elems) {
15853 println!("{line}");
15854 }
15855 return;
15856 }
15857 let f = hn.node.flags as u32;
15858 if (f & PM_HASHELEM) == 0
15859 && (printflags & PRINT_WITH_NAMESPACE) == 0
15860 && hn.node.nam.starts_with('.')
15861 {
15862 return;
15863 }
15864 if (f & PM_UNSET) != 0 {
15865 // c:Src/params.c — PM_SPECIAL params (HOME/PATH/PWD/etc.)
15866 // carry PM_UNSET when their u_str cache hasn't been seeded
15867 // but the value is available via the GSU getfn. zshrs's
15868 // -fc init path doesn't run createparamtable() so the env
15869 // import + value-cache step is skipped — yet env::var()
15870 // still gives a live value. Probe getsparam for a
15871 // non-empty value before treating as truly unset so
15872 // `typeset -p HOME` (which goes through this print) emits
15873 // the expected `export HOME=…` line instead of nothing.
15874 let has_special_value =
15875 (f & PM_SPECIAL) != 0 && hn.u_str.is_none() && getsparam(&hn.node.nam).is_some();
15876 // c:6133-6143 — POSIX readonly/exported keep + PM_DEFAULTED
15877 // path: show as readonly/exported even if unset, with no
15878 // value (NAMEONLY).
15879 let posix_keep = (printflags & (PRINT_POSIX_READONLY | PRINT_POSIX_EXPORT)) != 0
15880 && (f & (PM_READONLY | PM_EXPORTED)) != 0;
15881 let defaulted = (f & PM_DEFAULTED) == PM_DEFAULTED; // c:6137
15882 if has_special_value {
15883 // Seed u_str so the value-emit arm below picks it up.
15884 hn.u_str = getsparam(&hn.node.nam);
15885 hn.node.flags &= !(PM_UNSET as i32);
15886 } else if posix_keep || defaulted {
15887 printflags |= PRINT_NAMEONLY;
15888 } else {
15889 return;
15890 }
15891 }
15892 if (f & PM_AUTOLOAD) != 0 || crate::vm_helper::module_param_is_autoload_stub(&hn.node.nam) {
15893 printflags |= PRINT_NAMEONLY;
15894 }
15895 // c:Src/params.c — the outer block runs whenever the attribute
15896 // walk is also wanted: PRINT_TYPE (bare typeset) OR
15897 // PRINT_TYPESET (typeset -p) OR PRINT_POSIX_*. The C source has
15898 // two SEPARATE parallel blocks, but the Rust port nested the
15899 // attribute walk inside the prefix block — bug #42 in
15900 // docs/BUGS.md. Widen the outer gate so PRINT_TYPE also enters
15901 // it; the prefix-print arms below gate themselves on the
15902 // narrower `PRINT_TYPESET | PRINT_POSIX_*` set so bare typeset
15903 // skips the `typeset ` / `export ` / `local ` prefix and goes
15904 // straight to the attribute walk + name=value tail.
15905 if (printflags & (PRINT_TYPE | PRINT_TYPESET | PRINT_POSIX_READONLY | PRINT_POSIX_EXPORT)) != 0
15906 {
15907 let needs_prefix =
15908 (printflags & (PRINT_TYPESET | PRINT_POSIX_READONLY | PRINT_POSIX_EXPORT)) != 0;
15909 // c:6151-6157 — `if (p->node.flags & PM_AUTOLOAD) { /* It's not
15910 // possible to restore the state of these, so don't output. */
15911 // return; }`.
15912 //
15913 // A module parameter nothing has read yet is, in C, exactly such a
15914 // PM_AUTOLOAD stub (Src/module.c:1218-1223 `add_autoparam`), and in a
15915 // shell that never loaded zsh/parameter it does not exist at all.
15916 // zshrs seeds the real specials eagerly (vm_helper::init_partab_params)
15917 // and models the stub state as the MATERIALIZED_MODULE_PARAMS side-set,
15918 // so ask that predicate here — the same one bin_unset consults. Without
15919 // it `readonly -p` in a bare `zsh -f` listed `dis_patchars`,
15920 // `dis_reswords`, `funcfiletrace`, `funcsourcetrace`, `funcstack`,
15921 // `functrace`, `historywords`, `keymaps`, `patchars`, `reswords` and
15922 // `zsh_scheduled_events` alongside the user's own read-only params.
15923 //
15924 // c:Src/module.c:2317 `load_module` sets MOD_INIT_B after
15925 // `do_boot_module` — the same bit `zmodload -e` reads, and the one
15926 // `autoload_param_stubs` (vm_helper.rs:6808-6827) keys on. Until a
15927 // module boots, its `patab[]` rows are not in C's paramtab AT ALL
15928 // (`add_autoparam`, c:Src/module.c:1218-1223, plants a PM_AUTOLOAD
15929 // stub, and c:6150-6155 returns for it), so `readonly -p` in a bare
15930 // `zsh -f` lists none of them. zshrs seeds the rows eagerly, and the
15931 // `MATERIALIZED_MODULE_PARAMS` side-set that stands in for
15932 // PM_AUTOLOAD is STICKY: `typeset -h +g -m '*'; unset -m '*'` reads
15933 // every matched value once, which marks `keymaps` / `zsh_scheduled_events`
15934 // materialized forever — where C's scan hits the function-local
15935 // shadow and `endparamscope` puts the untouched stub straight back.
15936 // So a later `readonly -p` printed `typeset -g -ar keymaps` /
15937 // `typeset -g -ar zsh_scheduled_events` (B02typeset.ztst:47).
15938 // Ask the module's BOOT STATE as well — that is C's actual
15939 // precondition, and unlike the side-set it cannot go stale.
15940 // Verified against the oracle: with `zmodload zsh/zleparameter;
15941 // zmodload zsh/sched` loaded, zsh DOES print `typeset -ar keymaps`,
15942 // `typeset -Ar widgets`, `typeset -ar zsh_scheduled_events` — so the
15943 // suppression must key on boot state, not on the row's identity.
15944 let owning_module_unbooted = crate::vm_helper::AUTOLOAD_PARAMS
15945 .iter()
15946 .find(|(pnam, _)| *pnam == hn.node.nam)
15947 .is_some_and(|(_, modname)| {
15948 crate::ported::module::MODULESTAB
15949 .lock()
15950 .ok()
15951 .map(|t| {
15952 // c:Src/module.c:218-241 — "boot ran" is
15953 // MOD_INIT_B && !MOD_UNLOAD (printmodulenode's test).
15954 !t.modules.get(*modname).is_some_and(|md| {
15955 (md.node.flags & crate::ported::zsh_h::MOD_INIT_B) != 0
15956 && (md.node.flags & crate::ported::zsh_h::MOD_UNLOAD) == 0
15957 })
15958 })
15959 .unwrap_or(false)
15960 });
15961 let autoload_stub = (crate::vm_helper::module_param_is_autoload_stub(&hn.node.nam)
15962 || owning_module_unbooted)
15963 && (crate::ported::modules::parameter::PARTAB
15964 .iter()
15965 .any(|e| e.name == hn.node.nam)
15966 || crate::ported::modules::parameter::PARTAB_ARRAY
15967 .iter()
15968 .any(|e| e.name == hn.node.nam));
15969 if ((f & PM_AUTOLOAD) != 0 || autoload_stub) && needs_prefix {
15970 return;
15971 }
15972 // c:6157-6162 — `if (p->node.flags & PM_RO_BY_DESIGN) {
15973 // if (p->level != locallevel || p->level == 0) return; }`.
15974 // RO_BY_DESIGN specials can't be restored out of context, so
15975 // they're shown only when printed in the scope of their own
15976 // declaration (level matches AND is non-zero). At top level
15977 // (`p->level == 0`) they're always skipped — this is what keeps
15978 // the positional/special params (`*` `@` `#` `?` `-` `!` `$`,
15979 // ARGC, status, HISTCMD, LINENO, PPID, TTYIDLE, ZSH_SUBSHELL)
15980 // out of `typeset -p`. The `|| p->level == 0` clause was missing.
15981 if (f & PM_RO_BY_DESIGN) != 0 && needs_prefix {
15982 let cur_ll = locallevel.load(Ordering::Relaxed) as i32;
15983 if hn.level != cur_ll || hn.level == 0 {
15984 // c:6157-6158
15985 return;
15986 }
15987 }
15988 let mut altname: u8 = 0;
15989 if needs_prefix {
15990 if (printflags & PRINT_POSIX_EXPORT) != 0 {
15991 if (f & PM_EXPORTED) == 0 {
15992 return;
15993 }
15994 altname = b'x';
15995 print!("export ");
15996 } else if (printflags & PRINT_POSIX_READONLY) != 0 {
15997 if (f & PM_READONLY) == 0 {
15998 return;
15999 }
16000 altname = b'r';
16001 print!("readonly ");
16002 } else if (f & PM_EXPORTED) != 0 && (f & (PM_ARRAY | PM_HASHED)) == 0 {
16003 // c:6181-6188 — exported scalar: `local` or `export`.
16004 let cur_ll = locallevel.load(Ordering::Relaxed) as i32;
16005 if hn.level != 0 && hn.level >= cur_ll {
16006 print!("local ");
16007 } else {
16008 altname = b'x';
16009 print!("export ");
16010 }
16011 } else {
16012 let cur_ll = locallevel.load(Ordering::Relaxed) as i32;
16013 if cur_ll != 0 && hn.level >= cur_ll {
16014 if (f & PM_EXPORTED) != 0 {
16015 print!("local ");
16016 } else {
16017 print!("typeset ");
16018 }
16019 } else if cur_ll != 0 {
16020 print!("typeset -g ");
16021 } else {
16022 print!("typeset ");
16023 }
16024 }
16025 }
16026
16027 // c:6199-6259 — attribute walk via pmtypes table. Each row
16028 // tests `p->node.flags & binflag`; on match, PRINT_TYPESET
16029 // emits the `-X` letter, PRINT_TYPE the long word.
16030 // Port of `Src/params.c:6010 static const struct paramtypes
16031 // pmtypes[]`.
16032 const PMTF_USE_BASE: u32 = 1 << 0;
16033 const PMTF_USE_WIDTH: u32 = 1 << 1;
16034 const PMTF_TEST_LEVEL: u32 = 1 << 2;
16035 struct PmType {
16036 binflag: u32,
16037 string: &'static str,
16038 typeflag: u8,
16039 flags: u32,
16040 }
16041 const PMTYPES: &[PmType] = &[
16042 PmType {
16043 binflag: PM_AUTOLOAD,
16044 string: "undefined",
16045 typeflag: 0,
16046 flags: 0,
16047 },
16048 PmType {
16049 binflag: PM_INTEGER,
16050 string: "integer",
16051 typeflag: b'i',
16052 flags: PMTF_USE_BASE,
16053 },
16054 PmType {
16055 binflag: PM_EFLOAT,
16056 string: "float",
16057 typeflag: b'E',
16058 flags: 0,
16059 },
16060 PmType {
16061 binflag: PM_FFLOAT,
16062 string: "float",
16063 typeflag: b'F',
16064 flags: 0,
16065 },
16066 PmType {
16067 binflag: PM_ARRAY,
16068 string: "array",
16069 typeflag: b'a',
16070 flags: 0,
16071 },
16072 PmType {
16073 binflag: PM_HASHED,
16074 string: "association",
16075 typeflag: b'A',
16076 flags: 0,
16077 },
16078 PmType {
16079 binflag: 0,
16080 string: "local",
16081 typeflag: 0,
16082 flags: PMTF_TEST_LEVEL,
16083 },
16084 // 5.9.2 pmtypes[] (params.c:5783) has no PM_HIDE row; the dev tree adds
16085 // `{ PM_HIDE, "hide", 'h', 0 }` and `typeset -p` then prints `-h`.
16086 PmType {
16087 binflag: PM_LEFT,
16088 string: "left justified",
16089 typeflag: b'L',
16090 flags: PMTF_USE_WIDTH,
16091 },
16092 PmType {
16093 binflag: PM_RIGHT_B,
16094 string: "right justified",
16095 typeflag: b'R',
16096 flags: PMTF_USE_WIDTH,
16097 },
16098 PmType {
16099 binflag: PM_RIGHT_Z,
16100 string: "zero filled",
16101 typeflag: b'Z',
16102 flags: PMTF_USE_WIDTH,
16103 },
16104 PmType {
16105 binflag: PM_LOWER,
16106 string: "lowercase",
16107 typeflag: b'l',
16108 flags: 0,
16109 },
16110 PmType {
16111 binflag: PM_UPPER,
16112 string: "uppercase",
16113 typeflag: b'u',
16114 flags: 0,
16115 },
16116 PmType {
16117 binflag: PM_READONLY,
16118 string: "readonly",
16119 typeflag: b'r',
16120 flags: 0,
16121 },
16122 PmType {
16123 binflag: PM_TAGGED,
16124 string: "tagged",
16125 typeflag: b't',
16126 flags: 0,
16127 },
16128 PmType {
16129 binflag: PM_EXPORTED,
16130 string: "exported",
16131 typeflag: b'x',
16132 flags: 0,
16133 },
16134 PmType {
16135 binflag: PM_UNIQUE,
16136 string: "unique",
16137 typeflag: b'U',
16138 flags: 0,
16139 },
16140 PmType {
16141 binflag: PM_TIED,
16142 string: "tied",
16143 typeflag: b'T',
16144 flags: 0,
16145 },
16146 PmType {
16147 binflag: PM_NAMEREF,
16148 string: "nameref",
16149 typeflag: b'n',
16150 flags: 0,
16151 },
16152 ];
16153 if (printflags & (PRINT_TYPE | PRINT_TYPESET)) != 0 {
16154 let mut doneminus = false; // c:6200
16155 for pmptr in PMTYPES.iter() {
16156 // c:6204
16157 if altname != 0 && altname == pmptr.typeflag {
16158 // c:6207
16159 continue;
16160 }
16161 // PM_RO_BY_DESIGN-expansion for the readonly attribute.
16162 // The C-side IPDEF4 entries declare PM_READONLY_SPECIAL =
16163 // PM_SPECIAL | PM_READONLY | PM_RO_BY_DESIGN
16164 // (c:Src/params.c:351, zsh.h:1925), so both bits are set
16165 // together and matching either is equivalent for them
16166 // (`init_partab_params` stamps the full triple). The
16167 // expansion is kept so the walk still reports "readonly"
16168 // for any row that reaches here carrying only the
16169 // by-design bit; mirrors the `bin_typeset` listing filter
16170 // at builtin.rs:3620. Bug #297 in docs/BUGS.md.
16171 //
16172 // GATE on `!PM_REMOVABLE`: the IPDEF4 specials are NOT
16173 // removable, but `private` vars (c:Modules/param_private.c:
16174 // 174 — PM_HIDE|PM_SPECIAL|PM_REMOVABLE|PM_RO_BY_DESIGN)
16175 // carry PM_RO_BY_DESIGN WITHOUT PM_READONLY, exactly as in
16176 // C, and must NOT print "readonly". The C walk prints
16177 // "readonly" only from the real PM_READONLY bit, which a
16178 // private var never has. Without this gate the expansion
16179 // mis-fires on private vars — V10private.ztst:31 expects
16180 // `local hide scalar_test`, not `local hide readonly
16181 // scalar_test`.
16182 let effective_binflag = if pmptr.binflag == PM_READONLY && (f & PM_REMOVABLE) == 0 {
16183 PM_READONLY | crate::ported::zsh_h::PM_RO_BY_DESIGN
16184 } else {
16185 pmptr.binflag
16186 };
16187 let doprint = if (pmptr.flags & PMTF_TEST_LEVEL) != 0 {
16188 // c:6209
16189 hn.level != 0 // c:6211
16190 } else if (pmptr.binflag != PM_EXPORTED
16191 || hn.level != 0
16192 || (f & (PM_LOCAL | PM_ARRAY | PM_HASHED)) != 0)
16193 && (f & effective_binflag) != 0
16194 {
16195 // c:6225-6227
16196 true
16197 } else {
16198 false
16199 };
16200 if doprint {
16201 // c:6230
16202 if (printflags & PRINT_TYPESET) != 0 {
16203 // c:6231
16204 if pmptr.typeflag != 0 {
16205 // c:6232
16206 if !doneminus {
16207 // c:6233
16208 print!("-"); // c:6234
16209 doneminus = true;
16210 }
16211 print!("{}", pmptr.typeflag as char); // c:6237
16212 }
16213 } else {
16214 print!("{} ", pmptr.string); // c:6240
16215 }
16216 if (pmptr.flags & PMTF_USE_BASE) != 0 && hn.base != 0 {
16217 // c:6242
16218 print!("{} ", hn.base); // c:6243
16219 doneminus = false;
16220 }
16221 if (pmptr.flags & PMTF_USE_WIDTH) != 0 && hn.width != 0 {
16222 // c:6245
16223 print!("{} ", hn.width); // c:6246
16224 doneminus = false;
16225 }
16226 }
16227 }
16228 if doneminus {
16229 // c:6252
16230 print!(" ");
16231 }
16232 }
16233
16234 // c:Src/params.c:6256-6285 — PM_TIED partner emission. For
16235 // tied scalar/array pairs (PATH↔path, FPATH↔fpath, etc.) the
16236 // partner name is printed before the entry's own name, and
16237 // for `typeset -p` on the SCALAR side the value is swapped to
16238 // the ARRAY peer's contents (so the output is re-parseable
16239 // and doesn't collapse `(a b c)` vs `('a b c')` to the same
16240 // colon-string). Bug #410.
16241 // c:6204/c:6257 — the tied peer is printed only inside the
16242 // `if (printflags & (PRINT_TYPE|PRINT_TYPESET))` attribute block, so
16243 // the POSIX `readonly -p` / `export -p` forms never show it.
16244 if (f & PM_TIED) != 0 && (printflags & (PRINT_TYPE | PRINT_TYPESET)) != 0 {
16245 if let Some(ename) = hn.ename.clone() {
16246 // c:Src/params.c:6275 `paramtab->getnode(paramtab,
16247 // p->ename)`. The bin_typeset caller pre-clones the
16248 // pm before calling printparamnode (see builtin.rs)
16249 // so no paramtab lock is held here. Read the peer
16250 // directly. Bug #410.
16251 let peer_info: Option<(String, Vec<String>)> =
16252 paramtab().read().ok().and_then(|t| {
16253 t.get(&ename).map(|peer_pm| {
16254 (
16255 peer_pm.node.nam.clone(),
16256 peer_pm.u_arr.clone().unwrap_or_default(),
16257 )
16258 })
16259 });
16260 if let Some((peer_name, peer_arr)) = peer_info {
16261 let typeset_mode = (printflags & PRINT_TYPESET) != 0;
16262 let we_are_scalar = (f & PM_ARRAY) == 0;
16263 if typeset_mode && we_are_scalar {
16264 // c:6280-6284 — swap p with the array peer so
16265 // value emission uses the array form. Print
16266 // OUR name first (the scalar side), then
16267 // swap.
16268 let _ = std::io::Write::write_all(&mut std::io::stdout(), &crate::ported::utils::unmetafy_str(&format!("{} ", quotedzputs(&hn.node.nam))));
16269 hn.node.nam = peer_name;
16270 hn.u_arr = Some(peer_arr);
16271 hn.u_str = None;
16272 // Flip the PM_TYPE bits to PM_ARRAY so
16273 // printparamvalue dispatches the array arm.
16274 // PM_TYPE is a const fn that masks the
16275 // type-bits union (PM_SCALAR | PM_INTEGER |
16276 // PM_EFLOAT | PM_FFLOAT | PM_ARRAY | PM_HASHED
16277 // | PM_NAMEREF). Clear them all, then set
16278 // PM_ARRAY.
16279 let type_mask = crate::ported::zsh_h::PM_SCALAR
16280 | crate::ported::zsh_h::PM_INTEGER
16281 | crate::ported::zsh_h::PM_EFLOAT
16282 | crate::ported::zsh_h::PM_FFLOAT
16283 | crate::ported::zsh_h::PM_ARRAY
16284 | crate::ported::zsh_h::PM_HASHED
16285 | crate::ported::zsh_h::PM_NAMEREF;
16286 hn.node.flags = (hn.node.flags & !(type_mask as i32)) | (PM_ARRAY as i32);
16287 // Drop the scalar getfn so printparamvalue
16288 // doesn't pull from the scalar gsu (which
16289 // would resolve PATH-the-colon-string).
16290 hn.gsu_s = None;
16291 } else {
16292 // c:6286 — non-swap path: just print peer's
16293 // name + space. The downstream name+value
16294 // emission still uses hn (our own data).
16295 let _ = std::io::Write::write_all(&mut std::io::stdout(), &crate::ported::utils::unmetafy_str(&format!("{} ", quotedzputs(&peer_name))));
16296 }
16297 }
16298 }
16299 }
16300 }
16301 if (printflags & PRINT_KV_PAIR) != 0 {
16302 // hashelem path: print key without name= leader.
16303 }
16304 // c:Src/params.c:6290 — `quotedzputs(p->node.nam, stdout)`. Names
16305 // containing shell metacharacters get single-quoted so the
16306 // output is re-parseable (`'#'=0`, `'$'=2609`, `'?'=0`). Plain
16307 // identifiers pass through unchanged. Bug #97 in docs/BUGS.md:
16308 // bare `print!("{}", nam)` produced unquoted `#=0` etc.
16309 let _ = std::io::Write::write_all(&mut std::io::stdout(), &crate::ported::utils::unmetafy_str("edzputs(&hn.node.nam)));
16310 // c:6289 — `(printflags & PRINT_NAMEONLY) ||
16311 // ((p->node.flags & PM_HIDEVAL) && !(printflags & PRINT_INCLUDEVALUE))`
16312 // PM_HIDEVAL (set by `typeset -H`, see TYPESET_OPTSTR position
16313 // 15 in Src/builtin.c bin_typeset → PM_HIDEVAL = 1<<15) hides
16314 // the value in `typeset -p` output. Bare `typeset NAME` passes
16315 // PRINT_INCLUDEVALUE (Src/builtin.c:2246) which overrides the
16316 // hide. Bug #233 in docs/BUGS.md — printparamnode didn't honor
16317 // PM_HIDEVAL, so `typeset -H X=hello; typeset -p X` printed the
16318 // value instead of just the name.
16319 let hideval = (f & PM_HIDEVAL) != 0 && (printflags & PRINT_INCLUDEVALUE) == 0;
16320 if (printflags & PRINT_NAMEONLY) != 0 || hideval {
16321 if (printflags & PRINT_KV_PAIR) == 0 {
16322 println!();
16323 }
16324 return;
16325 }
16326 if (printflags & (PRINT_INCLUDEVALUE | PRINT_TYPESET)) != 0
16327 || (printflags & PRINT_NAMEONLY) == 0
16328 {
16329 printparamvalue(hn, printflags);
16330 }
16331 // c:Src/params.c:6305-6318 —
16332 // if (peer && (printflags & PRINT_TYPESET) &&
16333 // !(p->node.flags & PM_SPECIAL)) {
16334 // /* append the join char for tied parameters if different from
16335 // * colon for typeset -p output. */
16336 // unsigned char joinchar =
16337 // (unsigned char)((struct tieddata *)peer->u.data)->joinchar;
16338 // if (joinchar != ':') { … putchar(' '); quotedzputs(buf, stdout); }
16339 // }
16340 // `peer` is always the SCALAR half under PRINT_TYPESET (c:6280-6285), and
16341 // the scalar is where the tieddata rides — so in the swapped scalar path
16342 // `hn.u_tied` still holds it (the swap above rewrites only nam/value/type),
16343 // and in the array path it is reached through `hn.ename`. Missing entirely
16344 // before, so `typeset -TU MORESTUFF morestuff '-'; typeset -p MORESTUFF`
16345 // printed no trailing ` -` and the output was not re-parseable.
16346 if (printflags & PRINT_TYPESET) != 0
16347 && (f & PM_TIED) != 0
16348 && (hn.node.flags as u32 & PM_SPECIAL) == 0
16349 {
16350 let joinchar: Option<i32> = hn.u_tied.as_deref().map(|td| td.joinchar).or_else(|| {
16351 hn.ename.as_deref().and_then(|e| {
16352 paramtab().read().ok().and_then(|t| {
16353 t.get(e)
16354 .and_then(|p| p.u_tied.as_deref().map(|td| td.joinchar))
16355 })
16356 })
16357 });
16358 if let Some(jc) = joinchar {
16359 if jc != ':' as i32 {
16360 // c:6313-6316 — one-char buf, space-separated, quoted.
16361 // `buf[0] = joinchar; buf[1] = '\0';` — a NUL join character
16362 // is an EMPTY C string, so it prints as `''`.
16363 let buf = if jc == 0 { String::new() } else { ((jc as u8) as char).to_string() };
16364 let _ = std::io::Write::write_all(&mut std::io::stdout(), &crate::ported::utils::unmetafy_str(&format!(" {}", quotedzputs(&buf))));
16365 }
16366 }
16367 }
16368 if (printflags & PRINT_KV_PAIR) == 0 {
16369 println!();
16370 }
16371}
16372
16373/// Port of `resolve_nameref()` from `Src/params.c:6325`. C body:
16374/// ```c
16375/// mod_export Param
16376/// resolve_nameref(Param pm)
16377/// {
16378/// return resolve_nameref_rec(pm, NULL, 0);
16379/// }
16380/// ```
16381/// Public entry point that walks the nameref alias chain to the
16382/// final non-nameref `param`. Stop-pm and keep_lastref are
16383/// internal; this wrapper hardcodes both per the C body.
16384/// WARNING: param names don't match C — Rust=() vs C=(pm)
16385pub fn resolve_nameref(
16386 // c:6325
16387 pm: Option<Param>,
16388) -> Option<Param> {
16389 resolve_nameref_rec(pm, None, 0) // c:6327
16390}
16391
16392/// Port of `resolve_nameref_rec()` from `Src/params.c:6332`. C
16393/// recursive helper for `resolve_nameref()`. Walks the chain of
16394/// nameref indirections via `gethashnode2(realparamtab, refname)`
16395/// + `loadparamnode(paramtab, upscope(pm, ref), refname)`,
16396/// checking PM_TAGGED for cycle detection, and returns the
16397/// final non-nameref Param. Returns the input `pm` unchanged
16398/// for the early-exit path (no NAMEREF / UNSET / has subscript /
16399/// empty refname). The chain walk delegates to
16400/// `resolve_nameref_name` which operates on the live paramtab by
16401/// name (the Rust table hands out clones, so the by-name walk is
16402/// the canonical chain follower).
16403#[allow(unused_variables)]
16404pub fn resolve_nameref_rec(
16405 pm: Option<Param>,
16406 stop: Option<¶m>,
16407 keep_lastref: i32,
16408) -> Option<Param> {
16409 let pm_ref = pm.as_deref()?;
16410 let f = pm_ref.node.flags as u32;
16411 // c:6336-6339 — early exits return pm unchanged.
16412 if (f & PM_NAMEREF) == 0 || (f & PM_UNSET) != 0 || pm_ref.width != 0 {
16413 return pm;
16414 }
16415 let refname = pm_ref.u_str.as_deref().unwrap_or("");
16416 if refname.is_empty() {
16417 if keep_lastref != 0 {
16418 return pm; // c:6353-6354
16419 }
16420 return pm; // empty refname is also an early-exit shape (c:6339)
16421 }
16422 if (f & PM_TAGGED) != 0 {
16423 // c:6340-6343 — `zerr("%s: invalid self reference", pm->node.nam)`.
16424 let nam = pm_ref.node.nam.clone();
16425 zerr(&format!("{}: invalid self reference", nam));
16426 return None;
16427 }
16428 let stop_key = stop.map(|s| (s.node.nam.clone(), s.level));
16429 match crate::ported::params::resolve_nameref_name(
16430 &pm_ref.node.nam,
16431 stop_key.as_ref().map(|(n, l)| (n.as_str(), *l)),
16432 ) {
16433 crate::ported::params::nameref_resolution::Target {
16434 pm: Some(target), ..
16435 } => Some(target),
16436 crate::ported::params::nameref_resolution::Target { pm: None, .. } => {
16437 // c:6347 miss + keep_lastref (c:6353-6354).
16438 if keep_lastref != 0 {
16439 pm
16440 } else {
16441 None
16442 }
16443 }
16444 crate::ported::params::nameref_resolution::Placeholder(last, _) => {
16445 // Chain ended on an empty-refname ref: C returns that ref
16446 // (the early-exit at c:6336-6339 of the recursive call).
16447 let tab = paramtab().read().ok()?;
16448 tab.get(&last).cloned().or(pm)
16449 }
16450 crate::ported::params::nameref_resolution::SelfRef => None, // c:6343
16451 crate::ported::params::nameref_resolution::OutOfScope => None, // c:6347-6349
16452 crate::ported::params::nameref_resolution::NotRef => pm,
16453 }
16454}
16455
16456/// ```c
16457/// Param pm = (Param) gethashnode2(realparamtab, name);
16458/// if (pm && (pm->node.flags & PM_NAMEREF)) {
16459/// if (pm->node.flags & PM_READONLY) {
16460/// zerr("read-only reference: %s", pm->node.nam); return;
16461/// }
16462/// pm->base = pm->width = 0;
16463/// SETREFNAME(pm, ztrdup(value));
16464/// pm->node.flags &= ~PM_UNSET;
16465/// setscope(pm);
16466/// } else
16467/// setsparam(name, ztrdup(value));
16468/// ```
16469/// `gethashnode2` is the no-autoload paramtab lookup. The
16470/// nameref branch updates the alias target in-place; the normal
16471/// branch falls through to `setsparam`.
16472/// Port of `setloopvar()` from `Src/params.c:6394` (the valid_refname
16473/// check is c:6404-6407, added by 54718).
16474pub fn setloopvar(name: &str, value: &str) {
16475 // c:6367 — `Param pm = (Param) gethashnode2(realparamtab, name);`
16476 // realparamtab and paramtab are the same backing store in zshrs
16477 // (the alias-flip during assoc iteration isn't modelled); the
16478 // operative table is `paramtab()`.
16479 // Scope the write lock so we drop it before calling setsparam below.
16480 let nameref_branch = {
16481 let mut tab = paramtab().write().unwrap();
16482 if let Some(pm) = tab.get_mut(name) {
16483 // c:6369 — `if (pm && (pm->node.flags & PM_NAMEREF))`
16484 if (pm.node.flags as u32 & PM_NAMEREF) != 0 {
16485 // c:6370 — `if (pm->node.flags & PM_READONLY)`
16486 if (pm.node.flags as u32 & PM_READONLY) != 0 {
16487 // c:6372 — `zerr("read-only reference: %s", pm->node.nam);`
16488 let nam = pm.node.nam.clone();
16489 drop(tab); // zerr redraws ZLE, which reads paramtab
16490 zerr(&format!("read-only reference: {}", nam));
16491 // c:6373 — `return;`
16492 return;
16493 }
16494 // c:6404-6407 (54718) — `if (!valid_refname(value,
16495 // pm->node.flags)) { zerr("invalid variable name: %s",
16496 // value); return; }`
16497 if !valid_refname(value, pm.node.flags) {
16498 drop(tab); // zerr redraws ZLE, which reads paramtab
16499 zerr(&format!("invalid variable name: {}", value));
16500 return;
16501 }
16502 // c:6408 — `pm->base = 0;`
16503 pm.base = 0;
16504 pm.width = 0;
16505 // c:6377 — `SETREFNAME(pm, ztrdup(value));`
16506 // SETREFNAME (params.c:482) macro: for PM_SPECIAL,
16507 // call gsu_s.setfn(pm, S); else free pm->u.str and
16508 // assign new. The PM_SPECIAL gsu vtable isn't fully
16509 // wired in zshrs; both branches collapse to the
16510 // direct `u_str` assignment which matches the
16511 // non-special path verbatim.
16512 pm.u_str = Some(value.to_string());
16513 // c:6378 — `pm->node.flags &= ~PM_UNSET;`
16514 pm.node.flags &= !(PM_UNSET as i32);
16515 true
16516 } else {
16517 false
16518 }
16519 } else {
16520 false
16521 }
16522 };
16523 if nameref_branch {
16524 // c:6379 — `setscope(pm);` — run the full-table variant with
16525 // no lock held (it manages its own short-lived locks).
16526 crate::ported::params::setscope_by_name(name, None);
16527 } else {
16528 // c:6381 — `setsparam(name, ztrdup(value));`
16529 setsparam(name, value);
16530 }
16531}
16532
16533/// PM_NAMEREF: extract `refname = GETREFNAME(pm)`, locate first
16534/// `[` to split name vs subscript (sets pm->width), look up the
16535/// base param via `gethashnode2(realparamtab, refname)` →
16536/// `loadparamnode` (skipping self) → `setscope_base(pm,
16537/// basepm->level)`; if pm->base > pm->level emits the KSH global
16538/// reference error or WARNNESTEDVAR diagnostic; finally walks the
16539/// `resolve_nameref_rec` chain to detect self-references.
16540/// In-place variant: operates only on the passed `&mut param`
16541/// (width computation + literal self-name check). Callers that
16542/// hold no paramtab lock should use `setscope_by_name` which runs
16543/// the full base-scope + chain self-ref detection against the
16544/// live table.
16545/// Port of `setscope()` from `Src/params.c:6382`.
16546pub fn setscope(pm: &mut param) {
16547 queue_signals();
16548 if (pm.node.flags as u32 & PM_NAMEREF) != 0 {
16549 // Refname is stored in pm.u_str for nameref-typed params.
16550 let refname = pm.u_str.clone();
16551 if let Some(rn) = refname {
16552 // c:6391-6400 — compute pm->width by finding the first `[`.
16553 if let Some(i) = rn.find('[') {
16554 pm.width = i as i32;
16555 }
16556 }
16557 }
16558 unqueue_signals();
16559}
16560
16561/// ```c
16562/// if ((pm->base = base) > pm->level) {
16563/// LinkList refs;
16564/// /* grow scoperefs[] to base+1 entries */
16565/// refs = scoperefs[base];
16566/// if (!refs) refs = scoperefs[base] = znewlinklist();
16567/// zpushnode(refs, pm);
16568/// }
16569/// ```
16570/// Records `pm` on the per-scope reference list so a future
16571/// scope-pop can resolve nameref/upper bindings. Rust port
16572/// records the param's name on `SCOPEREFS[base]` so a future
16573/// scope-pop can resolve upper/nameref references.
16574/// Port of `setscope_base()` from `Src/params.c:6436`.
16575pub fn setscope_base(pm: &mut param, base: i32) {
16576 // c:6438
16577 // c:6440 — `if ((pm->base = base) > pm->level) {`
16578 pm.base = base;
16579 if base > pm.level {
16580 SCOPEREFS.with(|sr| {
16581 let mut sr = sr.borrow_mut();
16582 // c:6442-6447 — `if (base >= scoperefs_num) { ... grow ... }`
16583 // Rust Vec grows on demand via resize; mirrors
16584 // the C double-and-zero-init pattern via the
16585 // max(8, 2*base) growth heuristic.
16586 if (base as usize) >= sr.len() {
16587 let new_num = (2 * base as usize).max(8);
16588 sr.resize(new_num, Vec::new());
16589 }
16590 // c:6448-6451 — `refs = scoperefs[base]; if (!refs)
16591 // refs = scoperefs[base] = znewlinklist();
16592 // zpushnode(refs, pm);`
16593 // Rust pushes the param NAME (Vec<String>),
16594 // not a raw pointer — borrow-safe and the
16595 // name is the canonical key for upscope walks.
16596 sr[base as usize].insert(0, pm.node.nam.clone()); // c:6451
16597 });
16598 }
16599}
16600
16601/// `scoperefs` — port of `static LinkList *scoperefs` from `Src/params.c:503`.
16602/// One Vec<String> (param names) per scope index. Per-evaluator (bucket 1)
16603/// because each worker thread has its own nameref-resolution context.
16604thread_local! {
16605 /// `SCOPEREFS` static.
16606 pub static SCOPEREFS: std::cell::RefCell<Vec<Vec<String>>>
16607 = const { std::cell::RefCell::new(Vec::new()) };
16608}
16609
16610/// Port of `upscope()` from `Src/params.c:6455`. C body:
16611/// ```c
16612/// if (ref->node.flags & PM_UPPER)
16613/// while (pm->level > ref->level - 1 && (pm = pm->old));
16614/// else
16615/// for (; pm->old && pm->old->level >= ref->base; pm = pm->old);
16616/// return pm;
16617/// ```
16618/// Walks `pm->old` chain to the param at the right scope depth
16619/// for a nameref. Rust signature mirrors C `Param upscope(Param,
16620/// const Param ref)`.
16621/// WARNING: param names don't match C — Rust=(pm, reference) vs C=(pm, ref)
16622pub fn upscope(mut pm: Param, reference: ¶m) -> Param {
16623 if (reference.node.flags as u32 & PM_UPPER) != 0 {
16624 while pm.level > reference.level - 1 {
16625 match pm.old.take() {
16626 Some(o) => pm = o,
16627 None => break,
16628 }
16629 }
16630 } else {
16631 loop {
16632 let next_level = pm.old.as_ref().map(|o| o.level);
16633 match next_level {
16634 Some(l) if l >= reference.base => {
16635 pm = pm.old.take().unwrap();
16636 }
16637 _ => break,
16638 }
16639 }
16640 }
16641 pm
16642}
16643
16644/// Port of `valid_refname()` from `Src/params.c:6489-6501` (as rewritten
16645/// by 54718, which dropped references to subscripted variables).
16646///
16647/// The referent is an identifier (namespaced or not) or one of the
16648/// one-char specials `! ? $ -`. A `-u` (PM_UPPER) reference may not name
16649/// a positional, `argv` or `ARGC`.
16650pub fn valid_refname(val: &str, flags: i32) -> bool {
16651 if (flags as u32 & PM_UPPER) != 0 {
16652 // c:6491-6495 — upward reference to positionals is doomed to fail
16653 if val.as_bytes().first().is_some_and(u8::is_ascii_digit) || val == "argv" || val == "ARGC" {
16654 return false; // c:6494
16655 }
16656 }
16657 // c:6497-6498
16658 if matches!(val.as_bytes().first(), Some(b'!' | b'?' | b'$' | b'-')) {
16659 return val.len() == 1;
16660 }
16661 // c:6500 — `return !*itype_end(val, INAMESPC, 0) && isident(val);`
16662 crate::ported::utils::itype_end(val, crate::ported::ztype_h::INAMESPC, false) == val.len()
16663 && isident(val)
16664}
16665
16666/// !!! WARNING: RUST-ONLY HELPER !!!
16667/// No C counterpart: reader for the C global `static Param foundparam`
16668/// (`Src/params.c:640`). C dereferences the global directly; Rust needs an
16669/// accessor because the store is a `OnceLock<Mutex<Option<String>>>`.
16670/// Read `foundparam`. Returns the last param name observed by
16671/// `scanparamvals`; cleared by callers after consumption.
16672pub fn foundparam() -> Option<String> {
16673 foundparam_lock().lock().unwrap().clone()
16674}
16675
16676/// !!! WARNING: RUST-ONLY HELPER !!!
16677/// No C counterpart: writer for the C global `static Param foundparam`
16678/// (`Src/params.c:640`) — C assigns the global in place (`foundparam = pm;`).
16679/// Set `foundparam`. Called from `scanparamvals`.
16680pub fn set_foundparam(nam: Option<String>) {
16681 *foundparam_lock().lock().unwrap() = nam;
16682}
16683
16684/// Port of `fetchvalue(Value v, char **pptr, int bracks, int scanflags)` from `Src/params.c:2180` — see real
16685/// implementation below; this slot kept for the C-source linenum
16686/// citation and is now an alias.
16687// (real fetchvalue is defined later)
16688
16689/// Port of `static int delunset;` from `Src/params.c:610`. Flag
16690/// `deleteparamtable` flips to 1 around the inner `deletehashtable`
16691/// call so each freed node runs its `unsetfn`. `freeparamnode`
16692/// consults this before invoking the unset hook (c:5986).
16693pub static DELUNSET: std::sync::atomic::AtomicI32 = // c:610
16694 std::sync::atomic::AtomicI32::new(0);
16695
16696/// !!! WARNING: RUST-ONLY HELPER !!!
16697/// No C counterpart: C keeps an associative array's elements in a nested
16698/// `HashTable` hanging off `pm->u.hash`, walked through the Param's GSU vtable.
16699/// zshrs stores them in this side table keyed by the owning param's name, because
16700/// the Rust `param` struct has no owned nested-table field.
16701pub(crate) fn paramtab_hashed_storage(
16702) -> &'static Mutex<crate::cow_map::CowHashMap<String, IndexMap<String, String>>> {
16703 PARAMTAB_HASHED_STORAGE_INNER
16704 .get_or_init(|| Mutex::new(crate::cow_map::CowHashMap::new()))
16705}
16706
16707/// Shadow stack for `paramtab_hashed_storage` entries displaced by
16708/// `local -A NAME` / `typeset -A NAME` shadows inside a function. The
16709/// canonical assoc data lives in `paramtab_hashed_storage` (a flat
16710/// HashMap keyed by name with NO scope dimension — Rust-only parallel
16711/// store; the C side keeps assoc data in pm.u_hash so it rides the
16712/// pm.old chain automatically, and the local's own bag is freed with
16713/// the local by `stdunsetfn`'s PM_HASHED arm, Src/params.c:3922-3925).
16714/// createparam pushes the displaced value whenever a PM_LOCAL shadow
16715/// installs and either side is PM_HASHED; endparamscope pops the frame
16716/// whose recorded level matches the local it is unwinding, so the outer
16717/// scope's bag comes back and the local's bag goes away. Mirrors C's
16718/// `copyparam` (Src/builtin.c:2382-2424) via parallel storage. Bug #415.
16719///
16720/// Each frame is `(level, row)`: `level` is the `locallevel` of the local
16721/// that displaced the row, `row` is `Some(map)` when the name had a row
16722/// before the shadow and `None` when it had none. The level tag is what
16723/// keeps push and pop matched — the pop side used to re-derive the push
16724/// condition from the param flags and the two spellings drifted.
16725pub(crate) static PARAMTAB_HASHED_SHADOW_STACK: OnceLock<
16726 Mutex<HashMap<String, Vec<(i32, Option<IndexMap<String, String>>)>>>,
16727> = OnceLock::new();
16728
16729/// !!! WARNING: RUST-ONLY HELPER !!!
16730/// No C counterpart: port-transition shim. C's `prefork()` (`Src/subst.c`) reads
16731/// parameters straight out of `paramtab`; zshrs's `subst.rs` still carries
16732/// `state.variables` / `state.arrays` / `state.assoc_arrays` as transient backing,
16733/// so this mirrors the table into them. It goes away when `subst.rs` reads
16734/// `paramtab()` directly.
16735/// Mirror the global `paramtab` (and the parallel hashed-storage
16736/// table) into the three HashMaps that `SubstState` uses as its
16737/// transient backing during `prefork()` (Src/subst.c:100). This
16738/// is a port-transition shim: once `subst.rs` reads parameters
16739/// directly through `paramtab().read()` / `.write()` instead of carrying
16740/// `state.variables`/`state.arrays`/`state.assoc_arrays`, this
16741/// helper goes away.
16742pub fn sync_state_from_paramtab(
16743 variables: &mut HashMap<String, String>,
16744 arrays: &mut HashMap<String, Vec<String>>,
16745 assoc_arrays: &mut HashMap<String, IndexMap<String, String>>,
16746) {
16747 let tab = paramtab().read().unwrap();
16748 for (name, pm) in tab.iter() {
16749 let f = pm.node.flags as u32;
16750 if (f & PM_ARRAY) != 0 {
16751 if let Some(arr) = pm.u_arr.as_ref() {
16752 arrays.insert(name.clone(), arr.clone());
16753 }
16754 variables.remove(name);
16755 assoc_arrays.remove(name);
16756 } else if (f & PM_HASHED) != 0 {
16757 if let Some(map) = paramtab_hashed_storage().lock().unwrap().get(name) {
16758 assoc_arrays.insert(name.clone(), map.clone());
16759 }
16760 variables.remove(name);
16761 arrays.remove(name);
16762 } else if let Some(s) = pm.u_str.as_ref() {
16763 // PM_SCALAR / PM_NAMEREF / numeric — fold to the string view.
16764 variables.insert(name.clone(), s.clone());
16765 arrays.remove(name);
16766 assoc_arrays.remove(name);
16767 }
16768 }
16769}
16770
16771/// !!! WARNING: RUST-ONLY HELPER !!!
16772/// No C counterpart: C has `convbase_underscore()` (`Src/params.c:5646`) for
16773/// integers but no underscore-grouping variant of `convfloat()`; the float path in
16774/// C never groups digits. This helper applies the same grouping to a formatted
16775/// float for zshrs's `typeset -F`/`-E` underscore support.
16776/// Format float with underscores
16777pub fn convfloat_underscore(dval: f64, underscore: i32) -> String {
16778 let s = convfloat(dval, 0, 0);
16779 if underscore <= 0 {
16780 return s;
16781 }
16782
16783 let u = underscore as usize;
16784 let (sign, rest) = if let Some(after) = s.strip_prefix('-') {
16785 ("-", after)
16786 } else {
16787 ("", s.as_str())
16788 };
16789
16790 let (int_part, frac_exp) = if let Some(dot_pos) = rest.find('.') {
16791 (&rest[..dot_pos], &rest[dot_pos..])
16792 } else {
16793 (rest, "")
16794 };
16795
16796 // Add underscores to integer part
16797 let int_chars: Vec<char> = int_part.chars().collect();
16798 let mut result = sign.to_string();
16799 let first_group = int_chars.len() % u;
16800 if first_group > 0 {
16801 result.extend(&int_chars[..first_group]);
16802 if first_group < int_chars.len() {
16803 result.push('_');
16804 }
16805 }
16806 for (i, chunk) in int_chars[first_group..].chunks(u).enumerate() {
16807 if i > 0 {
16808 result.push('_');
16809 }
16810 result.extend(chunk);
16811 }
16812
16813 // Add underscores to fractional part
16814 if let Some(frac) = frac_exp.strip_prefix('.') {
16815 result.push('.');
16816 let (frac_digits, exp) = if let Some(e_pos) = frac.find('e') {
16817 (&frac[..e_pos], &frac[e_pos..])
16818 } else {
16819 (frac, "")
16820 };
16821
16822 let frac_chars: Vec<char> = frac_digits.chars().collect();
16823 for (i, chunk) in frac_chars.chunks(u).enumerate() {
16824 if i > 0 {
16825 result.push('_');
16826 }
16827 result.extend(chunk);
16828 }
16829 result.push_str(exp);
16830 } else {
16831 result.push_str(frac_exp);
16832 }
16833
16834 result
16835}
16836
16837/// !!! WARNING: RUST-ONLY HELPER !!!
16838/// No C counterpart: `OnceLock` accessor for the C global `char *ifs`
16839/// (`Src/params.c:73`).
16840pub(crate) fn ifs_lock() -> &'static Mutex<Option<String>> {
16841 // `None` is C's `ifs == NULL` (an unset IFS, c:Src/params.c:4748).
16842 static IFS_VAR: OnceLock<Mutex<Option<String>>> = OnceLock::new();
16843 IFS_VAR.get_or_init(|| Mutex::new(Some(" \t\n\0".to_string())))
16844}
16845
16846/// !!! WARNING: RUST-ONLY HELPER !!!
16847/// No C counterpart: `OnceLock` accessor for the C global `char *home`
16848/// (`Src/params.c:91`).
16849fn home_lock() -> &'static Mutex<String> {
16850 static HOME_VAR: OnceLock<Mutex<String>> = OnceLock::new();
16851 HOME_VAR.get_or_init(|| Mutex::new(env::var("HOME").unwrap_or_default()))
16852}
16853
16854/// !!! WARNING: RUST-ONLY HELPER !!!
16855/// No C counterpart: `OnceLock` accessor for the C global `char *term`
16856/// (`Src/params.c:93`).
16857fn term_lock() -> &'static Mutex<String> {
16858 static TERM_VAR: OnceLock<Mutex<String>> = OnceLock::new();
16859 TERM_VAR.get_or_init(|| Mutex::new(env::var("TERM").unwrap_or_default()))
16860}
16861
16862/// !!! WARNING: RUST-ONLY HELPER !!!
16863/// No C counterpart: `OnceLock` accessor for the C global `char *wordchars`
16864/// (`Src/params.c:85`).
16865pub(crate) fn wordchars_lock() -> &'static Mutex<String> {
16866 static WORDCHARS_VAR: OnceLock<Mutex<String>> = OnceLock::new();
16867 WORDCHARS_VAR.get_or_init(|| Mutex::new("*?_-.[]~=/&;!#$%^(){}<>".to_string()))
16868}
16869
16870/// !!! WARNING: RUST-ONLY HELPER !!!
16871/// No C counterpart: `OnceLock` accessor for the three C globals behind
16872/// `$histchars` — `mod_export unsigned char bangchar;` (`Src/params.c:130`) and
16873/// `unsigned char hatchar, hashchar;` (`Src/params.c:132`) — held here as one
16874/// `[u8; 3]` so the getfn/setfn pair reads and writes them atomically.
16875fn histchars_lock() -> &'static Mutex<[u8; 3]> {
16876 static HISTCHARS_VAR: OnceLock<Mutex<[u8; 3]>> = OnceLock::new();
16877 HISTCHARS_VAR.get_or_init(|| Mutex::new([b'!', b'^', b'#']))
16878}
16879
16880/// !!! WARNING: RUST-ONLY HELPER !!!
16881/// No C counterpart: `OnceLock` accessor for the C global
16882/// `unsigned char keyboardhackchar = '\0';` (`Src/params.c:135`).
16883///
16884/// `pub(crate)` because C's global is file-scope-external and
16885/// `Src/options.c:876` assigns it directly (`keyboardhackchar = (value ?
16886/// '`' : '\0')`). That call site used to reach the byte by dispatching
16887/// `keyboardhacksetfn` under the paramtab write guard, which is neither
16888/// what C does nor safe to do; it now writes the global, like C.
16889pub(crate) fn keyboardhack_lock() -> &'static Mutex<u8> {
16890 static KEYBOARDHACK_VAR: OnceLock<Mutex<u8>> = OnceLock::new();
16891 KEYBOARDHACK_VAR.get_or_init(|| Mutex::new(0))
16892}
16893
16894/// !!! WARNING: RUST-ONLY HELPER !!!
16895/// No C counterpart: `OnceLock` accessor for the C global `zlong histsiz`
16896/// (`Src/hist.c:108`), seeded the way `Src/init.c:1094` seeds it
16897/// (`histsiz = DEFAULT_HISTSIZE;`).
16898/// Port of `zlong histsiz` (`Src/hist.c:108`), seeded by
16899/// `Src/init.c:1094` — `histsiz = DEFAULT_HISTSIZE;`.
16900/// `DEFAULT_HISTSIZE` is 30 (`configure.ac:2978`).
16901fn histsiz_lock() -> &'static Mutex<i64> {
16902 static HISTSIZ_VAR: OnceLock<Mutex<i64>> = OnceLock::new();
16903 HISTSIZ_VAR.get_or_init(|| Mutex::new(crate::ported::config_h::DEFAULT_HISTSIZE as i64))
16904 // c:Src/init.c:1094
16905}
16906
16907/// !!! WARNING: RUST-ONLY HELPER !!!
16908/// No C counterpart: `OnceLock` accessor for the C global `zlong savehistsiz`
16909/// (`Src/hist.c:113`). Nothing seeds it — C leaves the BSS global at 0.
16910/// Port of `zlong savehistsiz` (`Src/hist.c:113`). Nothing seeds it —
16911/// C zeroes the BSS global and `SAVEHIST` (`IPDEF1` at
16912/// `Src/params.c:303`) reads it through `savehistsizegetfn`
16913/// (`c:4985 return savehistsiz;`), so a shell with no rc file reports 0.
16914fn savehistsiz_lock() -> &'static Mutex<i64> {
16915 static SAVEHISTSIZ_VAR: OnceLock<Mutex<i64>> = OnceLock::new();
16916 SAVEHISTSIZ_VAR.get_or_init(|| Mutex::new(0)) // c:Src/hist.c:113
16917}
16918
16919/// !!! WARNING: RUST-ONLY HELPER !!!
16920/// No C counterpart: `OnceLock` accessor for the C global `char *zsh_terminfo`
16921/// (`Src/params.c:87`).
16922fn zsh_terminfo_lock() -> &'static Mutex<String> {
16923 static TERMINFO_VAR: OnceLock<Mutex<String>> = OnceLock::new();
16924 TERMINFO_VAR.get_or_init(|| Mutex::new(env::var("TERMINFO").unwrap_or_default()))
16925}
16926
16927/// !!! WARNING: RUST-ONLY HELPER !!!
16928/// No C counterpart: `OnceLock` accessor for the C global `char *zsh_terminfodirs`
16929/// (`Src/params.c:88`).
16930fn zsh_terminfodirs_lock() -> &'static Mutex<String> {
16931 static TERMINFODIRS_VAR: OnceLock<Mutex<String>> = OnceLock::new();
16932 TERMINFODIRS_VAR.get_or_init(|| Mutex::new(env::var("TERMINFO_DIRS").unwrap_or_default()))
16933}
16934
16935/// !!! WARNING: RUST-ONLY HELPER !!!
16936/// No C counterpart: `OnceLock` accessor for the C global `char *cached_username`
16937/// (`Src/utils.c:1071`).
16938fn cached_username_lock() -> &'static Mutex<String> {
16939 static USERNAME_VAR: OnceLock<Mutex<String>> = OnceLock::new();
16940 USERNAME_VAR.get_or_init(|| Mutex::new(initial_username()))
16941}
16942
16943// Port of `static unsigned numparamvals;` (params.c:626) and the
16944// related per-scan statics at params.c:637-640. Per PORT.md Rule D
16945// these are file-scope statics, NOT aggregated into a state struct.
16946//
16947// c:626 static unsigned numparamvals;
16948// c:637 static Patprog scanprog;
16949// c:638 static char *scanstr;
16950// c:639 static char **paramvals;
16951// c:640 static Param foundparam; <-- exposed earlier as FOUNDPARAM
16952/// `NUMPARAMVALS` static.
16953pub static NUMPARAMVALS: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0); // c:626
16954/// `SCANPROG` static.
16955pub static SCANPROG: OnceLock<Mutex<Option<String>>> = OnceLock::new(); // c:637
16956/// `SCANSTR` static.
16957pub static SCANSTR: OnceLock<Mutex<Option<String>>> = OnceLock::new(); // c:638
16958/// `PARAMVALS` static.
16959pub static PARAMVALS: OnceLock<Mutex<Vec<String>>> = OnceLock::new(); // c:639
16960
16961/// !!! WARNING: RUST-ONLY HELPER !!!
16962/// No C counterpart under this name. It performs the `getpwuid(getuid())->pw_name`
16963/// lookup that C does inside `get_username()` (`Src/utils.c:1075`) to seed the
16964/// `cached_username` global, plus an env fallback C does not have. The name-parity
16965/// port of `get_username()` itself lives in `utils.rs`; this is only the seed for
16966/// the `cached_username_lock` mirror.
16967///
16968/// Resolves the current user's name from `getpwuid(getuid())->pw_name`
16969/// rather than `$USER`. Falls back to env vars only if the
16970/// passwd lookup fails (rare on real systems).
16971fn initial_username() -> String {
16972 #[cfg(unix)]
16973 {
16974 let uid = unsafe { libc::getuid() };
16975 let mut pwd: libc::passwd = unsafe { std::mem::zeroed() };
16976 // libc::c_char is i8 on x86_64/aarch64-darwin and x86_64-linux but u8 on
16977 // aarch64-linux. Use c_char so getpwuid_r's pointer type matches per-target.
16978 let mut buf: Vec<libc::c_char> = vec![0; 1024];
16979 let mut result: *mut libc::passwd = std::ptr::null_mut();
16980 let rc =
16981 unsafe { libc::getpwuid_r(uid, &mut pwd, buf.as_mut_ptr(), buf.len(), &mut result) };
16982 if rc == 0 && !result.is_null() && !pwd.pw_name.is_null() {
16983 let cstr = unsafe { std::ffi::CStr::from_ptr(pwd.pw_name) };
16984 return cstr.to_string_lossy().into_owned();
16985 }
16986 }
16987 env::var("USER")
16988 .or_else(|_| env::var("LOGNAME"))
16989 .unwrap_or_default()
16990}
16991
16992/// !!! WARNING: RUST-ONLY HELPER !!!
16993/// No C counterpart: `OnceLock` accessor for the C global
16994/// `int numpipestats, pipestats[MAX_PIPESTATS];` (`Src/jobs.c:131`).
16995fn pipestats_lock() -> &'static Mutex<Vec<i32>> {
16996 static PIPESTATS_VAR: OnceLock<Mutex<Vec<i32>>> = OnceLock::new();
16997 PIPESTATS_VAR.get_or_init(|| Mutex::new(Vec::new()))
16998}
16999/// !!! WARNING: RUST-ONLY HELPER !!!
17000/// No C counterpart: `OnceLock` accessor for the C global `struct timespec shtimer;`
17001/// (`Src/params.c:142`).
17002/// `shtimer_lock` — see implementation.
17003pub fn shtimer_lock() -> &'static Mutex<Duration> {
17004 static SHTIMER_VAR: OnceLock<Mutex<Duration>> = OnceLock::new();
17005 SHTIMER_VAR.get_or_init(|| {
17006 Mutex::new(
17007 SystemTime::now()
17008 .duration_since(UNIX_EPOCH)
17009 .unwrap_or_default(),
17010 )
17011 })
17012}
17013
17014/// !!! WARNING: RUST-ONLY HELPER !!!
17015/// No C counterpart: `OnceLock` accessor for the C global `char **pparams`
17016/// (`Src/params.c:59`). Points at the single canonical store in `builtin::PPARAMS`
17017/// so `$#` reads the live positional-parameter vector.
17018fn pparams_lock() -> &'static Mutex<Vec<String>> {
17019 // Mirror of zsh's `pparams` (positional params $1, $2, ...).
17020 // Used by `poundgetfn` for `$#`. The canonical store is
17021 // `builtin::PPARAMS` (Src/init.c `pparams`); set/shift builtins
17022 // write there. Point at that single store so `$#` reads the
17023 // live value instead of an isolated empty mirror.
17024 &PPARAMS
17025}
17026
17027/// !!! WARNING: RUST-ONLY HELPER !!!
17028/// No C counterpart: accessor for the C global `char *zunderscore;`
17029/// (`Src/init.c:49`). Hands back the canonical port in `init::zunderscore` so there
17030/// is one store, as in C.
17031///
17032/// C has a single `char *zunderscore` global (`Src/init.c:49`) that
17033/// `setunderscore` (`Src/exec.c:2652`) writes and `underscoregetfn`
17034/// (`Src/params.c:5152`) reads. zshrs had TWO stores: the canonical
17035/// port `init::zunderscore` (written by the ported `setunderscore`,
17036/// src/ported/exec.rs:1612) and a private mutex here that
17037/// `set_zunderscore` wrote and `underscoregetfn` read — so a
17038/// `setunderscore("")` from the assignment-only-command path
17039/// (c:Src/exec.c:1370) never reached the reader, and
17040/// `fusevm_bridge.rs:5979-5989` had to dual-write to both. Point this
17041/// accessor at the canonical global so there is one store, as in C.
17042fn zunderscore_lock() -> &'static Mutex<String> {
17043 &crate::ported::init::zunderscore // c:Src/init.c:49
17044}
17045
17046/// !!! WARNING: RUST-ONLY HELPER !!!
17047/// No C counterpart: C assigns the `zunderscore` global directly at each site
17048/// (`setunderscore()`, `Src/exec.c:2652`, and the inline writes in `execcmd_exec`).
17049/// This helper packages the last-argv-slot write for the Rust dispatch hooks.
17050/// Update `$_` with the last argument of the just-completed
17051/// command. Mirrors C zsh's writeback in `execcmd_exec` (Src/exec.c)
17052/// where `zunderscore` is set to the last argv slot before
17053/// returning. Callers: every command-dispatch hook in
17054/// fusevm_bridge / vm_helper.
17055pub fn set_zunderscore(argv: &[String]) {
17056 let new = if let Some(last) = argv.last() {
17057 last.clone()
17058 } else {
17059 String::new()
17060 };
17061 *zunderscore_lock().lock().expect("zunderscore poisoned") = new;
17062}
17063
17064/// Port of `dontimport()` from `Src/params.c:796`.
17065/// ```c
17066/// /* If explicitly marked as don't import */
17067/// if (flags & PM_DONTIMPORT)
17068/// return 1;
17069/// /* If value already exported */
17070/// if (flags & PM_EXPORTED)
17071/// return 1;
17072/// /* If security issue when importing and running with some privilege */
17073/// if ((flags & PM_DONTIMPORT_SUID) && isset(PRIVILEGED))
17074/// return 1;
17075/// /* OK to import */
17076/// return 0;
17077/// ```
17078/// Port of `dontimport(int flags)` from `Src/params.c:796`.
17079fn dontimport(flags: i32) -> i32 {
17080 // c:796
17081 let flags = flags as u32;
17082 // c:799-800 — `if (flags & PM_DONTIMPORT) return 1`.
17083 if flags & PM_DONTIMPORT != 0 {
17084 // c:799
17085 return 1; // c:800
17086 }
17087 // c:802-803 — `if (flags & PM_EXPORTED) return 1`.
17088 if flags & PM_EXPORTED != 0 {
17089 // c:802
17090 return 1; // c:803
17091 }
17092 // c:805-806 — `if ((flags & PM_DONTIMPORT_SUID) && isset(PRIVILEGED)) return 1`.
17093 if flags & PM_DONTIMPORT_SUID != 0 // c:805
17094 && isset(PRIVILEGED)
17095 {
17096 return 1; // c:806
17097 }
17098 0 // c:809
17099}
17100
17101// ===========================================================
17102// GSU dispatch table — maps special-parameter NAMES to their
17103// getfn callback. C zsh dispatches reads of `$RANDOM` /
17104// `$USERNAME` / `$UID` / etc. through `Param.gsu->getfn`, where
17105// each special parameter has a `Param` entry in `paramtab`
17106// pointing at its specific getfn (Src/params.c:225 SPECIAL_PARAM
17107// table seeds these mappings).
17108//
17109// zshrs has the GSU callbacks ported (uidgetfn, randomgetfn,
17110// usernamegetfn, etc. above) but the shell's parameter-read path
17111// (fusevm_bridge::expand_param) reads from ShellExecutor.variables
17112// directly — never dispatching through the callbacks. Result:
17113// `echo $RANDOM` returned the cached HashMap value (or empty),
17114// not a fresh `rand() & 0x7fff` from `randomgetfn`.
17115//
17116// `lookup_special_var(name)` is the bridge: given a variable
17117// name, returns the GSU getfn's output if `name` is a recognized
17118// special, else None. Callers (expand_param, subst.rs reads)
17119// check this before falling back to `variables.get(name)`.
17120// ===========================================================
17121
17122/// !!! WARNING: RUST-ONLY HELPER !!!
17123/// No C counterpart: side table. C marks an unset special by setting `PM_UNSET` on
17124/// the pm node (`unsetparam_pm`, `Src/params.c:3841`), which every getfn checks.
17125/// zshrs's `lookup_special_var` dispatches to the getfn without a pm node, so the
17126/// PM_UNSET bit is tracked in this set instead.
17127/// Registry of special-parameter names that have been `unset` and
17128/// should bypass the getfn regenerator.
17129///
17130/// c:Src/params.c:3853 — `unsetparam_pm` sets `PM_UNSET` on the pm
17131/// node which getfn callbacks check. zshrs's `lookup_special_var`
17132/// dispatches to libc/getfn directly without a paramtab pm node, so
17133/// PM_UNSET tracking happens in this side-set instead. Bug #417/#418.
17134fn unset_specials() -> &'static std::sync::Mutex<std::collections::HashSet<String>> {
17135 static SET: std::sync::OnceLock<std::sync::Mutex<std::collections::HashSet<String>>> =
17136 std::sync::OnceLock::new();
17137 SET.get_or_init(|| std::sync::Mutex::new(std::collections::HashSet::new()))
17138}
17139
17140/// !!! WARNING: RUST-ONLY HELPER !!!
17141/// No C counterpart: sets the side-table bit that stands in for `PM_UNSET` on a
17142/// special's pm node (see `unset_specials`).
17143/// Mark a special-parameter NAME as unset. Future
17144/// `lookup_special_var(name)` calls return `None` instead of dispatching
17145/// to the getfn regenerator. Re-assigning the name (e.g. `RANDOM=42`)
17146/// should clear this flag via `clear_unset_special`.
17147pub fn mark_unset_special(name: &str) {
17148 if let Ok(mut s) = unset_specials().lock() {
17149 s.insert(name.to_string());
17150 }
17151}
17152
17153/// !!! WARNING: RUST-ONLY HELPER !!!
17154/// No C counterpart: clears the side-table bit that stands in for `PM_UNSET` on a
17155/// special's pm node (see `unset_specials`).
17156/// Clear the unset flag for a special-parameter NAME — called when the
17157/// name is re-assigned so the getfn regenerator becomes active again.
17158pub fn clear_unset_special(name: &str) {
17159 if let Ok(mut s) = unset_specials().lock() {
17160 s.remove(name);
17161 }
17162}
17163
17164/// !!! WARNING: RUST-ONLY HELPER !!!
17165/// No C counterpart: reads the side-table bit that stands in for `PM_UNSET` on a
17166/// special's pm node (see `unset_specials`).
17167fn is_unset_special(name: &str) -> bool {
17168 unset_specials()
17169 .lock()
17170 .map(|s| s.contains(name))
17171 .unwrap_or(false)
17172}
17173
17174/// !!! WARNING: RUST-ONLY HELPER !!!
17175/// No C counterpart: C reaches a special's getter through the Param's GSU vtable
17176/// (`pm->gsu.s->getfn(pm)`, dispatched from `getstrvalue`, `Src/params.c:2335`).
17177/// Rust has no vtable on the `param` struct, so this function is the name-keyed
17178/// dispatch table that routes to the ported getfn callbacks above.
17179/// Look up a special-parameter NAME and dispatch to its GSU getfn.
17180///
17181/// Returns `Some(value_string)` if `name` is one of zshrs's
17182/// recognized specials with a real GSU getfn; `None` otherwise
17183/// (caller should fall back to `variables.get`).
17184///
17185/// This is the bridge between the named getfn callbacks above
17186/// (uidgetfn / randomgetfn / etc.) and the shell's parameter-read
17187/// path. Mirrors the `Param.gsu->getfn` dispatch C zsh does
17188/// inside `getsparam` / `getstrvalue` (Src/params.c:3076 / 2335).
17189pub fn lookup_special_var(name: &str) -> Option<String> {
17190 // ksh93 / mksh `$KSH_VERSION` under --ksh / --mksh / --pdksh (the strings the
17191 // reference shells report; scripts probe it with `${KSH_VERSION+set}`).
17192 if name == "KSH_VERSION" && crate::dash_mode::korn_mode() {
17193 return Some(crate::dash_mode::ksh_version_string().to_string());
17194 }
17195 // bash special SCALARS under --bash: `$BASH_VERSION` (version string) and
17196 // the scalar/bare reads of bash's special arrays (bash: `$PIPESTATUS` ==
17197 // `${PIPESTATUS[0]}`, `$FUNCNAME` == current function, `$BASH_VERSINFO` ==
17198 // major). No-op in --zsh (gate) — those names are ordinary there.
17199 if crate::dash_mode::bash_mode() {
17200 match name {
17201 "BASH_VERSION" => return Some(crate::dash_mode::bash_version()),
17202 // bash(1): HOSTNAME is the host name — zsh's HOST.
17203 "HOSTNAME" => return crate::ported::params::getsparam("HOST"),
17204 // bash(1): HOSTTYPE is the machine type, OSTYPE the OS, and
17205 // MACHTYPE the GNU `cpu-vendor-os` triple.
17206 "HOSTTYPE" => return Some(crate::ported::config_h::MACHTYPE.to_string()),
17207 "MACHTYPE" => {
17208 return Some(format!(
17209 "{}-{}-{}",
17210 crate::ported::config_h::MACHTYPE,
17211 crate::ported::config_h::VENDOR,
17212 crate::ported::config_h::OSTYPE
17213 ))
17214 }
17215 // bash(1) "Shell Variables": SHELLOPTS is the colon-separated
17216 // list of the `set -o` options currently enabled. Built from the
17217 // same table `set -o` lists and `set -o NAME` writes.
17218 "SHELLOPTS" => return Some(crate::dash_mode::bash_shellopts()),
17219 // bash(1) "Shell Variables": BASHOPTS is "a colon-separated list
17220 // of enabled shell options. Each word in the list is a valid
17221 // argument for the -s option to the shopt builtin." Twin of
17222 // SHELLOPTS one line up, over the OTHER option namespace — built
17223 // from the same table `shopt` lists and `shopt -s|-u` writes, so
17224 // the parameter and the builtin can never disagree.
17225 //
17226 // Missing until now: `shopt -s dotglob` recorded the flag and
17227 // `shopt -p dotglob` reported it, but `$BASHOPTS` expanded empty,
17228 // so every `case ":$BASHOPTS:" in *:NAME:*)` probe — the shape
17229 // scripts use to test for an option — answered "not set" for all
17230 // 59 names.
17231 "BASHOPTS" => return Some(crate::dash_mode::bash_shoptsopts()),
17232 // bash BASH_SUBSHELL ≈ zsh ZSH_SUBSHELL — the subshell nesting
17233 // depth, 0 in the top-level shell. bash(1): "Incremented by one
17234 // within each subshell or subshell environment when the shell
17235 // begins executing in that environment."
17236 "BASH_SUBSHELL" => {
17237 return Some(
17238 crate::ported::params::getsparam("ZSH_SUBSHELL")
17239 .unwrap_or_else(|| "0".to_string()),
17240 )
17241 }
17242 "PIPESTATUS" | "FUNCNAME" | "BASH_VERSINFO" => {
17243 // bare read = element 0 of the aliased array (empty if none).
17244 return Some(
17245 crate::dash_mode::bash_special_array(name)
17246 .and_then(|a| a.into_iter().next())
17247 .unwrap_or_default(),
17248 );
17249 }
17250 _ => {}
17251 }
17252 }
17253 // c:Src/builtin.c:2083-2090 — `local -h NAME` / `private -h NAME`
17254 // (newspecial stays NS_NONE under PM_HIDE) creates an ORDINARY
17255 // parameter that hides the special, so C's read goes through that
17256 // node's plain gsu, never the special's getfn. A hidden node that is
17257 // not PM_SPECIAL — or is PM_SPECIAL only because makeprivate tagged it
17258 // (Src/Modules/param_private.c:174) — is therefore not this special:
17259 // fall back to the stored value (`() { local -h SECONDS; SECONDS=5;
17260 // echo $SECONDS }` prints 5). This holds for every special, not only the
17261 // regenerators: `() { local -h HOME; typeset -p HOME }` is `HOME=''`.
17262 // A non-special node that shadows another (`pm->old`) is such a local
17263 // even before its PM_HIDE attribute is stamped.
17264 if paramtab()
17265 .read()
17266 .ok()
17267 .and_then(|t| {
17268 t.get(name).map(|pm| {
17269 let f = pm.node.flags as u32;
17270 ((f & PM_SPECIAL) == 0 && pm.old.is_some())
17271 || ((f & PM_HIDE) != 0
17272 && ((f & PM_SPECIAL) == 0
17273 || crate::ported::modules::param_private::is_private(
17274 &**pm as *const param,
17275 ) != 0))
17276 })
17277 })
17278 .unwrap_or(false)
17279 {
17280 return None;
17281 }
17282 // c:Src/params.c:3853 — PM_UNSET-flagged specials skip getfn.
17283 // Only applies to regenerator-style specials (RANDOM, SECONDS,
17284 // EPOCHSECONDS, TTYIDLE, ERRNO) — identity specials like UID, GID,
17285 // PPID stay live since they're not user-clearable in zsh either.
17286 //
17287 // Two sources of "is unset" — the side-set populated by
17288 // `mark_unset_special` from explicit `unset NAME`, and the
17289 // initial PM_UNSET flag set by `Src/params.c:298 IPDEF1(...,
17290 // PM_UNSET)`. ERRNO is the only IPDEF1-special with the initial
17291 // PM_UNSET flag (params.c:298) — zsh -fc reports `$ERRNO` as
17292 // empty because nothing has set errno since startup. Mirror by
17293 // also consulting the paramtab pm flags. Without this, `$ERRNO`
17294 // returned the live errno value instead of empty.
17295 //
17296 // `_` (c:Src/params.c:326 `IPDEF2("_", underscore_gsu,
17297 // PM_DONTIMPORT)`) belongs to the same family: its getfn
17298 // (`underscoregetfn`, c:5147) reads the `zunderscore` global, which
17299 // `unset _` does NOT clear — C's `stdunsetfn` (c:3908-3937) only
17300 // flips PM_UNSET on the node, and `unsetparam_pm` keeps the node in
17301 // the table because it is PM_SPECIAL without PM_REMOVABLE (c:3877).
17302 // So the flag is the ONLY record that `_` was unset; without
17303 // consulting it `${+_}` stayed 1 and `$_` kept regenerating from
17304 // zunderscore after `unset _`.
17305 if matches!(
17306 name,
17307 "RANDOM" | "SECONDS" | "EPOCHSECONDS" | "EPOCHREALTIME" | "TTYIDLE" | "ERRNO" | "_"
17308 ) && (is_unset_special(name)
17309 // !!! DASH-FAITHFUL GATE (no C counterpart) !!! dash has no $RANDOM or
17310 // $SECONDS: they are ordinary (unset) variables there.
17311 || (crate::dash_mode::dash_faithful() && matches!(name, "RANDOM" | "SECONDS")) || {
17312 // c:Src/params.c — paramtab PM_UNSET check. ERRNO carries
17313 // this flag from IPDEF1 initialization (params.c:298); reads
17314 // route through getsparam → paramtab pm.flags check at
17315 // line 4356-4358 normally, but ERRNO/RANDOM/etc. short-
17316 // circuit through lookup_special_var BEFORE that check.
17317 paramtab()
17318 .read()
17319 .ok()
17320 .and_then(|t| t.get(name).map(|pm| (pm.node.flags as u32 & PM_UNSET) != 0))
17321 .unwrap_or(false)
17322 }) {
17323 return None;
17324 }
17325 // All-digit positional: $1..$N from canonical PPARAMS.
17326 // C zsh dispatches positional params through pparams (Src/init.c).
17327 if !name.is_empty() && name.chars().all(|c| c.is_ascii_digit()) {
17328 let n: usize = name.parse().ok()?;
17329 if n == 0 {
17330 // c:4906-4911 argzerogetfn — `if (isset(POSIXARGZERO)) return
17331 // posixzero; return argzero;`
17332 if isset(POSIXARGZERO) {
17333 return crate::ported::utils::posixzero(); // c:4909
17334 }
17335 return argzero(); // c:4910
17336 }
17337 let pp = pparams_lock().lock().ok()?;
17338 return pp.get(n - 1).cloned();
17339 }
17340 match name {
17341 // libc identity callbacks.
17342 "UID" => Some(uidgetfn().to_string()),
17343 "GID" => Some(gidgetfn().to_string()),
17344 "EUID" => Some(euidgetfn().to_string()),
17345 "EGID" => Some(egidgetfn().to_string()),
17346 // c:Src/params.c:350 `IPDEF4("PPID", &ppid)` — ppid is the
17347 // file-static set at shell startup from getppid(). zshrs's
17348 // ported special_paramdef list registers PPID but nothing
17349 // populates the paramtab slot from getppid(2), so $PPID
17350 // always read 0. Read the `ppid` global setupvals fills
17351 // (c:Src/init.c:1226) — NOT a live getppid(), which a forked
17352 // pipeline stage would answer with the shell's own pid.
17353 "PPID" => Some(ppid.load(std::sync::atomic::Ordering::Relaxed).to_string()), // c:350 IPDEF4("PPID", &ppid)
17354 // c:Src/params.c:348 + c:4202 — `IPDEF4("HISTCMD", &curhist)`
17355 // binds `intvargetfn` (`return *pm->u.valptr;`) to `&curhist`
17356 // (`Src/hist.c:88`). Same valptr-bound shape as PPID above: the
17357 // value is a live global, never stored on the pm. `curhist` is
17358 // bumped by `linkcurline()` (`Src/hist.c:1093`) from `hbegin()`
17359 // (c:1166) BEFORE the line is parsed, so during event N the
17360 // parameter reads N and at the next prompt N+1.
17361 "HISTCMD" => Some(
17362 crate::ported::hist::curhist
17363 .load(Ordering::SeqCst)
17364 .to_string(),
17365 ), // c:348 + c:4202
17366 // c:Src/params.c:362-363 — `IPDEF5("COLUMNS", &zterm_columns,
17367 // zlevar_gsu)` / `IPDEF5("LINES", &zterm_lines, zlevar_gsu)`, whose
17368 // getfn is `intvargetfn` (c:4156 `return *pm->u.valptr;`). See
17369 // `intvargetfn` for why the node's own `u_val` is not the answer.
17370 // `unset COLUMNS` keeps the node (c:3877) and reads empty, so the
17371 // PM_UNSET flag decides whether the global is the answer at all.
17372 "COLUMNS" | "LINES" => {
17373 if is_unset_special(name) {
17374 return None; // c:3877
17375 }
17376 let tab = paramtab().read().ok()?;
17377 let pm = tab.get(name)?;
17378 if (pm.node.flags as u32 & PM_UNSET) != 0 {
17379 return None; // c:3877
17380 }
17381 Some(intvargetfn(pm).to_string()) // c:362-363 + c:4202
17382 }
17383 // libc syscall callbacks.
17384 "RANDOM" => Some(randomgetfn().to_string()),
17385 "TTYIDLE" => Some(ttyidlegetfn().to_string()),
17386 "ERRNO" => Some(errnogetfn().to_string()),
17387 // Time callbacks.
17388 "SECONDS" => {
17389 // c:Src/params.c:4561/4591 — PM_TYPE dispatches between
17390 // intsecondsgetfn (PM_INTEGER, default) and floatsecondsgetfn
17391 // (PM_EFLOAT/PM_FFLOAT after `typeset -F`/`-E SECONDS`). The
17392 // pm's gsu_i vs gsu_f vtable swap happens in setfn during
17393 // typeset; we read pm.flags from paramtab here.
17394 let pm_type = paramtab()
17395 .read()
17396 .ok()
17397 .and_then(|t| t.get("SECONDS").map(|pm| PM_TYPE(pm.node.flags as u32)))
17398 .unwrap_or(PM_INTEGER);
17399 if pm_type == PM_EFLOAT || pm_type == PM_FFLOAT {
17400 let v = floatsecondsgetfn();
17401 let base = paramtab()
17402 .read()
17403 .ok()
17404 .and_then(|t| t.get("SECONDS").map(|pm| pm.base))
17405 .unwrap_or(0);
17406 Some(convfloat(v, base, pm_type))
17407 } else {
17408 Some(intsecondsgetfn().to_string())
17409 }
17410 }
17411 // zsh/datetime module params. In recent zsh these are
17412 // auto-loaded via `zmodload zsh/datetime` and the param
17413 // appears in paramtab via the module's `p:NAME` feature
17414 // descriptor (Src/Modules/datetime.c:25). In zshrs the
17415 // datetime module's per-param wireup hasn't been ported
17416 // through paramtab yet — the getters exist
17417 // (modules::datetime::getcurrentsecs / getcurrentrealtime)
17418 // but no Param entry is created on `zmodload`. Mirror the C
17419 // behavior by routing the names through their canonical
17420 // getters here so `$EPOCHSECONDS` / `$EPOCHREALTIME` read
17421 // live time values regardless of explicit zmodload. p10k
17422 // and many other prompts rely on this without an explicit
17423 // zmodload (zsh ships datetime preloaded in most configs).
17424 // c:Src/params.c:1130-1153 createparam / c:2264 getparamnode — a
17425 // `local NAME=…` over a module special SPLICES A NEW Param into
17426 // paramtab ahead of the special (`pm->old = oldpm`), and every read
17427 // from then on goes through the NEW node's `stdgetfn`, not the
17428 // module's getfn. zshrs has no `old` chain, but `local` does replace
17429 // the paramtab node, so the shadow is observable as "the node for
17430 // this name is no longer PM_SPECIAL". Decline the module getter in
17431 // that state so the ordinary paramtab read wins.
17432 // Without this, `fn() { local EPOCHSECONDS=scruts; print $EPOCHSECONDS }`
17433 // printed the live epoch (V04features.ztst "Module special parameter
17434 // is hidden by a local parameter") even though `${(t)EPOCHSECONDS}`
17435 // already reported `scalar-local`.
17436 "EPOCHSECONDS" | "EPOCHREALTIME"
17437 if paramtab()
17438 .read()
17439 .ok()
17440 .and_then(|t| {
17441 t.get(name)
17442 .map(|pm| (pm.node.flags as u32 & PM_SPECIAL) == 0)
17443 })
17444 .unwrap_or(false) =>
17445 {
17446 None
17447 }
17448 "EPOCHSECONDS" => {
17449 // c:Src/Modules/datetime.c:206 `getcurrentsecs`. zsh requires
17450 // explicit `zmodload zsh/datetime` before EPOCHSECONDS is
17451 // bound (Src/Modules/datetime.c:25 `p:EPOCHSECONDS` feature
17452 // descriptor). Without the load, the name is unset and
17453 // ${EPOCHSECONDS:-x} falls through to "x". Match by gating
17454 // the getter on the module's loaded state. Bug #31 in
17455 // docs/BUGS.md.
17456 // bash(1) "Shell Variables": EPOCHSECONDS / EPOCHREALTIME are
17457 // always-on specials, no module load.
17458 if !crate::dash_mode::bash_mode()
17459 && !crate::ported::module::MODULESTAB
17460 .lock()
17461 .unwrap()
17462 .is_loaded("zsh/datetime")
17463 {
17464 return None;
17465 }
17466 Some(crate::ported::modules::datetime::getcurrentsecs().to_string())
17467 }
17468 "EPOCHREALTIME" => {
17469 // c:Src/Modules/datetime.c:212 `getcurrentrealtime`. Same
17470 // zsh/datetime gate as EPOCHSECONDS above. Bug #31.
17471 // bash(1) "Shell Variables": EPOCHSECONDS / EPOCHREALTIME are
17472 // always-on specials, no module load.
17473 if !crate::dash_mode::bash_mode()
17474 && !crate::ported::module::MODULESTAB
17475 .lock()
17476 .unwrap()
17477 .is_loaded("zsh/datetime")
17478 {
17479 return None;
17480 }
17481 let v = crate::ported::modules::datetime::getcurrentrealtime();
17482 if crate::dash_mode::bash_mode() {
17483 // bash(1): "seconds since the Unix epoch as a floating
17484 // point value with micro-second granularity" — `%d.%06d`.
17485 return Some(format!("{:.6}", v));
17486 }
17487 Some(format!("{:.10}", v))
17488 }
17489 "epochtime" => {
17490 // c:Src/Modules/datetime.c:220 `getcurrenttime` returns
17491 // [tv_sec, tv_nsec]. Bare `$epochtime` joins the two with
17492 // ` ` (the default IFS separator), matching the C path
17493 // `getstrvalue` → sepjoin on PM_ARRAY. Bug #317 in
17494 // docs/BUGS.md. Gate on zsh/datetime load per #31.
17495 if !crate::ported::module::MODULESTAB
17496 .lock()
17497 .unwrap()
17498 .is_loaded("zsh/datetime")
17499 {
17500 return None;
17501 }
17502 let arr = crate::ported::modules::datetime::getcurrenttime();
17503 Some(arr.join(" "))
17504 }
17505 // Cached-state callbacks. C dispatches `pm->gsu.s->getfn(pm)`
17506 // where pm is `paramtab->getnode(name)`. Mirror: look up pm,
17507 // pass it through. Each getfn here ignores pm (matches C's
17508 // UNUSED(Param pm)), so a fallback default-constructed param
17509 // is acceptable when the table isn't populated yet.
17510 //
17511 // c:Src/params.c paramsubst c:3193 — the `vunset = (!v || ...)`
17512 // check inspects `pm->node.flags & PM_UNSET`, not the value
17513 // returned by getfn. For specials whose getfn reads global
17514 // cached state (`ifs`, `wordchars`, `keyboardhackchar`), the
17515 // global keeps its last value across `unset NAME` (because
17516 // stdunsetfn only flips PM_UNSET; it doesn't clear the global).
17517 // Without consulting PM_UNSET here, `${IFS+set}` after
17518 // `unset IFS` still returned the default-IFS getter result,
17519 // diverging from zsh.
17520 "USERNAME" | "HOME" | "TERM" | "WORDCHARS" | "IFS" | "TERMINFO" | "TERMINFO_DIRS"
17521 | "KEYBOARD_HACK" | "histchars" | "HISTCHARS" => {
17522 let tab = paramtab().read().ok()?;
17523 let pm = tab.get(name)?;
17524 if (pm.node.flags as u32 & crate::ported::zsh_h::PM_UNSET) != 0 {
17525 return None;
17526 }
17527 Some(match name {
17528 "USERNAME" => usernamegetfn(pm),
17529 "HOME" => homegetfn(pm),
17530 "TERM" => termgetfn(pm),
17531 "WORDCHARS" => wordcharsgetfn(pm),
17532 "IFS" => ifsgetfn(pm),
17533 "TERMINFO" => terminfogetfn(pm),
17534 "TERMINFO_DIRS" => terminfodirsgetfn(pm),
17535 "KEYBOARD_HACK" => keyboardhackgetfn(pm),
17536 "histchars" | "HISTCHARS" => histcharsgetfn(pm),
17537 _ => unreachable!(),
17538 })
17539 }
17540 "_" => Some(underscoregetfn()),
17541 // Counters with int return.
17542 "HISTSIZE" => Some(histsizegetfn().to_string()),
17543 "SAVEHIST" => Some(savehistsizegetfn().to_string()),
17544 "#" | "ARGC" => Some(poundgetfn().to_string()),
17545 // c:Src/params.c:871 — `setsparam("TIMEFMT", DEFAULT_TIMEFMT)`.
17546 // createparamtable seeds this; the zshrs main binary skips
17547 // that init path so paramtab is empty for TIMEFMT. Route
17548 // here from the canonical default. paramtab check first so
17549 // an explicit user-set value sticks.
17550 "TIMEFMT" => {
17551 let tab_val = paramtab()
17552 .read()
17553 .ok()
17554 .and_then(|t| t.get("TIMEFMT").and_then(|pm| pm.u_str.clone()));
17555 if let Some(v) = tab_val {
17556 if !v.is_empty() {
17557 return Some(v);
17558 }
17559 }
17560 Some(crate::ported::zsh_system_h::DEFAULT_TIMEFMT.to_string())
17561 }
17562 // c:Src/init.c:1214-1215 — `nullcmd = ztrdup("cat");
17563 // readnullcmd = ztrdup(DEFAULT_READNULLCMD);`. C seeds these
17564 // into the REAL paramtab at startup; `unset NULLCMD` then
17565 // removes the entry and getsparam returns NULL — which is
17566 // load-bearing: A04redirect "null redir with NULLCMD unset"
17567 // requires `unset NULLCMD; >file` to error "redirection with
17568 // no command". The previous read-time fallback here faked the
17569 // seed on EVERY lookup, making unset impossible. The seed now
17570 // lives in ShellExecutor::new (vm_helper.rs, next to TIMEFMT)
17571 // and absent means absent.
17572 // $0 routes through utils::argzero.
17573 "0" => argzero(),
17574 // POSIX shell-special scalars. C dispatches these through
17575 // dedicated gsu getfn callbacks (Src/params.c special_assigns).
17576 // c:Src/params.c lastvalgetfn — `?` and `status` are aliases
17577 // for the last-command exit code (lastval). C wires them via
17578 // separate IPDEF entries that share the same getfn.
17579 "?" | "status" => Some(LASTVAL.load(Ordering::Relaxed).to_string()),
17580 // c:Src/loop.c:719 — `try_errflag = -1` reset before
17581 // each `{ try } always { catch }` block; reads `-1` when
17582 // outside a try block. zsh exposes TRY_BLOCK_ERROR as an
17583 // integer special-param: inside an always-arm after a
17584 // normal-exit try, reads 0; -1 only when no try has yet
17585 // fired in this scope. BUILTIN_SET_TRY_BLOCK_ERROR writes
17586 // via set_scalar (u_str), so accept either storage form
17587 // and treat a present-but-empty u_str as 0 too.
17588 "TRY_BLOCK_ERROR" => {
17589 // c:Src/loop.c:719 — `zlong try_errflag = -1;` global,
17590 // exported via IPDEF6 (c:Src/params.c:364) so `$TRY_BLOCK_ERROR`
17591 // reads it directly. Initialized to -1 (sentinel: "no try
17592 // block has fired yet"); the `exectry` always-arm sets it
17593 // to `errflag & ERRFLAG_ERROR` (c:765) before running the
17594 // body, then restores at c:787. Read straight from the
17595 // canonical atomic — paramtab's u_str / u_val are NOT the
17596 // source of truth (matches C's IPDEF6 getfn signature).
17597 Some(
17598 crate::ported::r#loop::try_errflag
17599 .load(std::sync::atomic::Ordering::Relaxed)
17600 .to_string(),
17601 )
17602 }
17603 "TRY_BLOCK_INTERRUPT" => {
17604 // c:Src/loop.c:727 — `zlong try_interrupt = -1;` global.
17605 // Same shape as TRY_BLOCK_ERROR.
17606 Some(
17607 crate::ported::r#loop::try_interrupt
17608 .load(std::sync::atomic::Ordering::Relaxed)
17609 .to_string(),
17610 )
17611 }
17612 "$" => Some(mypid.load(std::sync::atomic::Ordering::Relaxed).to_string()), // c:353 IPDEF4("$", &mypid)
17613 "!" => {
17614 // c:Src/params.c:345 IPDEF4("!", &lastpid) — `$!` reads
17615 // directly from the `lastpid` atomic (Src/jobs.c:73).
17616 // The previous Rust port read from paramtab["!"].u_str,
17617 // which only had a value if something wrote to it via
17618 // setsparam/assignsparam — and `"!"` is not a valid
17619 // identifier (isident("!") == 0 per params.c:1288), so
17620 // those calls failed loudly. Read directly from the
17621 // canonical store like the C getter does.
17622 let pid =
17623 crate::ported::modules::clone::lastpid.load(std::sync::atomic::Ordering::Relaxed);
17624 // bash(1): `$!` is unset until a background job has been started
17625 // (expands to ""); zsh's `lastpid` reads 0 there.
17626 if pid == 0 && crate::dash_mode::bash_mode() {
17627 return Some(String::new());
17628 }
17629 Some(pid.to_string())
17630 }
17631 // $* / $@ join positional params via sepjoin's IFS default —
17632 // c:Src/utils.c:3936-3945: set-but-empty IFS joins with ""
17633 // (`IFS=""; echo "$*"` concatenates); unset IFS joins with
17634 // " ". The previous `.unwrap_or(' ')` collapsed empty-IFS to
17635 // a space.
17636 //
17637 // c:Src/params.c:392,393,430 — `IPDEF9("*", &pparams, …)`,
17638 // `IPDEF9("@", &pparams, …)` and `IPDEF9("argv", &pparams, …)` are
17639 // THREE names over ONE storage, all reading through `vararray_gsu`
17640 // (c:189-190). This arm IS that gsu's `arrvargetfn`, so the third
17641 // spelling belongs here too: c:Src/params.c:2350-2358 `getstrvalue`
17642 // takes the PM_ARRAY case for all three and answers
17643 // `sepjoin(getvaluearr(v), NULL, 1)`.
17644 //
17645 // Without the `argv` spelling, `getsparam("argv")` fell through to
17646 // the paramtab read below, found the seeded PM_ARRAY node whose own
17647 // `u_arr` is empty (the positionals live in `PPARAMS`, not on the
17648 // node — see `printparamnode`'s refresh at params.rs:13874), and
17649 // answered `Some("")`. Every operator that decides on the VALUE
17650 // rather than on set-ness then took the null branch:
17651 // c:Src/subst.c:3188-3189 `if (colf && !vunset) vunset = (isarr) ?
17652 // !*aval : !*val`, so `${argv:-n}` / `${argv:+Y}` / `${argv:?}`
17653 // diverged while the non-colon `${argv-n}` / `${argv+Y}` / `${argv?}`
17654 // — which test set-ness alone — were already right.
17655 "*" | "@" | "argv" => pparams_lock()
17656 .lock()
17657 .ok()
17658 .map(|p| crate::ported::utils::sepjoin(&p, None)),
17659 // $- : current option-letter set.
17660 // c:Src/params.c:3262 (IPDEF) → dashparamgetfn in options.c:890.
17661 // Canonical C body walks `zshletters[FIRST_OPT..=LAST_OPT]`
17662 // (c:292-368) emitting each letter whose mapped option is
17663 // active (XOR-ing with the c:295 negation prefix `-OPT`).
17664 // The previous Rust port hand-rolled a hardcoded subset
17665 // ("569X" + 8 ad-hoc letters) that diverged from C's table:
17666 // letter 'h' wrongly mapped to `hashall` (c:271 ALIAS for
17667 // HASHCMDS) instead of HISTIGNOREDUPS (c:349). Parity bug
17668 // #32 — `$-` last char differed (zsh `f`, zshrs `fh`).
17669 // Route through `dashgetfn` (options.rs:835) which is the
17670 // direct port of `Src/options.c:890`.
17671 "-" => Some(crate::ported::options::dashgetfn()),
17672 // Arrays — joined with space for scalar context.
17673 "pipestatus" => {
17674 let arr = pipestatgetfn();
17675 if arr.is_empty() {
17676 None
17677 } else {
17678 Some(arr.join(" "))
17679 }
17680 }
17681 _ => None,
17682 }
17683}
17684
17685/// Shared test mutex for histsiz mutations (gsu_tests +
17686/// tests submodules both write the same global; this lock
17687/// serialises them under parallel test execution).
17688#[cfg(test)]
17689pub(crate) static HISTSIZ_TEST_LOCK: Mutex<()> = Mutex::new(());
17690
17691/// Shared test mutex for histchars mutations (gsu_tests +
17692/// tests submodules both write bangchar/hatchar/hashchar atomics;
17693/// this lock serialises them under parallel test execution).
17694#[cfg(test)]
17695pub(crate) static HISTCHARS_TEST_LOCK_SHARED: Mutex<()> = Mutex::new(());
17696
17697#[cfg(test)]
17698mod gsu_tests {
17699 use super::*;
17700
17701 #[test]
17702 fn test_libc_id_callbacks_match_libc() {
17703 let _g = crate::test_util::global_state_lock();
17704 assert_eq!(uidgetfn(), unsafe { libc::getuid() } as i64);
17705 assert_eq!(gidgetfn(), unsafe { libc::getgid() } as i64);
17706 assert_eq!(euidgetfn(), unsafe { libc::geteuid() } as i64);
17707 assert_eq!(egidgetfn(), unsafe { libc::getegid() } as i64);
17708 }
17709
17710 /// Pin: `usernamegetfn` routes through `get_username()` per
17711 /// `Src/params.c:4658` (which refreshes cache on uid change
17712 /// per `Src/utils.c:1082`). The previous Rust port read a
17713 /// stale cached value directly. Verify the getter returns
17714 /// the same name as a direct libc `getpwuid(getuid())` —
17715 /// confirming the path WENT through the refresh helper, not
17716 /// the stale paramtab Mutex.
17717 #[test]
17718 fn usernamegetfn_matches_libc_getpwuid_for_current_uid() {
17719 let _g = crate::test_util::global_state_lock();
17720 let __pm = crate::ported::zsh_h::param::default();
17721 let uname = usernamegetfn(&__pm);
17722 // The current process is running as some uid; the getter
17723 // must return either a populated name OR an empty string
17724 // (when getpwuid fails, e.g. sandboxed builds). It must
17725 // NOT panic and must NOT return a stale cached value
17726 // from a different uid.
17727 let direct = unsafe {
17728 let pw = libc::getpwuid(libc::getuid());
17729 if pw.is_null() {
17730 String::new()
17731 } else {
17732 std::ffi::CStr::from_ptr((*pw).pw_name)
17733 .to_string_lossy()
17734 .into_owned()
17735 }
17736 };
17737 assert_eq!(
17738 uname, direct,
17739 "c:4658 — usernamegetfn must match getpwuid(getuid())->pw_name"
17740 );
17741 }
17742
17743 #[test]
17744 fn test_random_returns_15_bit_value() {
17745 let _g = crate::test_util::global_state_lock();
17746 for _ in 0..100 {
17747 let v = randomgetfn();
17748 assert!(v >= 0 && v < 0x8000);
17749 }
17750 }
17751
17752 #[test]
17753 fn test_random_set_seeds_deterministically() {
17754 let _g = crate::test_util::global_state_lock();
17755 randomsetfn(42);
17756 let a = randomgetfn();
17757 randomsetfn(42);
17758 let b = randomgetfn();
17759 assert_eq!(a, b);
17760 }
17761
17762 #[test]
17763 fn test_ifs_round_trip() {
17764 let _g = crate::test_util::global_state_lock();
17765 let mut __pm = crate::ported::zsh_h::param::default();
17766 let original = ifsgetfn(&__pm);
17767 ifssetfn(&mut __pm, ":,;".to_string());
17768 assert_eq!(ifsgetfn(&__pm), ":,;");
17769 ifssetfn(&mut __pm, original);
17770 }
17771
17772 #[test]
17773 fn test_histsiz_clamps_to_1() {
17774 let _g = crate::test_util::global_state_lock();
17775 let _g = HISTSIZ_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
17776 let original = histsizegetfn();
17777 histsizesetfn(0);
17778 assert_eq!(histsizegetfn(), 1);
17779 histsizesetfn(-5);
17780 assert_eq!(histsizegetfn(), 1);
17781 histsizesetfn(500);
17782 assert_eq!(histsizegetfn(), 500);
17783 histsizesetfn(original);
17784 }
17785
17786 #[test]
17787 fn test_savehistsiz_clamps_to_0() {
17788 let _g = crate::test_util::global_state_lock();
17789 let original = savehistsizegetfn();
17790 savehistsizesetfn(-5);
17791 assert_eq!(savehistsizegetfn(), 0);
17792 savehistsizesetfn(100);
17793 assert_eq!(savehistsizegetfn(), 100);
17794 savehistsizesetfn(original);
17795 }
17796
17797 /// Pin: `savehistsizesetfn` syncs BOTH storage mirrors so the
17798 /// twin-storage Rust adaptation behaves like the single global
17799 /// in C. The params.rs Mutex<i64> drives `$SAVEHIST` reads;
17800 /// the hist.rs AtomicI64 drives the history-file writer cap.
17801 /// Previously only the params.rs side was written, so
17802 /// `SAVEHIST=10000` left hist.rs at 0 and the writer would
17803 /// cap at zero lines.
17804 #[test]
17805 fn savehistsizesetfn_syncs_to_hist_module() {
17806 let _g = crate::test_util::global_state_lock();
17807 let original_params = savehistsizegetfn();
17808 let original_hist = savehistsiz.load(Ordering::SeqCst);
17809 // Set via the setfn — both storages must reflect the value.
17810 savehistsizesetfn(12345);
17811 assert_eq!(
17812 savehistsizegetfn(),
17813 12345,
17814 "c:4994 — params.rs Mutex<i64> reflects new value"
17815 );
17816 assert_eq!(
17817 savehistsiz.load(Ordering::SeqCst),
17818 12345,
17819 "c:4994 — hist.rs AtomicI64 synced (was the previous gap)"
17820 );
17821 // Negative clamps to 0 in BOTH stores.
17822 savehistsizesetfn(-99);
17823 assert_eq!(savehistsizegetfn(), 0, "c:4998 — params.rs clamps to 0");
17824 assert_eq!(
17825 savehistsiz.load(Ordering::SeqCst),
17826 0,
17827 "c:4998 — hist.rs clamps to 0 too"
17828 );
17829 // Restore.
17830 savehistsizesetfn(original_params);
17831 savehistsiz.store(original_hist, Ordering::SeqCst);
17832 }
17833
17834 #[test]
17835 fn test_pipestat_round_trip() {
17836 let _g = crate::test_util::global_state_lock();
17837 pipestatsetfn(Some(vec![
17838 "1".to_string(),
17839 "0".to_string(),
17840 "127".to_string(),
17841 ]));
17842 let v = pipestatgetfn();
17843 assert_eq!(v, vec!["1", "0", "127"]);
17844 pipestatsetfn(None);
17845 assert_eq!(pipestatgetfn(), Vec::<String>::new());
17846 }
17847
17848 /// Pin: `setnumvalue` actually STORES the scalar string per
17849 /// `Src/params.c:2862-2872`. The previous Rust port computed
17850 /// the string then dropped it via `let _ = s;` — meaning a
17851 /// numeric assignment to a SCALAR param stored NOTHING.
17852 ///
17853 /// C body for PM_SCALAR: `setstrvalue(v, convbase_underscore(
17854 /// val.u.l, pm->base, pm->width));`. We pin the round-trip
17855 /// for an integer assigned to a scalar param.
17856 #[test]
17857 fn setnumvalue_stores_int_value_into_scalar_pm() {
17858 let _g = crate::test_util::global_state_lock();
17859 // c:2860 — setnumvalue bails when unset(EXECOPT). The unit-test
17860 // env doesn't run through createoptiontable so we set "exec"
17861 // explicitly to simulate normal runtime.
17862 let saved_exec = opt_state_get("exec").unwrap_or(false);
17863 opt_state_set("exec", true);
17864 // Build a scalar Param with no special base/width.
17865 let mut pm = Box::new(param {
17866 node: hashnode {
17867 next: None,
17868 nam: "x".to_string(),
17869 flags: PM_SCALAR as i32,
17870 },
17871 u_data: 0,
17872 u_tied: None,
17873 u_arr: None,
17874 u_str: Some(String::new()),
17875 u_val: 0,
17876 u_dval: 0.0,
17877 u_hash: None,
17878 gsu_s: None,
17879 gsu_i: None,
17880 gsu_f: None,
17881 gsu_a: None,
17882 gsu_h: None,
17883 base: 0,
17884 width: 0,
17885 env: None,
17886 ename: None,
17887 old: None,
17888 level: 0,
17889 });
17890 let mut v = value {
17891 pm: Some(pm.clone()),
17892 arr: Vec::new(),
17893 scanflags: 0,
17894 valflags: 0,
17895 start: 0,
17896 end: -1,
17897 };
17898 let val = mnumber {
17899 l: 42,
17900 d: 0.0,
17901 type_: MN_INTEGER,
17902 };
17903 setnumvalue(Some(&mut v), val);
17904 // c:2871 — the scalar storage now holds "42".
17905 let stored = v.pm.as_ref().unwrap().u_str.clone().unwrap_or_default();
17906 assert_eq!(
17907 stored, "42",
17908 "c:2871 — setnumvalue must store the rendered integer; \
17909 was previously dropped via `let _ = s;`"
17910 );
17911 let _ = pm;
17912 opt_state_set("exec", saved_exec);
17913 }
17914
17915 #[test]
17916 fn test_simple_arrayuniq_first_wins() {
17917 let _g = crate::test_util::global_state_lock();
17918 let v = vec![
17919 "a".to_string(),
17920 "b".to_string(),
17921 "a".to_string(),
17922 "c".to_string(),
17923 ];
17924 assert_eq!(simple_arrayuniq(v), vec!["a", "b", "c"]);
17925 }
17926
17927 #[test]
17928 fn test_split_env_string() {
17929 let _g = crate::test_util::global_state_lock();
17930 assert_eq!(
17931 split_env_string("PATH=/usr/bin:/bin"),
17932 Some(("PATH".to_string(), "/usr/bin:/bin".to_string()))
17933 );
17934 assert_eq!(
17935 split_env_string("EMPTY="),
17936 Some(("EMPTY".to_string(), "".to_string()))
17937 );
17938 assert_eq!(split_env_string("NOEQUALS"), None);
17939 }
17940
17941 #[test]
17942 fn test_mkenvstr() {
17943 let _g = crate::test_util::global_state_lock();
17944 assert_eq!(mkenvstr("PATH", "/usr/bin", 0), "PATH=/usr/bin");
17945 assert_eq!(mkenvstr("EMPTY", "", 0), "EMPTY=");
17946 }
17947
17948 #[test]
17949 fn test_seconds_round_trip() {
17950 let _g = crate::test_util::global_state_lock();
17951 intsecondssetfn(0);
17952 let s1 = intsecondsgetfn();
17953 std::thread::sleep(Duration::from_millis(5));
17954 let s2 = intsecondsgetfn();
17955 assert!(s2 >= s1);
17956 // Reset to a known offset and read back.
17957 setrawseconds(100.0);
17958 assert_eq!(getrawseconds(), 100.0);
17959 }
17960
17961 #[test]
17962 fn test_argzero_round_trip() {
17963 let _g = crate::test_util::global_state_lock();
17964 // pm is UNUSED in argzerosetfn / argzerogetfn (C signature
17965 // matches Rust). Use Param::default() as the dummy carrier.
17966 let mut pm = param::default();
17967 argzerosetfn(&mut pm, "/bin/zsh".to_string());
17968 assert_eq!(argzerogetfn(&pm), "/bin/zsh");
17969 argzerosetfn(&mut pm, String::new());
17970 }
17971
17972 #[test]
17973 fn test_env_get_set() {
17974 let _g = crate::test_util::global_state_lock();
17975 let result = zputenv("ZSHRS_TEST_VAR=hello");
17976 assert_eq!(result, 0);
17977 assert_eq!(zgetenv("ZSHRS_TEST_VAR"), Some("hello".to_string()));
17978 delenv("ZSHRS_TEST_VAR");
17979 assert_eq!(zgetenv("ZSHRS_TEST_VAR"), None);
17980 }
17981
17982 #[test]
17983 fn test_keyboardhack_one_char() {
17984 let _g = crate::test_util::global_state_lock();
17985 let mut __pm = crate::ported::zsh_h::param::default();
17986 keyboardhacksetfn(&mut __pm, "\\".to_string());
17987 assert_eq!(keyboardhackgetfn(&__pm), "\\");
17988 keyboardhacksetfn(&mut __pm, String::new());
17989 assert_eq!(keyboardhackgetfn(&__pm), "");
17990 }
17991
17992 /// Pin: `keyboardhacksetfn` accepts ASCII chars cleanly per
17993 /// `Src/params.c:5040-5060`. Tests the canonical happy path
17994 /// — single ASCII char, empty input, and the ASCII guard.
17995 ///
17996 /// The previous Rust port skipped `unmetafy(x, &len)` (c:5044)
17997 /// before the length and ASCII checks. This test exercises
17998 /// the surface API; the unmetafy fix is doc-pinned in the
17999 /// fn body since constructing Meta-encoded String values for
18000 /// the test fixture would require unsafe (Rust strings must
18001 /// be valid UTF-8 and the Meta byte 0x83 is not a valid
18002 /// UTF-8 lead).
18003 #[test]
18004 fn keyboardhacksetfn_handles_ascii_and_empty() {
18005 let _g = crate::test_util::global_state_lock();
18006 let mut __pm = crate::ported::zsh_h::param::default();
18007 // c:5056 — single ASCII char stored.
18008 keyboardhacksetfn(&mut __pm, ";".to_string());
18009 assert_eq!(
18010 keyboardhackgetfn(&__pm),
18011 ";",
18012 "c:5056 — single ASCII char stored verbatim"
18013 );
18014 // c:5056 — different ASCII char stored.
18015 keyboardhacksetfn(&mut __pm, ",".to_string());
18016 assert_eq!(keyboardhackgetfn(&__pm), ",");
18017 // c:5058 — empty input clears to '\0'.
18018 keyboardhacksetfn(&mut __pm, String::new());
18019 assert_eq!(keyboardhackgetfn(&__pm), "");
18020 }
18021
18022 #[test]
18023 fn test_histchars_default() {
18024 let _g = crate::test_util::global_state_lock();
18025 let _g = HISTCHARS_TEST_LOCK_SHARED
18026 .lock()
18027 .unwrap_or_else(|e| e.into_inner());
18028 histcharssetfn(&mut param::default(), "!^#".to_string());
18029 assert_eq!(histcharsgetfn(¶m::default()), "!^#");
18030 histcharssetfn(&mut param::default(), "@$&".to_string());
18031 assert_eq!(histcharsgetfn(¶m::default()), "@$&");
18032 // c:5095-5097 — an empty string is len 0, not NULL: all three
18033 // characters become '\0' and the value reads back empty.
18034 histcharssetfn(&mut param::default(), String::new());
18035 assert_eq!(bangchar.load(Ordering::SeqCst), 0);
18036 assert_eq!(histcharsgetfn(¶m::default()), "");
18037 histcharssetfn(&mut param::default(), "!^#".to_string());
18038 }
18039
18040 /// Pin: `histcharssetfn` runs `unmetafy` per Src/params.c:5086
18041 /// BEFORE the length truncation and ASCII guard. Previously
18042 /// the Rust port skipped unmetafy, so a Meta-pair would
18043 /// inflate the byte count past 3 and the truncation would
18044 /// drop valid characters.
18045 ///
18046 /// Test the happy path: 1-char, 2-char, 3-char ASCII inputs
18047 /// all parse correctly and each char-position fills the
18048 /// matching atomic.
18049 #[test]
18050 fn histcharssetfn_handles_1_2_3_char_inputs() {
18051 let _g = crate::test_util::global_state_lock();
18052 let _g = HISTCHARS_TEST_LOCK_SHARED
18053 .lock()
18054 .unwrap_or_else(|e| e.into_inner());
18055 // 1-char: bangchar=='Q', hatchar=='\0', hashchar=='\0'.
18056 histcharssetfn(&mut param::default(), "Q".to_string());
18057 assert_eq!(bangchar.load(Ordering::SeqCst), b'Q' as i32);
18058 assert_eq!(hatchar.load(Ordering::SeqCst), 0);
18059 assert_eq!(hashchar.load(Ordering::SeqCst), 0);
18060 // 2-char: bangchar=='X', hatchar=='Y', hashchar=='\0'.
18061 histcharssetfn(&mut param::default(), "XY".to_string());
18062 assert_eq!(bangchar.load(Ordering::SeqCst), b'X' as i32);
18063 assert_eq!(hatchar.load(Ordering::SeqCst), b'Y' as i32);
18064 assert_eq!(hashchar.load(Ordering::SeqCst), 0);
18065 // 3-char: bangchar=='A', hatchar=='B', hashchar=='C'.
18066 histcharssetfn(&mut param::default(), "ABC".to_string());
18067 assert_eq!(bangchar.load(Ordering::SeqCst), b'A' as i32);
18068 assert_eq!(hatchar.load(Ordering::SeqCst), b'B' as i32);
18069 assert_eq!(hashchar.load(Ordering::SeqCst), b'C' as i32);
18070 // 4+ char: c:5087-5088 truncates to 3.
18071 histcharssetfn(&mut param::default(), "WXYZ".to_string());
18072 assert_eq!(bangchar.load(Ordering::SeqCst), b'W' as i32);
18073 assert_eq!(hatchar.load(Ordering::SeqCst), b'X' as i32);
18074 assert_eq!(hashchar.load(Ordering::SeqCst), b'Y' as i32);
18075 // Back to the default characters (an explicit value: "" is not NULL).
18076 histcharssetfn(&mut param::default(), "!^#".to_string());
18077 assert_eq!(bangchar.load(Ordering::SeqCst), b'!' as i32);
18078 assert_eq!(hatchar.load(Ordering::SeqCst), b'^' as i32);
18079 assert_eq!(hashchar.load(Ordering::SeqCst), b'#' as i32);
18080 }
18081}
18082
18083// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
18084// ─── RUST-ONLY ACCESSORS ───
18085//
18086// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
18087// RwLock<T>>` globals declared above. C zsh uses direct global
18088// access; Rust needs these wrappers because `OnceLock::get_or_init`
18089// is the only way to lazily construct shared state. These ported sit
18090// here so the body of this file reads in C source order without
18091// the accessor wrappers interleaved between real port ported.
18092// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
18093
18094// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
18095// ─── RUST-ONLY ACCESSORS ───
18096//
18097// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
18098// RwLock<T>>` globals declared above. C zsh uses direct global
18099// access; Rust needs these wrappers because `OnceLock::get_or_init`
18100// is the only way to lazily construct shared state. These ported sit
18101// here so the body of this file reads in C source order without
18102// the accessor wrappers interleaved between real port ported.
18103// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
18104
18105// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
18106// ─── RUST-ONLY ACCESSORS ───
18107//
18108// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
18109// RwLock<T>>` globals declared above. C zsh uses direct global
18110// access; Rust needs these wrappers because `OnceLock::get_or_init`
18111// is the only way to lazily construct shared state. These ported sit
18112// here so the body of this file reads in C source order without
18113// the accessor wrappers interleaved between real port ported.
18114// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
18115
18116/// !!! WARNING: RUST-ONLY HELPER !!!
18117/// No C counterpart: `OnceLock` accessor for the C global `static Param foundparam`
18118/// (`Src/params.c:640`).
18119fn foundparam_lock() -> &'static Mutex<Option<String>> {
18120 FOUNDPARAM.get_or_init(|| Mutex::new(None))
18121}
18122
18123/// !!! WARNING: RUST-ONLY HELPER !!!
18124/// No C counterpart: `OnceLock` accessor for the C global
18125/// `mod_export HashTable paramtab, realparamtab;` (`Src/params.c:515`). C names the
18126/// global directly; Rust needs a function to lazily construct the `RwLock`.
18127/// Accessor for the global `paramtab` (Src/params.c:515).
18128/// Mirrors C's `paramtab->...` dereference by handing back the
18129/// inner RwLock; callers `.read()` for lookups and `.write()` for
18130/// mutation, operating on the `HashMap<String, Param>` directly.
18131pub fn paramtab() -> &'static RwLock<hashtable_nodes<Param>> {
18132 // c:854 — `paramtab = realparamtab = newparamtable(151, "paramtab");`
18133 PARAMTAB_INNER.get_or_init(|| RwLock::new(hashtable_nodes::newhashtable(151)))
18134}
18135
18136/// !!! WARNING: RUST-ONLY HELPER !!!
18137/// No C counterpart: `OnceLock` accessor for the C global `realparamtab`, declared
18138/// alongside `paramtab` at `Src/params.c:515`.
18139/// Accessor for the global `realparamtab` (Src/params.c:515).
18140/// Same role as `paramtab` for the not-currently-redirected case;
18141/// the alias-flip during assoc-array iteration isn't modelled yet.
18142pub fn realparamtab() -> &'static RwLock<hashtable_nodes<Param>> {
18143 // c:854 — same 151-bucket table C aliases `paramtab` onto.
18144 REALPARAMTAB_INNER.get_or_init(|| RwLock::new(hashtable_nodes::newhashtable(151)))
18145}
18146
18147/// !!! WARNING: RUST-ONLY HELPER !!!
18148/// No C counterpart: `OnceLock` accessor for the C global `static Patprog scanprog;`
18149/// (`Src/params.c:637`).
18150fn scanprog_lock() -> &'static Mutex<Option<String>> {
18151 SCANPROG.get_or_init(|| Mutex::new(None))
18152}
18153
18154/// !!! WARNING: RUST-ONLY HELPER !!!
18155/// No C counterpart: `OnceLock` accessor for the C global `static char *scanstr;`
18156/// (`Src/params.c:638`).
18157fn scanstr_lock() -> &'static Mutex<Option<String>> {
18158 SCANSTR.get_or_init(|| Mutex::new(None))
18159}
18160
18161/// !!! WARNING: RUST-ONLY HELPER !!!
18162/// No C counterpart: `OnceLock` accessor for the C global `static char **paramvals;`
18163/// (`Src/params.c:639`).
18164fn paramvals_lock() -> &'static Mutex<Vec<String>> {
18165 PARAMVALS.get_or_init(|| Mutex::new(Vec::new()))
18166}
18167
18168#[cfg(test)]
18169mod tests {
18170 use super::*;
18171 use crate::ported::zsh_h::Pound;
18172 use crate::zsh_h::hashnode;
18173
18174 /// `environ_image` hands back exactly the environment it took: an
18175 /// added variable goes, a removed one returns AT ITS OLD POSITION
18176 /// (`env` prints the array in order), and a changed value is
18177 /// rewritten. The fork this stands in for leaves all three untouched.
18178 #[test]
18179 fn environ_image_restores_values_membership_and_order() {
18180 let _g = crate::test_util::global_state_lock();
18181 let names = ["ZQ_ENVIMG_A", "ZQ_ENVIMG_B", "ZQ_ENVIMG_C"];
18182 for n in names {
18183 env::set_var(n, "outer");
18184 }
18185 let position = |name: &str| env::vars_os().position(|(k, _)| k == name);
18186 let before: Vec<_> = names.iter().map(|n| position(n)).collect();
18187
18188 let img = environ_image::save();
18189 env::set_var("ZQ_ENVIMG_B", "inner-and-longer");
18190 env::remove_var("ZQ_ENVIMG_A");
18191 env::set_var("ZQ_ENVIMG_NEW", "inner");
18192 img.restore();
18193
18194 for n in names {
18195 assert_eq!(env::var(n).as_deref(), Ok("outer"), "{n}");
18196 }
18197 assert!(env::var_os("ZQ_ENVIMG_NEW").is_none());
18198 let after: Vec<_> = names.iter().map(|n| position(n)).collect();
18199 assert_eq!(before, after, "environ order changed");
18200
18201 // A body that changed nothing leaves the array alone.
18202 let img = environ_image::save();
18203 img.restore();
18204 let again: Vec<_> = names.iter().map(|n| position(n)).collect();
18205 assert_eq!(before, again);
18206 for n in names {
18207 env::remove_var(n);
18208 }
18209 }
18210
18211 /// `setscope_base` pushes the param name onto `SCOPEREFS[base]`
18212 /// when `base > pm.level` (c:6440). Grows the SCOPEREFS Vec as
18213 /// needed (c:6442-6447).
18214 #[test]
18215 fn setscope_base_pushes_name_when_base_above_level() {
18216 let _g = crate::test_util::global_state_lock();
18217 SCOPEREFS.with(|s| s.borrow_mut().clear());
18218 let mut pm = param {
18219 node: hashnode {
18220 next: None,
18221 nam: "foo".to_string(),
18222 flags: 0,
18223 },
18224 u_data: 0,
18225 u_tied: None,
18226 u_arr: None,
18227 u_str: None,
18228 u_val: 0,
18229 u_dval: 0.0,
18230 u_hash: None,
18231 gsu_s: None,
18232 gsu_i: None,
18233 gsu_f: None,
18234 gsu_a: None,
18235 gsu_h: None,
18236 base: 0,
18237 width: 0,
18238 env: None,
18239 ename: None,
18240 old: None,
18241 level: 2,
18242 };
18243 setscope_base(&mut pm, 5);
18244 assert_eq!(pm.base, 5);
18245 SCOPEREFS.with(|s| {
18246 let s = s.borrow();
18247 assert!(s.len() >= 6, "SCOPEREFS grew to fit index 5");
18248 assert_eq!(s[5], vec!["foo".to_string()]);
18249 });
18250 }
18251
18252 /// `assignaparam` rejects slice into PM_HASHED with the canonical
18253 /// "attempt to set slice of associative array" zerr (c:3386-3390).
18254 #[test]
18255 fn assignaparam_rejects_slice_into_hashed() {
18256 let _g = crate::test_util::global_state_lock();
18257 opt_state_set("exec", true);
18258 unsetparam("aa_h");
18259 // Create a hashed param.
18260 sethparam("aa_h", vec!["k".to_string(), "v".to_string()]);
18261 let before = paramtab().read().unwrap().contains_key("aa_h");
18262 assert!(before);
18263 // Slice write should be rejected.
18264 let result = assignaparam("aa_h[idx]", vec!["x".to_string()], 0);
18265 assert!(result.is_none(), "slice into hashed must return None");
18266 unsetparam("aa_h");
18267 opt_state_set("exec", false);
18268 }
18269
18270 /// `assignaparam` on a PM_NAMEREF param resolves the chain first
18271 /// (fetchvalue at c:3392): a ref bound to a not-yet-defined name
18272 /// REDIRECTS the array assignment, creating the TARGET as an
18273 /// array (K01nameref.ztst "assign new array via nameref":
18274 /// `typeset -n ptr=var; ptr=(val1 val2)` → `typeset -g -a var`).
18275 /// The "can't change type of a named reference" rejection
18276 /// (c:3395-3398) only fires for PLACEHOLDER refs (empty refname),
18277 /// covered by the K01 test-setting-ref matrix.
18278 #[test]
18279 fn assignaparam_rejects_nameref_type_change() {
18280 let _g = crate::test_util::global_state_lock();
18281 opt_state_set("exec", true);
18282 unsetparam("aa_nr");
18283 // Insert a PM_NAMEREF param directly.
18284 let pm = param {
18285 node: hashnode {
18286 next: None,
18287 nam: "aa_nr".to_string(),
18288 flags: (PM_NAMEREF | PM_SCALAR) as i32,
18289 },
18290 u_data: 0,
18291 u_tied: None,
18292 u_arr: None,
18293 u_str: Some("target".to_string()),
18294 u_val: 0,
18295 u_dval: 0.0,
18296 u_hash: None,
18297 gsu_s: None,
18298 gsu_i: None,
18299 gsu_f: None,
18300 gsu_a: None,
18301 gsu_h: None,
18302 base: 0,
18303 width: 0,
18304 env: None,
18305 ename: None,
18306 old: None,
18307 level: 0,
18308 };
18309 paramtab()
18310 .write()
18311 .unwrap()
18312 .insert("aa_nr".to_string(), Box::new(pm));
18313
18314 unsetparam("target");
18315 let result = assignaparam("aa_nr", vec!["a".to_string(), "b".to_string()], 0);
18316 assert!(
18317 result.is_some(),
18318 "assignment redirects to the ref's target (c:3392 fetchvalue resolve)"
18319 );
18320
18321 // PM_NAMEREF flag still present on the REF (not stripped).
18322 let pm = paramtab().read().unwrap().get("aa_nr").cloned().unwrap();
18323 assert_ne!(pm.node.flags as u32 & PM_NAMEREF, 0, "PM_NAMEREF preserved");
18324 assert_eq!(
18325 pm.u_str.as_deref(),
18326 Some("target"),
18327 "refname unchanged by assignment-through"
18328 );
18329 // The TARGET was created as an array with the value.
18330 let t = paramtab().read().unwrap().get("target").cloned().unwrap();
18331 assert_eq!(
18332 t.u_arr.as_deref(),
18333 Some(&["a".to_string(), "b".to_string()][..]),
18334 "target holds the assigned array"
18335 );
18336
18337 unsetparam("target");
18338 unsetparam("aa_nr");
18339 opt_state_set("exec", false);
18340 }
18341
18342 /// `assignaparam` with ASSPM_AUGMENT against a scalar param must
18343 /// prepend the previous scalar value as `val[0]` of the new array
18344 /// (c:3404-3412). Implements `a=x; a+=(y z)` → `a=(x y z)`. A
18345 /// regression that drops the prepend would yield `a=(y z)` and
18346 /// silently lose the original scalar — invisible at write time,
18347 /// surfaces only when the caller reads `${a[1]}`.
18348 #[test]
18349 fn assignaparam_augment_prepends_old_scalar() {
18350 let _g = crate::test_util::global_state_lock();
18351 opt_state_set("exec", true);
18352 unsetparam("aa_aug");
18353 // Seed a scalar.
18354 setsparam("aa_aug", "old");
18355 // Augment with two new values.
18356 let pm = assignaparam(
18357 "aa_aug",
18358 vec!["new1".to_string(), "new2".to_string()],
18359 ASSPM_AUGMENT,
18360 )
18361 .expect("augment should succeed");
18362 let arr = pm.u_arr.expect("ASSPM_AUGMENT must produce u_arr");
18363 assert_eq!(
18364 arr,
18365 vec!["old".to_string(), "new1".to_string(), "new2".to_string()],
18366 "c:3408-3411 — scalar prepended at index 0, then new values follow"
18367 );
18368 unsetparam("aa_aug");
18369 opt_state_set("exec", false);
18370 }
18371
18372 /// `assignaparam` against a PM_UNIQUE-flagged target dedupes
18373 /// the value array (c:3401 + arrsetfn's uniqarray). Implements
18374 /// `typeset -U arr; arr=(a b a c b)` → `arr=(a b c)`. A
18375 /// regression that drops the uniqarray call would let duplicates
18376 /// linger — invisible until a downstream `[[ -n ${arr[(r)b]} ]]`
18377 /// check counts more matches than expected, or `$path` grows
18378 /// unbounded with repeated directory entries.
18379 #[test]
18380 fn assignaparam_unique_flag_dedupes_values() {
18381 let _g = crate::test_util::global_state_lock();
18382 opt_state_set("exec", true);
18383 unsetparam("aa_uniq");
18384 // Seed an empty PM_ARRAY|PM_UNIQUE.
18385 setaparam("aa_uniq", vec![]);
18386 {
18387 let mut tab = paramtab().write().unwrap();
18388 let pm = tab.get_mut("aa_uniq").expect("aa_uniq must exist");
18389 pm.node.flags |= PM_UNIQUE as i32;
18390 }
18391 // Now write duplicates; PM_UNIQUE must collapse them.
18392 let pm = assignaparam(
18393 "aa_uniq",
18394 vec!["a".into(), "b".into(), "a".into(), "c".into(), "b".into()],
18395 0,
18396 )
18397 .expect("assignment succeeds");
18398 let arr = pm.u_arr.expect("u_arr populated");
18399 assert_eq!(
18400 arr,
18401 vec!["a".to_string(), "b".to_string(), "c".to_string()],
18402 "c:3401 — PM_UNIQUE collapses duplicates, keeping first occurrence"
18403 );
18404 // The PM_UNIQUE flag must persist through the assignment.
18405 let pm_check = paramtab().read().unwrap().get("aa_uniq").cloned().unwrap();
18406 assert_ne!(
18407 pm_check.node.flags as u32 & PM_UNIQUE,
18408 0,
18409 "PM_UNIQUE flag preserved across assignment"
18410 );
18411 unsetparam("aa_uniq");
18412 opt_state_set("exec", false);
18413 }
18414
18415 /// `getsparam` reads PM_INTEGER params via convbase, not via
18416 /// `u_str` (which is None for typed integers). Pins the latent
18417 /// bug fix that made `read REPLY` after `(( REPLY=42 ))` return
18418 /// nothing — every numeric param read used to fall through to
18419 /// the OS env-var fallback.
18420 #[test]
18421 fn getsparam_returns_integer_via_convbase() {
18422 let _g = crate::test_util::global_state_lock();
18423 opt_state_set("exec", true);
18424 unsetparam("gs_int");
18425 setiparam("gs_int", 999);
18426 assert_eq!(getsparam("gs_int").as_deref(), Some("999"));
18427 unsetparam("gs_int");
18428 opt_state_set("exec", false);
18429 }
18430
18431 /// `getsparam` reads PM_FFLOAT params via convfloat. Same fix
18432 /// shape as the PM_INTEGER path.
18433 #[test]
18434 fn getsparam_returns_float_via_convfloat() {
18435 let _g = crate::test_util::global_state_lock();
18436 opt_state_set("exec", true);
18437 unsetparam("gs_f");
18438 // Stash a float via the setnumvalue / setnparam path.
18439 let v = mnumber {
18440 l: 0,
18441 d: 2.5,
18442 type_: MN_FLOAT,
18443 };
18444 setnparam("gs_f", v);
18445 let s = getsparam("gs_f").expect("PM_FFLOAT should serialize");
18446 // convfloat formats with default precision; just check it's
18447 // not empty and parses back.
18448 assert!(
18449 s.parse::<f64>()
18450 .map(|f| (f - 2.5).abs() < 1e-6)
18451 .unwrap_or(false),
18452 "expected ~2.5 round-trip, got {:?}",
18453 s
18454 );
18455 unsetparam("gs_f");
18456 opt_state_set("exec", false);
18457 }
18458
18459 /// `getsparam` against a non-default `pm.base` integer param must
18460 /// render using that base via `convbase`. The c:2364 dispatch
18461 /// passes `pm.base` (or 10) to convbase; a regression that
18462 /// hardcodes base=10 would silently break `typeset -i 16 hex=255`
18463 /// readers (would see "255" instead of the C-faithful "16#FF").
18464 #[test]
18465 fn getsparam_integer_honors_pm_base() {
18466 let _g = crate::test_util::global_state_lock();
18467 opt_state_set("exec", true);
18468 unsetparam("hex_param");
18469 // Manually insert a PM_INTEGER param with base=16.
18470 let pm = param {
18471 node: hashnode {
18472 next: None,
18473 nam: "hex_param".to_string(),
18474 flags: PM_INTEGER as i32,
18475 },
18476 u_data: 0,
18477 u_tied: None,
18478 u_arr: None,
18479 u_str: None,
18480 u_val: 255,
18481 u_dval: 0.0,
18482 u_hash: None,
18483 gsu_s: None,
18484 gsu_i: None,
18485 gsu_f: None,
18486 gsu_a: None,
18487 gsu_h: None,
18488 base: 16,
18489 width: 0,
18490 env: None,
18491 ename: None,
18492 old: None,
18493 level: 0,
18494 };
18495 paramtab()
18496 .write()
18497 .unwrap()
18498 .insert("hex_param".to_string(), Box::new(pm));
18499
18500 let s = getsparam("hex_param").expect("PM_INTEGER must serialize");
18501 // convbase emits "16#FF" form for base 16.
18502 assert!(
18503 s.contains("FF") || s.contains("ff"),
18504 "c:2364 — base-16 must render hex digits; got {:?}",
18505 s
18506 );
18507
18508 unsetparam("hex_param");
18509 opt_state_set("exec", false);
18510 }
18511
18512 /// `endparamscope` clears `SCOPEREFS[old_locallevel]` and resets
18513 /// nameref params' base when their base exceeds the new locallevel.
18514 /// End-to-end of the setscope_base writer + endparamscope reader.
18515 #[test]
18516 fn endparamscope_resets_scoperefs_and_nameref_base() {
18517 let _g = crate::test_util::global_state_lock();
18518 SCOPEREFS.with(|s| s.borrow_mut().clear());
18519
18520 // Set up: locallevel = 3, push a PM_NAMEREF param with base=5 onto SCOPEREFS[3].
18521 set_locallevel(3);
18522 let pm = param {
18523 node: hashnode {
18524 next: None,
18525 nam: "ref1".to_string(),
18526 flags: (PM_NAMEREF | PM_SCALAR) as i32,
18527 },
18528 u_data: 0,
18529 u_tied: None,
18530 u_arr: None,
18531 u_str: Some(String::new()),
18532 u_val: 0,
18533 u_dval: 0.0,
18534 u_hash: None,
18535 gsu_s: None,
18536 gsu_i: None,
18537 gsu_f: None,
18538 gsu_a: None,
18539 gsu_h: None,
18540 base: 5,
18541 width: 0,
18542 env: None,
18543 ename: None,
18544 old: None,
18545 level: 0,
18546 };
18547 paramtab()
18548 .write()
18549 .unwrap()
18550 .insert("ref1".to_string(), Box::new(pm));
18551 // Populate SCOPEREFS[3] manually with "ref1".
18552 SCOPEREFS.with(|sr| {
18553 let mut sr = sr.borrow_mut();
18554 sr.resize(8, Vec::new());
18555 sr[3].push("ref1".to_string());
18556 });
18557
18558 endparamscope();
18559
18560 // After endparamscope: locallevel decremented to 2; "ref1"'s
18561 // base reset to 0 (was 5 > new ll=2). SCOPEREFS[3] cleared.
18562 let pm_after = paramtab().read().unwrap().get("ref1").cloned();
18563 assert!(pm_after.is_some(), "ref1 should still exist (level=0)");
18564 assert_eq!(pm_after.unwrap().base, 0, "PM_NAMEREF.base reset to 0");
18565 SCOPEREFS.with(|sr| {
18566 assert!(sr.borrow()[3].is_empty(), "SCOPEREFS[3] cleared");
18567 });
18568
18569 // Cleanup
18570 paramtab().write().unwrap().remove("ref1");
18571 set_locallevel(0);
18572 }
18573
18574 /// `endparamscope` MUST NOT reset `base` on PM_UPPER namerefs
18575 /// (c:5891 — the `!(pm->node.flags & PM_UPPER)` clause guards
18576 /// the reset). PM_UPPER namerefs point UPWARD in the scope chain
18577 /// (e.g. `typeset -n -u up=outer` from inside a function) and
18578 /// their base must persist across scope pops so the upward
18579 /// resolution keeps working. A regression that drops the PM_UPPER
18580 /// guard would silently degrade upward namerefs into local ones
18581 /// after the first function return.
18582 #[test]
18583 fn endparamscope_preserves_pm_upper_nameref_base() {
18584 let _g = crate::test_util::global_state_lock();
18585 SCOPEREFS.with(|s| s.borrow_mut().clear());
18586
18587 set_locallevel(3);
18588 let pm = param {
18589 node: hashnode {
18590 next: None,
18591 nam: "up_ref".to_string(),
18592 flags: (PM_NAMEREF | PM_SCALAR | PM_UPPER) as i32,
18593 },
18594 u_data: 0,
18595 u_tied: None,
18596 u_arr: None,
18597 u_str: Some(String::new()),
18598 u_val: 0,
18599 u_dval: 0.0,
18600 u_hash: None,
18601 gsu_s: None,
18602 gsu_i: None,
18603 gsu_f: None,
18604 gsu_a: None,
18605 gsu_h: None,
18606 base: 5,
18607 width: 0,
18608 env: None,
18609 ename: None,
18610 old: None,
18611 level: 0,
18612 };
18613 paramtab()
18614 .write()
18615 .unwrap()
18616 .insert("up_ref".to_string(), Box::new(pm));
18617 SCOPEREFS.with(|sr| {
18618 let mut sr = sr.borrow_mut();
18619 sr.resize(8, Vec::new());
18620 sr[3].push("up_ref".to_string());
18621 });
18622
18623 endparamscope();
18624
18625 let pm_after = paramtab()
18626 .read()
18627 .unwrap()
18628 .get("up_ref")
18629 .cloned()
18630 .expect("up_ref must survive scope pop");
18631 assert_eq!(
18632 pm_after.base, 5,
18633 "c:5891 — PM_UPPER nameref base MUST be preserved (was 5, must stay 5)"
18634 );
18635 assert_ne!(
18636 pm_after.node.flags as u32 & PM_UPPER,
18637 0,
18638 "PM_UPPER flag itself must persist"
18639 );
18640
18641 paramtab().write().unwrap().remove("up_ref");
18642 set_locallevel(0);
18643 }
18644
18645 /// `setscope_base` does NOT push when `base <= pm.level` (the C
18646 /// `if ((pm->base = base) > pm->level)` guard at c:6440 fails).
18647 #[test]
18648 fn setscope_base_no_push_when_base_below_level() {
18649 let _g = crate::test_util::global_state_lock();
18650 SCOPEREFS.with(|s| s.borrow_mut().clear());
18651 let mut pm = param {
18652 node: hashnode {
18653 next: None,
18654 nam: "bar".to_string(),
18655 flags: 0,
18656 },
18657 u_data: 0,
18658 u_tied: None,
18659 u_arr: None,
18660 u_str: None,
18661 u_val: 0,
18662 u_dval: 0.0,
18663 u_hash: None,
18664 gsu_s: None,
18665 gsu_i: None,
18666 gsu_f: None,
18667 gsu_a: None,
18668 gsu_h: None,
18669 base: 0,
18670 width: 0,
18671 env: None,
18672 ename: None,
18673 old: None,
18674 level: 10,
18675 };
18676 setscope_base(&mut pm, 3); // 3 <= 10 → no push
18677 assert_eq!(pm.base, 3);
18678 SCOPEREFS.with(|s| {
18679 // No push happened; SCOPEREFS stays empty.
18680 assert!(s.borrow().is_empty() || s.borrow().iter().all(|v| v.is_empty()));
18681 });
18682 }
18683
18684 /// `setscope_base` boundary: `base == pm.level` must NOT push.
18685 /// The C guard c:6440 is strictly `>` (`> pm->level`, not `>=`).
18686 /// A regression that uses `>=` would push every assignment at
18687 /// the current scope into SCOPEREFS, causing endparamscope to
18688 /// re-process every same-scope param on every function return —
18689 /// O(n) extra work per call PLUS spurious base resets.
18690 #[test]
18691 fn setscope_base_equal_level_does_not_push() {
18692 let _g = crate::test_util::global_state_lock();
18693 SCOPEREFS.with(|s| s.borrow_mut().clear());
18694 let mut pm = param {
18695 node: hashnode {
18696 next: None,
18697 nam: "edge".to_string(),
18698 flags: 0,
18699 },
18700 u_data: 0,
18701 u_tied: None,
18702 u_arr: None,
18703 u_str: None,
18704 u_val: 0,
18705 u_dval: 0.0,
18706 u_hash: None,
18707 gsu_s: None,
18708 gsu_i: None,
18709 gsu_f: None,
18710 gsu_a: None,
18711 gsu_h: None,
18712 base: 0,
18713 width: 0,
18714 env: None,
18715 ename: None,
18716 old: None,
18717 level: 5,
18718 };
18719 setscope_base(&mut pm, 5); // base == level → strict `>` fails
18720 assert_eq!(pm.base, 5, "base assignment always happens");
18721 SCOPEREFS.with(|sr| {
18722 let any_push = sr.borrow().iter().any(|v| !v.is_empty());
18723 assert!(
18724 !any_push,
18725 "c:6440 — `base > pm->level` is STRICT; equal must not push"
18726 );
18727 });
18728 }
18729
18730 #[test]
18731 fn test_colonarr_conversion() {
18732 let _g = crate::test_util::global_state_lock();
18733 let arr = colonsplit("/bin:/usr/bin:/usr/local/bin", false);
18734 assert_eq!(arr, vec!["/bin", "/usr/bin", "/usr/local/bin"]);
18735 let path = colonarrgetfn(&arr);
18736 assert_eq!(path, "/bin:/usr/bin:/usr/local/bin");
18737 }
18738 #[test]
18739 fn test_isident() {
18740 let _g = crate::test_util::global_state_lock();
18741 assert!(isident("foo"));
18742 assert!(isident("_bar"));
18743 assert!(isident("FOO_BAR"));
18744 assert!(isident("x123"));
18745 assert!(isident("123")); // positional params
18746 assert!(!isident(""));
18747 assert!(!isident("foo bar"));
18748 }
18749
18750 /// Pin: `isident` requires balanced `[...]` per `Src/params.c:1329-1330`:
18751 /// if (*ss != '[') return 0;
18752 /// if (!(ss = parse_subscript(++ss, 1, ']'))) return 0;
18753 ///
18754 /// The previous Rust port accepted ANY `[` as a valid
18755 /// terminator (`if c == '[' { return true; }`) without
18756 /// checking for a matching `]`. So `foo[` (no close) was
18757 /// accepted as a valid identifier — diverging from C which
18758 /// rejects.
18759 #[test]
18760 fn isident_requires_balanced_subscript_brackets() {
18761 let _g = crate::test_util::global_state_lock();
18762 // Balanced `[...]` is valid.
18763 assert!(
18764 isident("foo[0]"),
18765 "c:1330 — balanced [0] passes parse_subscript"
18766 );
18767 assert!(
18768 isident("foo[bar]"),
18769 "c:1330 — balanced [bar] passes parse_subscript"
18770 );
18771 // UNBALANCED — open without close — must be rejected.
18772 assert!(
18773 !isident("foo["),
18774 "c:1330 — `foo[` missing `]` MUST be rejected"
18775 );
18776 // Trailing chars after `]` — C parse_subscript returns
18777 // a position INSIDE the string, the surrounding isident
18778 // body checks that nothing follows; our port currently
18779 // returns true at the first `[` either way, but pin the
18780 // balanced case as a working invariant.
18781 assert!(isident("a[1]"), "c:1330 — short balanced subscript valid");
18782 }
18783
18784 #[test]
18785 fn test_unique_array() {
18786 let _g = crate::test_util::global_state_lock();
18787 let arr = vec!["a".into(), "b".into(), "a".into(), "c".into(), "b".into()];
18788 let result = uniqarray(arr);
18789 assert_eq!(result, vec!["a", "b", "c"]);
18790 }
18791
18792 #[test]
18793 fn test_convbase() {
18794 let _g = crate::test_util::global_state_lock();
18795 // CBASES off (default): `16#FF` / `8#7` form. The `0x.../
18796 // 0...` short-prefix output is gated on `setopt CBASES` —
18797 // see Src/params.c:5599-5605.
18798 assert_eq!(convbase(255, 16), "16#FF");
18799 assert_eq!(convbase(10, 10), "10");
18800 assert_eq!(convbase(-5, 10), "-5");
18801 assert_eq!(convbase(7, 8), "8#7");
18802 assert_eq!(convbase(5, 2), "2#101");
18803 }
18804
18805 #[test]
18806 fn test_convfloat() {
18807 let _g = crate::test_util::global_state_lock();
18808 // Use 2.5 instead of 3.14 — clippy errors on the latter as
18809 // an approx PI constant. The test checks 2-decimal formatting
18810 // round-trips, which the exact value doesn't influence.
18811 let s = convfloat(2.5, 2, PM_FFLOAT);
18812 assert!(s.starts_with("2.50"));
18813
18814 assert_eq!(convfloat(f64::INFINITY, 0, 0), "Inf");
18815 assert_eq!(convfloat(f64::NEG_INFINITY, 0, 0), "-Inf");
18816 assert_eq!(convfloat(f64::NAN, 0, 0), "NaN");
18817 }
18818
18819 #[test]
18820 fn test_getarrvalue() {
18821 let _g = crate::test_util::global_state_lock();
18822 let arr = vec!["a".into(), "b".into(), "c".into(), "d".into()];
18823 assert_eq!(getarrvalue(&arr, 2, 3), vec!["b", "c"]);
18824 assert_eq!(getarrvalue(&arr, -2, -1), vec!["c", "d"]);
18825 assert_eq!(getarrvalue(&arr, 1, 4), vec!["a", "b", "c", "d"]);
18826 }
18827
18828 #[test]
18829 fn test_setarrvalue() {
18830 let _g = crate::test_util::global_state_lock();
18831 // c:2897 — setarrvalue bails when unset(EXECOPT). Set "exec"
18832 // for the unit-test env (real zsh defaults exec=true).
18833 let saved_exec = opt_state_get("exec").unwrap_or(false);
18834 opt_state_set("exec", true);
18835 // C-faithful: setarrvalue takes a Value pointing at a Param
18836 // with u_arr set. Construct one inline.
18837 let pm = Box::new(param {
18838 node: hashnode {
18839 next: None,
18840 nam: "test".to_string(),
18841 flags: PM_ARRAY as i32,
18842 },
18843 u_data: 0,
18844 u_tied: None,
18845 u_arr: Some(vec!["a".into(), "b".into(), "c".into(), "d".into()]),
18846 u_str: None,
18847 u_val: 0,
18848 u_dval: 0.0,
18849 u_hash: None,
18850 gsu_s: None,
18851 gsu_i: None,
18852 gsu_f: None,
18853 gsu_a: None,
18854 gsu_h: None,
18855 base: 0,
18856 width: 0,
18857 env: None,
18858 ename: None,
18859 old: None,
18860 level: 0,
18861 });
18862 let mut v = value {
18863 pm: Some(pm),
18864 arr: Vec::new(),
18865 scanflags: 0,
18866 valflags: 0,
18867 start: 2,
18868 end: 3,
18869 };
18870 setarrvalue(&mut v, vec!["X".into(), "Y".into()]);
18871 let arr = v.pm.unwrap().u_arr.unwrap();
18872 assert_eq!(arr, vec!["a", "X", "Y", "d"]);
18873 opt_state_set("exec", saved_exec);
18874 }
18875
18876 #[test]
18877 fn test_valid_refname() {
18878 let _g = crate::test_util::global_state_lock();
18879 assert!(valid_refname("foo", 0));
18880 assert!(valid_refname("_bar", 0));
18881 assert!(valid_refname("1", 0));
18882 assert!(valid_refname("!", 0));
18883 // c:6497-6500 (54718) — no subscripted referents.
18884 assert!(!valid_refname("arr[1]", 0));
18885 assert!(!valid_refname("hash[y][1,2]", 0));
18886 assert!(!valid_refname("1[1]", 0));
18887 assert!(!valid_refname("![1]", 0));
18888 assert!(valid_refname(".a.b", 0));
18889 assert!(!valid_refname("not[2]good", 0));
18890 assert!(!valid_refname("arr[1", 0));
18891 assert!(!valid_refname("12x", 0));
18892 assert!(!valid_refname("@", 0));
18893 assert!(!valid_refname("", 0));
18894 // C semantics: a leader that is neither an identifier nor `! ? $ -`.
18895 assert!(!valid_refname(" ", 0));
18896 // PM_UPPER rejects digit-leader and argv/ARGC.
18897 assert!(!valid_refname("1", PM_UPPER as i32));
18898 assert!(!valid_refname("argv", PM_UPPER as i32));
18899 assert!(!valid_refname("ARGC", PM_UPPER as i32));
18900 }
18901
18902 #[test]
18903 fn test_uniq_array_empty() {
18904 let _g = crate::test_util::global_state_lock();
18905 let empty: Vec<String> = Vec::new();
18906 assert!(uniqarray(empty).is_empty());
18907 }
18908
18909 #[test]
18910 fn test_convbase_underscore() {
18911 let _g = crate::test_util::global_state_lock();
18912 let s = convbase_underscore(1234567, 10, 3);
18913 assert_eq!(s, "1_234_567");
18914 }
18915
18916 fn val_str(v: getarg_out<'_>) -> String {
18917 match v {
18918 getarg_out::Value(v) => v.to_str(),
18919 getarg_out::Flags { .. } => panic!("expected Value, got Flags"),
18920 }
18921 }
18922
18923 #[test]
18924 fn getarg_n_flag_picks_second_exact_match() {
18925 let _g = crate::test_util::global_state_lock();
18926 // C params.c:1431-1442 + 1758 — `(en.2.)pat` picks 2nd exact match.
18927 let arr: Vec<String> = vec!["foo".into(), "bar".into(), "foo".into(), "baz".into()];
18928 let out = getarg("(en.2.r)foo", Some(&arr), None, None).expect("Some");
18929 assert_eq!(val_str(out), "foo");
18930 }
18931
18932 #[test]
18933 fn getarg_n_flag_third_exact_match() {
18934 let _g = crate::test_util::global_state_lock();
18935 let arr: Vec<String> = vec!["a".into(), "a".into(), "a".into(), "b".into()];
18936 let out = getarg("(en.3.r)a", Some(&arr), None, None).expect("Some");
18937 assert_eq!(val_str(out), "a");
18938 }
18939
18940 #[test]
18941 fn getarg_n_flag_returns_index_with_i() {
18942 let _g = crate::test_util::global_state_lock();
18943 // (en.2.i) — return INDEX of 2nd exact match.
18944 let arr: Vec<String> = vec!["x".into(), "y".into(), "x".into(), "y".into()];
18945 let out = getarg("(en.2.i)x", Some(&arr), None, None).expect("Some");
18946 assert_eq!(val_str(out), "3");
18947 }
18948
18949 #[test]
18950 fn getarg_negative_n_flips_search_direction() {
18951 let _g = crate::test_util::global_state_lock();
18952 // C params.c:1488-1491 — negative `num` flips down (reverse).
18953 // (en.-1.) on forward-default search matches from the end.
18954 let arr: Vec<String> = vec!["a".into(), "a".into(), "a".into()];
18955 let out = getarg("(en.-1.i)a", Some(&arr), None, None).expect("Some");
18956 assert_eq!(val_str(out), "3");
18957 }
18958
18959 #[test]
18960 fn getarg_n_flag_zero_treated_as_one() {
18961 let _g = crate::test_util::global_state_lock();
18962 // C params.c:1438-1439 — `if (!num) num = 1`.
18963 let arr: Vec<String> = vec!["x".into(), "y".into()];
18964 let out = getarg("(en.0.r)x", Some(&arr), None, None).expect("Some");
18965 assert_eq!(val_str(out), "x");
18966 }
18967
18968 #[test]
18969 fn getarg_unknown_flag_char_returns_none() {
18970 let _g = crate::test_util::global_state_lock();
18971 // C params.c:1477-1483 flagerr — invalid flag char reports error.
18972 let arr: Vec<String> = vec!["x".into()];
18973 assert!(getarg("(z)x", Some(&arr), None, None).is_none());
18974 }
18975
18976 #[test]
18977 fn getarg_n_flag_unterminated_arg_returns_none() {
18978 let _g = crate::test_util::global_state_lock();
18979 // (n.5 missing closing delimiter — flagerr.
18980 let arr: Vec<String> = vec!["x".into()];
18981 assert!(getarg("(n.5", Some(&arr), None, None).is_none());
18982 }
18983
18984 #[test]
18985 fn getarg_b_flag_starts_search_at_index() {
18986 let _g = crate::test_util::global_state_lock();
18987 // C params.c:1748-1760 — `(b.N.e)pat` skips first N-1 elements
18988 // forward (parsed value `N`, normalized to `beg = N-1`).
18989 let arr: Vec<String> = vec!["x".into(), "y".into(), "x".into(), "y".into()];
18990 // Forward, beg=2 (skip first 2) → starts at idx 2 → 'x' at 3.
18991 let out = getarg("(b.3.ei)x", Some(&arr), None, None).expect("Some");
18992 assert_eq!(val_str(out), "3");
18993 }
18994
18995 #[test]
18996 fn getarg_b_flag_with_R_reverse_from_offset() {
18997 let _g = crate::test_util::global_state_lock();
18998 // C params.c:1750-1755 — reverse search starting at parsed-1 idx.
18999 // arr=(x y x y), beg=2 (parsed 3-1), reverse → walks 2,1,0; first
19000 // exact 'x' is at idx 2 → 1-based index 3.
19001 let arr: Vec<String> = vec!["x".into(), "y".into(), "x".into(), "y".into()];
19002 // `I` reports that index...
19003 let out = getarg("(b.3.eI)x", Some(&arr), None, None).expect("Some");
19004 assert_eq!(val_str(out), "3");
19005 // ...while `R` reports the matched VALUE, and wins when both are
19006 // given (`${arr[(b:3:eIR)x]}` → `x` in zsh 5.9).
19007 let out = getarg("(b.3.eIR)x", Some(&arr), None, None).expect("Some");
19008 assert_eq!(val_str(out), "x");
19009 }
19010
19011 #[test]
19012 fn getarg_b_flag_out_of_bounds_forward_returns_empty() {
19013 let _g = crate::test_util::global_state_lock();
19014 // c:1746 — beg >= len returns len+1 (empty for value-mode).
19015 let arr: Vec<String> = vec!["x".into()];
19016 let out = getarg("(b.5.er)x", Some(&arr), None, None).expect("Some");
19017 assert_eq!(val_str(out), "");
19018 }
19019
19020 #[test]
19021 fn getarg_b_flag_out_of_bounds_index_mode_returns_len_plus_one() {
19022 let _g = crate::test_util::global_state_lock();
19023 let arr: Vec<String> = vec!["x".into(), "y".into()];
19024 let out = getarg("(b.5.ei)x", Some(&arr), None, None).expect("Some");
19025 assert_eq!(val_str(out), "3");
19026 }
19027
19028 #[test]
19029 fn getarg_hash_neg_num_on_lowercase_r_returns_all() {
19030 let _g = crate::test_util::global_state_lock();
19031 // C params.c:1488-1491 — neg `num` flips down on `r`,
19032 // converting hash search to return-all-matches semantics.
19033 let mut h: IndexMap<String, String> = IndexMap::new();
19034 h.insert("a".into(), "1".into());
19035 h.insert("b".into(), "1".into());
19036 h.insert("c".into(), "2".into());
19037 let out = getarg("(en.-1.r)1", None, Some(&h), None).expect("Some");
19038 // r + neg = R semantics → all values where pat matches value.
19039 assert_eq!(val_str(out), "1 1");
19040 }
19041
19042 #[test]
19043 fn getarg_hash_neg_num_on_uppercase_R_returns_single() {
19044 let _g = crate::test_util::global_state_lock();
19045 // R + neg `num` un-flips back to single-match (r semantics).
19046 let mut h: IndexMap<String, String> = IndexMap::new();
19047 h.insert("a".into(), "1".into());
19048 h.insert("b".into(), "1".into());
19049 h.insert("c".into(), "2".into());
19050 let out = getarg("(en.-1.R)1", None, Some(&h), None).expect("Some");
19051 // R + neg → r → single first match.
19052 assert_eq!(val_str(out), "1");
19053 }
19054
19055 #[test]
19056 fn getarg_hash_i_flag_matches_keys_not_values() {
19057 let _g = crate::test_util::global_state_lock();
19058 // On an associative array the `i` flag matches the pattern against
19059 // KEYS (not values) and returns the matching key. A value-search
19060 // therefore finds nothing, and the `b<NUM>` begin-offset does not
19061 // skip entries for an associative key search. Verified against
19062 // zsh 5.9: `h=(a 1 b 1 c 1); ${h[(i)b]}` → "b"; `${h[(b:3:ei)1]}` → "".
19063 // (The prior expectation "c" — a b-offset skip returning a value —
19064 // does not match real zsh; the code already matches zsh.)
19065 let mut h: IndexMap<String, String> = IndexMap::new();
19066 h.insert("a".into(), "1".into());
19067 h.insert("b".into(), "1".into());
19068 h.insert("c".into(), "1".into());
19069 // `i` globs the KEY "b" and returns that key.
19070 let by_key = getarg("(i)b", None, Some(&h), None).expect("Some");
19071 assert_eq!(val_str(by_key), "b");
19072 // Searching a VALUE ("1") matches no key named "1" → empty.
19073 let by_val = getarg("(b.3.ei)1", None, Some(&h), None).expect("Some");
19074 assert_eq!(val_str(by_val), "");
19075 }
19076
19077 #[test]
19078 fn getarg_hash_I_flag_matches_all_keys() {
19079 let _g = crate::test_util::global_state_lock();
19080 // The `I` flag returns ALL matching keys. `(I)*` globs every key;
19081 // a value-search matches no key. Verified against zsh 5.9:
19082 // `h=(a 1 b 1 c 1); ${h[(I)*]}` → "a b c"; `${h[(b:2:eI)1]}` → "".
19083 // (The prior expectation "b c" for a value-search does not match
19084 // real zsh; the code already matches zsh.)
19085 let mut h: IndexMap<String, String> = IndexMap::new();
19086 h.insert("a".into(), "1".into());
19087 h.insert("b".into(), "1".into());
19088 h.insert("c".into(), "1".into());
19089 // `(I)*` matches all keys via glob, in insertion order.
19090 let all_keys = getarg("(I)*", None, Some(&h), None).expect("Some");
19091 assert_eq!(val_str(all_keys), "a b c");
19092 // Value-search "1" matches no key → empty.
19093 let out = getarg("(b.2.eI)1", None, Some(&h), None).expect("Some");
19094 assert_eq!(val_str(out), "");
19095 }
19096
19097 #[test]
19098 fn getarg_hash_b_flag_out_of_bounds_returns_empty() {
19099 let _g = crate::test_util::global_state_lock();
19100 // c:1746 — beg >= len with single-match → empty.
19101 let mut h: IndexMap<String, String> = IndexMap::new();
19102 h.insert("a".into(), "1".into());
19103 let out = getarg("(b.5.e)1", None, Some(&h), None).expect("Some");
19104 assert_eq!(val_str(out), "");
19105 }
19106
19107 #[test]
19108 fn getarg_w_flag_splits_multi_word_array_elements() {
19109 let _g = crate::test_util::global_state_lock();
19110 // C params.c:1761-1797 — `(w)N` joins array then re-splits by
19111 // IFS-default whitespace. arr=("a b" "c d"); (w)2 → "b" not "c d".
19112 let arr: Vec<String> = vec!["a b".into(), "c d".into()];
19113 let out = getarg("(w)2", Some(&arr), None, None).expect("Some");
19114 assert_eq!(val_str(out), "b");
19115 }
19116
19117 #[test]
19118 fn getarg_w_flag_simple_array_indexing_still_works() {
19119 let _g = crate::test_util::global_state_lock();
19120 let arr: Vec<String> = vec!["one".into(), "two".into(), "three".into()];
19121 let out = getarg("(w)2", Some(&arr), None, None).expect("Some");
19122 assert_eq!(val_str(out), "two");
19123 }
19124
19125 #[test]
19126 fn getarg_f_flag_splits_by_newline() {
19127 let _g = crate::test_util::global_state_lock();
19128 // C params.c:1424-1427 — `f` flag aliases `w` with sep="\n".
19129 // arr=("a b\nc d"); (f)2 → "c d" (split by \n only, not space).
19130 let arr: Vec<String> = vec!["a b\nc d".into()];
19131 let out = getarg("(f)2", Some(&arr), None, None).expect("Some");
19132 assert_eq!(val_str(out), "c d");
19133 }
19134
19135 #[test]
19136 fn getarg_scalar_w_flag_picks_nth_word() {
19137 let _g = crate::test_util::global_state_lock();
19138 // C params.c:1761-1797 — scalar word-mode arm. `(w)2` on
19139 // scalar "hello world foo" returns the 2nd whitespace word.
19140 let out = getarg("(w)2", None, None, Some("hello world foo")).expect("Some");
19141 assert_eq!(val_str(out), "world");
19142 }
19143
19144 #[test]
19145 fn getarg_scalar_w_flag_negative_index_counts_from_end() {
19146 let _g = crate::test_util::global_state_lock();
19147 let out = getarg("(w)-1", None, None, Some("alpha beta gamma")).expect("Some");
19148 assert_eq!(val_str(out), "gamma");
19149 }
19150
19151 #[test]
19152 fn getarg_scalar_re_returns_char_at_match_position() {
19153 let _g = crate::test_util::global_state_lock();
19154 // C params.c:1798-1980 — char-search returns CHAR at match
19155 // position, not full substring. Verified empirically:
19156 // /bin/zsh -c 's="barfooxyz"; print "${s[(r)foo]}"' → "f"
19157 let out = getarg("(re)bc", None, None, Some("abcdef")).expect("Some");
19158 assert_eq!(val_str(out), "b");
19159 }
19160
19161 #[test]
19162 fn getarg_scalar_ie_returns_position_of_first_match() {
19163 let _g = crate::test_util::global_state_lock();
19164 let out = getarg("(ie)cd", None, None, Some("abcdef")).expect("Some");
19165 // 'cd' starts at 1-based position 3.
19166 assert_eq!(val_str(out), "3");
19167 }
19168
19169 #[test]
19170 fn getarg_scalar_Ie_returns_position_of_last_match() {
19171 let _g = crate::test_util::global_state_lock();
19172 let out = getarg("(Ie)b", None, None, Some("abcabc")).expect("Some");
19173 // Last 'b' is at 1-based position 5.
19174 assert_eq!(val_str(out), "5");
19175 }
19176
19177 #[test]
19178 fn getarg_scalar_ie_no_match_returns_len_plus_one() {
19179 let _g = crate::test_util::global_state_lock();
19180 let out = getarg("(ie)z", None, None, Some("abc")).expect("Some");
19181 assert_eq!(val_str(out), "4");
19182 }
19183
19184 #[test]
19185 fn getarg_scalar_Ie_no_match_returns_zero() {
19186 let _g = crate::test_util::global_state_lock();
19187 let out = getarg("(Ie)z", None, None, Some("abc")).expect("Some");
19188 assert_eq!(val_str(out), "0");
19189 }
19190
19191 #[test]
19192 fn getarg_scalar_n_flag_picks_second_match() {
19193 let _g = crate::test_util::global_state_lock();
19194 // C params.c:1929/1964 — `!--num` Nth-match counter on
19195 // scalar char-search. abcabc: 'a' at idx 0 and 3 → 2nd match
19196 // at byte position 4 (1-based).
19197 let out = getarg("(en.2.i)a", None, None, Some("abcabc")).expect("Some");
19198 assert_eq!(val_str(out), "4");
19199 }
19200
19201 #[test]
19202 fn getarg_scalar_b_flag_starts_from_offset() {
19203 let _g = crate::test_util::global_state_lock();
19204 // C params.c:1740-1742 — `(b.N.)` starts search from idx N-1.
19205 // abc bc abc: with b=4, skip first 3 chars; first 'b' at byte 5.
19206 let out = getarg("(b.4.ei)b", None, None, Some("abcbc")).expect("Some");
19207 assert_eq!(val_str(out), "4");
19208 }
19209
19210 #[test]
19211 fn getarg_scalar_re_n2_picks_second_substring() {
19212 let _g = crate::test_util::global_state_lock();
19213 let out = getarg("(en.2.r)b", None, None, Some("abab")).expect("Some");
19214 assert_eq!(val_str(out), "b");
19215 }
19216
19217 /// c:3076/3193 — assignsparam writes into paramtab; getsparam
19218 /// reads it back. The round-trip is the spine of every
19219 /// `foo=bar; print $foo` flow. Regression here would silently
19220 /// drop assignments.
19221 #[test]
19222 fn assignsparam_then_getsparam_round_trips() {
19223 let _g = crate::test_util::global_state_lock(); // c:3193
19224 // c:2697 — assignsparam → assignstrvalue bails when
19225 // unset(EXECOPT). The unit-test env doesn't run through
19226 // createoptiontable so we set "exec" explicitly to simulate
19227 // normal runtime. Mirrors the same setup used by
19228 // `setnumvalue_stores_int_value_into_scalar_pm` above.
19229 let saved_exec = opt_state_get("exec") // c:2697
19230 .unwrap_or(false); // c:2697
19231 opt_state_set("exec", true); // c:2697
19232 let name = "ZSHRS_TEST_ASSIGN_GET"; // c:3193
19233 assignsparam(name, "test_value_42", 0); // c:3193
19234 assert_eq!(
19235 // c:3076
19236 getsparam(name).as_deref(), // c:3076
19237 Some("test_value_42") // c:3076
19238 ); // c:3076
19239 // Cleanup so other tests don't see leaked param.
19240 let _ = paramtab().write().unwrap().remove(name); // c:3819
19241 opt_state_set("exec", saved_exec); // c:2697
19242 }
19243
19244 /// Regression: a subscript WRITE to a hash-storage-backed special assoc
19245 /// (`$compstate`, populated during completion via set_compstate_str) with a
19246 /// STRING key must succeed, not error "assignment to invalid subscript
19247 /// range". After the undeclared-subscript auto-vivify was removed, the
19248 /// completion system's `compstate[insert]=menu` writes started arithmetic-
19249 /// evaluating the `insert` key to 0 and failing — flooding real shells with
19250 /// errors on every completion. The fix recognises any name present in
19251 /// paramtab_hashed_storage() as associative. A name NOT in that store still
19252 /// errors (matches zsh: `compstate` only exists during completion).
19253 #[test]
19254 fn subscript_write_to_hash_backed_special_assoc_succeeds() {
19255 let _g = crate::test_util::global_state_lock();
19256 let saved_exec = opt_state_get("exec").unwrap_or(false);
19257 opt_state_set("exec", true);
19258 // Start from a clean `compstate`: completion tests elsewhere in
19259 // the binary leave one behind in the paramtab, and a stale
19260 // non-assoc entry sends the write down the scalar-subscript path
19261 // instead of the hash-backed one this test pins.
19262 unsetparam("compstate");
19263 // Simulate the completion setup that populates $compstate before the
19264 // user completer widget runs (compcore::callcompfunc → set_compstate_str).
19265 paramtab_hashed_storage()
19266 .lock()
19267 .unwrap()
19268 .entry("compstate".to_string())
19269 .or_default();
19270 // User completer write: `compstate[insert]=menu`.
19271 let ok = assignsparam("compstate[insert]", "menu", 0);
19272 assert!(
19273 ok.is_some(),
19274 "compstate[insert]=menu must succeed, got error"
19275 );
19276 assert_eq!(
19277 paramtab_hashed_storage()
19278 .lock()
19279 .unwrap()
19280 .get("compstate")
19281 .and_then(|m| m.get("insert"))
19282 .map(String::as_str),
19283 Some("menu"),
19284 );
19285 // Negative: a name with NO hash-storage backing and a non-numeric key
19286 // still errors (returns None) — the undeclared-subscript guard holds.
19287 let bad = assignsparam("zshrs_no_such_assoc[k1]", "bb", 0);
19288 assert!(
19289 bad.is_none(),
19290 "undeclared non-numeric subscript must still error"
19291 );
19292 // Cleanup.
19293 let _ = paramtab_hashed_storage()
19294 .lock()
19295 .unwrap()
19296 .remove("compstate");
19297 let _ = paramtab().write().unwrap().remove("compstate");
19298 let _ = paramtab().write().unwrap().remove("zshrs_no_such_assoc");
19299 opt_state_set("exec", saved_exec);
19300 }
19301
19302 /// c:3076 — getsparam on a non-existent param returns None.
19303 /// A regression returning Some("") would mask unset-param errors.
19304 #[test]
19305 fn getsparam_unknown_param_returns_none() {
19306 let _g = crate::test_util::global_state_lock();
19307 assert!(getsparam("ZSHRS_TEST_DEFINITELY_UNSET").is_none());
19308 }
19309
19310 /// c:3819 — direct paramtab.remove drops the entry; subsequent
19311 /// getsparam returns None. The set→remove→lookup gap verifies
19312 /// the canonical paramtab is actually backing both reads + writes.
19313 #[test]
19314 fn paramtab_remove_makes_getsparam_return_none() {
19315 let _g = crate::test_util::global_state_lock();
19316 let name = "ZSHRS_TEST_UNSET_FLOW";
19317 assignsparam(name, "to_be_removed", 0);
19318 assert!(
19319 getsparam(name).is_some(),
19320 "param must be set before remove path"
19321 );
19322 let _ = paramtab().write().unwrap().remove(name);
19323 assert!(
19324 getsparam(name).is_none(),
19325 "after remove, getsparam must return None"
19326 );
19327 }
19328
19329 /// c:3357 — assignaparam stores an array. getsparam on an array
19330 /// param returns the first element OR a join (depends on IFS).
19331 /// Verify the slot was populated AT ALL by querying paramtab.
19332 #[test]
19333 fn assignaparam_populates_paramtab_with_array() {
19334 let _g = crate::test_util::global_state_lock();
19335 let name = "ZSHRS_TEST_ARR_X";
19336 assignaparam(name, vec!["a".into(), "b".into(), "c".into()], 0);
19337 let tab = paramtab().read().expect("paramtab poisoned");
19338 let pm = tab.get(name).expect("param installed");
19339 assert_eq!(
19340 pm.u_arr.as_deref(),
19341 Some(&["a".to_string(), "b".to_string(), "c".to_string()][..]),
19342 "assignaparam stores all three elements"
19343 );
19344 drop(tab);
19345 let _ = paramtab().write().unwrap().remove(name);
19346 }
19347
19348 // Use the module-scope HISTCHARS_TEST_LOCK_SHARED (declared
19349 // outside the test modules) so gsu_tests + tests serialise
19350 // against the same Mutex rather than two independent ones.
19351
19352 /// `Src/params.c:5095-5097` — `histcharssetfn` stores bangchar /
19353 /// hatchar / hashchar in the per-char globals. Pin the round-trip
19354 /// for ALL THREE: change HISTCHARS to a custom 3-char string,
19355 /// verify each atomic global reflects the new value, and verify
19356 /// setting the default `"!^#"` back restores all three.
19357 #[test]
19358 fn histcharssetfn_syncs_all_three_histchar_globals() {
19359 let _g = crate::test_util::global_state_lock();
19360 let _g = HISTCHARS_TEST_LOCK_SHARED
19361 .lock()
19362 .unwrap_or_else(|e| e.into_inner());
19363 // Default state.
19364 histcharssetfn(&mut param::default(), "!^#".to_string());
19365 assert_eq!(bangchar.load(Ordering::SeqCst), b'!' as i32);
19366 assert_eq!(hatchar.load(Ordering::SeqCst), b'^' as i32);
19367 assert_eq!(hashchar.load(Ordering::SeqCst), b'#' as i32);
19368 // Set HISTCHARS to "@:%".
19369 histcharssetfn(&mut param::default(), "@:%".to_string());
19370 assert_eq!(
19371 bangchar.load(Ordering::SeqCst),
19372 b'@' as i32,
19373 "c:5095 — bangchar = first byte of HISTCHARS"
19374 );
19375 assert_eq!(
19376 hatchar.load(Ordering::SeqCst),
19377 b':' as i32,
19378 "c:5096 — hatchar = second byte of HISTCHARS"
19379 );
19380 assert_eq!(
19381 hashchar.load(Ordering::SeqCst),
19382 b'%' as i32,
19383 "c:5097 — hashchar = third byte of HISTCHARS"
19384 );
19385 // Restore.
19386 histcharssetfn(&mut param::default(), "!^#".to_string());
19387 assert_eq!(bangchar.load(Ordering::SeqCst), b'!' as i32);
19388 assert_eq!(hashchar.load(Ordering::SeqCst), b'#' as i32);
19389 }
19390
19391 /// `Src/params.c:5064-5074` — `histcharsgetfn` reads from the
19392 /// three atomic globals and returns a string of non-NUL bytes.
19393 /// Pin set→get symmetry: after `histcharssetfn(Some("@&%"))`,
19394 /// `histcharsgetfn(¶m::default())` returns `"@&%"`.
19395 #[test]
19396 fn histcharsgetfn_round_trips_with_histcharssetfn() {
19397 let _g = crate::test_util::global_state_lock();
19398 let _g = HISTCHARS_TEST_LOCK_SHARED
19399 .lock()
19400 .unwrap_or_else(|e| e.into_inner());
19401 histcharssetfn(&mut param::default(), "@&%".to_string());
19402 assert_eq!(
19403 histcharsgetfn(¶m::default()),
19404 "@&%",
19405 "c:5068-5073 — getfn reads atomic globals setfn wrote"
19406 );
19407 // Restore default and verify round-trip (an explicit value: "" is
19408 // len 0, not NULL, c:5095-5097).
19409 histcharssetfn(&mut param::default(), "!^#".to_string());
19410 assert_eq!(
19411 histcharsgetfn(¶m::default()),
19412 "!^#",
19413 "default `!^#` round-trips through atomics"
19414 );
19415 }
19416
19417 /// `Src/params.c:5118-5128` — `homesetfn(x)` round-trip:
19418 /// `homesetfn(s); homegetfn() == s` for non-symlink paths and
19419 /// CHASELINKS-off. Pins the basic store-then-read contract.
19420 #[test]
19421 fn homesetfn_stores_value_for_getfn() {
19422 let _g = crate::test_util::global_state_lock();
19423 let mut __pm = crate::ported::zsh_h::param::default();
19424 let saved = homegetfn(&__pm);
19425 homesetfn(&mut __pm, "/tmp/zshrs_test_home".to_string());
19426 assert_eq!(
19427 homegetfn(&__pm),
19428 "/tmp/zshrs_test_home",
19429 "c:5121-5126 — homesetfn → homegetfn round-trip"
19430 );
19431 // Restore.
19432 homesetfn(&mut __pm, saved);
19433 }
19434
19435 /// `Src/params.c:5125-5126` — empty input becomes `ztrdup("")`.
19436 /// Pin empty-string handling.
19437 #[test]
19438 fn homesetfn_empty_input_stores_empty() {
19439 let _g = crate::test_util::global_state_lock();
19440 let mut __pm = crate::ported::zsh_h::param::default();
19441 let saved = homegetfn(&__pm);
19442 homesetfn(&mut __pm, String::new());
19443 assert_eq!(
19444 homegetfn(&__pm),
19445 "",
19446 "c:5126 — empty x stores empty (no panic)"
19447 );
19448 homesetfn(&mut __pm, saved);
19449 }
19450
19451 /// `Src/params.c:5004-5011` — `errnosetfn(x)` writes errno
19452 /// unconditionally, then warns (NOT errors) on truncation. The
19453 /// store happens regardless of warning. Pin set→get round-trip.
19454 #[test]
19455 #[cfg(any(target_os = "macos", target_os = "linux"))]
19456 fn errnosetfn_writes_through_to_libc_errno_getfn() {
19457 let _g = crate::test_util::global_state_lock();
19458 // Set errno to a small int.
19459 errnosetfn(42);
19460 assert_eq!(
19461 errnogetfn(),
19462 42,
19463 "c:5006 — errno = (int)x; subsequent getfn must read it back"
19464 );
19465 errnosetfn(0);
19466 assert_eq!(errnogetfn(), 0);
19467 }
19468
19469 /// `Src/params.c:5008-5010` — truncation check fires when
19470 /// `(zlong)errno != x`. C also resets errno indirectly inside
19471 /// `zwarn` (libc calls touch errno) — so after the warning,
19472 /// the user's observed `$ERRNO` is the post-warning value, NOT
19473 /// the truncated cast. Faithful Rust port has the same behavior.
19474 /// Pin only that the function returns normally and doesn't crash;
19475 /// any specific post-call errno value is implementation-defined.
19476 #[test]
19477 #[cfg(any(target_os = "macos", target_os = "linux"))]
19478 fn errnosetfn_does_not_panic_on_truncation() {
19479 let _g = crate::test_util::global_state_lock();
19480 // i64::MAX → truncates to i32 = -1 → warning fires inside.
19481 // The store at c:5008 happens; whether the warning's libc
19482 // calls then overwrite errno is implementation-defined.
19483 errnosetfn(i64::MAX);
19484 // Just verify the call returned (no panic) and getfn works.
19485 let _ = errnogetfn();
19486 // Reset.
19487 errnosetfn(0);
19488 }
19489
19490 /// `Src/params.c:5090-5093` — non-ASCII chars in HISTCHARS
19491 /// produce a warning and the function returns WITHOUT updating
19492 /// any globals. Pin the rejection: state before == state after
19493 /// when a non-ASCII byte is in position 0/1/2.
19494 #[test]
19495 fn histcharssetfn_rejects_non_ascii_chars() {
19496 let _g = crate::test_util::global_state_lock();
19497 let _g = HISTCHARS_TEST_LOCK_SHARED
19498 .lock()
19499 .unwrap_or_else(|e| e.into_inner());
19500 // Reset to defaults.
19501 histcharssetfn(&mut param::default(), "!^#".to_string());
19502 let bang_before = bangchar.load(Ordering::SeqCst);
19503 let hat_before = hatchar.load(Ordering::SeqCst);
19504 // Try to set HISTCHARS with non-ASCII char.
19505 histcharssetfn(&mut param::default(), "é".to_string());
19506 // c:5092 — rejection returns BEFORE any state changes.
19507 assert_eq!(
19508 bangchar.load(Ordering::SeqCst),
19509 bang_before,
19510 "c:5092 — bangchar unchanged after non-ASCII rejection"
19511 );
19512 assert_eq!(hatchar.load(Ordering::SeqCst), hat_before);
19513 }
19514
19515 /// Shared mutex for tests that mutate argzero/posixzero — both
19516 /// share global state and race when run in parallel.
19517 static ARGZERO_TEST_LOCK: Mutex<()> = Mutex::new(());
19518
19519 // HISTSIZ_TEST_LOCK is defined at module scope to share between
19520 // gsu_tests and tests submodules — both mutate histsiz.
19521
19522 /// `Src/params.c:4974-4977` — `histsizesetfn` floors at 1 then
19523 /// calls `resizehistents()` to prune the in-memory ring to the
19524 /// new cap. The previous Rust port skipped the resize call (and
19525 /// also failed to mirror the value into `hist::histsiz`), so
19526 /// HISTSIZE shrinks didn't take effect until the next implicit
19527 /// prune. Pin: setting HISTSIZE to N caps both the param store
19528 /// AND the hist::histsiz atomic used by resizehistents.
19529 #[test]
19530 fn histsizesetfn_floors_at_one_and_mirrors_to_hist_module() {
19531 let _g = crate::test_util::global_state_lock();
19532 let _g = HISTSIZ_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
19533 let saved_param = histsizegetfn();
19534 let saved_hist = histsiz.load(Ordering::SeqCst);
19535
19536 // c:4976 — value < 1 floors at 1.
19537 histsizesetfn(0);
19538 assert_eq!(histsizegetfn(), 1, "c:4976 — HISTSIZE 0 must floor at 1");
19539 assert_eq!(
19540 histsiz.load(Ordering::SeqCst),
19541 1,
19542 "c:4977 — mirror into hist::histsiz so resizehistents sees it"
19543 );
19544
19545 // Negative floors too.
19546 histsizesetfn(-5);
19547 assert_eq!(histsizegetfn(), 1, "c:4976 — negative floors at 1");
19548
19549 // Positive passes through.
19550 histsizesetfn(500);
19551 assert_eq!(histsizegetfn(), 500);
19552 assert_eq!(histsiz.load(Ordering::SeqCst), 500);
19553
19554 // Restore.
19555 *histsiz_lock().lock().unwrap() = saved_param;
19556 histsiz.store(saved_hist, Ordering::SeqCst);
19557 }
19558
19559 /// `Src/params.c:5152-5158` — `underscoregetfn` returns
19560 /// `dupstring(zunderscore)` then runs `untokenize(u)` on it.
19561 /// The Rust port previously skipped untokenize, exposing raw
19562 /// lexer-injected token bytes (Stringg, Equals, ...) in `$_`
19563 /// reads.
19564 #[test]
19565 fn underscoregetfn_runs_untokenize_on_zunderscore() {
19566 let _g = crate::test_util::global_state_lock();
19567 // Inject zunderscore containing a Pound token byte (\u{e184})
19568 // and verify it gets stripped by untokenize in the return.
19569 let saved = zunderscore_lock().lock().unwrap().clone();
19570
19571 // Set zunderscore to a string containing a Pound token byte
19572 // surrounded by literals.
19573 let pound = Pound;
19574 let mut s = String::new();
19575 s.push('a');
19576 s.push(pound);
19577 s.push('b');
19578 *zunderscore_lock().lock().unwrap() = s;
19579
19580 let result = underscoregetfn();
19581 // c:5156 — untokenize replaces Pound (ITOK) with '#'
19582 // (its ztokens entry). The raw \u{e184} byte must NOT survive.
19583 assert!(
19584 !result.contains(pound),
19585 "c:5156 — untokenize must strip Pound token byte from $_"
19586 );
19587 assert!(
19588 result.contains('#') || result.contains("a"),
19589 "c:5156 — Pound (ITOK) maps to '#' via ztokens[0]"
19590 );
19591
19592 // Restore.
19593 *zunderscore_lock().lock().unwrap() = saved;
19594 }
19595
19596 /// `Src/params.c:4954-4961` — `argzerogetfn` returns `posixzero`
19597 /// when `isset(POSIXARGZERO)`, else `argzero`. After mutating
19598 /// argzero (e.g. `exec -a foo`), `$0` under POSIXARGZERO must
19599 /// report the ORIGINAL startup argv[0], not the rewritten name.
19600 #[test]
19601 fn argzerogetfn_respects_posixargzero_option() {
19602 let _g = crate::test_util::global_state_lock();
19603 let _g = ARGZERO_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
19604
19605 // Save state.
19606 let saved_argzero = argzero();
19607 let saved_posixzero = posixzero();
19608 let saved_pos_option = opt_state_get("posixargzero").unwrap_or(false);
19609
19610 // Set up: posixzero (original) ≠ argzero (rewritten).
19611 set_posixzero(Some("/bin/zsh".to_string()));
19612 set_argzero(Some("rewritten-name".to_string()));
19613 // The set_argzero call mirrors to posixzero only if unset,
19614 // and we set posixzero first → mirror skipped. Confirm separation.
19615
19616 // pm is UNUSED in argzerogetfn (C signature matches Rust).
19617 // Use param::default() as the dummy carrier.
19618 let pm = param::default();
19619
19620 // POSIXARGZERO off → returns argzero.
19621 opt_state_set("posixargzero", false);
19622 assert_eq!(
19623 argzerogetfn(&pm),
19624 "rewritten-name",
19625 "c:4960 — !POSIXARGZERO returns argzero (current display name)"
19626 );
19627
19628 // POSIXARGZERO on → returns posixzero (the preserved startup argv[0]).
19629 opt_state_set("posixargzero", true);
19630 assert_eq!(
19631 argzerogetfn(&pm),
19632 "/bin/zsh",
19633 "c:4959 — POSIXARGZERO on returns posixzero (original startup argv[0])"
19634 );
19635
19636 // Restore.
19637 set_argzero(saved_argzero);
19638 set_posixzero(saved_posixzero);
19639 opt_state_set("posixargzero", saved_pos_option);
19640 }
19641
19642 /// `Src/init.c:271` — `argv0 = argzero = posixzero = *argv++`.
19643 /// At shell init both share the same source. The Rust port
19644 /// preserves this contract by having `set_argzero` mirror to
19645 /// `posixzero` ONLY on first call (when posixzero is None).
19646 /// Subsequent argzero changes (function frames, exec -a) must
19647 /// NOT clobber posixzero.
19648 #[test]
19649 fn set_argzero_mirrors_to_posixzero_only_on_first_call() {
19650 let _g = crate::test_util::global_state_lock();
19651 let _g = ARGZERO_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
19652
19653 let saved_argzero = argzero();
19654 let saved_posixzero = posixzero();
19655
19656 // Reset both to None.
19657 set_argzero(None);
19658 set_posixzero(None);
19659 // First call: posixzero is None, so it should mirror.
19660 set_argzero(Some("/usr/local/bin/zsh".to_string()));
19661 assert_eq!(
19662 posixzero().as_deref(),
19663 Some("/usr/local/bin/zsh"),
19664 "c:271 — first set_argzero mirrors to posixzero (was None)"
19665 );
19666 // Second call: posixzero now Some, so mirror is skipped.
19667 set_argzero(Some("function-name".to_string()));
19668 assert_eq!(
19669 posixzero().as_deref(),
19670 Some("/usr/local/bin/zsh"),
19671 "c:271 — second set_argzero does NOT clobber posixzero"
19672 );
19673 assert_eq!(
19674 argzero().as_deref(),
19675 Some("function-name"),
19676 "argzero updated as normal"
19677 );
19678
19679 // Restore.
19680 set_posixzero(saved_posixzero);
19681 set_argzero(saved_argzero);
19682 }
19683
19684 /// Locale-touching tests share process-wide env + libc state.
19685 /// Cargo runs tests in parallel by default, so without
19686 /// serialization a concurrent `env::set_var("LC_ALL")` can race
19687 /// a `env::remove_var("LC_ALL")` and corrupt assertions. Pin
19688 /// every locale test through this Mutex.
19689 fn locale_test_lock() -> &'static Mutex<()> {
19690 static L: OnceLock<Mutex<()>> = OnceLock::new();
19691 L.get_or_init(|| Mutex::new(()))
19692 }
19693
19694 /// Pin `LC_NAMES` to the canonical zsh `lc_names[]` table at
19695 /// `Src/params.c:4805-4825`. The five categories in entry order
19696 /// (LC_COLLATE, LC_CTYPE, LC_MESSAGES, LC_NUMERIC, LC_TIME) MUST
19697 /// match — `lcsetfn` walks this table by `strcmp(ln->name, pm->node.nam)`
19698 /// per c:4926 and dispatches to `setlocale(ln->category, ...)`.
19699 #[test]
19700 fn lc_names_match_zsh_canonical_table() {
19701 let _g = crate::test_util::global_state_lock();
19702 let names: Vec<&str> = LC_NAMES.iter().map(|(n, _)| *n).collect();
19703 assert_eq!(
19704 names,
19705 vec![
19706 "LC_COLLATE",
19707 "LC_CTYPE",
19708 "LC_MESSAGES",
19709 "LC_NUMERIC",
19710 "LC_TIME"
19711 ],
19712 "Src/params.c:4805-4825 — lc_names entry order must be preserved"
19713 );
19714 // Verify each name maps to a distinct libc category — proves
19715 // we aren't aliasing LC_NUMERIC to LC_TIME etc.
19716 let cats: Vec<libc::c_int> = LC_NAMES.iter().map(|(_, c)| *c).collect();
19717 let mut sorted = cats.clone();
19718 sorted.sort();
19719 sorted.dedup();
19720 assert_eq!(sorted.len(), 5, "all five LC_* categories must be distinct");
19721 assert!(cats.contains(&libc::LC_COLLATE));
19722 assert!(cats.contains(&libc::LC_CTYPE));
19723 assert!(cats.contains(&libc::LC_MESSAGES));
19724 assert!(cats.contains(&libc::LC_NUMERIC));
19725 assert!(cats.contains(&libc::LC_TIME));
19726 }
19727
19728 /// Pin `lcsetfn` to the canonical `setlocale` invocation at
19729 /// `Src/params.c:4925-4927`. When LC_ALL is empty and pm matches
19730 /// an entry in `lc_names`, libc setlocale MUST be called with
19731 /// the corresponding category. Verified by reading libc state
19732 /// back via `setlocale(cat, NULL)` after the assignment.
19733 #[test]
19734 fn lcsetfn_invokes_libc_setlocale_for_matching_category() {
19735 let _g = crate::test_util::global_state_lock();
19736 let _g = locale_test_lock().lock().unwrap_or_else(|e| e.into_inner());
19737 // Save LC_ALL/LC_CTYPE state.
19738 let saved_lc_all = env::var("LC_ALL").ok();
19739 let saved_lc_ctype = env::var("LC_CTYPE").ok();
19740 env::remove_var("LC_ALL"); // c:4912 LC_ALL must be empty for body to run
19741 // Drop LC_ALL from paramtab too — the sibling
19742 // `lcsetfn_short_circuits_when_lc_all_set` test sets it via
19743 // setsparam (params.rs:12986) and getsparam reads paramtab
19744 // BEFORE env::var (params.rs:4346). Without this clear, lcsetfn
19745 // sees the leftover paramtab value and short-circuits.
19746 unsetparam("LC_ALL");
19747
19748 // Read libc's current LC_CTYPE setting.
19749 let before = unsafe {
19750 let p = libc::setlocale(libc::LC_CTYPE, std::ptr::null());
19751 if p.is_null() {
19752 String::new()
19753 } else {
19754 std::ffi::CStr::from_ptr(p).to_string_lossy().into_owned()
19755 }
19756 };
19757
19758 // Call lcsetfn with LC_CTYPE → "C" (universally available POSIX locale).
19759 // c:4876 matches the category on `pm->node.nam`, so the parameter
19760 // being assigned IS the category selector — the name has to live on
19761 // the `param` handed in, not in a separate argument.
19762 let mut pm = param::default();
19763 pm.node.nam = "LC_CTYPE".to_string();
19764 lcsetfn(&mut pm, "C".to_string());
19765
19766 // Read it back — must report "C" since C invokes setlocale(LC_CTYPE, "C").
19767 let after = unsafe {
19768 let p = libc::setlocale(libc::LC_CTYPE, std::ptr::null());
19769 if p.is_null() {
19770 String::new()
19771 } else {
19772 std::ffi::CStr::from_ptr(p).to_string_lossy().into_owned()
19773 }
19774 };
19775 assert_eq!(
19776 after, "C",
19777 "Src/params.c:4927 — lcsetfn must call setlocale(LC_CTYPE, \"C\")"
19778 );
19779
19780 // Env mirror also set.
19781 assert_eq!(env::var("LC_CTYPE").unwrap_or_default(), "C");
19782
19783 // Restore libc + env state.
19784 let _ = unsafe {
19785 let c = std::ffi::CString::new(before.as_bytes()).unwrap_or_default();
19786 libc::setlocale(libc::LC_CTYPE, c.as_ptr())
19787 };
19788 match saved_lc_all {
19789 Some(v) => env::set_var("LC_ALL", v),
19790 None => env::remove_var("LC_ALL"),
19791 }
19792 match saved_lc_ctype {
19793 Some(v) => env::set_var("LC_CTYPE", v),
19794 None => env::remove_var("LC_CTYPE"),
19795 }
19796 }
19797
19798 /// Pin `lcsetfn`'s LC_ALL early-return per c:4912-4913: when
19799 /// LC_ALL is non-empty, lcsetfn must short-circuit BEFORE
19800 /// touching libc setlocale for the per-category override.
19801 #[test]
19802 fn lcsetfn_short_circuits_when_lc_all_set() {
19803 let _g = crate::test_util::global_state_lock();
19804 let _g = locale_test_lock().lock().unwrap_or_else(|e| e.into_inner());
19805 let saved_lc_all = env::var("LC_ALL").ok();
19806 let saved_lc_ctype = env::var("LC_CTYPE").ok();
19807 env::set_var("LC_ALL", "C"); // c:4912 non-empty LC_ALL
19808 // Also stamp paramtab — `getsparam` reads paramtab FIRST
19809 // (params.rs:4346) and only falls through to `env::var` when
19810 // the key is absent. A prior test that left LC_ALL in paramtab
19811 // with an empty value (PM_UNSET tombstone or similar) makes
19812 // getsparam return Some("") and the env::set_var above never
19813 // reaches lcsetfn. Setting paramtab here pins the contract.
19814 setsparam("LC_ALL", "C");
19815
19816 // Capture libc state before.
19817 let before = unsafe {
19818 let p = libc::setlocale(libc::LC_CTYPE, std::ptr::null());
19819 if p.is_null() {
19820 String::new()
19821 } else {
19822 std::ffi::CStr::from_ptr(p).to_string_lossy().into_owned()
19823 }
19824 };
19825
19826 // Try to set LC_CTYPE; should NOT touch libc state.
19827 let mut pm = param::default();
19828 pm.node.nam = "LC_CTYPE".to_string();
19829 lcsetfn(&mut pm, "POSIX".to_string());
19830
19831 // libc state must be unchanged.
19832 let after = unsafe {
19833 let p = libc::setlocale(libc::LC_CTYPE, std::ptr::null());
19834 if p.is_null() {
19835 String::new()
19836 } else {
19837 std::ffi::CStr::from_ptr(p).to_string_lossy().into_owned()
19838 }
19839 };
19840 assert_eq!(
19841 before, after,
19842 "c:4912-4913 — lcsetfn must early-return when LC_ALL is non-empty"
19843 );
19844
19845 // Restore.
19846 match saved_lc_all {
19847 Some(v) => env::set_var("LC_ALL", v),
19848 None => env::remove_var("LC_ALL"),
19849 }
19850 match saved_lc_ctype {
19851 Some(v) => env::set_var("LC_CTYPE", v),
19852 None => env::remove_var("LC_CTYPE"),
19853 }
19854 }
19855
19856 /// Pin `getsparam_u` to its canonical C body at
19857 /// `Src/params.c:3089-3094`: returns `unmeta(getsparam(s))`,
19858 /// NOT a PM_SCALAR-checked `getstrvalue` wrapper.
19859 ///
19860 /// Before this fix, the Rust port took `Option<&mut value>`
19861 /// and gated on `PM_TYPE == PM_SCALAR` — a complete fabrication
19862 /// with no caller because no caller's type fit the bogus sig.
19863 #[test]
19864 fn getsparam_u_unmetas_getsparam_result() {
19865 let _g = crate::test_util::global_state_lock();
19866 let _g = locale_test_lock().lock().unwrap_or_else(|e| e.into_inner());
19867
19868 // Plain ASCII: getsparam_u returns the same content as
19869 // getsparam (no Meta bytes to strip).
19870 let saved = env::var("ZSHRS_TEST_LOCALE_GSU").ok();
19871 env::set_var("ZSHRS_TEST_LOCALE_GSU", "en_US.UTF-8");
19872 assert_eq!(
19873 getsparam_u("ZSHRS_TEST_LOCALE_GSU"),
19874 Some("en_US.UTF-8".to_string()),
19875 "Src/params.c:3092 — getsparam_u returns unmeta(getsparam(s)) for ASCII"
19876 );
19877
19878 // Missing param: returns None (matches C `if ((s = getsparam(s)))` false branch).
19879 env::remove_var("ZSHRS_TEST_LOCALE_GSU_MISSING");
19880 assert_eq!(
19881 getsparam_u("ZSHRS_TEST_LOCALE_GSU_MISSING"),
19882 None,
19883 "Src/params.c:3094 — getsparam_u returns NULL when getsparam returns NULL"
19884 );
19885
19886 // Restore.
19887 match saved {
19888 Some(v) => env::set_var("ZSHRS_TEST_LOCALE_GSU", v),
19889 None => env::remove_var("ZSHRS_TEST_LOCALE_GSU"),
19890 }
19891 }
19892
19893 /// Pin `setarrvalue` EXECOPT bail per `Src/params.c:2897-2898`.
19894 /// Same NO_EXEC semantic as setnumvalue: dry-run shell evaluation
19895 /// must not mutate array params.
19896 #[test]
19897 fn setarrvalue_bails_under_no_exec() {
19898 let _g = crate::test_util::global_state_lock();
19899
19900 let saved_exec = opt_state_get("exec").unwrap_or(false);
19901
19902 opt_state_set("exec", false);
19903 let pm = Box::new(param {
19904 node: hashnode {
19905 next: None,
19906 nam: "noexec_arr".to_string(),
19907 flags: PM_ARRAY as i32,
19908 },
19909 u_data: 0,
19910 u_tied: None,
19911 u_arr: Some(vec!["initial".to_string()]),
19912 u_str: None,
19913 u_val: 0,
19914 u_dval: 0.0,
19915 u_hash: None,
19916 gsu_s: None,
19917 gsu_i: None,
19918 gsu_f: None,
19919 gsu_a: None,
19920 gsu_h: None,
19921 base: 0,
19922 width: 0,
19923 env: None,
19924 ename: None,
19925 old: None,
19926 level: 0,
19927 });
19928 let mut v = value {
19929 pm: Some(pm),
19930 arr: Vec::new(),
19931 scanflags: 0,
19932 valflags: 0,
19933 start: 0,
19934 end: -1,
19935 };
19936 // Under NO_EXEC, the assign must be skipped.
19937 setarrvalue(&mut v, vec!["new1".to_string(), "new2".to_string()]);
19938 let arr = v.pm.as_ref().unwrap().u_arr.clone().unwrap_or_default();
19939 assert_eq!(
19940 arr,
19941 vec!["initial".to_string()],
19942 "c:2897 — NO_EXEC: setarrvalue must NOT replace u_arr"
19943 );
19944
19945 // With exec=true, the same call replaces.
19946 opt_state_set("exec", true);
19947 setarrvalue(&mut v, vec!["new1".to_string(), "new2".to_string()]);
19948 let arr = v.pm.as_ref().unwrap().u_arr.clone().unwrap_or_default();
19949 assert_eq!(
19950 arr,
19951 vec!["new1".to_string(), "new2".to_string()],
19952 "with EXEC set, setarrvalue replaces u_arr"
19953 );
19954
19955 opt_state_set("exec", saved_exec);
19956 }
19957
19958 /// Pin `setnumvalue` EXECOPT bail per `Src/params.c:2860`.
19959 /// When unset(EXECOPT) (i.e. NO_EXEC mode via `zsh -n` or
19960 /// `set -n`), param mutations MUST be skipped so dry-run shell
19961 /// evaluation doesn't leak state into the param table.
19962 #[test]
19963 fn setnumvalue_bails_under_no_exec() {
19964 let _g = crate::test_util::global_state_lock();
19965
19966 let saved_exec = opt_state_get("exec").unwrap_or(false);
19967
19968 // c:2860 — NO_EXEC: setnumvalue must not mutate the param.
19969 opt_state_set("exec", false);
19970 let mut pm = Box::new(param {
19971 node: hashnode {
19972 next: None,
19973 nam: "ne".to_string(),
19974 flags: PM_INTEGER as i32,
19975 },
19976 u_data: 0,
19977 u_tied: None,
19978 u_arr: None,
19979 u_str: None,
19980 u_val: 999,
19981 u_dval: 0.0,
19982 u_hash: None,
19983 gsu_s: None,
19984 gsu_i: None,
19985 gsu_f: None,
19986 gsu_a: None,
19987 gsu_h: None,
19988 base: 0,
19989 width: 0,
19990 env: None,
19991 ename: None,
19992 old: None,
19993 level: 0,
19994 });
19995 let mut v = value {
19996 pm: Some(pm.clone()),
19997 arr: Vec::new(),
19998 scanflags: 0,
19999 valflags: 0,
20000 start: 0,
20001 end: -1,
20002 };
20003 let val = mnumber {
20004 l: 42,
20005 d: 0.0,
20006 type_: MN_INTEGER,
20007 };
20008 setnumvalue(Some(&mut v), val);
20009 // pm.u_val MUST still be 999 (the initial), not 42.
20010 let stored = v.pm.as_ref().unwrap().u_val;
20011 assert_eq!(
20012 stored, 999,
20013 "c:2860 — NO_EXEC: setnumvalue must NOT mutate pm.u_val \
20014 (was {} but should stay 999)",
20015 stored
20016 );
20017
20018 // With exec=true, the same call mutates.
20019 opt_state_set("exec", true);
20020 setnumvalue(Some(&mut v), val);
20021 let stored = v.pm.as_ref().unwrap().u_val;
20022 assert_eq!(stored, 42, "with EXEC set, setnumvalue stores u_val = 42");
20023
20024 let _ = pm;
20025 opt_state_set("exec", saved_exec);
20026 }
20027
20028 /// Pin `$-` rendering to honor `set -n` (noexec). The previous
20029 /// Rust port called `opt("noexec")` which isn't a real option
20030 /// name in zsh — the lookup always returned false so `$-` never
20031 /// included 'n' even when `set -n` was active.
20032 #[test]
20033 fn dash_param_rendering_honors_noexec_via_exec_negation() {
20034 let _g = crate::test_util::global_state_lock();
20035 let saved = opt_state_get("exec").unwrap_or(false);
20036
20037 // With exec=true (default), $- should NOT include 'n'.
20038 opt_state_set("exec", true);
20039 let s = lookup_special_var("-").unwrap_or_default();
20040 assert!(
20041 !s.contains('n'),
20042 "exec=true → $-=`{}` must NOT include 'n'",
20043 s
20044 );
20045
20046 // With exec=false (`set -n`), $- SHOULD include 'n'.
20047 opt_state_set("exec", false);
20048 let s = lookup_special_var("-").unwrap_or_default();
20049 assert!(
20050 s.contains('n'),
20051 "exec=false → $-=`{}` MUST include 'n' (was silently dropped \
20052 when reading non-existent option name `noexec`)",
20053 s
20054 );
20055
20056 opt_state_set("exec", saved);
20057 }
20058
20059 /// Pin `TERM_UNKNOWN` bit value to the canonical C value at
20060 /// `Src/zsh.h:1986`. The previous params.rs duplicate had
20061 /// `1 << 0 = 0x01` which is actually C's TERM_BAD (Src/zsh.h:1985);
20062 /// the correct TERM_UNKNOWN value is 0x02. This single-byte
20063 /// drift caused the params.rs term-init code to silently set the
20064 /// TERM_BAD bit instead of TERM_UNKNOWN, while prompt.rs guards
20065 /// imported the correct 0x02 value from zsh_h.rs — the two
20066 /// paths disagreed about which bit means \"unknown terminal\".
20067 #[test]
20068 fn term_unknown_bit_value_matches_c() {
20069 let _g = crate::test_util::global_state_lock();
20070 assert_eq!(
20071 TERM_UNKNOWN, 0x02,
20072 "Src/zsh.h:1986 — TERM_UNKNOWN must be 0x02, got {:#x}",
20073 TERM_UNKNOWN
20074 );
20075 assert_eq!(
20076 TERM_BAD, 0x01,
20077 "Src/zsh.h:1985 — TERM_BAD must be 0x01 (and != TERM_UNKNOWN)"
20078 );
20079 // Crucially: TERM_BAD and TERM_UNKNOWN must be DISTINCT bits.
20080 assert_ne!(
20081 TERM_BAD, TERM_UNKNOWN,
20082 "TERM_BAD and TERM_UNKNOWN must be distinct (caught the 1<<0 drift bug)"
20083 );
20084 }
20085
20086 /// Pin `getstrvalue` PM_INTEGER branch to canonical C convbase
20087 /// dispatch at `Src/params.c:2373`. The previous Rust port used
20088 /// naked `.to_string()` (base-10) regardless of `pm.base`; C
20089 /// honors the param's stored base so `typeset -i 16 x=255` renders
20090 /// as `0xff` not `255`.
20091 #[test]
20092 fn getstrvalue_pm_integer_honors_pm_base() {
20093 let _g = crate::test_util::global_state_lock();
20094
20095 let saved_cbases_top = opt_state_get("cbases").unwrap_or(false);
20096 opt_state_set("cbases", true);
20097
20098 // Build a PM_INTEGER param with u_val=255 and base=16.
20099 let mut pm = Box::new(param {
20100 node: hashnode {
20101 next: None,
20102 nam: "test_hex_var".to_string(),
20103 flags: PM_INTEGER as i32,
20104 },
20105 u_data: 0,
20106 u_tied: None,
20107 u_arr: None,
20108 u_str: None,
20109 u_val: 255,
20110 u_dval: 0.0,
20111 u_hash: None,
20112 gsu_s: None,
20113 gsu_i: None,
20114 gsu_f: None,
20115 gsu_a: None,
20116 gsu_h: None,
20117 base: 16,
20118 width: 0,
20119 env: None,
20120 ename: None,
20121 old: None,
20122 level: 0,
20123 });
20124 let mut v = value {
20125 pm: Some(pm.clone()),
20126 arr: Vec::new(),
20127 scanflags: 0,
20128 valflags: 0,
20129 start: 0,
20130 end: -1,
20131 };
20132 let rendered = getstrvalue(Some(&mut v));
20133 assert_eq!(
20134 rendered, "0xFF",
20135 "c:2373 / c:5621 — PM_INTEGER base=16 + u_val=255 with CBASES \
20136 renders as `0xFF` (uppercase per C `dig - 10 + 'A'`), got {:?}",
20137 rendered
20138 );
20139
20140 // Base-8 (octal) with OCTALZEROES.
20141 let saved_oct = opt_state_get("octalzeroes").unwrap_or(false);
20142 let saved_cbases = opt_state_get("cbases").unwrap_or(false);
20143 opt_state_set("cbases", true);
20144 opt_state_set("octalzeroes", true);
20145 pm.base = 8;
20146 pm.u_val = 8;
20147 v.pm = Some(pm.clone());
20148 let rendered = getstrvalue(Some(&mut v));
20149 assert_eq!(
20150 rendered, "010",
20151 "c:2373 — PM_INTEGER base=8 with OCTALZEROES renders as `010`, got {:?}",
20152 rendered
20153 );
20154 opt_state_set("cbases", saved_cbases);
20155 opt_state_set("octalzeroes", saved_oct);
20156
20157 // Base=0 (default) → base-10.
20158 pm.base = 0;
20159 pm.u_val = 42;
20160 v.pm = Some(pm.clone());
20161 let rendered = getstrvalue(Some(&mut v));
20162 assert_eq!(
20163 rendered, "42",
20164 "c:2373 — PM_INTEGER base=0 defaults to base-10"
20165 );
20166
20167 opt_state_set("cbases", saved_cbases_top);
20168 }
20169
20170 /// Pin `unsetparam` to its canonical C body at `Src/params.c:3819-3833`.
20171 /// Two guards the previous Rust port skipped:
20172 /// 1. PM_NAMEREF params are NOT removed by unsetparam (c:3830).
20173 /// 2. PM_READONLY rejection per unsetparam_pm c:3850 — readonly
20174 /// params survive the unset call.
20175 #[test]
20176 fn unsetparam_skips_nameref_and_readonly() {
20177 let _g = crate::test_util::global_state_lock();
20178
20179 let saved_exec = opt_state_get("exec").unwrap_or(false);
20180 opt_state_set("exec", true);
20181
20182 // Helper: install a scalar param with the given flag-set.
20183 fn install(name: &str, value: &str, flags: u32) {
20184 let mut tab = paramtab().write().unwrap();
20185 tab.insert(
20186 name.to_string(),
20187 Box::new(param {
20188 node: hashnode {
20189 next: None,
20190 nam: name.to_string(),
20191 flags: (PM_SCALAR | flags) as i32,
20192 },
20193 u_data: 0,
20194 u_tied: None,
20195 u_arr: None,
20196 u_str: Some(value.to_string()),
20197 u_val: 0,
20198 u_dval: 0.0,
20199 u_hash: None,
20200 gsu_s: None,
20201 gsu_i: None,
20202 gsu_f: None,
20203 gsu_a: None,
20204 gsu_h: None,
20205 base: 0,
20206 width: 0,
20207 env: None,
20208 ename: None,
20209 old: None,
20210 level: 0,
20211 }),
20212 );
20213 }
20214
20215 // c:3830 — nameref params skip the unset.
20216 let nameref_name = "zshrs_test_unsetparam_nameref";
20217 install(nameref_name, "target_var_name", PM_NAMEREF);
20218 unsetparam(nameref_name);
20219 {
20220 let tab = paramtab().read().unwrap();
20221 assert!(
20222 tab.contains_key(nameref_name),
20223 "c:3830 — PM_NAMEREF param survives unsetparam"
20224 );
20225 }
20226
20227 // c:3850 (via unsetparam_pm) — readonly rejection.
20228 let ro_name = "zshrs_test_unsetparam_readonly";
20229 install(ro_name, "locked", PM_READONLY);
20230 unsetparam(ro_name);
20231 {
20232 let tab = paramtab().read().unwrap();
20233 assert!(
20234 tab.contains_key(ro_name),
20235 "c:3850 — PM_READONLY param survives unsetparam"
20236 );
20237 }
20238
20239 // Plain scalar removed normally.
20240 let plain_name = "zshrs_test_unsetparam_plain";
20241 install(plain_name, "removable", 0);
20242 unsetparam(plain_name);
20243 {
20244 let tab = paramtab().read().unwrap();
20245 assert!(
20246 !tab.contains_key(plain_name),
20247 "plain scalar successfully removed"
20248 );
20249 }
20250
20251 // Clean up.
20252 {
20253 let mut tab = paramtab().write().unwrap();
20254 tab.remove(nameref_name);
20255 tab.remove(ro_name);
20256 tab.remove(plain_name);
20257 }
20258 opt_state_set("exec", saved_exec);
20259 }
20260
20261 /// Pin `assigniparam` to its canonical C body at `Src/params.c:3754-3761`.
20262 /// Three-arg signature: `(s, val, flags)`. Previous Rust port
20263 /// dropped the flags arg AND returned void; this restores both.
20264 #[test]
20265 fn assigniparam_takes_flags_arg_and_returns_param() {
20266 let _g = crate::test_util::global_state_lock();
20267
20268 let saved_exec = opt_state_get("exec").unwrap_or(false);
20269 opt_state_set("exec", true);
20270
20271 let name = "zshrs_test_assigniparam_x";
20272 {
20273 let mut tab = paramtab().write().unwrap();
20274 tab.remove(name);
20275 }
20276
20277 // c:3755-3760 — assigniparam returns Param and threads flags through.
20278 let r = assigniparam(name, 77, ASSPM_WARN as i32);
20279 assert!(
20280 r.is_some(),
20281 "c:3760 — returns Some(Param) for new int param"
20282 );
20283 {
20284 let tab = paramtab().read().unwrap();
20285 let pm = tab.get(name).expect("integer param created");
20286 assert_ne!(
20287 (pm.node.flags as u32) & PM_INTEGER,
20288 0,
20289 "c:3757-3760 — PM_INTEGER flag set"
20290 );
20291 assert_eq!(pm.u_val, 77, "c:3759 — value stored in u_val");
20292 }
20293
20294 // Reassign with a different flag value (0 — no warnings).
20295 let r = assigniparam(name, 88, 0);
20296 assert!(r.is_some(), "reassign returns Some");
20297 {
20298 let tab = paramtab().read().unwrap();
20299 let pm = tab.get(name).expect("param still present");
20300 assert_eq!(pm.u_val, 88, "reassign updates u_val");
20301 }
20302
20303 // Clean up.
20304 {
20305 let mut tab = paramtab().write().unwrap();
20306 tab.remove(name);
20307 }
20308 opt_state_set("exec", saved_exec);
20309 }
20310
20311 /// Pin `setnparam` to its canonical C body at `Src/params.c:3745-3749`.
20312 /// MUST accept `mnumber` (integer or float) and return Param.
20313 /// Previous Rust port took `f64` only and returned void — losing
20314 /// the integer side and the Param return entirely.
20315 #[test]
20316 fn setnparam_accepts_both_integer_and_float() {
20317 let _g = crate::test_util::global_state_lock();
20318
20319 let saved_exec = opt_state_get("exec").unwrap_or(false);
20320 opt_state_set("exec", true);
20321
20322 // Clean up any leftover.
20323 let int_name = "zshrs_test_setnparam_i";
20324 let flt_name = "zshrs_test_setnparam_f";
20325 {
20326 let mut tab = paramtab().write().unwrap();
20327 tab.remove(int_name);
20328 tab.remove(flt_name);
20329 }
20330
20331 // c:3748 — integer branch: setnparam returns Some(Param) with
20332 // PM_INTEGER flag and u_val set.
20333 let r = setnparam(
20334 int_name,
20335 mnumber {
20336 l: 999,
20337 d: 0.0,
20338 type_: MN_INTEGER,
20339 },
20340 );
20341 assert!(r.is_some(), "setnparam returns Some for new param");
20342 {
20343 let tab = paramtab().read().unwrap();
20344 let pm = tab.get(int_name).expect("integer param created");
20345 assert_ne!(
20346 (pm.node.flags as u32) & PM_INTEGER,
20347 0,
20348 "c:3748 — PM_INTEGER flag set for integer mnumber"
20349 );
20350 assert_eq!(pm.u_val, 999, "c:3748 — integer value stored in u_val");
20351 }
20352
20353 // c:3748 — float branch: setnparam with MN_FLOAT creates PM_FFLOAT.
20354 let r = setnparam(
20355 flt_name,
20356 mnumber {
20357 l: 0,
20358 d: 3.14,
20359 type_: MN_FLOAT,
20360 },
20361 );
20362 assert!(r.is_some(), "setnparam returns Some for new float param");
20363 {
20364 let tab = paramtab().read().unwrap();
20365 let pm = tab.get(flt_name).expect("float param created");
20366 assert_ne!(
20367 (pm.node.flags as u32) & PM_FFLOAT,
20368 0,
20369 "c:3748 — PM_FFLOAT flag set for float mnumber"
20370 );
20371 assert!(
20372 (pm.u_dval - 3.14).abs() < 1e-10,
20373 "c:3748 — float value stored in u_dval"
20374 );
20375 }
20376
20377 // Clean up.
20378 {
20379 let mut tab = paramtab().write().unwrap();
20380 tab.remove(int_name);
20381 tab.remove(flt_name);
20382 }
20383 opt_state_set("exec", saved_exec);
20384 }
20385
20386 /// Pin `setiparam` to its canonical C body at `Src/params.c:3767-3773`.
20387 /// MUST create the param as PM_INTEGER via `assignnparam`, not as
20388 /// PM_SCALAR via `assignsparam` with a stringified value.
20389 #[test]
20390 fn setiparam_creates_pm_integer_param() {
20391 let _g = crate::test_util::global_state_lock();
20392 let name = "zshrs_test_setiparam_x";
20393
20394 // C: `assignnparam` bails when `unset(EXECOPT)` (Src/params.c:3679).
20395 // Real zsh startup sets exec=true; the unit-test env doesn't run
20396 // through `createoptiontable` so we set "exec" explicitly to
20397 // simulate normal runtime.
20398 let saved_exec = opt_state_get("exec").unwrap_or(false);
20399 opt_state_set("exec", true);
20400
20401 // Clean up any leftover.
20402 {
20403 let mut tab = paramtab().write().unwrap();
20404 tab.remove(name);
20405 }
20406
20407 // Set integer value.
20408 setiparam(name, 42);
20409
20410 // Param should exist with PM_INTEGER flag set + u_val == 42.
20411 {
20412 let tab = paramtab().read().unwrap();
20413 let pm = tab.get(name).expect("setiparam must create the param");
20414 assert_ne!(
20415 (pm.node.flags as u32) & PM_INTEGER,
20416 0,
20417 "c:3770-3772 — created param must have PM_INTEGER flag set, \
20418 got flags = {:#x}",
20419 pm.node.flags
20420 );
20421 assert_eq!(pm.u_val, 42, "c:3771 — integer value stored in pm.u_val");
20422 }
20423
20424 // Reassign to verify update path also keeps PM_INTEGER.
20425 setiparam(name, 100);
20426 {
20427 let tab = paramtab().read().unwrap();
20428 let pm = tab.get(name).expect("setiparam reassign must keep param");
20429 assert_eq!(pm.u_val, 100, "reassign updates the integer value");
20430 assert_ne!(
20431 (pm.node.flags as u32) & PM_INTEGER,
20432 0,
20433 "reassign keeps PM_INTEGER flag"
20434 );
20435 }
20436
20437 // Clean up.
20438 {
20439 let mut tab = paramtab().write().unwrap();
20440 tab.remove(name);
20441 }
20442 // Restore EXECOPT.
20443 opt_state_set("exec", saved_exec);
20444 }
20445
20446 /// Pin `gethparam` / `gethkparam` to their canonical C bodies at
20447 /// `Src/params.c:3117-3140`. Same signature-fix family as `getaparam`:
20448 /// the `name: &str` path with digit-first reject + PM_HASHED check.
20449 #[test]
20450 fn gethparam_and_gethkparam_signature_matches_c() {
20451 let _g = crate::test_util::global_state_lock();
20452 // c:3122 / c:3136 — digit-first name reject.
20453 assert_eq!(
20454 gethparam("123abc"),
20455 None,
20456 "c:3122 — digit-first name rejected"
20457 );
20458 assert_eq!(
20459 gethkparam("123abc"),
20460 None,
20461 "c:3136 — digit-first name rejected"
20462 );
20463
20464 // Missing param → None.
20465 assert_eq!(
20466 gethparam("zshrs_test_hashparam_xyz"),
20467 None,
20468 "missing param returns None"
20469 );
20470 assert_eq!(
20471 gethkparam("zshrs_test_hashparam_xyz"),
20472 None,
20473 "missing param returns None"
20474 );
20475
20476 // PM_SCALAR param (not hashed) → None.
20477 {
20478 let mut tab = paramtab().write().unwrap();
20479 tab.insert(
20480 "zshrs_test_gethp_scalar".to_string(),
20481 Box::new(param {
20482 node: hashnode {
20483 next: None,
20484 nam: "zshrs_test_gethp_scalar".to_string(),
20485 flags: PM_SCALAR as i32,
20486 },
20487 u_data: 0,
20488 u_tied: None,
20489 u_arr: None,
20490 u_str: Some("scalar value".to_string()),
20491 u_val: 0,
20492 u_dval: 0.0,
20493 u_hash: None,
20494 gsu_s: None,
20495 gsu_i: None,
20496 gsu_f: None,
20497 gsu_a: None,
20498 gsu_h: None,
20499 base: 0,
20500 width: 0,
20501 env: None,
20502 ename: None,
20503 old: None,
20504 level: 0,
20505 }),
20506 );
20507 }
20508 assert_eq!(
20509 gethparam("zshrs_test_gethp_scalar"),
20510 None,
20511 "c:3123 — non-PM_HASHED returns None"
20512 );
20513 assert_eq!(
20514 gethkparam("zshrs_test_gethp_scalar"),
20515 None,
20516 "c:3137 — non-PM_HASHED returns None"
20517 );
20518
20519 // PM_HASHED param → Some(Vec::new()) for an empty association.
20520 {
20521 let mut tab = paramtab().write().unwrap();
20522 tab.insert(
20523 "zshrs_test_gethp_hash".to_string(),
20524 Box::new(param {
20525 node: hashnode {
20526 next: None,
20527 nam: "zshrs_test_gethp_hash".to_string(),
20528 flags: PM_HASHED as i32,
20529 },
20530 u_data: 0,
20531 u_tied: None,
20532 u_arr: None,
20533 u_str: None,
20534 u_val: 0,
20535 u_dval: 0.0,
20536 u_hash: None,
20537 gsu_s: None,
20538 gsu_i: None,
20539 gsu_f: None,
20540 gsu_a: None,
20541 gsu_h: None,
20542 base: 0,
20543 width: 0,
20544 env: None,
20545 ename: None,
20546 old: None,
20547 level: 0,
20548 }),
20549 );
20550 }
20551 assert_eq!(
20552 gethparam("zshrs_test_gethp_hash"),
20553 Some(Vec::new()),
20554 "c:3123-3124 — PM_HASHED empty-storage returns Some(empty vec)"
20555 );
20556 assert_eq!(
20557 gethkparam("zshrs_test_gethp_hash"),
20558 Some(Vec::new()),
20559 "c:3137-3138 — PM_HASHED empty-storage returns Some(empty vec)"
20560 );
20561
20562 // Populate paramtab_hashed_storage and verify gethparam/gethkparam
20563 // return the actual values/keys per c:3124 (SCANPM_WANTVALS) and
20564 // c:3138 (SCANPM_WANTKEYS). IndexMap preserves insertion order.
20565 {
20566 let mut store = paramtab_hashed_storage().lock().unwrap();
20567 let mut map: IndexMap<String, String> = IndexMap::new();
20568 map.insert("k1".to_string(), "v1".to_string());
20569 map.insert("k2".to_string(), "v2".to_string());
20570 store.insert("zshrs_test_gethp_hash".to_string(), map);
20571 }
20572 assert_eq!(
20573 gethparam("zshrs_test_gethp_hash"),
20574 Some(vec!["v1".to_string(), "v2".to_string()]),
20575 "c:3124 — paramvalarr(SCANPM_WANTVALS) returns values from hashed-storage"
20576 );
20577 assert_eq!(
20578 gethkparam("zshrs_test_gethp_hash"),
20579 Some(vec!["k1".to_string(), "k2".to_string()]),
20580 "c:3138 — paramvalarr(SCANPM_WANTKEYS) returns keys from hashed-storage"
20581 );
20582
20583 // Clean up.
20584 {
20585 let mut tab = paramtab().write().unwrap();
20586 tab.remove("zshrs_test_gethp_scalar");
20587 tab.remove("zshrs_test_gethp_hash");
20588 }
20589 paramtab_hashed_storage()
20590 .lock()
20591 .unwrap()
20592 .remove("zshrs_test_gethp_hash");
20593 }
20594
20595 /// c:Src/params.c:717 — `paramvalarr(v->pm->gsu.h->getfn(v->pm),
20596 /// v->scanflags)`. For a SPECIALPMDEF magic hash that `getfn` hands
20597 /// back the fake `HashTable` whose `scantab` IS the module's
20598 /// `scanpm*` function (`Src/module.c` createspecialhash), so the
20599 /// SCAN is the backing — there is no stored map to read.
20600 ///
20601 /// zshrs keeps ordinary assoc contents in the name-keyed
20602 /// `paramtab_hashed_storage` map, and a seeded row for a magic name
20603 /// left an EMPTY map there which `gethparam`/`gethkparam` consulted
20604 /// FIRST, shadowing the scanfn. Symptom: `compadd -k commands`
20605 /// (`Completion/Unix/Type/_path_commands` sh:103) reaches
20606 /// `get_data_arr` → `gethkparam` and returned ZERO matches in a
20607 /// fresh shell, so command-name completion offered builtins only —
20608 /// and only started working once some other `$commands` read had run
20609 /// the scan, which is what calls `fillcmdnamtable` under
20610 /// HASH_LIST_ALL (`Src/Modules/parameter.c:253`).
20611 ///
20612 /// `builtins` is the probe rather than `commands` because
20613 /// `scanpmbuiltins` enumerates the builtin table, which is populated
20614 /// in every process and does not depend on `$PATH` state left behind
20615 /// by another test.
20616 #[test]
20617 fn magic_hash_scanfn_outranks_an_empty_hashed_storage_row() {
20618 let _g = crate::test_util::global_state_lock();
20619 crate::vm_helper::seed_partab_param("builtins");
20620 // Exactly the row that used to answer instead of the scan.
20621 paramtab_hashed_storage()
20622 .lock()
20623 .unwrap()
20624 .insert("builtins".to_string(), IndexMap::new());
20625
20626 let keys = gethkparam("builtins").expect("c:3137 — `builtins` is PM_HASHED");
20627 assert!(
20628 keys.iter().any(|k| k == "print"),
20629 "c:3138 — an empty hashed-storage row must not shadow scanpmbuiltins \
20630 (got {} key(s))",
20631 keys.len()
20632 );
20633 let vals = gethparam("builtins").expect("c:3123 — `builtins` is PM_HASHED");
20634 assert_eq!(
20635 vals.len(),
20636 keys.len(),
20637 "c:3124 — the value scan must answer from the same source as the keys"
20638 );
20639
20640 paramtab_hashed_storage().lock().unwrap().remove("builtins");
20641 }
20642
20643 /// Pin `getaparam` to its canonical C body at `Src/params.c:3101-3110`.
20644 /// Three branches: digit-first reject (c:3107), PM_ARRAY return
20645 /// (c:3108-3109), non-array / missing-param return None (c:3110).
20646 #[test]
20647 fn getaparam_returns_array_for_pm_array_only() {
20648 let _g = crate::test_util::global_state_lock();
20649 // c:3107 — digit-first name → None (positional params reject).
20650 assert_eq!(
20651 getaparam("123abc"),
20652 None,
20653 "c:3107 — digit-first name rejected"
20654 );
20655
20656 // c:3110 — missing param → None.
20657 assert_eq!(
20658 getaparam("zshrs_test_arr_nonexistent_xyz"),
20659 None,
20660 "c:3110 — missing param returns None"
20661 );
20662
20663 // Helper that builds a Param via the canonical createparam
20664 // path so we don't reach into struct internals (param has
20665 // many fields and no Default).
20666 fn build_arr(name: &str, arr: Vec<String>) {
20667 let mut tab = paramtab().write().unwrap();
20668 tab.insert(
20669 name.to_string(),
20670 Box::new(param {
20671 node: hashnode {
20672 next: None,
20673 nam: name.to_string(),
20674 flags: PM_ARRAY as i32,
20675 },
20676 u_data: 0,
20677 u_tied: None,
20678 u_arr: Some(arr),
20679 u_str: None,
20680 u_val: 0,
20681 u_dval: 0.0,
20682 u_hash: None,
20683 gsu_s: None,
20684 gsu_i: None,
20685 gsu_f: None,
20686 gsu_a: None,
20687 gsu_h: None,
20688 base: 0,
20689 width: 0,
20690 env: None,
20691 ename: None,
20692 old: None,
20693 level: 0,
20694 }),
20695 );
20696 }
20697 fn build_scalar(name: &str, s: &str) {
20698 let mut tab = paramtab().write().unwrap();
20699 tab.insert(
20700 name.to_string(),
20701 Box::new(param {
20702 node: hashnode {
20703 next: None,
20704 nam: name.to_string(),
20705 flags: PM_SCALAR as i32,
20706 },
20707 u_data: 0,
20708 u_tied: None,
20709 u_arr: None,
20710 u_str: Some(s.to_string()),
20711 u_val: 0,
20712 u_dval: 0.0,
20713 u_hash: None,
20714 gsu_s: None,
20715 gsu_i: None,
20716 gsu_f: None,
20717 gsu_a: None,
20718 gsu_h: None,
20719 base: 0,
20720 width: 0,
20721 env: None,
20722 ename: None,
20723 old: None,
20724 level: 0,
20725 }),
20726 );
20727 }
20728
20729 // c:3108 — PM_ARRAY param returns the array contents.
20730 build_arr(
20731 "zshrs_test_getaparam_arr",
20732 vec!["one".to_string(), "two".to_string(), "three".to_string()],
20733 );
20734 assert_eq!(
20735 getaparam("zshrs_test_getaparam_arr"),
20736 Some(vec![
20737 "one".to_string(),
20738 "two".to_string(),
20739 "three".to_string()
20740 ]),
20741 "c:3108-3109 — PM_ARRAY param returns its array"
20742 );
20743
20744 // c:3108 — PM_SCALAR (non-array) param → None.
20745 build_scalar("zshrs_test_getaparam_scalar", "not an array");
20746 assert_eq!(
20747 getaparam("zshrs_test_getaparam_scalar"),
20748 None,
20749 "c:3108 — non-PM_ARRAY param returns None"
20750 );
20751
20752 // Clean up.
20753 {
20754 let mut tab = paramtab().write().unwrap();
20755 tab.remove("zshrs_test_getaparam_arr");
20756 tab.remove("zshrs_test_getaparam_scalar");
20757 }
20758 }
20759
20760 // ═══════════════════════════════════════════════════════════════════
20761 // setX/getX round-trips and cross-type coercion.
20762 // Anchored to zsh behavior:
20763 // - `typeset -i x=42; print -- $x` → "42"
20764 // - `x=42; print -- ${(t)x}` → "scalar" (set without typeset stays scalar)
20765 // - integer ↔ scalar coercion when reading back.
20766 // Each test sets up via setX, reads back via getX, and asserts the
20767 // observable shell-level value.
20768 // ═══════════════════════════════════════════════════════════════════
20769
20770 fn with_exec<F: FnOnce()>(body: F) {
20771 let _g = crate::test_util::global_state_lock();
20772 let saved = opt_state_get("exec").unwrap_or(false);
20773 opt_state_set("exec", true);
20774 body();
20775 opt_state_set("exec", saved);
20776 }
20777
20778 // ── Scalar round-trip ──────────────────────────────────────────
20779 /// `setsparam("X", "hello"); getsparam("X")` → Some("hello").
20780 /// Anchor: `X=hello; print -r -- "$X"` in zsh → `hello`.
20781 #[test]
20782 fn setsparam_then_getsparam_roundtrip_basic() {
20783 with_exec(|| {
20784 unsetparam("zshrs_rt_s1");
20785 setsparam("zshrs_rt_s1", "hello");
20786 assert_eq!(getsparam("zshrs_rt_s1").as_deref(), Some("hello"));
20787 unsetparam("zshrs_rt_s1");
20788 });
20789 }
20790
20791 /// Empty string round-trip — scalar can hold "".
20792 #[test]
20793 fn setsparam_empty_string_roundtrips_as_empty() {
20794 with_exec(|| {
20795 unsetparam("zshrs_rt_s2");
20796 setsparam("zshrs_rt_s2", "");
20797 assert_eq!(getsparam("zshrs_rt_s2").as_deref(), Some(""));
20798 unsetparam("zshrs_rt_s2");
20799 });
20800 }
20801
20802 /// Multi-byte UTF-8 scalar round-trip.
20803 /// Anchor: `X="日本語"; print -r -- "$X"` → "日本語"
20804 #[test]
20805 fn setsparam_multibyte_utf8_roundtrips() {
20806 with_exec(|| {
20807 unsetparam("zshrs_rt_s3");
20808 setsparam("zshrs_rt_s3", "日本語");
20809 assert_eq!(getsparam("zshrs_rt_s3").as_deref(), Some("日本語"));
20810 unsetparam("zshrs_rt_s3");
20811 });
20812 }
20813
20814 /// Embedded newline survives round-trip.
20815 #[test]
20816 fn setsparam_embedded_newline_roundtrips() {
20817 with_exec(|| {
20818 unsetparam("zshrs_rt_s4");
20819 setsparam("zshrs_rt_s4", "a\nb\nc");
20820 assert_eq!(getsparam("zshrs_rt_s4").as_deref(), Some("a\nb\nc"));
20821 unsetparam("zshrs_rt_s4");
20822 });
20823 }
20824
20825 /// Overwrite: setsparam twice returns the latter value.
20826 #[test]
20827 fn setsparam_overwrite_replaces_previous_value() {
20828 with_exec(|| {
20829 unsetparam("zshrs_rt_s5");
20830 setsparam("zshrs_rt_s5", "first");
20831 setsparam("zshrs_rt_s5", "second");
20832 assert_eq!(getsparam("zshrs_rt_s5").as_deref(), Some("second"));
20833 unsetparam("zshrs_rt_s5");
20834 });
20835 }
20836
20837 // ── Integer round-trip ─────────────────────────────────────────
20838 /// `setiparam("X", 42); getiparam("X")` → 42.
20839 /// Anchor: `typeset -i X=42; print -- $X` → "42".
20840 #[test]
20841 fn setiparam_then_getiparam_roundtrip_basic() {
20842 with_exec(|| {
20843 unsetparam("zshrs_rt_i1");
20844 setiparam("zshrs_rt_i1", 42);
20845 assert_eq!(getiparam("zshrs_rt_i1"), 42);
20846 unsetparam("zshrs_rt_i1");
20847 });
20848 }
20849
20850 /// Negative integer round-trip.
20851 #[test]
20852 fn setiparam_negative_value_roundtrips() {
20853 with_exec(|| {
20854 unsetparam("zshrs_rt_i2");
20855 setiparam("zshrs_rt_i2", -12345);
20856 assert_eq!(getiparam("zshrs_rt_i2"), -12345);
20857 unsetparam("zshrs_rt_i2");
20858 });
20859 }
20860
20861 /// `$COLUMNS` and `$LINES` read the live `zterm_*` global, both ways.
20862 ///
20863 /// c:Src/params.c:362-363 registers them as
20864 /// `IPDEF5("COLUMNS", &zterm_columns, zlevar_gsu)` /
20865 /// `IPDEF5("LINES", &zterm_lines, zlevar_gsu)`, so the getter
20866 /// (`intvargetfn`, c:4156 `return *pm->u.valptr;`) and the setter
20867 /// (`zlevarsetfn`, c:4176 `*p = x`) are two views of ONE cell. zshrs had
20868 /// only the setter half wired, and even that had no caller, so the two
20869 /// drifted: `adjustwinsize` published the new width to `ZTERM_COLUMNS`
20870 /// and to the parameter separately, and anything that rolled the
20871 /// parameter table back — an in-process `$(...)`'s saved state, a
20872 /// function scope — took the parameter back to the old width while the
20873 /// global kept the new one.
20874 ///
20875 /// What that cost: a completer forks a helper (`_call_program`), the
20876 /// foreground wait is where SIGWINCH is delivered, and `_git_commands`
20877 /// then pads every display string to `${(r.COLUMNS-4.)…}`. Strings built
20878 /// against the OLD width, counted against the NEW one, and `git <TAB>`
20879 /// across a 24x80 -> 24x60 resize asked to see "all 152 possibilities
20880 /// (236 lines)" for 152 one-line matches.
20881 #[test]
20882 fn columns_and_lines_are_two_views_of_the_zterm_globals() {
20883 with_exec(|| {
20884 let saved_cols = crate::ported::utils::ZTERM_COLUMNS.load(Ordering::SeqCst);
20885 let saved_lines = crate::ported::utils::ZTERM_LINES.load(Ordering::SeqCst);
20886 // `createparamtable` does not run under the unit harness, so make
20887 // the two nodes exist the way a real shell's startup does. The
20888 // read path deliberately requires a node: `unset COLUMNS` keeps
20889 // a PM_UNSET node (c:3877) and must read EMPTY, not the global.
20890 setiparam("COLUMNS", 80);
20891 setiparam("LINES", 24);
20892
20893 // READ half — c:4156. A resize publishes to the global; the
20894 // parameter must report it without anyone re-assigning it.
20895 crate::ported::utils::ZTERM_COLUMNS.store(60, Ordering::SeqCst);
20896 crate::ported::utils::ZTERM_LINES.store(12, Ordering::SeqCst);
20897 assert_eq!(getiparam("COLUMNS"), 60, "$COLUMNS must read zterm_columns");
20898 assert_eq!(getiparam("LINES"), 12, "$LINES must read zterm_lines");
20899 assert_eq!(getsparam("COLUMNS").as_deref(), Some("60"));
20900 assert_eq!(getsparam("LINES").as_deref(), Some("12"));
20901
20902 // WRITE half — c:4176. An assignment must move the global the
20903 // rest of the shell measures against, not just the node.
20904 setiparam("COLUMNS", 100);
20905 assert_eq!(
20906 crate::ported::utils::ZTERM_COLUMNS.load(Ordering::SeqCst),
20907 100,
20908 "COLUMNS=100 must write zterm_columns"
20909 );
20910 assert_eq!(getiparam("COLUMNS"), 100);
20911
20912 crate::ported::utils::ZTERM_COLUMNS.store(saved_cols, Ordering::SeqCst);
20913 crate::ported::utils::ZTERM_LINES.store(saved_lines, Ordering::SeqCst);
20914 setiparam("COLUMNS", saved_cols as i64);
20915 setiparam("LINES", saved_lines as i64);
20916 });
20917 }
20918
20919 /// Zero round-trip — distinct from "unset".
20920 #[test]
20921 fn setiparam_zero_roundtrips() {
20922 with_exec(|| {
20923 unsetparam("zshrs_rt_i3");
20924 setiparam("zshrs_rt_i3", 0);
20925 assert_eq!(getiparam("zshrs_rt_i3"), 0);
20926 unsetparam("zshrs_rt_i3");
20927 });
20928 }
20929
20930 /// `i64::MAX` and `i64::MIN` survive — zsh uses zlong (= long long
20931 /// on 64-bit hosts, which is i64 in Rust).
20932 #[test]
20933 fn setiparam_i64_extremes_roundtrip() {
20934 with_exec(|| {
20935 unsetparam("zshrs_rt_i4");
20936 setiparam("zshrs_rt_i4", i64::MAX);
20937 assert_eq!(getiparam("zshrs_rt_i4"), i64::MAX);
20938 setiparam("zshrs_rt_i4", i64::MIN);
20939 assert_eq!(getiparam("zshrs_rt_i4"), i64::MIN);
20940 unsetparam("zshrs_rt_i4");
20941 });
20942 }
20943
20944 // ── Cross-type coercion: setiparam → getsparam (int → string) ──
20945 /// `setiparam("X", 42); getsparam("X")` → Some("42").
20946 /// Anchor: `typeset -i X=42; print -r -- "$X"` → "42".
20947 #[test]
20948 fn setiparam_then_getsparam_coerces_to_decimal_string() {
20949 with_exec(|| {
20950 unsetparam("zshrs_rt_x1");
20951 setiparam("zshrs_rt_x1", 42);
20952 assert_eq!(getsparam("zshrs_rt_x1").as_deref(), Some("42"));
20953 unsetparam("zshrs_rt_x1");
20954 });
20955 }
20956
20957 /// Negative int → "-N" string form via getsparam.
20958 #[test]
20959 fn setiparam_negative_int_to_string_carries_minus_sign() {
20960 with_exec(|| {
20961 unsetparam("zshrs_rt_x2");
20962 setiparam("zshrs_rt_x2", -7);
20963 assert_eq!(getsparam("zshrs_rt_x2").as_deref(), Some("-7"));
20964 unsetparam("zshrs_rt_x2");
20965 });
20966 }
20967
20968 // ── Cross-type: setsparam("42") → getiparam → 42 ───────────────
20969 /// `setsparam("X", "42"); getiparam("X")` → 42 (numeric parse).
20970 /// Anchor: `X=42; print -- $((X+0))` → "42" (arith coerces).
20971 #[test]
20972 fn setsparam_numeric_string_then_getiparam_coerces_to_int() {
20973 with_exec(|| {
20974 unsetparam("zshrs_rt_x3");
20975 setsparam("zshrs_rt_x3", "42");
20976 assert_eq!(getiparam("zshrs_rt_x3"), 42);
20977 unsetparam("zshrs_rt_x3");
20978 });
20979 }
20980
20981 /// Non-numeric scalar → getiparam returns 0 (C zsh behavior).
20982 /// Anchor: `X=hello; print -- $((X+0))` → "0" (no numeric parse).
20983 #[test]
20984 fn setsparam_non_numeric_then_getiparam_returns_zero() {
20985 with_exec(|| {
20986 unsetparam("zshrs_rt_x4");
20987 setsparam("zshrs_rt_x4", "not a number");
20988 assert_eq!(getiparam("zshrs_rt_x4"), 0);
20989 unsetparam("zshrs_rt_x4");
20990 });
20991 }
20992
20993 // ── Array round-trip ───────────────────────────────────────────
20994 /// `setaparam("X", vec![...]); getaparam("X")` round-trips elements.
20995 /// Anchor: `X=(a b c); print -r -- "${X[@]}"` → "a b c".
20996 #[test]
20997 fn setaparam_then_getaparam_roundtrip_basic() {
20998 with_exec(|| {
20999 unsetparam("zshrs_rt_a1");
21000 setaparam("zshrs_rt_a1", vec!["a".into(), "b".into(), "c".into()]);
21001 assert_eq!(
21002 getaparam("zshrs_rt_a1"),
21003 Some(vec!["a".into(), "b".into(), "c".into()])
21004 );
21005 unsetparam("zshrs_rt_a1");
21006 });
21007 }
21008
21009 /// Empty array round-trip — distinct from unset.
21010 #[test]
21011 fn setaparam_empty_array_roundtrips_as_empty_vec() {
21012 with_exec(|| {
21013 unsetparam("zshrs_rt_a2");
21014 setaparam("zshrs_rt_a2", vec![]);
21015 assert_eq!(getaparam("zshrs_rt_a2"), Some(vec![]));
21016 unsetparam("zshrs_rt_a2");
21017 });
21018 }
21019
21020 /// Array element with embedded space stays one element (not split).
21021 /// Anchor: `X=("hi there" world); print ${#X}` → 2.
21022 #[test]
21023 fn setaparam_element_with_space_stays_one_element() {
21024 with_exec(|| {
21025 unsetparam("zshrs_rt_a3");
21026 setaparam("zshrs_rt_a3", vec!["hi there".into(), "world".into()]);
21027 assert_eq!(
21028 getaparam("zshrs_rt_a3"),
21029 Some(vec!["hi there".into(), "world".into()])
21030 );
21031 unsetparam("zshrs_rt_a3");
21032 });
21033 }
21034
21035 // ── unsetparam: idempotent + clears value ──────────────────────
21036 /// Unsetting a never-set param does nothing (no panic, no error).
21037 #[test]
21038 fn unsetparam_on_never_set_param_is_noop() {
21039 let _g = crate::test_util::global_state_lock();
21040 unsetparam("zshrs_rt_never_existed");
21041 // No assertion — just must not panic.
21042 }
21043
21044 /// Unset then get → None.
21045 #[test]
21046 fn unsetparam_then_getsparam_returns_none() {
21047 with_exec(|| {
21048 setsparam("zshrs_rt_u1", "value");
21049 unsetparam("zshrs_rt_u1");
21050 assert_eq!(getsparam("zshrs_rt_u1"), None);
21051 });
21052 }
21053
21054 /// Unsetting twice is idempotent.
21055 #[test]
21056 fn unsetparam_twice_is_idempotent() {
21057 with_exec(|| {
21058 setsparam("zshrs_rt_u2", "value");
21059 unsetparam("zshrs_rt_u2");
21060 unsetparam("zshrs_rt_u2"); // second call must not panic
21061 assert_eq!(getsparam("zshrs_rt_u2"), None);
21062 });
21063 }
21064
21065 // ── Cross-type: setiparam writes through scalar table, getaparam None ─
21066 /// PM_INTEGER is NOT PM_ARRAY — getaparam on integer returns None.
21067 /// Anchor: `typeset -i X=42; print -- ${X[1]}` errors (not an array).
21068 #[test]
21069 fn setiparam_then_getaparam_returns_none_not_an_array() {
21070 with_exec(|| {
21071 unsetparam("zshrs_rt_x5");
21072 setiparam("zshrs_rt_x5", 42);
21073 assert_eq!(
21074 getaparam("zshrs_rt_x5"),
21075 None,
21076 "PM_INTEGER is not PM_ARRAY — getaparam must return None"
21077 );
21078 unsetparam("zshrs_rt_x5");
21079 });
21080 }
21081
21082 // ── Read missing param: returns None / 0 ───────────────────────
21083 /// `getsparam("doesnt_exist")` → None.
21084 #[test]
21085 fn getsparam_on_unset_returns_none() {
21086 let _g = crate::test_util::global_state_lock();
21087 unsetparam("zshrs_rt_missing");
21088 assert_eq!(getsparam("zshrs_rt_missing"), None);
21089 }
21090
21091 /// `getiparam("doesnt_exist")` → 0 (C zsh semantics).
21092 /// Anchor: `unset X; print -- $((X+1))` → "1" (X read as 0).
21093 #[test]
21094 fn getiparam_on_unset_returns_zero() {
21095 let _g = crate::test_util::global_state_lock();
21096 unsetparam("zshrs_rt_missing2");
21097 assert_eq!(getiparam("zshrs_rt_missing2"), 0);
21098 }
21099
21100 /// `getaparam("doesnt_exist")` → None.
21101 #[test]
21102 fn getaparam_on_unset_returns_none() {
21103 let _g = crate::test_util::global_state_lock();
21104 unsetparam("zshrs_rt_missing3");
21105 assert_eq!(getaparam("zshrs_rt_missing3"), None);
21106 }
21107
21108 // ─── zsh corpus pins: params set/get round-trips ─────────────────
21109
21110 /// `setsparam("foo", "bar")` followed by `getsparam("foo")` returns
21111 /// `"bar"`. Round-trip scalar.
21112 #[test]
21113 fn params_corpus_scalar_round_trip_simple() {
21114 let _g = crate::test_util::global_state_lock();
21115 unsetparam("ZP_RT1");
21116 setsparam("ZP_RT1", "bar");
21117 assert_eq!(getsparam("ZP_RT1").as_deref(), Some("bar"));
21118 unsetparam("ZP_RT1");
21119 }
21120
21121 /// `setsparam("x", "")` then `getsparam("x")` returns `Some("")`,
21122 /// NOT `None` — empty-set is distinct from unset (per zsh semantics).
21123 #[test]
21124 fn params_corpus_empty_scalar_is_set_not_unset() {
21125 let _g = crate::test_util::global_state_lock();
21126 unsetparam("ZP_EMP");
21127 setsparam("ZP_EMP", "");
21128 assert_eq!(getsparam("ZP_EMP").as_deref(), Some(""), "empty-set is set");
21129 unsetparam("ZP_EMP");
21130 }
21131
21132 /// `unsetparam` after `setsparam` removes the param entirely:
21133 /// `getsparam` returns `None`.
21134 #[test]
21135 fn params_corpus_unset_after_set_returns_none() {
21136 let _g = crate::test_util::global_state_lock();
21137 setsparam("ZP_UR", "v");
21138 unsetparam("ZP_UR");
21139 assert_eq!(getsparam("ZP_UR"), None, "unsetparam removes param");
21140 }
21141
21142 /// `setiparam("i", 42)` and `getiparam` round-trip integer.
21143 #[test]
21144 fn params_corpus_integer_round_trip() {
21145 let _g = crate::test_util::global_state_lock();
21146 unsetparam("ZP_I");
21147 setiparam("ZP_I", 42);
21148 assert_eq!(getiparam("ZP_I"), 42);
21149 unsetparam("ZP_I");
21150 }
21151
21152 /// `setiparam` then `getsparam` reads integer as string.
21153 #[test]
21154 fn params_corpus_integer_read_as_string() {
21155 let _g = crate::test_util::global_state_lock();
21156 unsetparam("ZP_IS");
21157 setiparam("ZP_IS", 42);
21158 assert_eq!(
21159 getsparam("ZP_IS").as_deref(),
21160 Some("42"),
21161 "integer param reads as string repr"
21162 );
21163 unsetparam("ZP_IS");
21164 }
21165
21166 /// `setaparam("a", vec!["one","two","three"])` round-trips via
21167 /// `getaparam`.
21168 #[test]
21169 fn params_corpus_array_round_trip() {
21170 let _g = crate::test_util::global_state_lock();
21171 unsetparam("ZP_A");
21172 setaparam("ZP_A", vec!["one".into(), "two".into(), "three".into()]);
21173 assert_eq!(
21174 getaparam("ZP_A").as_deref(),
21175 Some(&["one".into(), "two".into(), "three".into()][..]),
21176 "array round-trip",
21177 );
21178 unsetparam("ZP_A");
21179 }
21180
21181 /// Empty array round-trip — `setaparam("a", vec![])` then
21182 /// `getaparam` returns `Some(empty vec)`, not `None`.
21183 #[test]
21184 fn params_corpus_empty_array_is_set_not_unset() {
21185 let _g = crate::test_util::global_state_lock();
21186 unsetparam("ZP_EA");
21187 setaparam("ZP_EA", vec![]);
21188 let got = getaparam("ZP_EA");
21189 assert!(got.is_some(), "empty array is set, not None");
21190 assert_eq!(got.unwrap().len(), 0, "len = 0");
21191 unsetparam("ZP_EA");
21192 }
21193
21194 /// `setsparam` overwrites previous value (scalar reassignment).
21195 #[test]
21196 fn params_corpus_scalar_overwrite() {
21197 let _g = crate::test_util::global_state_lock();
21198 unsetparam("ZP_O");
21199 setsparam("ZP_O", "first");
21200 setsparam("ZP_O", "second");
21201 assert_eq!(
21202 getsparam("ZP_O").as_deref(),
21203 Some("second"),
21204 "scalar overwrites cleanly"
21205 );
21206 unsetparam("ZP_O");
21207 }
21208
21209 /// Re-assigning a scalar to an array (changing type via setaparam).
21210 /// Default zsh allows this: array replaces scalar.
21211 #[test]
21212 fn params_corpus_scalar_to_array_replace_type() {
21213 let _g = crate::test_util::global_state_lock();
21214 unsetparam("ZP_ST");
21215 setsparam("ZP_ST", "scalar_val");
21216 setaparam("ZP_ST", vec!["a".into(), "b".into()]);
21217 assert_eq!(
21218 getaparam("ZP_ST").as_deref(),
21219 Some(&["a".into(), "b".into()][..]),
21220 "setaparam replaces scalar type",
21221 );
21222 unsetparam("ZP_ST");
21223 }
21224
21225 /// `unsetparam` on a name that doesn't exist is a no-op
21226 /// (zsh's idempotent unset).
21227 #[test]
21228 fn params_corpus_unset_on_missing_is_noop() {
21229 let _g = crate::test_util::global_state_lock();
21230 unsetparam("ZP_NEVER_EXISTED_42_xyz");
21231 // No panic: just returns None on subsequent get.
21232 assert_eq!(getsparam("ZP_NEVER_EXISTED_42_xyz"), None);
21233 }
21234
21235 /// Empty string passed to integer slot: `setiparam("z", 0)`
21236 /// returns `Some(0)` via getiparam (zero is a legitimate int).
21237 #[test]
21238 fn params_corpus_integer_zero_is_set() {
21239 let _g = crate::test_util::global_state_lock();
21240 unsetparam("ZP_IZ");
21241 setiparam("ZP_IZ", 0);
21242 assert_eq!(getiparam("ZP_IZ"), 0);
21243 assert_eq!(getsparam("ZP_IZ").as_deref(), Some("0"));
21244 unsetparam("ZP_IZ");
21245 }
21246
21247 /// Negative integers round-trip with sign preserved.
21248 #[test]
21249 fn params_corpus_integer_negative_preserves_sign() {
21250 let _g = crate::test_util::global_state_lock();
21251 unsetparam("ZP_IN");
21252 setiparam("ZP_IN", -42);
21253 assert_eq!(getiparam("ZP_IN"), -42);
21254 assert_eq!(getsparam("ZP_IN").as_deref(), Some("-42"));
21255 unsetparam("ZP_IN");
21256 }
21257
21258 // ─── associative array (hash) pins ───────────────────────────────
21259
21260 /// `sethparam` + `gethparam` round-trip. C `gethparam` returns
21261 /// `paramvalarr(..., SCANPM_WANTVALS)` (Src/params.c:3122) — i.e.
21262 /// the VALUES side of the hash, not flat k/v pairs. For
21263 /// `(key1 val1 key2 val2)` that's `[val1, val2]`. Keys come from
21264 /// `gethkparam` (covered by `..._hash_keys_only_returns_keys`).
21265 #[test]
21266 fn params_corpus_hash_round_trip_basic() {
21267 let _g = crate::test_util::global_state_lock();
21268 unsetparam("ZP_H");
21269 sethparam(
21270 "ZP_H",
21271 vec!["key1".into(), "val1".into(), "key2".into(), "val2".into()],
21272 );
21273 let got = gethparam("ZP_H");
21274 assert!(got.is_some(), "hash param set");
21275 let g = got.unwrap();
21276 assert_eq!(g.len(), 2, "2 values (SCANPM_WANTVALS) preserved");
21277 let mut sorted = g.clone();
21278 sorted.sort();
21279 assert_eq!(
21280 sorted,
21281 vec!["val1".to_string(), "val2".to_string()],
21282 "values come back regardless of hash-iter order"
21283 );
21284 unsetparam("ZP_H");
21285 }
21286
21287 /// `gethparam` on missing returns None.
21288 #[test]
21289 fn params_corpus_hash_missing_returns_none() {
21290 let _g = crate::test_util::global_state_lock();
21291 unsetparam("ZP_HM_xyz");
21292 assert!(gethparam("ZP_HM_xyz").is_none());
21293 }
21294
21295 /// `gethkparam` returns the keys-only view of a hash. With
21296 /// `{a:1,b:2}`, keys are `["a","b"]` (order may not be stable).
21297 #[test]
21298 fn params_corpus_hash_keys_only_returns_keys() {
21299 let _g = crate::test_util::global_state_lock();
21300 unsetparam("ZP_HK");
21301 sethparam(
21302 "ZP_HK",
21303 vec!["a".into(), "1".into(), "b".into(), "2".into()],
21304 );
21305 let keys = gethkparam("ZP_HK");
21306 assert!(keys.is_some(), "keys-only view exists");
21307 let k = keys.unwrap();
21308 assert_eq!(k.len(), 2, "2 keys");
21309 let mut sorted = k.clone();
21310 sorted.sort();
21311 assert_eq!(sorted, vec!["a".to_string(), "b".to_string()]);
21312 unsetparam("ZP_HK");
21313 }
21314
21315 /// Setting a hash with empty `Vec` removes the previous key/value
21316 /// pairs. After empty `sethparam`, lookups find no entries. Note
21317 /// `gethparam` returns SCANPM_WANTVALS (values only); `(a 1)` has
21318 /// 1 value, not 2 — see `..._hash_round_trip_basic`.
21319 #[test]
21320 fn params_corpus_hash_set_empty_clears_entries() {
21321 let _g = crate::test_util::global_state_lock();
21322 unsetparam("ZP_HC");
21323 sethparam("ZP_HC", vec!["a".into(), "1".into()]);
21324 assert_eq!(gethparam("ZP_HC").map(|v| v.len()), Some(1));
21325 sethparam("ZP_HC", vec![]);
21326 // empty hash still exists (some implementations distinguish
21327 // empty hash from unset)
21328 let after = gethparam("ZP_HC");
21329 if let Some(v) = after {
21330 assert!(v.is_empty(), "empty hash has 0 elements");
21331 }
21332 unsetparam("ZP_HC");
21333 }
21334
21335 /// Large integer at i64 boundary: i64::MAX round-trips.
21336 #[test]
21337 fn params_corpus_integer_max_round_trips() {
21338 let _g = crate::test_util::global_state_lock();
21339 unsetparam("ZP_IMX");
21340 setiparam("ZP_IMX", i64::MAX);
21341 assert_eq!(getiparam("ZP_IMX"), i64::MAX);
21342 unsetparam("ZP_IMX");
21343 }
21344
21345 /// i64::MIN round-trips with sign.
21346 #[test]
21347 fn params_corpus_integer_min_round_trips() {
21348 let _g = crate::test_util::global_state_lock();
21349 unsetparam("ZP_IMN");
21350 setiparam("ZP_IMN", i64::MIN);
21351 assert_eq!(getiparam("ZP_IMN"), i64::MIN);
21352 unsetparam("ZP_IMN");
21353 }
21354
21355 // ═══════════════════════════════════════════════════════════════════
21356 // C-parity tests pinning Src/params.c:1288 (isident). Foundational
21357 // identifier validator used by every param-name validation site.
21358 // ═══════════════════════════════════════════════════════════════════
21359
21360 /// `isident("")` returns false. C c:1292:
21361 /// `if (!*s) return 0;`
21362 #[test]
21363 fn isident_empty_string_returns_false() {
21364 let _g = crate::test_util::global_state_lock();
21365 assert!(!isident(""));
21366 }
21367
21368 /// `isident("foo")` returns true — basic identifier.
21369 #[test]
21370 fn isident_alpha_identifier_returns_true() {
21371 let _g = crate::test_util::global_state_lock();
21372 assert!(isident("foo"));
21373 }
21374
21375 /// `isident("_foo")` — underscore prefix allowed.
21376 #[test]
21377 fn isident_underscore_prefix_returns_true() {
21378 let _g = crate::test_util::global_state_lock();
21379 assert!(isident("_foo"));
21380 }
21381
21382 /// `isident("foo_bar123")` — alnum + underscore mid.
21383 #[test]
21384 fn isident_alnum_with_underscore_returns_true() {
21385 let _g = crate::test_util::global_state_lock();
21386 assert!(isident("foo_bar123"));
21387 }
21388
21389 /// `isident("123")` — all-digit positional param. C c:1300+:
21390 /// "All-digit names are valid (positional params)"
21391 #[test]
21392 fn isident_all_digit_positional_returns_true() {
21393 let _g = crate::test_util::global_state_lock();
21394 assert!(isident("123"), "all-digit names are positional params");
21395 }
21396
21397 /// `isident("123abc")` — digit prefix with letters → false.
21398 /// Mixed digit-first not a valid positional param OR identifier.
21399 #[test]
21400 fn isident_digit_prefix_alpha_returns_false() {
21401 let _g = crate::test_util::global_state_lock();
21402 assert!(!isident("123abc"));
21403 }
21404
21405 /// `isident(".ns.foo")` — ksh93 namespace dotted name allowed.
21406 /// C c:1296-1311 — leading `.` accepted if not followed by digit.
21407 #[test]
21408 fn isident_ksh93_namespace_dot_prefix_returns_true() {
21409 let _g = crate::test_util::global_state_lock();
21410 assert!(isident(".ns.foo"));
21411 }
21412
21413 /// `isident(".0bad")` — namespace must NOT start with digit.
21414 /// C c:1300: `if (idigit(s[1])) return 0;`
21415 #[test]
21416 fn isident_namespace_starting_with_digit_returns_false() {
21417 let _g = crate::test_util::global_state_lock();
21418 assert!(!isident(".0bad"), "namespace can't start with digit");
21419 }
21420
21421 /// `isident("foo-bar")` — dash not allowed in identifier.
21422 #[test]
21423 fn isident_dash_in_name_returns_false() {
21424 let _g = crate::test_util::global_state_lock();
21425 assert!(!isident("foo-bar"));
21426 }
21427
21428 /// `isident("foo bar")` — space not allowed.
21429 #[test]
21430 fn isident_space_in_name_returns_false() {
21431 let _g = crate::test_util::global_state_lock();
21432 assert!(!isident("foo bar"));
21433 }
21434
21435 /// `isident("foo[0]")` — subscript at end is allowed (C handles
21436 /// it in `isident_requires_balanced_subscript_brackets`).
21437 /// Pin: a simple `[0]` subscript validates.
21438 #[test]
21439 fn isident_with_simple_subscript_returns_true() {
21440 let _g = crate::test_util::global_state_lock();
21441 assert!(isident("foo[0]"));
21442 }
21443
21444 // ═══════════════════════════════════════════════════════════════════
21445 // Additional C-parity tests for Src/params.c isident.
21446 // ═══════════════════════════════════════════════════════════════════
21447
21448 /// c:1288 — `isident("")` returns false (empty not an identifier).
21449 #[test]
21450 fn isident_empty_returns_false() {
21451 let _g = crate::test_util::global_state_lock();
21452 assert!(!isident(""));
21453 }
21454
21455 /// c:1288 — single underscore IS valid ident.
21456 #[test]
21457 fn isident_single_underscore_is_valid() {
21458 let _g = crate::test_util::global_state_lock();
21459 assert!(isident("_"));
21460 }
21461
21462 /// c:1288 — single letter IS valid ident.
21463 #[test]
21464 fn isident_single_letter_is_valid() {
21465 let _g = crate::test_util::global_state_lock();
21466 assert!(isident("a"));
21467 assert!(isident("Z"));
21468 }
21469
21470 /// c:1288 — all-digit name (positional param like '$1') IS valid.
21471 #[test]
21472 fn isident_all_digits_is_valid_positional() {
21473 let _g = crate::test_util::global_state_lock();
21474 assert!(isident("1"));
21475 assert!(isident("123"));
21476 assert!(isident("0"));
21477 }
21478
21479 /// c:1288 — digit-first mixed (e.g. '1abc') is INVALID.
21480 #[test]
21481 fn isident_digit_first_mixed_is_invalid() {
21482 let _g = crate::test_util::global_state_lock();
21483 assert!(!isident("1abc"));
21484 assert!(!isident("0x"));
21485 }
21486
21487 /// c:1288 — underscore + alnum is valid.
21488 #[test]
21489 fn isident_underscore_prefix_alnum_is_valid() {
21490 let _g = crate::test_util::global_state_lock();
21491 assert!(isident("_foo"));
21492 assert!(isident("_123"));
21493 assert!(isident("foo_bar"));
21494 }
21495
21496 /// c:1288 — alphabetic + digits is valid.
21497 #[test]
21498 fn isident_alpha_digits_mix_is_valid() {
21499 let _g = crate::test_util::global_state_lock();
21500 assert!(isident("foo123"));
21501 assert!(isident("var2"));
21502 }
21503
21504 /// c:1326 — unbalanced subscript `foo[` is invalid.
21505 #[test]
21506 fn isident_unbalanced_open_bracket_returns_false() {
21507 let _g = crate::test_util::global_state_lock();
21508 assert!(!isident("foo["));
21509 assert!(!isident("foo[0"));
21510 }
21511
21512 /// c:1326 — special chars like `-`, `.`, `@` not allowed in identifier.
21513 #[test]
21514 fn isident_special_chars_rejected() {
21515 let _g = crate::test_util::global_state_lock();
21516 assert!(!isident("foo-bar")); // hyphen
21517 assert!(!isident("foo@bar"));
21518 assert!(!isident("foo!"));
21519 }
21520
21521 /// c:1288 — namespace prefix `.ns.var` requires a non-digit after dot.
21522 #[test]
21523 fn isident_namespace_prefix_dot_rejects_digit_after() {
21524 let _g = crate::test_util::global_state_lock();
21525 assert!(!isident(".0"), "dot + digit invalid");
21526 assert!(!isident("."), "lone dot invalid");
21527 }
21528
21529 /// c:1288 — namespace prefix `.foo` is valid.
21530 #[test]
21531 fn isident_namespace_prefix_alpha_after_dot_valid() {
21532 let _g = crate::test_util::global_state_lock();
21533 assert!(isident(".foo"));
21534 assert!(isident(".ns.var"));
21535 }
21536
21537 /// c:1288 — `isident` is deterministic.
21538 #[test]
21539 fn isident_is_deterministic() {
21540 let _g = crate::test_util::global_state_lock();
21541 for s in ["foo", "_bar", "123", "1abc", "", ".foo", "foo[", "foo-bar"] {
21542 let first = isident(s);
21543 for _ in 0..5 {
21544 assert_eq!(isident(s), first, "{:?} must be pure", s);
21545 }
21546 }
21547 }
21548
21549 // ═══════════════════════════════════════════════════════════════════
21550 // Additional C-parity tests for Src/params.c
21551 // c:1403 issetvar / c:2152 setsparam / c:2317 isident / c:4099 getiparam
21552 // c:4191 getsparam / c:4326 getsparam_u / c:4353 getaparam /
21553 // c:5363 setaparam / c:5696 setiparam / c:5775 unsetparam
21554 // ═══════════════════════════════════════════════════════════════════
21555
21556 /// c:1403 — `issetvar` returns i32 (compile-time type pin).
21557 #[test]
21558 fn issetvar_returns_i32_type() {
21559 let _g = crate::test_util::global_state_lock();
21560 let _: i32 = issetvar("anything");
21561 }
21562
21563 /// c:1403 — `issetvar("__nonexistent")` returns 0 for unset.
21564 #[test]
21565 fn issetvar_unknown_returns_zero() {
21566 let _g = crate::test_util::global_state_lock();
21567 let r = issetvar("__never_a_real_var_xyz_zshrs__");
21568 assert_eq!(r, 0, "unset var → 0");
21569 }
21570
21571 /// c:4191 — `getsparam` returns Option<String>.
21572 #[test]
21573 fn getsparam_returns_option_string_type() {
21574 let _g = crate::test_util::global_state_lock();
21575 let _: Option<String> = getsparam("anything");
21576 }
21577
21578 /// c:4191 — `getsparam("__nonexistent")` returns None.
21579 #[test]
21580 fn getsparam_unknown_returns_none() {
21581 let _g = crate::test_util::global_state_lock();
21582 assert!(
21583 getsparam("__never_a_real_var_xyz_zshrs__").is_none(),
21584 "unknown var → None"
21585 );
21586 }
21587
21588 /// c:4099 — `getiparam` returns i64.
21589 #[test]
21590 fn getiparam_returns_i64_type() {
21591 let _g = crate::test_util::global_state_lock();
21592 let _: i64 = getiparam("anything");
21593 }
21594
21595 /// c:4353 — `getaparam` returns Option<Vec<String>>.
21596 #[test]
21597 fn getaparam_returns_option_vec_string_type() {
21598 let _g = crate::test_util::global_state_lock();
21599 let _: Option<Vec<String>> = getaparam("anything");
21600 }
21601
21602 /// c:4326 — `getsparam_u("")` empty returns None.
21603 #[test]
21604 fn getsparam_u_empty_returns_none() {
21605 let _g = crate::test_util::global_state_lock();
21606 assert!(getsparam_u("").is_none(), "empty → None");
21607 }
21608
21609 /// c:2152 — `setsparam`/`getsparam` round-trip.
21610 #[test]
21611 fn setsparam_getsparam_round_trip() {
21612 let _g = crate::test_util::global_state_lock();
21613 let name = "ZSHRS_TEST_SETSPARAM_ROUND_TRIP";
21614 let _ = unsetparam(name);
21615 setsparam(name, "hello");
21616 assert_eq!(
21617 getsparam(name).as_deref(),
21618 Some("hello"),
21619 "setsparam/getsparam round-trip"
21620 );
21621 unsetparam(name);
21622 }
21623
21624 /// c:5696 — `setiparam`/`getiparam` round-trip.
21625 #[test]
21626 fn setiparam_getiparam_round_trip() {
21627 let _g = crate::test_util::global_state_lock();
21628 let name = "ZSHRS_TEST_SETIPARAM_ROUND_TRIP";
21629 unsetparam(name);
21630 setiparam(name, 42);
21631 assert_eq!(getiparam(name), 42, "setiparam/getiparam round-trip");
21632 unsetparam(name);
21633 }
21634
21635 /// c:5363 — `setaparam`/`getaparam` round-trip preserves array.
21636 #[test]
21637 fn setaparam_getaparam_round_trip() {
21638 let _g = crate::test_util::global_state_lock();
21639 let name = "ZSHRS_TEST_SETAPARAM_ROUND_TRIP";
21640 unsetparam(name);
21641 let v = vec!["a".to_string(), "b".to_string(), "c".to_string()];
21642 setaparam(name, v.clone());
21643 assert_eq!(getaparam(name), Some(v), "setaparam/getaparam round-trip");
21644 unsetparam(name);
21645 }
21646
21647 /// c:5775 — `unsetparam` is idempotent on unset names.
21648 #[test]
21649 fn unsetparam_unknown_name_no_panic() {
21650 let _g = crate::test_util::global_state_lock();
21651 unsetparam("__never_real_var_xyz__");
21652 unsetparam("__never_real_var_xyz__");
21653 }
21654
21655 // ═══════════════════════════════════════════════════════════════════
21656 // Additional C-parity tests for Src/params.c GSU vtable callbacks.
21657 // c:3993 intgetfn / c:4002 intsetfn / c:4011 floatgetfn
21658 // c:4020 floatsetfn / c:4029 strgetfn / c:4057 arrgetfn
21659 // c:4084 hashgetfn / c:4093 hashsetfn / c:4180 nullstrsetfn
21660 // c:4192 nullunsetfn
21661 // ═══════════════════════════════════════════════════════════════════
21662
21663 /// c:3993-3997 — `intgetfn` body is `return pm->u.val;`. Pin that
21664 /// the Rust port reads `pm.u_val` verbatim with no transform.
21665 #[test]
21666 fn intgetfn_returns_u_val_verbatim() {
21667 let _g = crate::test_util::global_state_lock();
21668 let mut pm = param::default();
21669 pm.u_val = 0;
21670 assert_eq!(intgetfn(&pm), 0, "u_val=0 → 0");
21671 pm.u_val = 42;
21672 assert_eq!(intgetfn(&pm), 42, "u_val=42 → 42");
21673 pm.u_val = i64::MIN;
21674 assert_eq!(intgetfn(&pm), i64::MIN, "u_val=i64::MIN preserved");
21675 pm.u_val = i64::MAX;
21676 assert_eq!(intgetfn(&pm), i64::MAX, "u_val=i64::MAX preserved");
21677 }
21678
21679 /// c:4002-4006 — `intsetfn` body is `pm->u.val = x;`. For a
21680 /// non-special name (not SECONDS/RANDOM), the Rust port must
21681 /// write `pm.u_val = x` and intgetfn must round-trip.
21682 #[test]
21683 fn intsetfn_intgetfn_round_trip_for_non_special_name() {
21684 let _g = crate::test_util::global_state_lock();
21685 let mut pm = param::default();
21686 pm.node.nam = "X".to_string(); // non-SECONDS, non-RANDOM
21687 intsetfn(&mut pm, 7);
21688 assert_eq!(intgetfn(&pm), 7, "non-special intsetfn writes u_val");
21689 intsetfn(&mut pm, -123);
21690 assert_eq!(intgetfn(&pm), -123, "negative i64 round-trips");
21691 }
21692
21693 /// c:4011-4015 — `floatgetfn` body is `return pm->u.dval;`.
21694 /// Read `pm.u_dval` verbatim with no transform.
21695 #[test]
21696 fn floatgetfn_returns_u_dval_verbatim() {
21697 let _g = crate::test_util::global_state_lock();
21698 let mut pm = param::default();
21699 pm.u_dval = 0.0;
21700 assert_eq!(floatgetfn(&pm), 0.0, "u_dval=0.0 → 0.0");
21701 pm.u_dval = 3.14;
21702 assert_eq!(floatgetfn(&pm), 3.14, "u_dval=3.14 preserved");
21703 pm.u_dval = -2.71;
21704 assert_eq!(floatgetfn(&pm), -2.71, "negative dval preserved");
21705 }
21706
21707 /// c:4020-4024 — `floatsetfn` body for non-SECONDS is
21708 /// `pm->u.dval = x;`. Round-trip via floatgetfn.
21709 #[test]
21710 fn floatsetfn_floatgetfn_round_trip_for_non_seconds() {
21711 let _g = crate::test_util::global_state_lock();
21712 let mut pm = param::default();
21713 pm.node.nam = "Y".to_string(); // non-SECONDS
21714 floatsetfn(&mut pm, 1.5);
21715 assert_eq!(floatgetfn(&pm), 1.5, "non-SECONDS floatsetfn writes u_dval");
21716 floatsetfn(&mut pm, -0.0);
21717 assert_eq!(floatgetfn(&pm), -0.0, "neg zero round-trips");
21718 }
21719
21720 /// c:4029-4033 — `strgetfn` C body returns `pm->u.str ? pm->u.str
21721 /// : (char *) hcalloc(1);` — when u_str is None the C path
21722 /// returns a freshly allocated empty C string. Rust port must
21723 /// return `String::new()` (empty owned String) for the None case.
21724 #[test]
21725 fn strgetfn_returns_empty_string_when_u_str_none() {
21726 let _g = crate::test_util::global_state_lock();
21727 let mut pm = param::default();
21728 pm.u_str = None;
21729 let s = strgetfn(&pm);
21730 assert!(
21731 s.is_empty(),
21732 "None u_str → empty String (port of hcalloc(1))"
21733 );
21734 }
21735
21736 /// c:4029-4033 — `strgetfn` returns u_str clone when present.
21737 #[test]
21738 fn strgetfn_returns_clone_when_u_str_some() {
21739 let _g = crate::test_util::global_state_lock();
21740 let mut pm = param::default();
21741 pm.u_str = Some("hello".to_string());
21742 assert_eq!(strgetfn(&pm), "hello", "Some(s) → s");
21743 }
21744
21745 /// c:4057-4061 — `arrgetfn` C body returns `pm->u.arr ? pm->u.arr
21746 /// : &nullarray;` — when u_arr is None, the C path returns a
21747 /// pointer to the static empty `nullarray`. Rust port returns
21748 /// `Vec::new()` for the None case (empty owned Vec).
21749 #[test]
21750 fn arrgetfn_returns_empty_vec_when_u_arr_none() {
21751 let _g = crate::test_util::global_state_lock();
21752 let mut pm = param::default();
21753 pm.u_arr = None;
21754 let v = arrgetfn(&pm);
21755 assert!(v.is_empty(), "None u_arr → empty Vec (port of &nullarray)");
21756 }
21757
21758 /// c:4057-4061 — `arrgetfn` returns u_arr clone when present.
21759 #[test]
21760 fn arrgetfn_returns_clone_when_u_arr_some() {
21761 let _g = crate::test_util::global_state_lock();
21762 let mut pm = param::default();
21763 let src = vec!["a".to_string(), "b".to_string(), "c".to_string()];
21764 pm.u_arr = Some(src.clone());
21765 assert_eq!(arrgetfn(&pm), src, "Some(v) → v clone");
21766 }
21767
21768 /// c:4084-4088 — `hashgetfn` C body is `return pm->u.hash;`. The
21769 /// Rust port returns `Option<&HashTable>` (borrowing form of the
21770 /// C nullable pointer). Pin both the None and Some cases.
21771 #[test]
21772 fn hashgetfn_returns_option_ref_hashtable() {
21773 let _g = crate::test_util::global_state_lock();
21774 let mut pm = param::default();
21775 pm.u_hash = None;
21776 assert!(hashgetfn(&pm).is_none(), "None u_hash → None");
21777 pm.u_hash = newparamtable(8, "h");
21778 assert!(hashgetfn(&pm).is_some(), "Some u_hash → Some");
21779 }
21780
21781 /// c:4093-4097 — `hashsetfn` C body installs `x` into `pm->u.hash`.
21782 /// Rust port writes `pm.u_hash = Some(x)`; round-trip via
21783 /// hashgetfn must observe the same table.
21784 #[test]
21785 fn hashsetfn_hashgetfn_round_trip() {
21786 let _g = crate::test_util::global_state_lock();
21787 let mut pm = param::default();
21788 let ht = newparamtable(8, "h").expect("newparamtable returns Some");
21789 let want_size = ht.hsize;
21790 hashsetfn(&mut pm, ht);
21791 let got = hashgetfn(&pm).expect("hashsetfn must store table");
21792 assert_eq!(got.hsize, want_size, "stored table size preserved");
21793 }
21794
21795 /// c:4180-4183 — `nullstrsetfn(UNUSED pm, char *x)` is the
21796 /// PM_SPECIAL "cannot be set" hook: `zsfree(x);` discards x and
21797 /// makes no change to pm. Pin that the Rust port leaves every
21798 /// field of pm untouched.
21799 #[test]
21800 fn nullstrsetfn_leaves_pm_unchanged() {
21801 let _g = crate::test_util::global_state_lock();
21802 let mut pm = param::default();
21803 pm.node.nam = "PINNED".to_string();
21804 pm.u_str = Some("orig".to_string());
21805 pm.u_val = 99;
21806 nullstrsetfn(&mut pm, "discarded".to_string());
21807 assert_eq!(pm.node.nam, "PINNED", "nam untouched");
21808 assert_eq!(pm.u_str.as_deref(), Some("orig"), "u_str untouched");
21809 assert_eq!(pm.u_val, 99, "u_val untouched");
21810 }
21811
21812 /// c:4192-4195 — `nullunsetfn(UNUSED pm, UNUSED exp)` is the
21813 /// PM_SPECIAL "cannot be unset" hook: empty body. Pin that the
21814 /// Rust port leaves every field of pm untouched.
21815 #[test]
21816 fn nullunsetfn_leaves_pm_unchanged() {
21817 let _g = crate::test_util::global_state_lock();
21818 let mut pm = param::default();
21819 pm.node.nam = "PINNED".to_string();
21820 pm.node.flags = PM_SCALAR as i32;
21821 pm.u_str = Some("keep".to_string());
21822 nullunsetfn(&mut pm, 0);
21823 nullunsetfn(&mut pm, 1);
21824 assert_eq!(pm.node.nam, "PINNED", "nam untouched");
21825 assert_eq!(pm.node.flags, PM_SCALAR as i32, "flags untouched");
21826 assert_eq!(pm.u_str.as_deref(), Some("keep"), "u_str untouched");
21827 }
21828}
21829
21830// ===========================================================
21831// !!! WARNING: RUST-ONLY NAMEREF ADAPTERS — NO DIRECT C
21832// COUNTERPARTS AS FREE FUNCTIONS !!!
21833//
21834// C's nameref machinery (Src/params.c:6325-6500 resolve_nameref /
21835// resolve_nameref_rec / upscope / setscope / valid_refname, plus the
21836// PM_NAMEREF arms of fetchvalue c:2247-2270 and assignstrvalue
21837// c:2690-2720) walks live Param POINTERS chained through
21838// realparamtab. zshrs's paramtab hands out CLONES behind an RwLock,
21839// so the chain walk must re-look-up each hop BY NAME — these
21840// adapters are that by-name walk plus its carriers. Every entry
21841// here exists to adapt a cited C pattern to the lock/clone storage,
21842// in the same class as fdtable_get / sess_get. The canonical
21843// C-named ports (resolve_nameref at params.rs, resolve_nameref_rec,
21844// upscope, setscope, valid_refname) delegate INTO this walk.
21845//
21846// Relocated from vm_helper.rs (2026-06-12): ported code was calling
21847// bridge-resident helpers, inverting the layering rule. Bridge
21848// callers reach these via the vm_helper re-export.
21849// ===========================================================
21850
21851/// Result of walking a nameref chain by name. Rust-side carrier for
21852/// what C's `resolve_nameref_rec` (Src/params.c:6332) communicates
21853/// via the returned `Param` pointer: the chain may end at the final
21854/// non-ref target, at a placeholder ref (empty refname), at a
21855/// subscripted ref (`pm->width` ends the chain per c:6339), at a
21856/// dangling name (target never defined), or in a loop.
21857#[allow(non_camel_case_types)]
21858pub enum nameref_resolution {
21859 /// `name` is not a nameref (or doesn't exist / is an unset ref).
21860 NotRef,
21861 /// Chain ends at a nameref whose refname is empty/None —
21862 /// the C `keep_lastref` shape (c:6354). Fields = that ref's name and
21863 /// scope level (the ref may be a hidden `old`-chain node).
21864 Placeholder(String, i32),
21865 /// Final non-ref target. `pm` is a clone of the resolved binding
21866 /// (None when the target name was never defined — C returns NULL
21867 /// at c:6347 when gethashnode2 misses). `level` is the resolved
21868 /// binding's scope level (for old-chain writes).
21869 Target {
21870 /// final target name
21871 name: String,
21872 /// subscript from a `name[sub]` refname (ends the chain, c:6339)
21873 subscript: Option<String>,
21874 /// clone of the resolved binding (possibly a hidden old-chain node)
21875 pm: Option<Param>,
21876 /// scope level of the resolved binding (only when pm is Some)
21877 level: i32,
21878 },
21879 /// Loop detected — `zerr("...: invalid self reference")` already
21880 /// emitted (c:6341-6343).
21881 SelfRef,
21882 /// The refname EXISTS in the table but the upscope walk excluded
21883 /// every binding (c:6347-6349: `loadparamnode(upscope(...))`
21884 /// returned NULL with gethashnode2 non-NULL, so the `keep_lastref`
21885 /// else-arm is SKIPPED). Reads see unset; assignment FAILS
21886 /// (createparam c:1108-1118 refuses the existing ref).
21887 OutOfScope,
21888}
21889
21890/// !!! WARNING: RUST-ONLY HELPER !!!
21891/// No C counterpart under this name: name-keyed adaptation of `resolve_nameref_rec`
21892/// (`Src/params.c:6332`), which walks `Param` pointers. zshrs's paramtab lives
21893/// behind an `RwLock`, so each hop must re-look-up by name and the C `PM_TAGGED`
21894/// pointer tag becomes a visited `(name, level)` set. The name-parity port of
21895/// `resolve_nameref_rec` itself is above.
21896/// Walk the nameref chain starting at `name` against the live
21897/// paramtab. Faithful adaptation of `resolve_nameref_rec`
21898/// (Src/params.c:6332-6357): per-hop `upscope` via the old-chain,
21899/// `pm->width` subscript chain-end (c:6338-6339), PM_TAGGED-style
21900/// loop detection (visited set of (name, level) stands in for the
21901/// C pointer tag — same revisit condition, borrow-safe), and the
21902/// `keep_lastref` placeholder shape. `stop_at` mirrors the C `stop`
21903/// param (compared by (name, level) identity).
21904pub fn resolve_nameref_name(name: &str, stop_at: Option<(&str, i32)>) -> nameref_resolution {
21905 // `${(!)…}` SCANPM_NONAMEREF scope (c:2247 `!(scanflags &
21906 // SCANPM_NONAMEREF)`) — treat nothing as a ref. Centralized here so
21907 // every caller inherits the gate (previously each caller guarded
21908 // via is_nameref first).
21909 if NAMEREF_SUPPRESS.with(|c| c.get()) > 0 {
21910 return nameref_resolution::NotRef;
21911 }
21912 // upscope (Src/params.c:6440-6463) over CLONED chain nodes —
21913 // nested in the engine (sole consumer) instead of a free fn.
21914 let upscope_clone =
21915 |visible: ¶m, ref_flags: u32, ref_level: i32, ref_base: i32| -> Option<Param> {
21916 let mut cur: ¶m = visible;
21917 if (ref_flags & PM_UPPER) != 0 {
21918 // c:6457-6458 — `while (pm->level > ref->level - 1 &&
21919 // (pm = pm->old));` — pm becomes NULL when the chain runs
21920 // out above the ref's scope (an upscope ref to a name that
21921 // only exists at/below its own level is DANGLING).
21922 while cur.level > ref_level - 1 {
21923 match cur.old.as_deref() {
21924 Some(o) => cur = o,
21925 None => return None,
21926 }
21927 }
21928 } else {
21929 // c:6460
21930 while let Some(o) = cur.old.as_deref() {
21931 if o.level >= ref_base {
21932 cur = o;
21933 } else {
21934 break;
21935 }
21936 }
21937 }
21938 Some(Box::new(cur.clone()))
21939 };
21940 let mut tab = match paramtab().read() {
21941 Ok(t) => t,
21942 Err(_) => return nameref_resolution::NotRef,
21943 };
21944 let first = match tab.get(name) {
21945 Some(p) => p,
21946 None => return nameref_resolution::NotRef,
21947 };
21948 let f = first.node.flags as u32;
21949 if (f & PM_NAMEREF) == 0 {
21950 return nameref_resolution::NotRef;
21951 }
21952 // c:6336-6339 — an UNSET nameref ends the chain at itself: reads
21953 // see "unset" (fetchvalue NULL), assignment writes its refname
21954 // (setstrvalue c:2712 clears PM_UNSET then the PM_NAMEREF arm
21955 // stores the new refname).
21956 if (f & PM_UNSET) != 0 {
21957 return nameref_resolution::Placeholder(first.node.nam.clone(), first.level);
21958 }
21959 let mut cur: Param = first.clone();
21960 let mut visited: Vec<(String, i32)> = Vec::new();
21961 let mut hops = 0usize;
21962 loop {
21963 hops += 1;
21964 if hops > 256 {
21965 // Hard backstop (C relies on PM_TAGGED; this can't fire
21966 // unless the tag logic above missed a cycle).
21967 zerr(&format!("{}: invalid self reference", cur.node.nam));
21968 return nameref_resolution::SelfRef;
21969 }
21970 let cf = cur.node.flags as u32;
21971 if (cf & PM_NAMEREF) == 0 || (cf & PM_UNSET) != 0 {
21972 // Final non-ref (or unset-ref) target reached.
21973 let lvl = cur.level;
21974 let nm = cur.node.nam.clone();
21975 return nameref_resolution::Target {
21976 name: nm,
21977 subscript: None,
21978 pm: Some(cur),
21979 level: lvl,
21980 };
21981 }
21982 // c:6338-6339 — refname; subscripted refname ends the chain.
21983 let refname_full = match cur.u_str.as_deref() {
21984 Some(r) if !r.is_empty() => r.to_string(),
21985 _ => {
21986 // c:6354 keep_lastref — placeholder ref.
21987 return nameref_resolution::Placeholder(cur.node.nam.clone(), cur.level);
21988 }
21989 };
21990 // c:6339 — `pm->width` is the subscript offset; a subscripted
21991 // ref is the end of any chain (see fetchvalue c:2250-2256).
21992 if cur.width > 0 && (cur.width as usize) < refname_full.len() {
21993 let (base_name, sub) = {
21994 let i = cur.width as usize;
21995 let tail = &refname_full[i + 1..];
21996 let key = tail.strip_suffix(']').unwrap_or(tail);
21997 (refname_full[..i].to_string(), Some(key.to_string()))
21998 };
21999 let resolved = tab
22000 .get(&base_name)
22001 .and_then(|vis| upscope_clone(vis, cur.node.flags as u32, cur.level, cur.base));
22002 // c:2258-2259 (fetchvalue) — `pm = loadparamnode(paramtab,
22003 // upscope(p1, pm), ref)`: a PM_AUTOLOAD stub loads its module;
22004 // a load that fails to define it leaves the element unset.
22005 let resolved = if resolved
22006 .as_ref()
22007 .is_some_and(|p| (p.node.flags as u32 & PM_AUTOLOAD) != 0)
22008 {
22009 drop(tab);
22010 newparamtable(0, "paramtab").and_then(|ht| loadparamnode(&ht, resolved, &base_name))
22011 } else {
22012 resolved
22013 };
22014 let lvl = resolved.as_ref().map(|p| p.level).unwrap_or(0);
22015 return nameref_resolution::Target {
22016 name: base_name,
22017 subscript: sub,
22018 pm: resolved,
22019 level: lvl,
22020 };
22021 }
22022 // c:6341-6343 — PM_TAGGED loop detection.
22023 let key = (cur.node.nam.clone(), cur.level);
22024 if visited.contains(&key) {
22025 zerr(&format!("{}: invalid self reference", cur.node.nam));
22026 return nameref_resolution::SelfRef;
22027 }
22028 visited.push(key);
22029 // c:6346-6347 — next hop via gethashnode2 + upscope.
22030 let next = match tab.get(&refname_full) {
22031 Some(vis) => {
22032 match upscope_clone(vis, cur.node.flags as u32, cur.level, cur.base) {
22033 // c:6347 `loadparamnode(paramtab, upscope(pm, ref),
22034 // refname)` — a PM_AUTOLOAD stub loads its module; if
22035 // that fails to define the parameter, resolution is NULL.
22036 Some(n) if (n.node.flags as u32 & PM_AUTOLOAD) != 0 => {
22037 drop(tab);
22038 match newparamtable(0, "paramtab")
22039 .and_then(|ht| loadparamnode(&ht, Some(n), &refname_full))
22040 {
22041 Some(loaded) => {
22042 tab = match paramtab().read() {
22043 Ok(t) => t,
22044 Err(_) => return nameref_resolution::NotRef,
22045 };
22046 loaded
22047 }
22048 None => return nameref_resolution::OutOfScope,
22049 }
22050 }
22051 Some(n) => n,
22052 None => {
22053 // c:6347-6349 — name exists but upscope ran
22054 // out: the keep_lastref else-arm is skipped,
22055 // resolve returns NULL.
22056 return nameref_resolution::OutOfScope;
22057 }
22058 }
22059 }
22060 None => {
22061 // c:6347 gethashnode2 miss — dangling target.
22062 return nameref_resolution::Target {
22063 name: refname_full,
22064 subscript: None,
22065 pm: None,
22066 level: 0,
22067 };
22068 }
22069 };
22070 // c:6348 — `pm != stop` guard: when the hop lands on the stop
22071 // param, do NOT recurse (used by setscope's self-ref check
22072 // via resolve_nameref_rec(pm, pm, 0) at c:6423).
22073 if let Some((snam, slvl)) = stop_at {
22074 if next.node.nam == snam && next.level == slvl {
22075 let lvl = next.level;
22076 let nm = next.node.nam.clone();
22077 return nameref_resolution::Target {
22078 name: nm,
22079 subscript: None,
22080 pm: Some(next),
22081 level: lvl,
22082 };
22083 }
22084 }
22085 // c:6348 — `!(pm->node.flags & PM_UNSET)` guard: an unset
22086 // target stops the recursion; the unset param is returned.
22087 if (next.node.flags as u32 & PM_UNSET) != 0 {
22088 // An unset NAMEREF hop behaves like a placeholder: the
22089 // assignment revives it by writing the refname (the
22090 // PM_NAMEREF assignstrvalue arm at c:2715).
22091 if (next.node.flags as u32 & PM_NAMEREF) != 0 {
22092 return nameref_resolution::Placeholder(next.node.nam.clone(), next.level);
22093 }
22094 let lvl = next.level;
22095 let nm = next.node.nam.clone();
22096 return nameref_resolution::Target {
22097 name: nm,
22098 subscript: None,
22099 pm: Some(next),
22100 level: lvl,
22101 };
22102 }
22103 cur = next;
22104 }
22105}
22106
22107thread_local! {
22108/// `${(!)name}` — SCANPM_NONAMEREF depth (Src/params.c:2232-2235:
22109/// the `!` flag routes the lookup through `getnode2`, skipping
22110/// `resolve_nameref`). Non-zero ⇒ `is_nameref` reports false so
22111/// every deref point reads the ref itself.
22112pub static NAMEREF_SUPPRESS: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
22113}
22114
22115/// RAII guard for `${(!)...}` — suppresses nameref resolution on this
22116/// thread for its lifetime (SCANPM_NONAMEREF scope).
22117pub struct NamerefSuppressGuard;
22118
22119impl NamerefSuppressGuard {
22120 /// Enter a SCANPM_NONAMEREF scope.
22121 #[allow(clippy::new_without_default)]
22122 pub fn new() -> Self {
22123 NAMEREF_SUPPRESS.with(|c| c.set(c.get() + 1));
22124 NamerefSuppressGuard
22125 }
22126}
22127
22128impl Drop for NamerefSuppressGuard {
22129 fn drop(&mut self) {
22130 NAMEREF_SUPPRESS.with(|c| c.set(c.get().saturating_sub(1)));
22131 }
22132}
22133
22134/// !!! WARNING: RUST-ONLY HELPER !!!
22135/// No C counterpart: C tests `pm->node.flags & PM_NAMEREF` inline at each site
22136/// (e.g. `fetchvalue`, `Src/params.c:2247`). This predicate does the paramtab
22137/// lookup that Rust needs before the flag can be read.
22138/// Is the visible binding of `name` a PM_NAMEREF? (Cheap pre-check
22139/// so the resolver isn't invoked on every plain variable read.)
22140///
22141/// A node still carrying `PM_NAMEREF` does NOT necessarily behave as a
22142/// reference. C gates every nameref-chase on the c:1088-1090 predicate
22143/// (`Src/params.c:1086-1090`, `createparam`):
22144///
22145/// ```c
22146/// if (oldpm && !(flags & PM_NAMEREF) &&
22147/// (oldpm->level == locallevel ?
22148/// !(oldpm->node.flags & PM_RO_BY_DESIGN) : !(flags & PM_LOCAL)) &&
22149/// (oldpm->node.flags & PM_NAMEREF) &&
22150/// (!(oldpm->node.flags & PM_UNSET) ||
22151/// (oldpm->node.flags & PM_DECLARED))) {
22152/// ```
22153///
22154/// `unset -n ref` on a LOCAL reference runs `unsetparam_pm`, which does
22155/// `pm->node.flags &= ~PM_DECLARED;` (c:3798 — "like ksh, not like bash")
22156/// and then stops at c:3851 `if ((pm->level && locallevel >= pm->level)
22157/// ... return 0;`, i.e. it KEEPS the node in `paramtab` with
22158/// `PM_NAMEREF | PM_UNSET` and PM_DECLARED cleared. C's c:1088-1090
22159/// predicate is then false, so `createparam` skips the chase entirely
22160/// and falls through to c:1135 (`pm = oldpm`), reusing the node with the
22161/// caller's fresh flags — the reference becomes an ordinary parameter of
22162/// whatever type the assignment/typeset asked for.
22163///
22164/// The same predicate also guards `fetchvalue` (c:2241-2243
22165/// `if (pm->node.flags & PM_UNSET && !(pm->node.flags & PM_DECLARED))
22166/// return NULL;`), so reads of such a node yield empty either way.
22167pub fn is_nameref(name: &str) -> bool {
22168 // `${(!)…}` SCANPM_NONAMEREF scope active → treat nothing as a ref.
22169 if NAMEREF_SUPPRESS.with(|c| c.get()) > 0 {
22170 return false;
22171 }
22172 paramtab()
22173 .read()
22174 .ok()
22175 .and_then(|t| {
22176 t.get(name).map(|p| {
22177 let f = p.node.flags as u32;
22178 // c:1089 — `(oldpm->node.flags & PM_NAMEREF)`.
22179 (f & PM_NAMEREF) != 0
22180 // c:1090 — `(!(oldpm->node.flags & PM_UNSET) ||
22181 // (oldpm->node.flags & PM_DECLARED))`.
22182 && ((f & PM_UNSET) == 0 || (f & PM_DECLARED) != 0)
22183 })
22184 })
22185 .unwrap_or(false)
22186}
22187
22188/// !!! WARNING: RUST-ONLY HELPER !!!
22189/// No C counterpart: the PM_TYPE dispatch inside `getstrvalue`
22190/// (`Src/params.c:2335`) lifted out so it can run against a borrowed `param`
22191/// without the VALFLAG_SUBST padding. C does this inline in the one function.
22192/// Raw scalar value of a (possibly hidden) param clone — the
22193/// type-dispatch heart of `getstrvalue` (Src/params.c:2350-2382)
22194/// without the VALFLAG_SUBST padding (refs to hidden bindings).
22195fn param_scalar_value(pm: ¶m) -> Option<String> {
22196 let t = PM_TYPE(pm.node.flags as u32);
22197 if t == PM_INTEGER {
22198 let base = if pm.base > 0 { pm.base } else { 10 };
22199 Some(convbase(pm.u_val, base as u32))
22200 } else if t == PM_EFLOAT || t == PM_FFLOAT {
22201 Some(convfloat(pm.u_dval, pm.base, pm.node.flags as u32))
22202 } else if t == PM_SCALAR && pm.gsu_s.is_some() {
22203 pm.gsu_s.as_ref().map(|gsu| (gsu.getfn)(pm))
22204 } else if let Some(s) = pm.u_str.as_ref() {
22205 Some(s.clone())
22206 } else if let Some(arr) = pm.u_arr.as_ref() {
22207 Some(arr.join(" "))
22208 } else if t == PM_HASHED {
22209 // assoc joined values (getstrvalue KSH `[0]` path is rare;
22210 // bare $assoc is empty in zsh, return empty)
22211 Some(String::new())
22212 } else {
22213 None
22214 }
22215}
22216
22217/// !!! WARNING: RUST-ONLY HELPER !!!
22218/// No C counterpart: C performs this read inside `fetchvalue`'s nameref branch
22219/// (`Src/params.c:2247`) followed by `getindex()` (`Src/params.c:2290`), operating
22220/// on `Param` pointers. Rust resolves the target by name, so the element read is
22221/// split out here.
22222/// Element read for a nameref bound to `target[sub]` — the
22223/// fetchvalue REFSLICE / getindex hand-off (Src/params.c:2247-2270
22224/// + c:2289). Arrays take a math index (1-based, negative from
22225/// end), assocs a literal key, scalars a char index.
22226fn nameref_element_read(pm: ¶m, target: &str, key: &str) -> Option<String> {
22227 let t = PM_TYPE(pm.node.flags as u32);
22228 // The resolved binding is the visible one unless the upscope walk
22229 // (c:6455) landed on a hidden `old`-chain node.
22230 let visible = paramtab()
22231 .read()
22232 .ok()
22233 .and_then(|tab| tab.get(target).map(|p| p.level))
22234 == Some(pm.level);
22235 // c:2265 `scanflags |= SCANPM_NOEXEC` for a reference slice, so
22236 // getarg's c:1563-1570 parses the subscript and singsubs it with
22237 // EXECOPT off: a `$(...)` in the referent expands to nothing.
22238 let key: String = {
22239 let exe = opt_state_get("exec").unwrap_or(true);
22240 let tok = match crate::ported::lex::parsestr(key) {
22241 Ok(t) => t,
22242 Err(_) => return None, // c:1566-1567
22243 };
22244 opt_state_set("exec", false); // c:1568-1569
22245 let out = crate::ported::subst::singsub(&tok);
22246 opt_state_set("exec", exe); // c:1571
22247 out
22248 };
22249 let key = key.as_str();
22250 if t == PM_HASHED {
22251 // c:2289 getindex reads the element through the hash's own
22252 // `gsu.h->getfn`: a zsh/parameter special answers per key.
22253 if (pm.node.flags as u32 & PM_SPECIAL) != 0 {
22254 if let Some(e) = crate::ported::modules::parameter::PARTAB.iter().find(|e| e.name == target) {
22255 return (e.getfn)(std::ptr::null_mut(), key).and_then(|p| p.u_str);
22256 }
22257 }
22258 // The assoc backing is name-keyed (paramtab_hashed_storage);
22259 // when the resolved binding is a HIDDEN old-chain node (a
22260 // `local` shadow covers it), the outer's data lives on the
22261 // shadow STACK (pushed by createparam's PM_HASHED save).
22262 if !visible {
22263 if let Some(stk) = crate::ported::params::PARAMTAB_HASHED_SHADOW_STACK.get() {
22264 if let Ok(stk) = stk.lock() {
22265 if let Some(frames) = stk.get(target) {
22266 // Frames are `(level, row)`; the hidden outer's bag is
22267 // the row saved by the innermost shadow.
22268 if let Some((_, Some(saved))) = frames.last() {
22269 return saved.get(key).cloned();
22270 }
22271 }
22272 }
22273 }
22274 }
22275 return paramtab_hashed_storage()
22276 .lock()
22277 .ok()
22278 .and_then(|m| m.get(target).and_then(|h| h.get(key).cloned()));
22279 }
22280 // c:1597 `r = mathevalarg(s, &s)` — reports a bad (or empty) index.
22281 let idx = crate::ported::math::mathevalarg(key);
22282 if errflag.load(Ordering::Relaxed) != 0 {
22283 return None;
22284 }
22285 // A visible binding reads through the canonical getters, so the
22286 // specials' getfns (`argv`, `ARGC`, `_`, ...) answer, not the
22287 // node's stored slot.
22288 if t == PM_ARRAY {
22289 let arr = if visible {
22290 getaparam(target)?
22291 } else {
22292 pm.u_arr.clone()?
22293 };
22294 let len = arr.len() as i64;
22295 let pos = if idx < 0 { len + idx + 1 } else { idx };
22296 if pos >= 1 && pos <= len {
22297 return Some(arr[(pos - 1) as usize].clone());
22298 }
22299 return Some(String::new());
22300 }
22301 // scalar char index (1-based)
22302 let s = if visible {
22303 getsparam(target)?
22304 } else {
22305 param_scalar_value(pm)?
22306 };
22307 let chars: Vec<char> = s.chars().collect();
22308 let len = chars.len() as i64;
22309 let pos = if idx < 0 { len + idx + 1 } else { idx };
22310 if pos >= 1 && pos <= len {
22311 return Some(chars[(pos - 1) as usize].to_string());
22312 }
22313 Some(String::new())
22314}
22315
22316/// !!! WARNING: RUST-ONLY HELPER !!!
22317/// No C counterpart under this name: full-table variant of `setscope(Param pm)`
22318/// (`Src/params.c:6382`). The name-parity `setscope` port above works on a borrowed
22319/// `param`; the base-scope computation, the WARNNESTEDVAR diagnostic and the
22320/// chain-walk self-reference unset all need the live paramtab, which cannot be
22321/// locked while that borrow is held.
22322/// Full-table variant of `setscope(Param pm)` (Src/params.c:6382).
22323/// Performs the parts the in-place `setscope` above cannot: the
22324/// base-scope computation against the live paramtab (c:6402-6410),
22325/// the WARNNESTEDVAR diagnostic (c:6411-6419), and the chain-walk
22326/// self-reference detection + unset (c:6421-6431). Returns 1 when
22327/// the self-reference error fired (the ref has been removed), else 0.
22328/// MUST be called without any paramtab lock held.
22329///
22330/// `level` picks the struct: `None` is the visible binding of `name`;
22331/// `Some(l)` is the node at scope `l` on its `old` chain, which is how a
22332/// ref hidden by a same-name local is addressed (C passes that struct's
22333/// pointer). A hidden ref gets the base computation only: the chain-walk
22334/// self-reference check starts from the visible binding of `name`.
22335pub fn setscope_by_name(name: &str, level: Option<i32>) -> i32 {
22336 queue_signals();
22337 // The struct setscope works on: the visible node, or the node at
22338 // `level` on its `old` chain.
22339 let with_node = |f: &mut dyn FnMut(&mut param)| {
22340 if let Ok(mut tab) = paramtab().write() {
22341 let mut cur = tab.get_mut(name).map(|b| b.as_mut());
22342 while let Some(pm) = cur {
22343 if level.is_none_or(|l| pm.level == l) {
22344 f(pm);
22345 return;
22346 }
22347 cur = pm.old.as_deref_mut();
22348 }
22349 }
22350 };
22351 // Snapshot the ref's state.
22352 let mut snap = None;
22353 with_node(&mut |pm: &mut param| {
22354 if (pm.node.flags as u32 & PM_NAMEREF) != 0 {
22355 snap = Some((pm.u_str.clone(), pm.level, pm.node.flags as u32));
22356 }
22357 });
22358 let visible = level.is_none()
22359 || paramtab().read().ok().and_then(|t| t.get(name).map(|p| p.level)) == level;
22360 let (refname_opt, ref_level, ref_flags) = match snap {
22361 Some(s) => s,
22362 None => {
22363 unqueue_signals();
22364 return 0;
22365 }
22366 };
22367 let refname_full = refname_opt.unwrap_or_default();
22368 // c:6388-6400 — `t = itype_end(refname, INAMESPC, 0)`; only a `[`
22369 // right after the NAME is a subscript, so `![1]` keeps width 0 and
22370 // names the (nonexistent) parameter `![1]`.
22371 let (head, width) = {
22372 let t = crate::ported::utils::itype_end(&refname_full, crate::ported::ztype_h::INAMESPC, false);
22373 if t > 0 && refname_full[t..].starts_with('[') {
22374 (refname_full[..t].to_string(), t as i32)
22375 } else {
22376 (refname_full.clone(), 0)
22377 }
22378 };
22379 if width != 0 {
22380 with_node(&mut |pm: &mut param| pm.width = width); // c:6398 pm->width = t - refname
22381 }
22382 // c:6402-6410 — compute pm->base from the target's visible level.
22383 // `basepm != pm || !basepm->old || (basepm = basepm->old)`: a ref
22384 // whose refname is its own name binds to the ENCLOSING binding.
22385 let mut basepm_is_self = false;
22386 if (ref_flags & PM_UPPER) == 0 && !head.is_empty() {
22387 // c:6405-6406 — `basepm = loadparamnode(realparamtab, basepm,
22388 // refname)`: binding a reference to an autoloadable parameter
22389 // loads its module.
22390 let stub = paramtab()
22391 .read()
22392 .ok()
22393 .and_then(|t| t.get(&head).filter(|p| (p.node.flags as u32 & PM_AUTOLOAD) != 0).cloned());
22394 if let Some(stub) = stub {
22395 let _ = newparamtable(0, "paramtab").and_then(|ht| loadparamnode(&ht, Some(stub), &head));
22396 } else if crate::vm_helper::module_param_is_autoload_stub(&head) {
22397 // The same load for a zsh/parameter row, whose PM_AUTOLOAD state
22398 // zshrs keeps in vm_helper's MATERIALIZED_MODULE_PARAMS side set.
22399 crate::vm_helper::mark_module_param_used(&head);
22400 }
22401 let base_level: Option<i32> = {
22402 let tab = paramtab().read().unwrap();
22403 match tab.get(&head) {
22404 Some(vis) => {
22405 if head == name && vis.level == ref_level {
22406 // basepm == pm — prefer pm->old (c:6407).
22407 match vis.old.as_deref() {
22408 Some(o) => Some(o.level),
22409 None => {
22410 basepm_is_self = true;
22411 None
22412 }
22413 }
22414 } else {
22415 Some(vis.level)
22416 }
22417 }
22418 None => None,
22419 }
22420 };
22421 if let Some(bl) = base_level {
22422 with_node(&mut |pm: &mut param| setscope_base(pm, bl)); // c:6409 setscope_base(pm, basepm->level)
22423 }
22424 // c:6411-6419 — base > level diagnostics.
22425 let mut base_now = 0;
22426 with_node(&mut |pm: &mut param| base_now = pm.base);
22427 if base_now > ref_level && isset(crate::ported::zsh_h::WARNNESTEDVAR) {
22428 // c:6417-6418
22429 zwarn(&format!(
22430 "reference {} in enclosing scope set to local variable {}",
22431 name, head
22432 ));
22433 }
22434 }
22435 // c:6421-6431 — self-reference detection via the chain walk with
22436 // stop == pm.
22437 let mut selfref = basepm_is_self;
22438 if !visible {
22439 unqueue_signals();
22440 return 0;
22441 }
22442 if !head.is_empty() && width == 0 && !basepm_is_self {
22443 match resolve_nameref_name(name, Some((name, ref_level))) {
22444 nameref_resolution::Target {
22445 name: tname, level, ..
22446 } if tname == name && level == ref_level => {
22447 selfref = true; // chain looped back to pm
22448 }
22449 nameref_resolution::SelfRef => {
22450 // zerr already emitted inside the walk (with the
22451 // looping ref's name, matching the recursive C zerr).
22452 unqueue_signals();
22453 return 1;
22454 }
22455 _ => {}
22456 }
22457 }
22458 if selfref {
22459 // c:6426-6429 — `zerr("%s: invalid self reference", refname);
22460 // unsetparam_pm(pm, 0, 1);`
22461 zerr(&format!("{}: invalid self reference", head));
22462 if let Some(mut pm) = paramtab().write().ok().and_then(|mut t| t.remove(name)) {
22463 if let Some(prev) = pm.old.take() {
22464 // uncover any outer binding (unsetparam_pm restore shape)
22465 if let Ok(mut tab) = paramtab().write() {
22466 tab.insert(name.to_string(), prev);
22467 }
22468 }
22469 }
22470 unqueue_signals();
22471 return 1;
22472 }
22473 unqueue_signals();
22474 0
22475}