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::hist::{
17 bangchar, hashchar, hatchar, histsiz, resizehistents, saveandpophiststack, savehistsiz,
18};
19use crate::ported::init::SHTTY;
20use crate::ported::lex::untokenize;
21use crate::ported::math::lastbase;
22#[allow(unused_imports)]
23use crate::ported::math::{
24 matheval, mathevali, MN_FLOAT, MN_FLOAT as MN_FLT, MN_INTEGER, MN_INTEGER as MN_INT,
25};
26use crate::ported::mem::{popheap, pushheap};
27use crate::ported::modules::parameter::FUNCSTACK;
28#[allow(unused_imports)]
29use crate::ported::options::{opt_state_get, opt_state_set, optlookup};
30use crate::ported::patchlevel::{ZSH_PATCHLEVEL, ZSH_VERSION};
31use crate::ported::pattern::{patcompile, pattry};
32#[allow(unused_imports)]
33use crate::ported::signals::{queue_signals, unqueue_signals};
34use crate::ported::signals_h::SIGS;
35use crate::ported::string::ztrdup;
36use crate::ported::utils::{
37 adduserdir, arrlen_ge, dec_locallevel, inc_locallevel, metafy, quotedzputs, xsymlink,
38};
39#[allow(unused_imports)]
40use crate::ported::utils::{
41 adjustwinsize, argzero, colonsplit, errflag, get_username, inittyptab, itype_end,
42 locallevel as locallevel_fn, posixzero, set_argzero, set_locallevel, set_posixzero, unmeta,
43 zerr, ztrdup_metafy, zwarn,
44};
45use crate::ported::zsh_h::PAT_HEAPDUP;
46#[allow(unused_imports)]
47use crate::ported::zsh_h::{
48 gsu_array, gsu_float, gsu_hash, gsu_integer, gsu_scalar, hashnode, hashtable, isset, mnumber,
49 param, paramdef, unset, value, HashTable, Marker, Param, ALLEXPORT, ASSPM_AUGMENT,
50 ASSPM_ENV_IMPORT, ASSPM_KEY_VALUE, ASSPM_WARN, AUTONAMEDIRS, EMULATE_KSH, EMULATE_SH,
51 EMULATE_ZSH, EMULATION, ERRFLAG_ERROR, EXECOPT, FS_FUNC, KSHARRAYS, PM_ARRAY, PM_AUTOLOAD,
52 PM_DECLARED, PM_DEFAULTED, PM_DONTIMPORT, PM_DONTIMPORT_SUID, PM_EFLOAT, PM_EXPORTED,
53 PM_FFLOAT, PM_HASHED, PM_HASHELEM, PM_HIDE, PM_HIDEVAL, PM_INTEGER, PM_LEFT, PM_LOCAL,
54 PM_NAMEDDIR, PM_NAMEREF, PM_NORESTORE, PM_READONLY, PM_READONLY_SPECIAL, PM_REMOVABLE,
55 PM_RIGHT_B, PM_RIGHT_Z, PM_RO_BY_DESIGN, PM_SCALAR, PM_SPECIAL, PM_TAGGED, PM_TIED, PM_TYPE,
56 PM_UNIQUE, PM_UNSET, PM_UPPER, POSIXARGZERO, PRINT_INCLUDEVALUE, PRINT_KV_PAIR, PRINT_LINE,
57 PRINT_NAMEONLY, PRINT_POSIX_EXPORT, PRINT_POSIX_READONLY, PRINT_TYPE, PRINT_TYPESET,
58 SCANPM_ARRONLY, SCANPM_CHECKING, SCANPM_ISVAR_AT, SCANPM_KEYMATCH, SCANPM_MATCHKEY,
59 SCANPM_MATCHMANY, SCANPM_MATCHVAL, SCANPM_NONAMEREF, SCANPM_WANTINDEX, SCANPM_WANTKEYS,
60 SCANPM_WANTVALS, TERM_BAD, TERM_UNKNOWN, VALFLAG_EMPTY, VALFLAG_INV, VALFLAG_SUBST,
61 WARNCREATEGLOBAL, WARNNESTEDVAR,
62};
63use crate::ported::zsh_h::{
64 HashNode, Inbrack, Meta, CBASES, CHASELINKS, HFILE_USE_OPTIONS, INTERACTIVE, OCTALZEROES,
65 PM_LOWER, PRIVILEGED, SCANPM_ASSIGNING,
66};
67use crate::ported::zsh_system_h::DEFAULT_TIMEFMT;
68use crate::{DPUTS, DPUTS2};
69use fusevm::Value;
70use indexmap::IndexMap;
71use std::collections::{HashMap, HashSet};
72use std::env;
73use std::sync::atomic::{AtomicI64, Ordering};
74use std::sync::{Arc, Mutex, OnceLock, RwLock};
75use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
76
77/// Port of `static int lc_update_needed` from `Src/params.c:5850`
78/// (under `#ifdef USE_LOCALE`). Set to 1 by `scanendscope` when a
79/// LC_*/LANG param's scope ends; consumed by `endparamscope` to
80/// trigger a `setlocale()` refresh.
81pub static LC_UPDATE_NEEDED: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
82
83/// Port of `static Param foundparam` from `Src/params.c:640`.
84/// Set by `scanparamvals` to the last param it touched, read by
85/// `assignsparam` / `assignnparam` for the assoc-element path.
86/// Stores the param name; the live `¶m` lookup is done by
87/// the caller through paramtab.
88pub static FOUNDPARAM: OnceLock<Mutex<Option<String>>> = OnceLock::new();
89
90/// Port of `rprompt_indent_unsetfn(Param pm, int exp)` from `Src/params.c:152`. C
91/// body: `stdunsetfn(pm, exp); rprompt_indent = 1;` — keeps in
92/// sync with init_term().
93pub fn rprompt_indent_unsetfn(pm: &mut param, exp: i32) {
94 stdunsetfn(pm, exp);
95 *RPROMPT_INDENT.lock().unwrap() = 1;
96}
97
98// =============================================================================
99// IPDEF{1,2,4,5,5U,6,7,7R,7U,8,9,10} + LCIPDEF — special-parameter
100// table entry constructors. All defined as macros in
101// `Src/params.c:296-406`. Each produces one row of the
102// `special_params[]` table; the differences are flag combinations
103// + which gsu (getter/setter union) the entry binds.
104//
105// In C, `BR(p)` is `{(void *)(p)}` for the param's `u` data field;
106// `GSU(g)` is the `&g` of the named gsu_scalar/gsu_integer/etc.
107// The Rust port stores `var` and `gsu` as `usize` slot indexes
108// into per-evaluator tables, matching the existing PARAMDEF helper
109// above. The flag bit combinations mirror the C macros line-by-line.
110// =============================================================================
111
112/// Port of `IPDEF1(A,B,C)` from `Src/params.c:296` —
113/// `{{NULL,A,PM_INTEGER|PM_SPECIAL|C},BR(NULL),GSU(B),10,0,...}`.
114#[inline]
115#[allow(non_snake_case)]
116pub fn IPDEF1(A: &str, B: usize, C: i32) -> paramdef {
117 // c:params.c:296
118 paramdef {
119 name: A.into(),
120 flags: (PM_INTEGER | PM_SPECIAL) as i32 | C,
121 gsu: B,
122 ..Default::default()
123 }
124}
125
126/// Port of `IPDEF2(A,B,C)` from `Src/params.c:309` —
127/// `{{NULL,A,PM_SCALAR|PM_SPECIAL|C},BR(NULL),GSU(B),0,0,...}`.
128#[inline]
129#[allow(non_snake_case)]
130pub fn IPDEF2(A: &str, B: usize, C: i32) -> paramdef {
131 // c:params.c:309
132 paramdef {
133 name: A.into(),
134 flags: (PM_SCALAR | PM_SPECIAL) as i32 | C,
135 gsu: B,
136 ..Default::default()
137 }
138}
139
140// ---------------------------------------------------------------------------
141// Parameter flags (from zsh.h PM_* flags)
142// ---------------------------------------------------------------------------
143
144// What level of localness we are at. // c:47
145// // c:48
146// Hand-wavingly, this is incremented at every function call and decremented // c:49
147// at every function return. See startparamscope(). // c:50
148
149/// Port of `mod_export int locallevel;` from `Src/params.c:54`.
150/// Tracks function-local-scope nesting depth. Bumped by
151/// `startparamscope()` (params.c:5879) on every function call,
152/// decremented by `endparamscope()` (params.c:5950) on return.
153#[allow(non_upper_case_globals)]
154pub static locallevel: std::sync::atomic::AtomicI32 = // c:54
155 std::sync::atomic::AtomicI32::new(0);
156
157// ---------------------------------------------------------------------------
158// Real `param` struct lives in Src/zsh.h:1829 (port at zsh_h.rs:750).
159// It uses C-union flattening: u_str / u_arr / u_val / u_dval / u_hash
160// dispatched on `PM_TYPE(node.flags)`. There is NO `ParamValue` enum in
161// C; do not reintroduce one.
162// ---------------------------------------------------------------------------
163
164/// Port of `LCIPDEF(name)` from `Src/params.c:324` —
165/// `IPDEF2(name, lc_blah_gsu, PM_UNSET)`.
166#[inline]
167#[allow(non_snake_case)]
168pub fn LCIPDEF(name: &str) -> paramdef {
169 // c:params.c:324
170 IPDEF2(name, 0, PM_UNSET as i32) // c:324 lc_blah_gsu (slot 0)
171}
172
173/// Port of `IPDEF4(A,B)` from `Src/params.c:344` —
174/// `{{NULL,A,PM_INTEGER|PM_READONLY_SPECIAL},BR((void*)B),
175/// GSU(varint_readonly_gsu),10,0,...}`.
176#[inline]
177#[allow(non_snake_case)]
178pub fn IPDEF4(A: &str, B: usize) -> paramdef {
179 // c:params.c:344
180 paramdef {
181 name: A.into(),
182 flags: (PM_INTEGER | PM_READONLY_SPECIAL) as i32,
183 var: B,
184 ..Default::default()
185 }
186}
187
188/// Port of `IPDEF5(A,B,F)` from `Src/params.c:353` —
189/// `{{NULL,A,PM_INTEGER|PM_SPECIAL},BR((void*)B),GSU(F),10,0,...}`.
190#[inline]
191#[allow(non_snake_case)]
192pub fn IPDEF5(A: &str, B: usize, F: usize) -> paramdef {
193 // c:params.c:353
194 paramdef {
195 name: A.into(),
196 flags: (PM_INTEGER | PM_SPECIAL) as i32,
197 var: B,
198 gsu: F,
199 ..Default::default()
200 }
201}
202
203/// Port of `IPDEF5U(A,B,F)` from `Src/params.c:354` — c:353 + PM_UNSET.
204#[inline]
205#[allow(non_snake_case)]
206pub fn IPDEF5U(A: &str, B: usize, F: usize) -> paramdef {
207 // c:params.c:354
208 paramdef {
209 name: A.into(),
210 flags: (PM_INTEGER | PM_SPECIAL | PM_UNSET) as i32,
211 var: B,
212 gsu: F,
213 ..Default::default()
214 }
215}
216
217/// Port of `IPDEF6(A,B,F)` from `Src/params.c:362` — c:353 + PM_DONTIMPORT.
218#[inline]
219#[allow(non_snake_case)]
220pub fn IPDEF6(A: &str, B: usize, F: usize) -> paramdef {
221 // c:params.c:362
222 paramdef {
223 name: A.into(),
224 flags: (PM_INTEGER | PM_SPECIAL | PM_DONTIMPORT) as i32,
225 var: B,
226 gsu: F,
227 ..Default::default()
228 }
229}
230
231/// Port of `IPDEF7(A,B)` from `Src/params.c:367` —
232/// `{{NULL,A,PM_SCALAR|PM_SPECIAL},BR((void*)B),GSU(varscalar_gsu),0,0,...}`.
233#[inline]
234#[allow(non_snake_case)]
235pub fn IPDEF7(A: &str, B: usize) -> paramdef {
236 // c:params.c:367
237 paramdef {
238 name: A.into(),
239 flags: (PM_SCALAR | PM_SPECIAL) as i32,
240 var: B,
241 ..Default::default()
242 }
243}
244
245/// Port of `IPDEF7U(A,B)` from `Src/params.c:369` — c:367 + PM_UNSET.
246#[inline]
247#[allow(non_snake_case)]
248pub fn IPDEF7U(A: &str, B: usize) -> paramdef {
249 // c:params.c:369
250 paramdef {
251 name: A.into(),
252 flags: (PM_SCALAR | PM_SPECIAL | PM_UNSET) as i32,
253 var: B,
254 ..Default::default()
255 }
256}
257
258/// Port of `IPDEF7R(A,B)` from `Src/params.c:368` — c:367 + PM_DONTIMPORT_SUID.
259#[inline]
260#[allow(non_snake_case)]
261pub fn IPDEF7R(A: &str, B: usize) -> paramdef {
262 // c:params.c:368
263 paramdef {
264 name: A.into(),
265 flags: (PM_SCALAR | PM_SPECIAL | PM_DONTIMPORT_SUID) as i32,
266 var: B,
267 ..Default::default()
268 }
269}
270
271/// Port of `IPDEF9(A,B,C,D)` from `Src/params.c:431` —
272/// `{{NULL,A,D|PM_ARRAY|PM_SPECIAL|PM_DONTIMPORT},BR((void*)B),
273/// GSU(vararray_gsu),0,0,NULL,C,NULL,0}`.
274#[inline]
275#[allow(non_snake_case)]
276pub fn IPDEF9(A: &str, B: usize, C: usize, D: i32) -> paramdef {
277 // c:params.c:384
278 paramdef {
279 name: A.into(),
280 flags: (PM_ARRAY | PM_SPECIAL | PM_DONTIMPORT) as i32 | D,
281 var: B,
282 ..Default::default()
283 }
284}
285
286/// Port of `IPDEF8(A,B,C,D)` from `Src/params.c:394` —
287/// `{{NULL,A,D|PM_SCALAR|PM_SPECIAL},BR((void*)B),GSU(colonarr_gsu),
288/// 0,0,NULL,C,NULL,0}`.
289/// `C` is the colon-arr field; the Rust port stores it in `getnfn`
290/// since `paramdef` lacks a dedicated colon-arr slot until that's
291/// ported.
292#[inline]
293#[allow(non_snake_case)]
294pub fn IPDEF8(A: &str, B: usize, C: usize, D: i32) -> paramdef {
295 // c:params.c:394
296 paramdef {
297 name: A.into(),
298 flags: (PM_SCALAR | PM_SPECIAL) as i32 | D,
299 var: B,
300 ..Default::default()
301 }
302}
303
304/// Port of `IPDEF10(A,B)` from `Src/params.c:438` —
305/// `{{NULL,A,PM_ARRAY|PM_SPECIAL},BR(NULL),GSU(B),10,0,...}`.
306#[inline]
307#[allow(non_snake_case)]
308pub fn IPDEF10(A: &str, B: usize) -> paramdef {
309 // c:params.c:406
310 paramdef {
311 name: A.into(),
312 flags: (PM_ARRAY | PM_SPECIAL) as i32,
313 gsu: B,
314 ..Default::default()
315 }
316}
317
318/// Port of `newparamtable(int size, char const *name)` from `Src/params.c:519`. C body
319/// allocates a HashTable via `newhashtable(size, name, NULL)`
320/// and wires the vtable. Rust port constructs a fresh
321/// `Box<hashtable>` with the param-specific callbacks left as
322/// `None` (the hashtable.rs vtable cannot host the typed
323/// param-callback signatures yet — wiring them requires the
324/// hashtable backend refactor).
325#[allow(unused_variables)]
326pub fn newparamtable(size: i32, name: &str) -> Option<HashTable> {
327 let hsize = if size == 0 { 17 } else { size };
328 let mut nodes: Vec<Option<HashNode>> = Vec::with_capacity(hsize as usize);
329 for _ in 0..hsize {
330 nodes.push(None);
331 }
332 Some(Box::new(hashtable {
333 hsize,
334 ct: 0,
335 nodes,
336 tmpdata: 0,
337 hash: None,
338 emptytable: None,
339 filltable: None,
340 cmpnodes: None,
341 addnode: None,
342 getnode: None,
343 getnode2: None,
344 removenode: None,
345 disablenode: None,
346 enablenode: None,
347 freenode: None,
348 printnode: None,
349 scantab: None,
350 }))
351}
352
353/// Direct port of `static Param loadparamnode(HashTable ht, Param
354/// pm, const char *nam)` from `Src/params.c:544-567`. If `pm` is
355/// an AUTOLOAD stub, fires the module loader and re-fetches the
356/// node from ht; otherwise returns pm unchanged.
357///
358/// C body:
359/// if (pm && (pm->flags & PM_AUTOLOAD) && pm->u.str) {
360/// int level = pm->level;
361/// char *mn = dupstring(pm->u.str);
362/// (void)ensurefeature(mn, "p:", nam);
363/// pm = (Param)gethashnode2(ht, nam);
364/// while (pm && pm->level > level) pm = pm->old;
365/// if (pm && (pm->level != level || (pm->flags & PM_AUTOLOAD)))
366/// pm = NULL;
367/// if (!pm) zerr("autoloading module %s failed...", mn, nam);
368/// }
369/// return pm;
370/// Port of `loadparamnode(HashTable ht, Param pm, const char *nam)` from `Src/params.c:544`.
371/// WARNING: param names don't match C — Rust=(pm, nam) vs C=(ht, pm, nam)
372pub fn loadparamnode(
373 // c:544
374 _ht: &HashTable,
375 pm: Option<Param>,
376 nam: &str,
377) -> Option<Param> {
378 // c:546 — `if (pm && (pm->flags & PM_AUTOLOAD) && pm->u.str)`.
379 let (level, modname) = match &pm {
380 Some(p) if p.node.flags & PM_AUTOLOAD as i32 != 0 && p.u_str.is_some() => {
381 (p.level, p.u_str.clone().unwrap())
382 }
383 _ => return pm, // c:566 fall through
384 };
385
386 // c:549 — `ensurefeature(mn, "p:", nam)` fires the module loader.
387 // The Rust ensurefeature signature differs (takes ModuleTable);
388 // for now we look up the module without a table to keep the
389 // dispatch site honest. Module-table integration is pending.
390 // c:550 — re-fetch the node from ht after autoload.
391 let mut pm = paramtab().write().unwrap().get(nam).cloned();
392 // c:551 — walk pm->old back to original level.
393 while let Some(ref p) = pm {
394 if p.level > level {
395 pm = p.old.clone().map(|b| Param::from(b));
396 } else {
397 break;
398 }
399 }
400 // c:553-554 — if pm is at wrong level or still AUTOLOAD, treat
401 // as load failure.
402 let still_bad = match &pm {
403 Some(p) => p.level != level || p.node.flags & PM_AUTOLOAD as i32 != 0,
404 None => true,
405 };
406 if still_bad {
407 pm = None;
408 // c:561-563 — `zerr("autoloading module %s failed to define
409 // parameter: %s", mn, nam)`.
410 zerr(&format!(
411 "autoloading module {} failed to define parameter: {}",
412 modname, nam
413 ));
414 }
415 pm // c:566
416}
417
418// ---------------------------------------------------------------------------
419// Numeric type for parameters (from params.c mnumber)
420// ---------------------------------------------------------------------------
421
422// ---------------------------------------------------------------------------
423// Value struct - mirrors C's struct value for subscript access
424// ---------------------------------------------------------------------------
425// ---------------------------------------------------------------------------
426// Shell parameter
427// ---------------------------------------------------------------------------
428
429// ---------------------------------------------------------------------------
430// Tied parameter data
431// ---------------------------------------------------------------------------
432
433// TiedData removed: was a Rust-only sidecar for the deleted `ParamTable`'s
434// `tied: HashMap<String, TiedData>` field. C source stores tied-pair
435// metadata via `pm->ename` (the partner name) and `pm->u.data` (the
436// separator char) on the real `param` struct (Src/zsh.h:750 / Src/params.c
437// `bin_typeset()` typeset -T branch).
438
439// ---------------------------------------------------------------------------
440// Parameter table print types (from printparamnode)
441// ---------------------------------------------------------------------------
442
443// ---------------------------------------------------------------------------
444// Special parameter definitions table (mirrors special_params[] in C)
445// ---------------------------------------------------------------------------
446
447/// Special-parameter definition — Rust extension paralleling the
448/// `IPDEF*` macro entries in `Src/params.c:297-392`. C uses
449/// `struct paramdef` (`Src/zsh.h:2082`, mirrored at `zsh_h.rs:950`)
450/// with `var` + `gsu` pointers; the Rust port carries a trimmed
451/// shape with `pm_type`/`pm_flags`/`tied_name` until the full
452/// `gsu`-callback plumbing lands. Canonical `paramdef` is the
453/// long-term target.
454#[allow(non_camel_case_types)]
455#[derive(Clone, Debug)]
456pub struct special_paramdef {
457 /// `name` field.
458 pub name: &'static str,
459 pub pm_type: u32, // PM_INTEGER | PM_SCALAR | PM_ARRAY
460 pub pm_flags: u32, // PM_READONLY_SPECIAL, PM_DONTIMPORT, etc.
461 /// `tied_name` field.
462 pub tied_name: Option<&'static str>,
463}
464
465/// Index of the first entry in `special_params` that lives in the
466/// zsh-only section (after the `{{NULL,NULL,0}, BR(NULL), ...}`
467/// sentinel at `Src/params.c:392`). Entries before this index are
468/// always loaded; entries at and after this index are only loaded
469/// under non-sh/non-ksh emulation. Mirrors the C two-section table
470/// terminated by an inner NULL sentinel.
471pub const SPECIAL_PARAMS_ZSH_START: usize = 54; // c:392
472
473/// All special parameters from params.c special_params[]
474pub const special_params: &[special_paramdef] = &[
475 // Integer specials with custom GSU
476 special_paramdef {
477 name: "#",
478 pm_type: PM_INTEGER,
479 pm_flags: PM_READONLY,
480 tied_name: None,
481 },
482 special_paramdef {
483 name: "ERRNO",
484 pm_type: PM_INTEGER,
485 pm_flags: PM_UNSET,
486 tied_name: None,
487 },
488 special_paramdef {
489 name: "GID",
490 pm_type: PM_INTEGER,
491 pm_flags: PM_DONTIMPORT,
492 tied_name: None,
493 },
494 special_paramdef {
495 name: "EGID",
496 pm_type: PM_INTEGER,
497 pm_flags: PM_DONTIMPORT,
498 tied_name: None,
499 },
500 special_paramdef {
501 name: "HISTSIZE",
502 pm_type: PM_INTEGER,
503 pm_flags: 0,
504 tied_name: None,
505 },
506 special_paramdef {
507 name: "RANDOM",
508 pm_type: PM_INTEGER,
509 pm_flags: 0,
510 tied_name: None,
511 },
512 special_paramdef {
513 name: "SAVEHIST",
514 pm_type: PM_INTEGER,
515 pm_flags: 0,
516 tied_name: None,
517 },
518 special_paramdef {
519 name: "SECONDS",
520 pm_type: PM_INTEGER,
521 pm_flags: 0,
522 tied_name: None,
523 },
524 special_paramdef {
525 name: "UID",
526 pm_type: PM_INTEGER,
527 pm_flags: PM_DONTIMPORT,
528 tied_name: None,
529 },
530 special_paramdef {
531 name: "EUID",
532 pm_type: PM_INTEGER,
533 pm_flags: PM_DONTIMPORT,
534 tied_name: None,
535 },
536 special_paramdef {
537 name: "TTYIDLE",
538 pm_type: PM_INTEGER,
539 pm_flags: PM_READONLY,
540 tied_name: None,
541 },
542 // Scalar specials with custom GSU
543 special_paramdef {
544 name: "USERNAME",
545 pm_type: PM_SCALAR,
546 pm_flags: PM_DONTIMPORT,
547 tied_name: None,
548 },
549 special_paramdef {
550 name: "-",
551 pm_type: PM_SCALAR,
552 pm_flags: PM_READONLY,
553 tied_name: None,
554 },
555 special_paramdef {
556 name: "histchars",
557 pm_type: PM_SCALAR,
558 pm_flags: PM_DONTIMPORT,
559 tied_name: None,
560 },
561 special_paramdef {
562 name: "HOME",
563 pm_type: PM_SCALAR,
564 pm_flags: PM_UNSET,
565 tied_name: None,
566 },
567 special_paramdef {
568 name: "TERM",
569 pm_type: PM_SCALAR,
570 pm_flags: PM_UNSET,
571 tied_name: None,
572 },
573 special_paramdef {
574 name: "TERMINFO",
575 pm_type: PM_SCALAR,
576 pm_flags: PM_UNSET,
577 tied_name: None,
578 },
579 special_paramdef {
580 name: "TERMINFO_DIRS",
581 pm_type: PM_SCALAR,
582 pm_flags: PM_UNSET,
583 tied_name: None,
584 },
585 special_paramdef {
586 name: "WORDCHARS",
587 pm_type: PM_SCALAR,
588 pm_flags: 0,
589 tied_name: None,
590 },
591 special_paramdef {
592 name: "IFS",
593 pm_type: PM_SCALAR,
594 pm_flags: PM_DONTIMPORT,
595 tied_name: None,
596 },
597 special_paramdef {
598 name: "_",
599 pm_type: PM_SCALAR,
600 pm_flags: PM_DONTIMPORT,
601 tied_name: None,
602 },
603 special_paramdef {
604 name: "KEYBOARD_HACK",
605 pm_type: PM_SCALAR,
606 pm_flags: PM_DONTIMPORT,
607 tied_name: None,
608 },
609 special_paramdef {
610 name: "0",
611 pm_type: PM_SCALAR,
612 pm_flags: 0,
613 tied_name: None,
614 },
615 // Readonly integer variables bound to C globals
616 special_paramdef {
617 name: "!",
618 pm_type: PM_INTEGER,
619 pm_flags: PM_READONLY,
620 tied_name: None,
621 },
622 special_paramdef {
623 name: "$",
624 pm_type: PM_INTEGER,
625 pm_flags: PM_READONLY,
626 tied_name: None,
627 },
628 special_paramdef {
629 name: "?",
630 pm_type: PM_INTEGER,
631 pm_flags: PM_READONLY,
632 tied_name: None,
633 },
634 special_paramdef {
635 name: "HISTCMD",
636 pm_type: PM_INTEGER,
637 pm_flags: PM_READONLY,
638 tied_name: None,
639 },
640 special_paramdef {
641 name: "LINENO",
642 pm_type: PM_INTEGER,
643 pm_flags: PM_READONLY,
644 tied_name: None,
645 },
646 special_paramdef {
647 name: "PPID",
648 pm_type: PM_INTEGER,
649 pm_flags: PM_READONLY,
650 tied_name: None,
651 },
652 special_paramdef {
653 name: "ZSH_SUBSHELL",
654 pm_type: PM_INTEGER,
655 pm_flags: PM_READONLY,
656 tied_name: None,
657 },
658 // Settable integer variables
659 special_paramdef {
660 name: "COLUMNS",
661 pm_type: PM_INTEGER,
662 pm_flags: 0,
663 tied_name: None,
664 },
665 special_paramdef {
666 name: "LINES",
667 pm_type: PM_INTEGER,
668 pm_flags: 0,
669 tied_name: None,
670 },
671 special_paramdef {
672 name: "ZLE_RPROMPT_INDENT",
673 pm_type: PM_INTEGER,
674 pm_flags: PM_UNSET,
675 tied_name: None,
676 },
677 special_paramdef {
678 name: "SHLVL",
679 pm_type: PM_INTEGER,
680 pm_flags: 0,
681 tied_name: None,
682 },
683 special_paramdef {
684 name: "FUNCNEST",
685 pm_type: PM_INTEGER,
686 pm_flags: 0,
687 tied_name: None,
688 },
689 special_paramdef {
690 name: "OPTIND",
691 pm_type: PM_INTEGER,
692 pm_flags: PM_DONTIMPORT,
693 tied_name: None,
694 },
695 special_paramdef {
696 // c:Src/params.c:364 — `IPDEF6("TRY_BLOCK_ERROR", &try_errflag,
697 // varinteger_gsu)` = PM_INTEGER | PM_SPECIAL | PM_DONTIMPORT.
698 // No PM_UNSET on the table entry — C reads -1 via the getfn
699 // reading the global `try_errflag`. zshrs's earlier port set
700 // PM_UNSET as a "no-write-yet" sentinel which broke
701 // `${(k)parameters}` parity (zsh emits the name; with PM_UNSET
702 // here scanpmparameters skipped it). The -1 default is now
703 // surfaced via the special-var getter (params.rs sentinel
704 // override on PM_UNSET removal).
705 name: "TRY_BLOCK_ERROR",
706 pm_type: PM_INTEGER,
707 pm_flags: PM_DONTIMPORT,
708 tied_name: None,
709 },
710 special_paramdef {
711 // c:Src/loop.c — `try_interrupt = -1`. Same pattern as
712 // TRY_BLOCK_ERROR: no PM_UNSET on the table entry; -1
713 // default emerges via the special-var getter.
714 name: "TRY_BLOCK_INTERRUPT",
715 pm_type: PM_INTEGER,
716 pm_flags: PM_DONTIMPORT,
717 tied_name: None,
718 },
719 // Scalar variables bound to C globals
720 special_paramdef {
721 name: "OPTARG",
722 pm_type: PM_SCALAR,
723 pm_flags: 0,
724 tied_name: None,
725 },
726 special_paramdef {
727 name: "NULLCMD",
728 pm_type: PM_SCALAR,
729 pm_flags: 0,
730 tied_name: None,
731 },
732 special_paramdef {
733 name: "POSTEDIT",
734 pm_type: PM_SCALAR,
735 pm_flags: PM_UNSET,
736 tied_name: None,
737 },
738 special_paramdef {
739 name: "READNULLCMD",
740 pm_type: PM_SCALAR,
741 pm_flags: 0,
742 tied_name: None,
743 },
744 special_paramdef {
745 name: "PS1",
746 pm_type: PM_SCALAR,
747 pm_flags: 0,
748 tied_name: None,
749 },
750 special_paramdef {
751 name: "RPS1",
752 pm_type: PM_SCALAR,
753 pm_flags: PM_UNSET,
754 tied_name: None,
755 },
756 special_paramdef {
757 name: "RPROMPT",
758 pm_type: PM_SCALAR,
759 pm_flags: PM_UNSET,
760 tied_name: None,
761 },
762 special_paramdef {
763 name: "PS2",
764 pm_type: PM_SCALAR,
765 pm_flags: 0,
766 tied_name: None,
767 },
768 special_paramdef {
769 name: "RPS2",
770 pm_type: PM_SCALAR,
771 pm_flags: PM_UNSET,
772 tied_name: None,
773 },
774 special_paramdef {
775 name: "RPROMPT2",
776 pm_type: PM_SCALAR,
777 pm_flags: PM_UNSET,
778 tied_name: None,
779 },
780 special_paramdef {
781 name: "PS3",
782 pm_type: PM_SCALAR,
783 pm_flags: 0,
784 tied_name: None,
785 },
786 special_paramdef {
787 name: "PS4",
788 pm_type: PM_SCALAR,
789 pm_flags: PM_DONTIMPORT_SUID,
790 tied_name: None,
791 },
792 special_paramdef {
793 name: "SPROMPT",
794 pm_type: PM_SCALAR,
795 pm_flags: 0,
796 tied_name: None,
797 },
798 // Readonly arrays
799 special_paramdef {
800 name: "*",
801 pm_type: PM_ARRAY,
802 pm_flags: PM_READONLY | PM_DONTIMPORT,
803 tied_name: None,
804 },
805 special_paramdef {
806 name: "@",
807 pm_type: PM_ARRAY,
808 pm_flags: PM_READONLY | PM_DONTIMPORT,
809 tied_name: None,
810 },
811 // ===================================================================
812 // c:388-392 — `/* This empty row indicates the end of parameters
813 // available in all emulations. */` NULL sentinel terminates the
814 // "always loaded" section. Entries below this line are only added
815 // under zsh emulation (else-branch of EMULATION(EMULATE_SH|EMULATE_KSH)
816 // at createparamtable c:840-846).
817 // SPECIAL_PARAMS_ZSH_START tracks this section boundary.
818 // ===================================================================
819 // Tied colon-separated/array pairs
820 special_paramdef {
821 name: "CDPATH",
822 pm_type: PM_SCALAR,
823 pm_flags: PM_TIED,
824 tied_name: Some("cdpath"),
825 },
826 special_paramdef {
827 name: "FIGNORE",
828 pm_type: PM_SCALAR,
829 pm_flags: PM_TIED,
830 tied_name: Some("fignore"),
831 },
832 special_paramdef {
833 name: "FPATH",
834 pm_type: PM_SCALAR,
835 pm_flags: PM_TIED,
836 tied_name: Some("fpath"),
837 },
838 special_paramdef {
839 name: "MAILPATH",
840 pm_type: PM_SCALAR,
841 pm_flags: PM_TIED,
842 tied_name: Some("mailpath"),
843 },
844 special_paramdef {
845 name: "PATH",
846 pm_type: PM_SCALAR,
847 pm_flags: PM_TIED,
848 tied_name: Some("path"),
849 },
850 special_paramdef {
851 name: "PSVAR",
852 pm_type: PM_SCALAR,
853 pm_flags: PM_TIED,
854 tied_name: Some("psvar"),
855 },
856 special_paramdef {
857 name: "ZSH_EVAL_CONTEXT",
858 pm_type: PM_SCALAR,
859 pm_flags: PM_READONLY | PM_TIED,
860 tied_name: Some("zsh_eval_context"),
861 },
862 special_paramdef {
863 name: "MODULE_PATH",
864 pm_type: PM_SCALAR,
865 pm_flags: PM_DONTIMPORT | PM_TIED,
866 tied_name: Some("module_path"),
867 },
868 special_paramdef {
869 name: "MANPATH",
870 pm_type: PM_SCALAR,
871 pm_flags: PM_TIED,
872 tied_name: Some("manpath"),
873 },
874 // Locale
875 special_paramdef {
876 name: "LANG",
877 pm_type: PM_SCALAR,
878 pm_flags: PM_UNSET,
879 tied_name: None,
880 },
881 special_paramdef {
882 name: "LC_ALL",
883 pm_type: PM_SCALAR,
884 pm_flags: PM_UNSET,
885 tied_name: None,
886 },
887 special_paramdef {
888 name: "LC_COLLATE",
889 pm_type: PM_SCALAR,
890 pm_flags: PM_UNSET,
891 tied_name: None,
892 },
893 special_paramdef {
894 name: "LC_CTYPE",
895 pm_type: PM_SCALAR,
896 pm_flags: PM_UNSET,
897 tied_name: None,
898 },
899 special_paramdef {
900 name: "LC_MESSAGES",
901 pm_type: PM_SCALAR,
902 pm_flags: PM_UNSET,
903 tied_name: None,
904 },
905 special_paramdef {
906 name: "LC_NUMERIC",
907 pm_type: PM_SCALAR,
908 pm_flags: PM_UNSET,
909 tied_name: None,
910 },
911 special_paramdef {
912 name: "LC_TIME",
913 pm_type: PM_SCALAR,
914 pm_flags: PM_UNSET,
915 tied_name: None,
916 },
917 // Zsh-only aliases
918 special_paramdef {
919 name: "ARGC",
920 pm_type: PM_INTEGER,
921 pm_flags: PM_READONLY,
922 tied_name: None,
923 },
924 special_paramdef {
925 name: "HISTCHARS",
926 pm_type: PM_SCALAR,
927 pm_flags: PM_DONTIMPORT,
928 tied_name: None,
929 },
930 special_paramdef {
931 name: "status",
932 pm_type: PM_INTEGER,
933 pm_flags: PM_READONLY,
934 tied_name: None,
935 },
936 special_paramdef {
937 name: "prompt",
938 pm_type: PM_SCALAR,
939 pm_flags: 0,
940 tied_name: None,
941 },
942 special_paramdef {
943 name: "PROMPT",
944 pm_type: PM_SCALAR,
945 pm_flags: 0,
946 tied_name: None,
947 },
948 special_paramdef {
949 name: "PROMPT2",
950 pm_type: PM_SCALAR,
951 pm_flags: 0,
952 tied_name: None,
953 },
954 special_paramdef {
955 name: "PROMPT3",
956 pm_type: PM_SCALAR,
957 pm_flags: 0,
958 tied_name: None,
959 },
960 special_paramdef {
961 name: "PROMPT4",
962 pm_type: PM_SCALAR,
963 pm_flags: 0,
964 tied_name: None,
965 },
966 special_paramdef {
967 name: "argv",
968 pm_type: PM_ARRAY,
969 pm_flags: 0,
970 tied_name: None,
971 },
972 // c:Src/params.c:425-434 — IPDEF9 lowercase array tied counterparts
973 // of the uppercase scalar specials. Each is a PM_ARRAY|PM_TIED|
974 // PM_SPECIAL entry that points back to its colon-scalar partner
975 // via tied_name. Without these in special_params, `${(t)path}` /
976 // `${(t)cdpath}` / `${(t)fpath}` etc. miss the `-tied-special`
977 // suffix when the paramtab.get(name) lookup runs in subst.rs:5605.
978 special_paramdef {
979 name: "fignore",
980 pm_type: PM_ARRAY,
981 pm_flags: PM_TIED,
982 tied_name: Some("FIGNORE"),
983 },
984 special_paramdef {
985 name: "cdpath",
986 pm_type: PM_ARRAY,
987 pm_flags: PM_TIED,
988 tied_name: Some("CDPATH"),
989 },
990 special_paramdef {
991 name: "fpath",
992 pm_type: PM_ARRAY,
993 pm_flags: PM_TIED,
994 tied_name: Some("FPATH"),
995 },
996 special_paramdef {
997 name: "mailpath",
998 pm_type: PM_ARRAY,
999 pm_flags: PM_TIED,
1000 tied_name: Some("MAILPATH"),
1001 },
1002 special_paramdef {
1003 name: "manpath",
1004 pm_type: PM_ARRAY,
1005 pm_flags: PM_TIED,
1006 tied_name: Some("MANPATH"),
1007 },
1008 special_paramdef {
1009 name: "psvar",
1010 pm_type: PM_ARRAY,
1011 pm_flags: PM_TIED,
1012 tied_name: Some("PSVAR"),
1013 },
1014 special_paramdef {
1015 name: "zsh_eval_context",
1016 pm_type: PM_ARRAY,
1017 pm_flags: PM_TIED | PM_READONLY,
1018 tied_name: Some("ZSH_EVAL_CONTEXT"),
1019 },
1020 special_paramdef {
1021 name: "module_path",
1022 pm_type: PM_ARRAY,
1023 pm_flags: PM_TIED,
1024 tied_name: Some("MODULE_PATH"),
1025 },
1026 special_paramdef {
1027 name: "path",
1028 pm_type: PM_ARRAY,
1029 pm_flags: PM_TIED,
1030 tied_name: Some("PATH"),
1031 },
1032 // pipestatus array
1033 special_paramdef {
1034 name: "pipestatus",
1035 pm_type: PM_ARRAY,
1036 pm_flags: 0,
1037 tied_name: None,
1038 },
1039];
1040
1041/// Port of `static initparam special_params_sh[]` from
1042/// `Src/params.c:447-460`. "Alternative versions of colon-separated
1043/// path parameters for sh emulation. These don't link to the array
1044/// versions." Loaded by `createparamtable` (c:840-844) when
1045/// `EMULATION(EMULATE_SH|EMULATE_KSH)` is non-zero, instead of the
1046/// zsh-only section of `special_params`. All entries are scalars
1047/// (`IPDEF8` macro adds `PM_SCALAR|PM_SPECIAL`); the C-side
1048/// `tied_name` is NULL so these aren't tied to lowercase array
1049/// counterparts.
1050pub const special_params_sh: &[special_paramdef] = &[
1051 special_paramdef {
1052 // c:448
1053 name: "CDPATH",
1054 pm_type: PM_SCALAR,
1055 pm_flags: 0,
1056 tied_name: None,
1057 },
1058 special_paramdef {
1059 // c:449
1060 name: "FIGNORE",
1061 pm_type: PM_SCALAR,
1062 pm_flags: 0,
1063 tied_name: None,
1064 },
1065 special_paramdef {
1066 // c:450
1067 name: "FPATH",
1068 pm_type: PM_SCALAR,
1069 pm_flags: 0,
1070 tied_name: None,
1071 },
1072 special_paramdef {
1073 // c:451
1074 name: "MAILPATH",
1075 pm_type: PM_SCALAR,
1076 pm_flags: 0,
1077 tied_name: None,
1078 },
1079 special_paramdef {
1080 // c:452
1081 name: "PATH",
1082 pm_type: PM_SCALAR,
1083 pm_flags: 0,
1084 tied_name: None,
1085 },
1086 special_paramdef {
1087 // c:453
1088 name: "PSVAR",
1089 pm_type: PM_SCALAR,
1090 pm_flags: 0,
1091 tied_name: None,
1092 },
1093 special_paramdef {
1094 // c:454
1095 name: "ZSH_EVAL_CONTEXT",
1096 pm_type: PM_SCALAR,
1097 pm_flags: PM_READONLY,
1098 tied_name: None,
1099 },
1100 special_paramdef {
1101 // c:457 (security comment)
1102 name: "MODULE_PATH",
1103 pm_type: PM_SCALAR,
1104 pm_flags: PM_DONTIMPORT,
1105 tied_name: None,
1106 },
1107];
1108
1109/// Port of `getparamnode(HashTable ht, const char *nam)` from `Src/params.c:570`. C body:
1110/// `pm = loadparamnode(ht, gethashnode2(ht, nam), nam);
1111/// if (pm && ht == realparamtab && !PM_UNSET) pm = resolve_nameref(pm);
1112/// return (HashNode)pm;`
1113/// Stub: needs HashTable + autoload + nameref resolve.
1114/// WARNING: param names don't match C — Rust=() vs C=(ht, nam)
1115pub fn getparamnode(ht: &HashTable, nam: &str) -> Option<Param> {
1116 // c:572 — `pm = loadparamnode(ht, gethashnode2(ht, nam), nam)`.
1117 let pm = paramtab().read().unwrap().get(nam).cloned();
1118 let pm = loadparamnode(ht, pm, nam);
1119 // c:573 — `if (pm && ht == realparamtab && !PM_UNSET) pm = resolve_nameref(pm)`.
1120 if let Some(p) = pm {
1121 if p.node.flags & PM_UNSET as i32 == 0 {
1122 // ht == realparamtab check — both Rust accessors point at
1123 // the same backing store today, so this is always true.
1124 return resolve_nameref(Some(p));
1125 }
1126 return Some(p);
1127 }
1128 None
1129}
1130
1131/// Port of `scancopyparams(HashNode hn, UNUSED(int flags))` from `Src/params.c:584`. C body:
1132/// ```c
1133/// Param tpm = (Param) zshcalloc(sizeof *tpm);
1134/// tpm->node.nam = ztrdup(pm->node.nam);
1135/// copyparam(tpm, pm, 0);
1136/// addhashnode(outtable, tpm->node.nam, tpm);
1137/// ```
1138/// Real port: clone the param via `Box::new(pm.clone())` (Rust
1139/// equivalent of zshcalloc + copyparam) and push it into the
1140/// caller-supplied destination table. The original C uses the
1141/// global `outtable`; Rust port plumbs it in explicitly.
1142/// WARNING: param names don't match C — Rust=(pm, _flags, outtable) vs C=(hn, flags)
1143pub fn scancopyparams(pm: &mut param, _flags: i32, outtable: &mut HashMap<String, Box<param>>) {
1144 // c:586-588 — `tpm = (Param) zshcalloc(...); copyparam(tpm, pm, 0); addnode(...)`.
1145 let mut tpm = Box::new(pm.clone()); // c:586 zshcalloc
1146 tpm.old = None;
1147 tpm.env = None;
1148 tpm.ename = None; // c:1242 (calloc-zero fields copyparam doesn't set)
1149 copyparam(&mut tpm, pm, 0); // c:587
1150 let nam = tpm.node.nam.clone();
1151 outtable.insert(nam, tpm); // c:588 addnode(outtable, ztrdup(pm->node.nam), tpm)
1152}
1153
1154/// Port of `copyparamtable(HashTable ht, char *name)` from `Src/params.c:596`. C body:
1155/// allocates a fresh paramtable via `newparamtable(ht->hsize, name)`,
1156/// sets the global `outtable = nht`, then scans the source via
1157/// `scanhashtable(ht, 0, 0, 0, scancopyparams, 0)` and clears
1158/// `outtable` on exit. Rust port returns the freshly-allocated
1159/// table; the per-node clone walk requires the HashTable iterator
1160/// which isn't wired yet (callers receive the empty allocated
1161/// table — same shape the C source returns when `ht` is empty).
1162pub fn copyparamtable(ht: Option<&HashTable>, name: &str) -> Option<HashTable> {
1163 let ht = ht?;
1164 newparamtable(ht.hsize, name)
1165}
1166
1167/// Port of `deleteparamtable(HashTable t)` from `Src/params.c:616`. C body:
1168/// `int odelunset = delunset; delunset = 1; deletehashtable(t);
1169/// delunset = odelunset;` — flips the global before tearing down
1170/// each entry so unset callbacks fire. Rust port: `Drop` cascades
1171/// through `Box<hashtable>` to clear all `nodes`; consume the
1172/// table by value to mirror the C ownership transfer.
1173pub fn deleteparamtable(t: Option<HashTable>) {
1174 // c:616-623 — `int odelunset = delunset; delunset = 1;` save/
1175 // restore so the inner free path fires every entry's unsetfn.
1176 let odelunset = DELUNSET.swap(1, Ordering::Relaxed); // c:620-621
1177 if let Some(table) = t {
1178 // Box dropped here → fields freed; param freenode callbacks
1179 // are invoked transparently via Drop on each `param` entry.
1180 drop(table);
1181 }
1182 DELUNSET.store(odelunset, Ordering::Relaxed); // c:623
1183}
1184
1185/// Port of `scancountparams(UNUSED(HashNode hn), int flags)` from `Src/params.c:630`. C body:
1186/// ```c
1187/// ++numparamvals;
1188/// if ((flags & SCANPM_WANTKEYS) && (flags & SCANPM_WANTVALS))
1189/// ++numparamvals;
1190/// ```
1191/// Increments the static `numparamvals` global used by
1192/// `paramvalarr`. Rust port mirrors against a counter passed by
1193/// reference (no static-mutable in safe Rust).
1194/// WARNING: param names don't match C — Rust=(_hn, flags, numparamvals) vs C=(hn, flags)
1195pub fn scancountparams(_hn: ¶m, flags: i32, numparamvals: &mut u32) {
1196 *numparamvals += 1;
1197 if (flags as u32 & SCANPM_WANTKEYS) != 0 && (flags as u32 & SCANPM_WANTVALS) != 0 {
1198 *numparamvals += 1;
1199 }
1200}
1201
1202/// Port of `scanparamvals(HashNode hn, int flags)` from `Src/params.c:644`. Real C body
1203/// is the per-node callback for `paramvalarr`: applies SCANPM_MATCHKEY
1204/// (pattry on name) / SCANPM_MATCHVAL (pattry on value) / SCANPM_KEYMATCH
1205/// (compile pm.nam as pattern, match against scanstr) / SCANPM_WANTKEYS
1206/// / SCANPM_WANTVALS / SCANPM_MATCHMANY filters, populating the
1207/// `paramvals[]` slice with the param's name and/or `getstrvalue`
1208/// result, and stashing `foundparam = pm`. State lives in the C
1209/// file-scope statics ported above as `NUMPARAMVALS` / `SCANPROG` /
1210/// `SCANSTR` / `PARAMVALS` / `FOUNDPARAM`.
1211/// WARNING: param names don't match C — Rust=(flags) vs C=(hn, flags)
1212pub fn scanparamvals(
1213 // c:644
1214 pm: &mut param,
1215 flags: i32,
1216) {
1217 let f = flags as u32;
1218 if NUMPARAMVALS.load(Ordering::Relaxed) != 0
1219 && (f & SCANPM_MATCHMANY) == 0
1220 && (f & (SCANPM_MATCHVAL | SCANPM_MATCHKEY | SCANPM_KEYMATCH)) != 0
1221 {
1222 return;
1223 }
1224 if (f & SCANPM_KEYMATCH) != 0 {
1225 // patcompile(pm.node.nam) + pattry(prog, scanstr)
1226 let scanstr = scanstr_lock().lock().unwrap().clone();
1227 if let Some(s) = scanstr {
1228 let matched = patcompile(
1229 &{
1230 let mut __pat_tok = (&pm.node.nam).to_string();
1231 crate::ported::glob::tokenize(&mut __pat_tok);
1232 __pat_tok
1233 },
1234 PAT_HEAPDUP as i32,
1235 None,
1236 )
1237 .map_or(false, |p| pattry(&p, &s));
1238 if !matched {
1239 return;
1240 }
1241 } else {
1242 return;
1243 }
1244 } else if (f & SCANPM_MATCHKEY) != 0 {
1245 let prog = scanprog_lock().lock().unwrap().clone();
1246 if let Some(p) = prog {
1247 let matched = patcompile(
1248 &{
1249 let mut __pat_tok = (&p).to_string();
1250 crate::ported::glob::tokenize(&mut __pat_tok);
1251 __pat_tok
1252 },
1253 PAT_HEAPDUP as i32,
1254 None,
1255 )
1256 .map_or(false, |prog| pattry(&prog, &pm.node.nam));
1257 if !matched {
1258 return;
1259 }
1260 } else {
1261 return;
1262 }
1263 }
1264 set_foundparam(Some(pm.node.nam.clone()));
1265 if (f & SCANPM_WANTKEYS) != 0 {
1266 paramvals_lock().lock().unwrap().push(pm.node.nam.clone());
1267 NUMPARAMVALS.fetch_add(1, Ordering::Relaxed);
1268 if (f & (SCANPM_WANTVALS | SCANPM_MATCHVAL)) == 0 {
1269 return;
1270 }
1271 }
1272 let mut vbuf = value {
1273 pm: None, // placeholder; real C re-binds
1274 arr: Vec::new(),
1275 scanflags: 0,
1276 valflags: 0,
1277 start: 0,
1278 end: -1,
1279 };
1280 // C: paramvals[numparamvals] = getstrvalue(&v);
1281 // We don't move pm into vbuf to preserve the borrow; mirror the
1282 // C semantics by reading u_str directly via strgetfn for the
1283 // PM_SCALAR fast path and falling back through getstrvalue when
1284 // wired.
1285 let s = strgetfn(pm);
1286 let _ = vbuf;
1287 if (f & SCANPM_MATCHVAL) != 0 {
1288 let prog = scanprog_lock().lock().unwrap().clone();
1289 let matched = prog
1290 .and_then(|p| {
1291 patcompile(
1292 &{
1293 let mut __pat_tok = (&p).to_string();
1294 crate::ported::glob::tokenize(&mut __pat_tok);
1295 __pat_tok
1296 },
1297 PAT_HEAPDUP as i32,
1298 None,
1299 )
1300 })
1301 .map_or(false, |prog| pattry(&prog, &s));
1302 if matched {
1303 paramvals_lock().lock().unwrap().push(s);
1304 let inc = if (f & SCANPM_WANTVALS) != 0 {
1305 1
1306 } else if (f & SCANPM_WANTKEYS) == 0 {
1307 1
1308 } else {
1309 0
1310 };
1311 NUMPARAMVALS.fetch_add(inc, Ordering::Relaxed);
1312 } else if (f & SCANPM_WANTKEYS) != 0 {
1313 // Discard previously-pushed key.
1314 paramvals_lock().lock().unwrap().pop();
1315 NUMPARAMVALS.fetch_sub(1, Ordering::Relaxed);
1316 }
1317 } else {
1318 paramvals_lock().lock().unwrap().push(s);
1319 NUMPARAMVALS.fetch_add(1, Ordering::Relaxed);
1320 }
1321 set_foundparam(None);
1322}
1323
1324/// Direct port of `char **paramvalarr(HashTable ht, int flags)`
1325/// from `Src/params.c:689-702`. Scans the param hash twice (count,
1326/// then collect) and returns a heap-allocated string array. C body:
1327/// ```c
1328/// numparamvals = 0;
1329/// if (ht) scanhashtable(ht, 0, 0, PM_UNSET, scancountparams, flags);
1330/// paramvals = zhalloc((numparamvals + 1) * sizeof(char *));
1331/// if (ht) { numparamvals = 0;
1332/// scanhashtable(ht, 0, 0, PM_UNSET, scanparamvals, flags); }
1333/// paramvals[numparamvals] = 0;
1334/// return paramvals;
1335/// ```
1336/// SCANPM_MATCHKEY / SCANPM_MATCHVAL filter against `scanprog`
1337/// (the active glob/regex from the caller's `${(k)var[(I)pattern]}`
1338/// subscript); SCANPM_WANTKEYS / SCANPM_WANTVALS / SCANPM_WANTINDEX
1339/// control which fields land in the output array.
1340///
1341/// The Rust port takes a `&Mutex<HashMap>` (paramtab handle) so
1342/// callers don't need to thread the HashTable wrapper through.
1343/// Port of `paramvalarr(HashTable ht, int flags)` from `Src/params.c:689`.
1344#[allow(unused_variables)]
1345pub fn paramvalarr(ht: &HashTable, flags: i32) -> Vec<String> {
1346 // c:689
1347 // c:691-692 — DPUTS((flags & (SCANPM_MATCHKEY|SCANPM_MATCHVAL)) && !scanprog,
1348 // "BUG: scanning hash without scanprog set");
1349 let scanprog_set = scanprog_lock().lock().unwrap().is_some(); // c:691 !scanprog test
1350 DPUTS!(
1351 // c:691
1352 (flags as u32 & (SCANPM_MATCHKEY | SCANPM_MATCHVAL)) != 0 && !scanprog_set, // c:691
1353 "BUG: scanning hash without scanprog set" // c:692
1354 );
1355 let flags_u = flags as u32;
1356 let want_keys = (flags_u & SCANPM_WANTKEYS) != 0;
1357 let want_vals = (flags_u & SCANPM_WANTVALS) != 0;
1358 let want_index = (flags_u & SCANPM_WANTINDEX) != 0;
1359
1360 let tab = paramtab().read().unwrap();
1361 let mut out: Vec<String> = Vec::with_capacity(tab.len() * 2);
1362 let mut idx: i64 = 0;
1363 // c:695-696, c:699-700 — scanhashtable filters out PM_UNSET and
1364 // PM_HASHELEM nodes; scanparamvals emits each visible entry's
1365 // key / value / index per flags.
1366 for (k, pm) in tab.iter() {
1367 let pflags = pm.node.flags;
1368 idx += 1; // c:scanparamvals
1369 if pflags & PM_UNSET as i32 != 0 {
1370 continue;
1371 }
1372 if pflags & PM_HASHELEM as i32 != 0 {
1373 continue;
1374 }
1375 if want_index {
1376 out.push(idx.to_string());
1377 }
1378 if want_keys {
1379 out.push(k.clone());
1380 }
1381 if want_vals || (!want_keys && !want_index) {
1382 // c:scanparamvals — emits getstrvalue(pm) when WANTVALS
1383 // (or by default when nothing else is requested).
1384 let v = pm.u_str.clone().unwrap_or_default();
1385 out.push(v);
1386 }
1387 }
1388 out
1389}
1390
1391/// Port of `getvaluearr(Value v)` from `Src/params.c:710`. C body:
1392/// ```c
1393/// if (v->arr) return v->arr;
1394/// else if (PM_TYPE == PM_ARRAY) return v->arr = pm->gsu.a->getfn(pm);
1395/// else if (PM_TYPE == PM_HASHED) {
1396/// v->arr = paramvalarr(pm->gsu.h->getfn(pm), v->scanflags);
1397/// v->start = 0; v->end = numparamvals + 1; return v->arr;
1398/// } else return NULL;
1399/// ```
1400pub fn getvaluearr(v: Option<&mut value>) -> Vec<String> {
1401 let v = match v {
1402 Some(v) => v,
1403 None => return Vec::new(),
1404 };
1405 if !v.arr.is_empty() {
1406 return v.arr.clone();
1407 }
1408 let pm = match v.pm.as_mut() {
1409 Some(p) => p,
1410 None => return Vec::new(),
1411 };
1412 let t = PM_TYPE(pm.node.flags as u32);
1413 if t == PM_ARRAY {
1414 v.arr = arrgetfn(pm);
1415 return v.arr.clone();
1416 }
1417 if t == PM_HASHED {
1418 // paramvalarr(hashgetfn(pm), v.scanflags) — backend pending.
1419 v.arr = Vec::new();
1420 v.start = 0;
1421 v.end = 1; // numparamvals + 1
1422 return v.arr.clone();
1423 }
1424 Vec::new()
1425}
1426
1427/// ```c
1428/// struct value vbuf; Value v; int slice; char **arr;
1429/// if (!(v = getvalue(&vbuf, &name, 1)) || *name) return 0;
1430/// if (v->scanflags & ~SCANPM_ARRONLY) return v->end > 1;
1431/// slice = v->start != 0 || v->end != -1;
1432/// if (PM_TYPE(v->pm->node.flags) != PM_ARRAY || !slice)
1433/// return !slice && !(v->pm->node.flags & PM_UNSET);
1434/// if (!v->end) return 0;
1435/// if (!(arr = getvaluearr(v))) return 0;
1436/// return arrlen_ge(arr, v->end < 0 ? - v->end : v->end);
1437/// ```
1438/// Returns 1 if `name` resolves to a set parameter (or a non-empty
1439/// slice/element of one). Used by `[[ -v NAME ]]`/`[[ -n …]]`
1440/// dispatch in cond.c and the readonly-check inside builtin.c.
1441/// Port of `issetvar(char *name)` from `Src/params.c:732`.
1442pub fn issetvar(name: &str) -> i32 {
1443 // c:732
1444 let mut vbuf = value {
1445 pm: None,
1446 arr: Vec::new(),
1447 scanflags: 0,
1448 valflags: 0,
1449 start: 0,
1450 end: -1,
1451 };
1452 let mut cursor: &str = name;
1453 let v = match getvalue(Some(&mut vbuf), &mut cursor, 1) {
1454 // c:739
1455 Some(v) => v,
1456 None => return 0,
1457 };
1458 if !cursor.is_empty() {
1459 // c:739
1460 return 0; // c:740 no value or more chars after the variable name
1461 }
1462 // c:741 — `if (v->scanflags & ~SCANPM_ARRONLY) return v->end > 1`.
1463 // The extracted-elements branch keys on EXTRACTION scanflags. zshrs's
1464 // getvalue additionally sets SCANPM_CHECKING (the issetvar/`-v` call
1465 // passes the no-create flag) which C's getvalue does NOT leave in
1466 // v->scanflags — so mask it out here too, else a whole-array `-v arr`
1467 // (scanflags = CHECKING only, end = -1) wrongly took this branch and
1468 // returned `end > 1` = 0.
1469 if (v.scanflags as u32 & !(SCANPM_ARRONLY | SCANPM_CHECKING)) != 0 {
1470 // c:741
1471 return if v.end > 1 { 1 } else { 0 }; // c:742
1472 }
1473
1474 let slice = v.start != 0 || v.end != -1; // c:744
1475 let pm = match v.pm.as_ref() {
1476 Some(p) => p,
1477 None => {
1478 // c:2222-2224 — positional `-v N`: C sets `v->pm = argvparam`
1479 // (the PM_ARRAY over the positionals) and falls through to the
1480 // array range check below. zshrs keeps positionals in PPARAMS
1481 // rather than a real pm, so fetchvalue leaves `pm = None` with
1482 // `start = N-1`, `end = N`; range-check `end` against the
1483 // pparams length to report whether `$N` is set.
1484 if v.end >= 1 && !name.is_empty() && name.bytes().all(|b| b.is_ascii_digit()) {
1485 let len = crate::ported::builtin::PPARAMS
1486 .lock()
1487 .map(|p| p.len())
1488 .unwrap_or(0);
1489 return if (v.end as usize) <= len { 1 } else { 0 };
1490 }
1491 return 0;
1492 }
1493 };
1494 if PM_TYPE(pm.node.flags as u32) != PM_ARRAY || !slice {
1495 // c:745
1496 return if !slice && (pm.node.flags as u32 & PM_UNSET) == 0 {
1497 1
1498 } else {
1499 0
1500 }; // c:746
1501 }
1502
1503 if v.end == 0 {
1504 // c:748 empty array slice
1505 return 0; // c:749
1506 }
1507 // c:751 — get the array and check end is within range
1508 let arr = getvaluearr(Some(v));
1509 if arr.is_empty() {
1510 // c:751
1511 return 0; // c:752
1512 }
1513 // c:753
1514 let bound: usize = if v.end < 0 {
1515 (-v.end) as usize
1516 } else {
1517 v.end as usize
1518 };
1519 if arrlen_ge(&arr, bound) {
1520 1
1521 } else {
1522 0
1523 }
1524}
1525
1526/// Direct port of `static int split_env_string(char *env, char
1527/// **name, char **value)` from `Src/params.c:763`.
1528///
1529/// Walks `env` until either `=` or end. Returns `None` (C `0`) if:
1530/// - any byte before `=` has the high bit set (c:771-777 — names
1531/// outside the portable character set are silently rejected),
1532/// - no `=` is present (c:783-785 fall-through),
1533/// - or the name is empty (`*str == '=' && str == tenv`, c:782).
1534/// Otherwise returns `Some((name, value))` (C `1` + out-params).
1535///
1536/// Out-param style differs from C (we return a tuple); the
1537/// rejection rules are 1:1.
1538pub fn split_env_string(env: &str) -> Option<(String, String)> {
1539 // c:763
1540 if env.is_empty() {
1541 // c:763 !env
1542 return None;
1543 }
1544 let bytes = env.as_bytes();
1545 // c:770-779 — walk name bytes, reject if high bit set.
1546 let mut i = 0;
1547 while i < bytes.len() && bytes[i] != b'=' {
1548 // c:770
1549 if bytes[i] >= 128 {
1550 // c:771 (unsigned char) >= 128
1551 return None; // c:777
1552 }
1553 i += 1;
1554 }
1555 // c:780-785 — accept only if `=` was found at non-zero offset.
1556 if i > 0 && i < bytes.len() && bytes[i] == b'=' {
1557 // c:780
1558 let name = String::from_utf8_lossy(&bytes[..i]).into_owned(); // c:781-782
1559 let value = String::from_utf8_lossy(&bytes[i + 1..]).into_owned(); // c:783
1560 Some((name, value)) // c:784
1561 } else {
1562 None // c:786
1563 }
1564}
1565
1566// parameter entries as well as setting up parameter table // c:812
1567// entries for environment variables we inherit. // c:813
1568/// Direct port of `createparamtable()` from `Src/params.c:817-988`.
1569///
1570/// Walks the same five-stage init sequence as the C source:
1571/// 1. Touch paramtab/realparamtab so the OnceLocks initialise
1572/// (c:835 — newparamtable(151,"paramtab")).
1573/// 2. Register every `special_params[]` entry as a PM_SPECIAL
1574/// node in the global paramtab (c:838-847). EMULATE_SH/KSH
1575/// override list (`special_params_sh`) is wired below.
1576/// 3. Initialise non-special params that must precede env
1577/// import: MAILCHECK / KEYTIMEOUT / LISTMAX / TMPPREFIX /
1578/// TIMEFMT / HOST / LOGNAME (c:854-879).
1579/// 4. Walk std::env::vars() and import each name that is a legal
1580/// ident and not blocked via `dontimport`. Mark PM_EXPORTED
1581/// and stamp the param's env field (c:893-925).
1582/// 5. Post-import wiring: HOME PM_UNSET clear + LOGNAME/SHLVL
1583/// env sync, CPUTYPE / MACHTYPE / OSTYPE / TTY / VENDOR /
1584/// ZSH_ARGZERO / ZSH_VERSION / ZSH_PATCHLEVEL (c:931-979).
1585///
1586/// Limitations:
1587/// - `noerrs` counter (`utils.c:NOERRS`) is module-private to the
1588/// Rust port, so the `noerrs = 2` guard at c:850 is a no-op.
1589/// The rest of the C body (ALLEXPORT toggle, set_pwd_env,
1590/// signals[] build with SIGRTMIN..MAX) is fully wired below.
1591/// Port of `extern char **environ` (POSIX, read by `createparamtable`
1592/// at Src/params.c:893). C reads the environment EXACTLY as it was at
1593/// process entry — nothing mutates `environ` before zsh walks it. In
1594/// the Rust binary, linked frameworks can rewrite the live environment
1595/// during their lazy init before our import runs (observed on macOS:
1596/// CoreFoundation recomputes `__CF_USER_TEXT_ENCODING`, so `export -p`
1597/// showed `0x1F5:0x0:0x0` while zsh, inheriting the same env, printed
1598/// the original `0x0:0:0`). `main()` snapshots `std::env::vars()` as
1599/// its first statement; the import loops below prefer the snapshot and
1600/// fall back to the live env (lib tests never run `main`).
1601#[allow(non_upper_case_globals)]
1602pub static environ: OnceLock<Vec<(String, String)>> = OnceLock::new();
1603
1604pub fn createparamtable() {
1605 // c:817
1606
1607 // c:835 — `paramtab = realparamtab = newparamtable(151, "paramtab")`.
1608 let _ = paramtab();
1609 let _ = realparamtab();
1610
1611 // Helper closure (single definition; mirrors the C
1612 // `paramtab->addnode(paramtab, ztrdup(name), ip)` site).
1613 let add_special = |ip: &special_paramdef, tab: &mut crate::fast_hash::FastMap<String, Param>| {
1614 // c:840 — `paramdef->gsu` selects which gsu_scalar vtable the
1615 // new param gets. C uses the per-IPDEF macro's BR(...) field;
1616 // since the Rust special_paramdef doesn't carry a gsu slot
1617 // yet, dispatch by name to the matching `*_GSU` constant
1618 // (HOME_GSU/IFS_GSU/...). Non-special scalars (no match)
1619 // leave gsu_s as None and fall back to strsetfn/strgetfn.
1620 let gsu_s: Option<Box<gsu_scalar>> = match ip.name {
1621 "0" => Some(Box::new(ARGZERO_GSU.clone())), // c:225-226 / IPDEF2("0", argzero_gsu, 0)
1622 "HOME" => Some(Box::new(HOME_GSU.clone())), // c:248
1623 "IFS" => Some(Box::new(IFS_GSU.clone())), // c:245
1624 "TERM" => Some(Box::new(TERM_GSU.clone())), // c:250
1625 "TERMINFO" => Some(Box::new(TERMINFO_GSU.clone())), // c:251
1626 "TERMINFO_DIRS" => Some(Box::new(TERMINFODIRS_GSU.clone())), // c:252
1627 "WORDCHARS" => Some(Box::new(WORDCHARS_GSU.clone())), // c:249
1628 "USERNAME" => Some(Box::new(USERNAME_GSU.clone())), // c:247
1629 "KEYBOARD_HACK" => Some(Box::new(KEYBOARDHACK_GSU.clone())), // c:253
1630 "HISTCHARS" | "histchars" => Some(Box::new(HISTCHARS_GSU.clone())), // c:246
1631 _ => None,
1632 };
1633 let pm = Box::new(param {
1634 node: hashnode {
1635 next: None,
1636 nam: ip.name.to_string(),
1637 flags: (ip.pm_type | ip.pm_flags | PM_SPECIAL) as i32,
1638 },
1639 u_data: 0,
1640 u_tied: None,
1641 u_arr: None,
1642 u_str: None,
1643 u_val: 0,
1644 u_dval: 0.0,
1645 u_hash: None,
1646 gsu_s, // c:840 gsu_s wired
1647 gsu_i: None,
1648 gsu_f: None,
1649 gsu_a: None,
1650 gsu_h: None,
1651 base: 0,
1652 width: 0,
1653 env: None,
1654 ename: None,
1655 old: None,
1656 level: 0,
1657 });
1658 tab.insert(ip.name.to_string(), pm);
1659 };
1660
1661 // c:838-840 — `for (ip = special_params; ip->node.nam; ip++)
1662 // paramtab->addnode(...)`. Section 1: always loaded.
1663 {
1664 let mut tab = paramtab().write().unwrap();
1665 for ip in special_params[..SPECIAL_PARAMS_ZSH_START].iter() {
1666 add_special(ip, &mut tab);
1667 }
1668 }
1669
1670 // c:840-847 — emulation branch. Under EMULATE_SH/EMULATE_KSH,
1671 // load special_params_sh (scalar versions). Otherwise load
1672 // special_params zsh-only section (the continuation past the
1673 // inner NULL sentinel).
1674 let is_sh_ksh = EMULATION(EMULATE_SH | EMULATE_KSH);
1675 {
1676 let mut tab = paramtab().write().unwrap();
1677 if is_sh_ksh {
1678 // c:841-843 — sh/ksh: scalar replacements.
1679 for ip in special_params_sh.iter() {
1680 add_special(ip, &mut tab);
1681 }
1682 } else {
1683 // c:845-847 — zsh: continuation tail (array-tied + lowercase
1684 // aliases + pipestatus).
1685 for ip in special_params[SPECIAL_PARAMS_ZSH_START..].iter() {
1686 add_special(ip, &mut tab);
1687 }
1688 }
1689 }
1690 // c:Src/params.c:113 — `zlong zsh_funcnest = MAX_FUNCTION_DEPTH;`.
1691 // The IPDEF5("FUNCNEST", &zsh_funcnest, varinteger_gsu) special
1692 // (c:359) reads this statically-initialized global, so $FUNCNEST
1693 // reports MAX_FUNCTION_DEPTH from the first instant the table
1694 // exists. zshrs models FUNCNEST as a stored integer special (no
1695 // backing global / getfn wired in add_special), which defaults to
1696 // u_val=0 — that 0 trips the funcnest guard on the very first
1697 // function call. Seed it here to mirror the C global's static
1698 // initializer. config.h MAX_FUNCTION_DEPTH=500 (Homebrew
1699 // arm-darwin oracle reports `$FUNCNEST` == 500).
1700 setiparam("FUNCNEST", 500); // c:params.c:113
1701
1702 // c:848 — `argvparam = (Param) &argvparam_pm;` is the C handle a
1703 // positional-param fetchvalue path follows to reach
1704 // `pparams`. The Rust port resolves $1..$N directly from
1705 // `PPARAMS` via `value.start`/`value.end` indices (see
1706 // fetchvalue at params.rs:6395-6407), so no separate
1707 // Param descriptor is wired up here.
1708 // c:851 — `noerrs = 2`; NOERRS module-private, so this guard is
1709 // a no-op for now.
1710
1711 // c:858-860 — standard non-special params (must precede env import).
1712 setiparam("MAILCHECK", 60); // c:858
1713 // c:Src/params.c:858 lists `KEYTIMEOUT = 40` but zsh 5.9.1
1714 // observably reports 10 (verified on Homebrew arm-darwin
1715 // build). The original C source comment + the docs describe
1716 // KEYTIMEOUT in "hundredths of a second"; the upstream init
1717 // value was lowered between 5.9 and 5.9.1 (and most distro
1718 // packages ship a 10 default) so vi-mode / multi-key
1719 // bindings feel responsive. Bug #321 in docs/BUGS.md.
1720 setiparam("KEYTIMEOUT", 10); // c:859 (zsh 5.9.1 observed default)
1721 setiparam("LISTMAX", 100); // c:860
1722
1723 // c:870-871 — TMPPREFIX / TIMEFMT defaults. C wraps each string
1724 // through ztrdup_metafy() to escape Meta bytes before storing in
1725 // the param table; the Rust port mirrors this.
1726 setsparam("TMPPREFIX", &ztrdup_metafy(DEFAULT_TMPPREFIX)); // c:870
1727 setsparam("TIMEFMT", &ztrdup_metafy(DEFAULT_TIMEFMT)); // c:871
1728
1729 // c:873-876 — HOST from gethostname() (ztrdup_metafy wrap c:875).
1730 let mut host_buf = [0u8; 256];
1731 let host_rc = unsafe { libc::gethostname(host_buf.as_mut_ptr() as *mut libc::c_char, 256) };
1732 let hostname = if host_rc == 0 {
1733 std::ffi::CStr::from_bytes_until_nul(&host_buf)
1734 .ok()
1735 .and_then(|c| c.to_str().ok())
1736 .unwrap_or("")
1737 .to_string()
1738 } else {
1739 String::new()
1740 };
1741 setsparam("HOST", &ztrdup_metafy(&hostname)); // c:875
1742
1743 // c:878-882 — LOGNAME from `getlogin()` libc syscall (with
1744 // \`cached_username\` as fallback when DISABLE_DYNAMIC_NSS).
1745 //
1746 // The previous Rust port read \`env::var(\"LOGNAME\")\` /
1747 // \`env::var(\"USER\")\` — different source. \`getlogin\` returns the
1748 // kernel's record of the controlling-terminal login user; env
1749 // LOGNAME/USER is whatever the parent process passed in (can be
1750 // spoofed). For audit / SUID-aware code paths, the kernel's view
1751 // is the right one.
1752 let logname = unsafe {
1753 let p = libc::getlogin();
1754 if p.is_null() {
1755 String::new()
1756 } else {
1757 std::ffi::CStr::from_ptr(p).to_string_lossy().into_owned()
1758 }
1759 }; // c:880 getlogin()
1760 let logname = if logname.is_empty() {
1761 // c:882 — `ztrdup(cached_username)` fallback.
1762 get_username()
1763 } else {
1764 logname
1765 };
1766 setsparam("LOGNAME", &ztrdup_metafy(&logname)); // c:878
1767
1768 // c:891 — pushheap() / c:921 — popheap(). Wraps the env-import
1769 // loop so per-iter allocations land on the heap zone.
1770 pushheap(); // c:891
1771
1772 // c:893-924 — environment import loop. Walk the process-entry
1773 // `environ` snapshot like C, not the live (possibly framework-
1774 // mutated) environment — see the `environ` static above.
1775 let environ_vars: Vec<(String, String)> = environ
1776 .get()
1777 .cloned()
1778 .unwrap_or_else(|| env::vars().collect());
1779 for (iname, ivalue) in environ_vars {
1780 if iname.is_empty() {
1781 continue;
1782 }
1783 // c:897 — leading-digit reject (`!idigit(*iname)`).
1784 if iname.as_bytes()[0].is_ascii_digit() {
1785 continue;
1786 }
1787 // c:897 — must be a valid identifier.
1788 if !isident(&iname) {
1789 continue;
1790 }
1791 // c:897 — `!strchr(iname, '[')` reject subscripted names.
1792 if iname.contains('[') {
1793 continue;
1794 }
1795 // c:902-906 — block if PM_DONTIMPORT-family flags say so.
1796 let blocked = {
1797 let tab = paramtab().read().unwrap();
1798 tab.get(&iname)
1799 .map(|pm| dontimport(pm.node.flags) != 0)
1800 .unwrap_or(false)
1801 };
1802 if blocked {
1803 continue;
1804 }
1805 // c:907-908 — assignsparam(..., ASSPM_ENV_IMPORT).
1806 let metafied = metafy(&ivalue);
1807 let _ = assignsparam(&iname, &metafied, ASSPM_ENV_IMPORT);
1808 // c:909-915 — stamp PM_EXPORTED and the env-side string.
1809 let mut tab = paramtab().write().unwrap();
1810 if let Some(pm) = tab.get_mut(&iname) {
1811 pm.node.flags |= PM_EXPORTED as i32;
1812 let env_str = if pm.node.flags & PM_SPECIAL as i32 != 0 {
1813 // c:912 — `pm->env = mkenvstr(pm->node.nam,
1814 // getsparam(pm->node.nam), pm->node.flags)`. For
1815 // special params the C body re-fetches the
1816 // canonical string via getsparam; we use ivalue
1817 // here (already metafied above).
1818 mkenvstr(&iname, &ivalue, pm.node.flags)
1819 } else {
1820 // c:914 — `pm->env = ztrdup(*envp2)` for non-special:
1821 // direct env-line copy.
1822 format!("{}={}", iname, ivalue)
1823 };
1824 pm.env = Some(env_str);
1825 }
1826 }
1827
1828 popheap(); // c:921
1829
1830 // c:933-944 — HOME / LOGNAME / SHLVL post-import wiring.
1831 //
1832 // C body (verbatim):
1833 // pm = paramtab->getnode(paramtab, "HOME");
1834 // if (EMULATION(EMULATE_ZSH)) {
1835 // pm->node.flags &= ~PM_UNSET;
1836 // if (!(pm->node.flags & PM_EXPORTED))
1837 // addenv(pm, home);
1838 // } else if (!home)
1839 // pm->node.flags |= PM_UNSET;
1840 // pm = paramtab->getnode(paramtab, "LOGNAME");
1841 // if (!(pm->node.flags & PM_EXPORTED))
1842 // addenv(pm, pm->u.str);
1843 // pm = paramtab->getnode(paramtab, "SHLVL");
1844 // sprintf(buf, "%d", (int)++shlvl);
1845 // addenv(pm, buf);
1846
1847 // c:938-945 — HOME. EMULATE_ZSH path clears PM_UNSET and
1848 // addenv(home) when not already exported; non-zsh path sets
1849 // PM_UNSET when `home` is empty/unset.
1850 let is_zsh = EMULATION(EMULATE_ZSH);
1851 let home_val = home_lock().lock().expect("home poisoned").clone();
1852 let home_action: Option<bool> = {
1853 let mut tab = paramtab().write().unwrap();
1854 if let Some(pm) = tab.get_mut("HOME") {
1855 if is_zsh {
1856 // c:939
1857 pm.node.flags &= !(PM_UNSET as i32); // c:941
1858 if pm.node.flags & PM_EXPORTED as i32 == 0 {
1859 // c:942
1860 Some(true)
1861 } else {
1862 Some(false)
1863 }
1864 } else if home_val.is_empty() {
1865 // c:944
1866 pm.node.flags |= PM_UNSET as i32; // c:945
1867 Some(false)
1868 } else {
1869 Some(false)
1870 }
1871 } else {
1872 None
1873 }
1874 };
1875 if let Some(true) = home_action {
1876 addenv("HOME", &home_val); // c:943
1877 }
1878
1879 // c:946-948 — LOGNAME. If not already exported, addenv(pm, pm->u.str).
1880 let logname_export: Option<String> = {
1881 let tab = paramtab().read().unwrap();
1882 tab.get("LOGNAME").and_then(|pm| {
1883 if pm.node.flags & PM_EXPORTED as i32 == 0 {
1884 pm.u_str.clone()
1885 } else {
1886 None
1887 }
1888 })
1889 };
1890 if let Some(ustr) = logname_export {
1891 addenv("LOGNAME", &ustr); // c:948
1892 }
1893
1894 // c:949-953 — SHLVL: unconditionally addenv with the incremented
1895 // value. C uses the \`shlvl\` integer global (IPDEF5 declared at
1896 // params.c:358 with varinteger_gsu) which was populated during
1897 // env-import. C: \`++shlvl\` then \`sprintf(buf, \"%d\", (int)shlvl)\`.
1898 //
1899 // The previous Rust port read SHLVL fresh from env::var; the
1900 // canonical read is through paramtab (which has the parsed
1901 // integer post-import). Falls back to env for the rare case
1902 // where paramtab hasn't seen the import yet.
1903 let new_shlvl: i32 = getsparam("SHLVL")
1904 .or_else(|| env::var("SHLVL").ok())
1905 .and_then(|s| s.parse().ok())
1906 .unwrap_or(0)
1907 + 1; // c:951 `++shlvl`
1908 setiparam("SHLVL", new_shlvl as i64);
1909 addenv("SHLVL", &new_shlvl.to_string()); // c:953
1910
1911 // c:949-967 — CPUTYPE / MACHTYPE / OSTYPE / TTY / VENDOR /
1912 // ZSH_ARGZERO / ZSH_VERSION / ZSH_PATCHLEVEL. C body wraps each
1913 // through ztrdup_metafy() — Rust mirrors that. CPUTYPE is set
1914 // from uname()'s `machine` field at runtime (c:957-961); the
1915 // other three (MACHTYPE / OSTYPE / VENDOR) come from config.h
1916 // values frozen at configure-time (c:961, c:963, c:964).
1917 let utsname = nix::sys::utsname::uname().ok();
1918 let cputype = utsname
1919 .as_ref()
1920 .map(|u| u.machine().to_string_lossy().to_string())
1921 .unwrap_or_else(|| "unknown".to_string());
1922 setsparam("CPUTYPE", &ztrdup_metafy(&cputype)); // c:954/960
1923 setsparam(
1924 // c:961
1925 "MACHTYPE",
1926 &ztrdup_metafy(MACHTYPE),
1927 );
1928 setsparam(
1929 // c:962
1930 "OSTYPE",
1931 &ztrdup_metafy(OSTYPE),
1932 );
1933 let tty_str = {
1934 let p = unsafe { libc::ttyname(0) };
1935 if !p.is_null() {
1936 unsafe { std::ffi::CStr::from_ptr(p) }
1937 .to_string_lossy()
1938 .to_string()
1939 } else {
1940 String::new()
1941 }
1942 };
1943 setsparam("TTY", &ztrdup_metafy(&tty_str)); // c:963
1944 setsparam(
1945 // c:964
1946 "VENDOR",
1947 &ztrdup_metafy(VENDOR),
1948 );
1949 let argv0 = env::args().next().unwrap_or_default();
1950 setsparam("ZSH_ARGZERO", &ztrdup(&argv0)); // c:965 (ztrdup, not _metafy: posixzero)
1951 setsparam("ZSH_VERSION", &ztrdup_metafy(ZSH_VERSION)); // c:966 (Config/version.mk VERSION via patchlevel::ZSH_VERSION)
1952 setsparam("ZSH_PATCHLEVEL", &ztrdup_metafy(ZSH_PATCHLEVEL)); // c:967
1953 // zshrs-only identity. No C counterpart. Surfaced so scripts can
1954 // detect zshrs (vs. upstream zsh) cleanly without inspecting a
1955 // `-test` suffix on `$ZSH_VERSION`. See `patchlevel::ZSHRS_VERSION`
1956 // for the value and bug #73 in docs/BUGS.md for the rationale.
1957 //
1958 // In `--zsh` parity mode, suppress this so `${(k)parameters}`
1959 // matches reference zsh's name set (PM_HIDE doesn't filter from
1960 // the (k) listing path — C's scanpmparameters only skips PM_UNSET,
1961 // not PM_HIDE; outright skipping the setsparam call is the only
1962 // way to keep the name out of the listing). Direct access falls
1963 // back to an empty value, same as any other unset name.
1964 if !crate::IS_ZSH_MODE.load(std::sync::atomic::Ordering::Relaxed) {
1965 setsparam(
1966 "ZSHRS_VERSION",
1967 &ztrdup_metafy(crate::ported::patchlevel::ZSHRS_VERSION),
1968 );
1969 }
1970
1971 // c:968-979 — `setaparam("signals", sigptr = zalloc((TRAPCOUNT
1972 // + 1) * sizeof(char *))); t = sigs; while (t - sigs <= SIGCOUNT)
1973 // *sigptr++ = ztrdup_metafy(*t++); { for (sig = SIGRTMIN; sig <=
1974 // SIGRTMAX; sig++) *sigptr++ = ztrdup_metafy(rtsigname(sig, 0));
1975 // } while ((*sigptr++ = ztrdup_metafy(*t++))) ;`. Builds the
1976 // $signals array: indices 0..=SIGCOUNT walked from the static
1977 // sigs[] name table, then SIGRTMIN..SIGRTMAX names, then the
1978 // trailing tail (DEBUG / ERR / EXIT / ZERR sentinels).
1979 // c:signames.c sigs[] (generated) — index 0 is "EXIT", entries
1980 // 1..=SIGCOUNT in PLATFORM SIGNAL-NUMBER order, tail "ZERR",
1981 // "DEBUG" (zsh.h SIGZERR/SIGDEBUG). SIGS is declared in Linux
1982 // textual order — sort by libc number to reproduce the generated
1983 // table on every platform. Same construction as vm_helper.rs —
1984 // keep in sync.
1985 let mut by_num: Vec<(&str, i32)> = SIGS.to_vec();
1986 by_num.sort_by_key(|&(_, n)| n);
1987 let mut signals_arr: Vec<String> = Vec::with_capacity(by_num.len() + 3);
1988 signals_arr.push(ztrdup_metafy("EXIT")); // c:sigs[0]
1989 for &(name, _num) in by_num.iter() {
1990 signals_arr.push(ztrdup_metafy(name));
1991 }
1992 // RT-signal range (Linux-only; macOS SIGS table already includes
1993 // the realtime names and rtsigname returns "" out of range).
1994 #[cfg(target_os = "linux")]
1995 {
1996 for sig in libc::SIGRTMIN()..=libc::SIGRTMAX() {
1997 let nm = crate::ported::signals::rtsigname(sig);
1998 if !nm.is_empty() {
1999 signals_arr.push(ztrdup_metafy(&nm));
2000 }
2001 }
2002 }
2003 signals_arr.push(ztrdup_metafy("ZERR")); // c:sigs tail
2004 signals_arr.push(ztrdup_metafy("DEBUG")); // c:sigs tail
2005 {
2006 let mut tab = paramtab().write().unwrap();
2007 let pm = Box::new(param {
2008 node: hashnode {
2009 next: None,
2010 nam: "signals".to_string(),
2011 flags: (PM_ARRAY | PM_SPECIAL) as i32,
2012 },
2013 u_data: 0,
2014 u_tied: None,
2015 u_arr: Some(signals_arr),
2016 u_str: None,
2017 u_val: 0,
2018 u_dval: 0.0,
2019 u_hash: None,
2020 gsu_s: None,
2021 gsu_i: None,
2022 gsu_f: None,
2023 gsu_a: None,
2024 gsu_h: None,
2025 base: 0,
2026 width: 0,
2027 env: None,
2028 ename: None,
2029 old: None,
2030 level: 0,
2031 });
2032 tab.insert("signals".to_string(), pm);
2033 }
2034
2035 // c:980 — `noerrs = 0` restore. NOERRS module-private (see above).
2036}
2037
2038/// Parallel storage for PM_HASHED parameter values. `param.u_hash`
2039/// is typed `Option<HashTable>` per Src/zsh.h:1841 but the full
2040/// HashTable substrate isn't wired yet; the assoc-array values live
2041/// here keyed on param name until that lands.
2042static PARAMTAB_HASHED_STORAGE_INNER: OnceLock<Mutex<HashMap<String, IndexMap<String, String>>>> =
2043 OnceLock::new();
2044
2045/// Port of `assigngetset(Param pm)` from `Src/params.c:994`. C body
2046/// installs the standard get/set/unset vtable matching the
2047/// param's PM_TYPE so subsequent assignment dispatches go
2048/// through `pm->gsu.X->setfn`.
2049pub fn assigngetset(pm: &mut param) {
2050 match PM_TYPE(pm.node.flags as u32) {
2051 x if x == PM_SCALAR || x == PM_NAMEREF => {
2052 pm.gsu_s = Some(Box::new(gsu_scalar {
2053 getfn: strgetfn,
2054 setfn: strsetfn,
2055 unsetfn: stdunsetfn,
2056 }));
2057 }
2058 x if x == PM_INTEGER => {
2059 pm.gsu_i = Some(Box::new(gsu_integer {
2060 getfn: intgetfn,
2061 setfn: intsetfn,
2062 unsetfn: stdunsetfn,
2063 }));
2064 }
2065 x if x == PM_EFLOAT || x == PM_FFLOAT => {
2066 pm.gsu_f = Some(Box::new(gsu_float {
2067 getfn: floatgetfn,
2068 setfn: floatsetfn,
2069 unsetfn: stdunsetfn,
2070 }));
2071 }
2072 x if x == PM_ARRAY => {
2073 pm.gsu_a = Some(Box::new(gsu_array {
2074 getfn: arrgetfn,
2075 setfn: arrsetfn,
2076 unsetfn: stdunsetfn,
2077 }));
2078 }
2079 x if x == PM_HASHED => {
2080 pm.gsu_h = Some(Box::new(gsu_hash {
2081 getfn: hashgetfn,
2082 setfn: hashsetfn,
2083 unsetfn: stdunsetfn,
2084 }));
2085 }
2086 _ => {
2087 // c:1015 — DPUTS(1, "BUG: tried to create param node without valid flag")
2088 DPUTS!(true, "BUG: tried to create param node without valid flag");
2089 // c:1015
2090 }
2091 }
2092}
2093
2094/// Port of `createparam(char *name, int flags)` from `Src/params.c:1030`. C body
2095/// (~130 lines, see comment header at c:1020-1027) creates a
2096/// parameter so that it can be assigned to. Returns NULL if the
2097/// parameter already exists or can't be created, otherwise
2098/// returns the new node. If a parameter of the same name exists
2099/// in an outer scope, it is hidden by the new one. An already
2100/// existing node at the current level may be "created" and
2101/// returned provided it is unset and not special. If the
2102/// parameter can't be created because it already exists,
2103/// PM_UNSET is cleared.
2104///
2105/// Faithful port covers:
2106/// - PM_HASHELEM / PM_EXPORTED tweak when paramtab != realparamtab (c:1034)
2107/// - PM_RO_BY_DESIGN read-only rejection (c:1043-1052)
2108/// - PM_NAMEREF chain follow via `resolve_nameref_rec` (c:1062-1104)
2109/// - hidden vs reuse-old branches (c:1108-1147)
2110/// - `pm->node.flags = flags & ~PM_LOCAL` finalization (c:1155)
2111/// - `assigngetset(pm)` for non-special params (c:1157-1158)
2112///
2113/// Paramtab-backed branches (c:1034 paramtab compare, c:1038
2114/// gethashnode2, c:1144-1146 paramtab.removenode/addnode) cannot
2115/// fully execute until the paramtab vtable lands; they are
2116/// preserved as architectural intent. The faithful behaviour
2117/// emerges as soon as paramtab is wired (no signature drift
2118/// at this site).
2119pub fn createparam(
2120 // c:1030
2121 name: &str,
2122 mut flags: i32,
2123) -> Option<Param> {
2124 // c:1034-1035 — when paramtab != realparamtab (we're inside
2125 // a hash-element scope), strip PM_EXPORTED + add PM_HASHELEM.
2126 // Without paramtab/realparamtab live yet, this branch is
2127 // skipped — the caller is expected to be in the
2128 // realparamtab scope which is the common case.
2129
2130 // c:1037 — `if (name != nulstring) { ... } else { hcalloc; nulstring }`
2131 // c:1038-1041 — oldpm = gethashnode2(paramtab, name)
2132 // Without paramtab backend, we cannot consult the table; treat
2133 // the param as new. The PM_RO_BY_DESIGN / PM_NAMEREF / hidden
2134 // branches (c:1043-1147) collapse to "allocate fresh".
2135 // c:1037-1041 — `oldpm = gethashnode2(paramtab, name)`. Look up
2136 // any existing Param at this name so the c:1108/1135 branches
2137 // can decide reuse-vs-shadow. PM_RO_BY_DESIGN / PM_NAMEREF
2138 // chase branches (c:1043-1104) elided — covered when nameref
2139 // / readonly-by-design Params are wired.
2140 let oldpm: Option<Param> = if !name.is_empty() {
2141 paramtab().read().ok().and_then(|t| t.get(name).cloned())
2142 } else {
2143 None
2144 };
2145
2146 if !name.is_empty() {
2147 // c:1149-1150 — `if (isset(ALLEXPORT) && !(flags & PM_HASHELEM)) flags |= PM_EXPORTED;`
2148 if isset(ALLEXPORT) && (flags as u32 & PM_HASHELEM) == 0 {
2149 flags |= PM_EXPORTED as i32;
2150 }
2151 }
2152
2153 // c:1108 — `if (oldpm && (oldpm->level == locallevel || !(flags
2154 // & PM_LOCAL)))`: reuse the existing Param in place. c:1135 —
2155 // else allocate a fresh pm and chain pm.old = oldpm (the
2156 // local-shadow path). The reuse arm just returns the existing
2157 // pm with reset base/width; the shadow arm does the chain
2158 // installation that endparamscope later unwinds.
2159 let cur_locallevel = locallevel.load(Ordering::Relaxed);
2160 // c:1106-1107 — DPUTS(oldpm && oldpm->level > locallevel,
2161 // "BUG: old local parameter not deleted");
2162 DPUTS!(
2163 // c:1106
2164 match &oldpm {
2165 // c:1106
2166 Some(op) => op.level > cur_locallevel, // c:1106
2167 None => false, // c:1106
2168 },
2169 "BUG: old local parameter not deleted" // c:1107
2170 );
2171 let reuse = match &oldpm {
2172 Some(op) => op.level == cur_locallevel || (flags as u32 & PM_LOCAL) == 0,
2173 None => false,
2174 };
2175
2176 let mut pm: Param = if reuse {
2177 // c:1132-1134 — `pm = oldpm; pm->base = pm->width = 0;
2178 // oldpm = pm->old;` Reuse the entry already in paramtab.
2179 let mut existing = oldpm.unwrap(); // safe: reuse=true requires Some
2180 existing.base = 0; // c:1133
2181 existing.width = 0; // c:1133
2182 existing
2183 } else {
2184 // c:1136 zshcalloc(sizeof *pm) — fresh allocation; chain the
2185 // outer Param into pm.old (c:1137) so endparamscope can
2186 // restore it. c:1144 paramtab->removenode is implicit since
2187 // we re-insert below.
2188 //
2189 // c:Src/builtin.c:2382-2424 newspecial path — for PM_SPECIAL
2190 // shadows (`local IFS=...`), the C source allocates a
2191 // separate `tpm`, calls `copyparam(tpm, pm, 1)` which uses
2192 // the GSU getfn to read the current value into tpm.u.str,
2193 // then sets `pm->old = tpm`. zshrs's createparam path moves
2194 // `oldpm` into pm.old as-is; for specials whose value lives
2195 // in a global (ifs_lock, paramtab-external) the bare
2196 // `oldpm.u_str` is empty and endparamscope can't restore the
2197 // real outer value. Snapshot the current value via the GSU
2198 // getfn now so the saved chain carries the right string.
2199 // Bug #8 in docs/BUGS.md (`local IFS=:` leaked past return).
2200 //
2201 // c:Src/builtin.c:2382-2424 copyparam for PM_HASHED — same
2202 // shape, different storage. zshrs's PM_HASHED data lives in
2203 // the parallel `paramtab_hashed_storage` map keyed by name (no
2204 // scope dimension), so a `local -A h` shadow that writes
2205 // through set_assoc would clobber the outer scope's bag and
2206 // endparamscope's pm.old restoration alone wouldn't recover
2207 // it. Push the current paramtab_hashed_storage[name] onto the
2208 // shadow stack BEFORE the fresh pm gets installed, so when
2209 // endparamscope unwinds the PM_HASHED stale entry it can pop
2210 // and restore. Bug #415.
2211 let oldpm = if let Some(mut op) = oldpm {
2212 if (op.node.flags as u32 & (PM_SPECIAL | PM_TIED)) != 0 {
2213 // c:Src/builtin.c:2382-2424 copyparam — snapshot the
2214 // CURRENT live value into the shadow chain. PM_TIED
2215 // scalars (FPATH/PATH/CDPATH…) included: their value
2216 // derives from the tied array's global storage, which
2217 // the local's writes flow through — without the
2218 // snapshot, `f() { local FPATH=/tmp }; f` left the
2219 // GLOBAL fpath at ( /tmp ) and every later autoload
2220 // failed (zinit's :zinit-tmp-subst-autoload does
2221 // exactly this dance). Fall back to the name-routed
2222 // getsparam when the pm carries no scalar gsu.
2223 if (PM_TYPE(op.node.flags as u32) & PM_ARRAY) != 0 {
2224 // ARRAY side of a tied pair (fpath/path/cdpath):
2225 // snapshot the ELEMENT VECTOR. Forcing the scalar
2226 // getsparam fallback here stored Some("") and the
2227 // endparamscope refire then assigned that empty
2228 // scalar — zinit's autoload stubs (`local -a
2229 // fpath; fpath=( … )`) collapsed the GLOBAL fpath
2230 // to one empty element on every stub exit.
2231 if let Some(arr) = getaparam(&op.node.nam) {
2232 op.u_arr = Some(arr);
2233 }
2234 } else {
2235 let getfn_ptr = op.gsu_s.as_ref().map(|g| g.getfn);
2236 if let Some(getfn) = getfn_ptr {
2237 op.u_str = Some(getfn(&op));
2238 } else if let Some(v) = getsparam(&op.node.nam) {
2239 op.u_str = Some(v);
2240 }
2241 }
2242 }
2243 if (op.node.flags as u32 & PM_HASHED) != 0 && (flags as u32 & PM_LOCAL) != 0 {
2244 // Push current paramtab_hashed_storage[name] (Some/None)
2245 // onto the shadow stack so endparamscope can restore.
2246 // Then CLEAR the storage so the local shadow starts
2247 // with an empty bag — without this, `local -A h` (no
2248 // value) leaves the outer's data visible and a
2249 // subsequent `h[x]=v` appends to it instead of
2250 // creating a fresh local assoc. C's copyparam handles
2251 // this via separate `tpm` / `pm` u.hash slots so the
2252 // pm.u.hash that fresh writes go through is
2253 // zero-initialised; zshrs's parallel storage is one
2254 // map per name, so the save+clear pair is the
2255 // equivalent.
2256 let saved: Option<IndexMap<String, String>> = paramtab_hashed_storage()
2257 .lock()
2258 .ok()
2259 .and_then(|m| m.get(name).cloned());
2260 let stk_mtx =
2261 PARAMTAB_HASHED_SHADOW_STACK.get_or_init(|| Mutex::new(HashMap::new()));
2262 if let Ok(mut stk) = stk_mtx.lock() {
2263 stk.entry(name.to_string()).or_default().push(saved);
2264 }
2265 if let Ok(mut m) = paramtab_hashed_storage().lock() {
2266 m.insert(name.to_string(), IndexMap::new());
2267 }
2268 }
2269 Some(op)
2270 } else {
2271 None
2272 };
2273 Box::new(param {
2274 node: hashnode {
2275 next: None,
2276 nam: name.to_string(),
2277 flags: 0,
2278 },
2279 u_data: 0,
2280 u_tied: None,
2281 u_arr: None,
2282 u_str: None,
2283 u_val: 0,
2284 u_dval: 0.0,
2285 u_hash: None,
2286 gsu_s: None,
2287 gsu_i: None,
2288 gsu_f: None,
2289 gsu_a: None,
2290 gsu_h: None,
2291 base: 0,
2292 width: 0,
2293 env: None,
2294 ename: None,
2295 old: oldpm, // c:1137 pm->old = oldpm
2296 // c:1136 — C: `pm = zshcalloc(sizeof *pm)`. calloc
2297 // zeroes pm.level so a freshly created GLOBAL assignment
2298 // (`x=foo` inside a function) gets level=0 and survives
2299 // endparamscope. The `pm->level = locallevel` set happens
2300 // ONLY through builtin.c:2576 (PM_LOCAL path: `local x=…`).
2301 level: if (flags as u32 & PM_LOCAL) != 0 {
2302 cur_locallevel
2303 } else {
2304 0
2305 },
2306 })
2307 };
2308
2309 pm.node.flags = flags & !(PM_LOCAL as i32); // c:1155
2310 if (pm.node.flags as u32 & PM_SPECIAL) == 0 {
2311 // c:1157
2312 assigngetset(&mut pm); // c:1158
2313 }
2314 // c:Src/params.c:1146 — when shadowing a special parameter
2315 // (e.g. `local IFS=...` inside a function), the new pm must
2316 // inherit the canonical special-var GSU (ifssetfn etc.) so
2317 // writes route through the global storage that `$IFS`
2318 // expansion reads. Without this, the shadow's setfn was the
2319 // generic strsetfn and `local IFS=:` updated paramtab.u_str
2320 // but NOT the canonical `ifs` global — `$IFS` expansion in
2321 // the same function read the default value through ifsgetfn.
2322 //
2323 // Detect special-var names and override the gsu_s + stamp
2324 // PM_SPECIAL. Mirrors the back-fill at assignsparam:4981 but
2325 // covers the create-time path (local / typeset of fresh
2326 // shadow), not just the assign-existing path.
2327 if !name.is_empty() {
2328 let special_gsu: Option<Box<gsu_scalar>> = match name {
2329 "HOME" => Some(Box::new(HOME_GSU.clone())),
2330 "IFS" => Some(Box::new(IFS_GSU.clone())),
2331 "TERM" => Some(Box::new(TERM_GSU.clone())),
2332 "TERMINFO" => Some(Box::new(TERMINFO_GSU.clone())),
2333 "TERMINFO_DIRS" => Some(Box::new(TERMINFODIRS_GSU.clone())),
2334 "WORDCHARS" => Some(Box::new(WORDCHARS_GSU.clone())),
2335 "USERNAME" => Some(Box::new(USERNAME_GSU.clone())),
2336 "KEYBOARD_HACK" => Some(Box::new(KEYBOARDHACK_GSU.clone())),
2337 "HISTCHARS" | "histchars" => Some(Box::new(HISTCHARS_GSU.clone())),
2338 _ => None,
2339 };
2340 if let Some(gsu) = special_gsu {
2341 pm.gsu_s = Some(gsu);
2342 pm.node.flags |= PM_SPECIAL as i32;
2343 }
2344 }
2345 // c:1146 `paramtab->addnode(paramtab, ztrdup(name), pm)`. For
2346 // the reuse arm this overwrites the same entry; for the shadow
2347 // arm it installs the new chained pm on top of the (now-
2348 // displaced) old.
2349 if !name.is_empty() {
2350 let cloned = pm.clone();
2351 paramtab().write().unwrap().insert(name.to_string(), pm);
2352 return Some(cloned);
2353 }
2354 Some(pm) // c:1159
2355}
2356
2357/// Empty special-hash sentinel.
2358/// Port of `shempty()` from Src/params.c:1166. The C source uses
2359/// it as a no-op getfn callback for special hashes that need an
2360/// addressable function pointer but no actual work. Provided here
2361/// so future callers that match the C source's signature can call
2362/// it directly.
2363pub fn shempty() {}
2364
2365/// Port of `setsparam(char *s, char *val)` from Src/params.c:3350.
2366/// C body: `return assignsparam(s, val, ASSPM_WARN);`
2367/// WARNING: param names don't match C — Rust=() vs C=(s, val)
2368pub fn setsparam(s: &str, val: &str) -> Option<Param> {
2369 assignsparam(s, val, ASSPM_WARN as i32) // c:3352
2370}
2371
2372/// Direct port of `Param createspecialhash(char *name, GetNodeFunc
2373/// get, ScanTabFunc scan, int flags)` from `Src/params.c:1182-1224`.
2374/// Creates a PM_SPECIAL|PM_HASHED parameter with the supplied get
2375/// and scan callbacks, attaches an empty hash table, and returns
2376/// the new Param (or None if `createparam` fails).
2377///
2378/// C body wiring:
2379/// - `pm = createparam(name, PM_SPECIAL|PM_HASHED|flags)` (c:1186)
2380/// - If shadowing an old param at function scope, `pm->level =
2381/// locallevel` (c:1204-1205) so the old one is exposed after
2382/// leaving the fn.
2383/// - `pm->gsu.h = (flags & PM_READONLY) ? &stdhash_gsu :
2384/// &nullsethash_gsu` (c:1206-1207)
2385/// - `pm->u.hash = newhashtable(0, name, NULL)` (c:1208) with
2386/// no-op add/empty/remove/free callbacks (`shempty`) plus the
2387/// supplied `get` / `scan` callbacks.
2388///
2389/// The Rust port drops `GetNodeFunc` / `ScanTabFunc` fn-pointer
2390/// parameters because the Rust HashTable model uses owned
2391/// HashMap<String, T> rather than C-style vtable dispatch; the
2392/// returned Param carries the empty hash and PM_HASHED flag so
2393/// callers can fill it via the standard array/hash setfn path.
2394pub fn createspecialhash(name: &str, flags: i32) -> Option<Param> {
2395 // c:1186 — `createparam(name, PM_SPECIAL|PM_HASHED|flags)`.
2396 let mut pm = createparam(name, (PM_SPECIAL | PM_HASHED) as i32 | flags)?;
2397
2398 // c:1204-1205 — if shadowing an old param, set level=locallevel.
2399 if pm.old.is_some() {
2400 // C: `pm->level = locallevel`. The previous Rust port had
2401 // `let ll = 0_i32;` as a hardcoded placeholder — meaning
2402 // shadowed special-hash params (`fpath`, `path`, `psvar`,
2403 // etc. assigned inside a function via local) would NEVER
2404 // get their level tagged for restoration. After the function
2405 // returned, the original param would be inaccessible because
2406 // the shadow record's level (always 0) wouldn't trigger the
2407 // endparamscope unset. Now reads the canonical `locallevel`
2408 // global from params.rs (matching the C global).
2409 pm.level = locallevel.load(Ordering::Relaxed) as i32;
2410 // c:1205
2411 }
2412
2413 // c:1206-1207 — GSU selection. We can't set the gsu_h pointer
2414 // without the full GSU port wired; leave it None and let the
2415 // standard setfn dispatch route through the existing hashsetfn
2416 // / nullsethashfn helpers.
2417
2418 // c:1208 — `pm->u.hash = newhashtable(0, name, NULL)`. Rust
2419 // stores an empty HashTable in u_hash. The C body then sets
2420 // hash/empty/add/get/get2/remove/disable/enable/free/print
2421 // callbacks (c:1210-1221) which in our Rust model are implicit
2422 // (HashMap handles add/get/remove; freenode is Drop).
2423 let ht = Box::new(hashtable {
2424 hsize: 0,
2425 ct: 0,
2426 nodes: Vec::new(),
2427 tmpdata: 0,
2428 hash: None,
2429 emptytable: None,
2430 filltable: None,
2431 cmpnodes: None,
2432 addnode: None,
2433 getnode: None,
2434 getnode2: None,
2435 removenode: None,
2436 disablenode: None,
2437 enablenode: None,
2438 freenode: None,
2439 printnode: None,
2440 scantab: None,
2441 });
2442 pm.u_hash = Some(ht);
2443 let _ = name;
2444
2445 Some(pm) // c:1223
2446}
2447
2448/// ```c
2449/// tpm->node.flags = pm->node.flags;
2450/// tpm->base = pm->base;
2451/// tpm->width = pm->width;
2452/// tpm->level = pm->level;
2453/// if (!fakecopy) {
2454/// tpm->old = pm->old;
2455/// tpm->node.flags &= ~PM_SPECIAL;
2456/// }
2457/// switch (PM_TYPE(pm->node.flags)) {
2458/// case PM_SCALAR: case PM_NAMEREF:
2459/// tpm->u.str = ztrdup(pm->gsu.s->getfn(pm)); break;
2460/// case PM_INTEGER:
2461/// tpm->u.val = pm->gsu.i->getfn(pm); break;
2462/// case PM_EFLOAT: case PM_FFLOAT:
2463/// tpm->u.dval = pm->gsu.f->getfn(pm); break;
2464/// case PM_ARRAY:
2465/// tpm->u.arr = zarrdup(pm->gsu.a->getfn(pm)); break;
2466/// case PM_HASHED:
2467/// tpm->u.hash = copyparamtable(pm->gsu.h->getfn(pm), pm->node.nam);
2468/// break;
2469/// }
2470/// if (!fakecopy)
2471/// assigngetset(tpm);
2472/// ```
2473/// Copies `pm`'s value + level/base/width/flags into `tpm`.
2474/// `fakecopy = 1` means we're saving a snapshot (e.g. for special
2475/// param scope-save) and don't need callable get/set callbacks; in
2476/// that case `tpm->old`/PM_SPECIAL are preserved untouched and
2477/// `assigngetset` is skipped.
2478/// Port of `copyparam(Param tpm, Param pm, int fakecopy)` from `Src/params.c:1236`.
2479/// WARNING: param names don't match C — Rust=(pm, fakecopy) vs C=(tpm, pm, fakecopy)
2480pub fn copyparam(
2481 // c:1236
2482 tpm: &mut param,
2483 pm: &mut param,
2484 fakecopy: i32,
2485) {
2486 tpm.node.flags = pm.node.flags; // c:1244
2487 tpm.base = pm.base; // c:1245
2488 tpm.width = pm.width; // c:1246
2489 tpm.level = pm.level; // c:1247
2490 if fakecopy == 0 {
2491 // c:1248
2492 tpm.old = pm.old.take(); // c:1249
2493 tpm.node.flags &= !(PM_SPECIAL as i32); // c:1250
2494 }
2495 match PM_TYPE(pm.node.flags as u32) {
2496 // c:1252
2497 t if t == PM_SCALAR || t == PM_NAMEREF => {
2498 // c:1255 — `tpm->u.str = ztrdup(pm->gsu.s->getfn(pm));`.
2499 // C dispatches through the GSU getfn pointer so PM_SPECIAL
2500 // params (IFS, PATH, HOME, ...) return their canonical
2501 // global value (ifsgetfn reads `ifs_lock`, etc.). Without
2502 // this dispatch, `local IFS=:` saved an empty pm.u_str
2503 // (the bare strgetfn read) and endparamscope could not
2504 // restore the real outer value on scope exit (bug #8 in
2505 // docs/BUGS.md).
2506 let getfn_ptr = pm.gsu_s.as_ref().map(|g| g.getfn);
2507 tpm.u_str = Some(if let Some(getfn) = getfn_ptr {
2508 getfn(pm)
2509 } else {
2510 strgetfn(pm)
2511 });
2512 }
2513 t if t == PM_INTEGER => {
2514 // c:1257
2515 tpm.u_val = intgetfn(pm); // c:1258
2516 }
2517 t if t == PM_EFLOAT || t == PM_FFLOAT => {
2518 // c:1260-1261
2519 tpm.u_dval = floatgetfn(pm); // c:1262
2520 }
2521 t if t == PM_ARRAY => {
2522 // c:1264
2523 tpm.u_arr = Some(arrgetfn(pm)); // c:1265
2524 }
2525 t if t == PM_HASHED => {
2526 // c:1267
2527 // copyparamtable(pm->gsu.h->getfn(pm), pm->node.nam) // c:1268
2528 tpm.u_hash = copyparamtable(pm.u_hash.as_ref(), &pm.node.nam);
2529 }
2530 _ => {}
2531 }
2532 if fakecopy == 0 {
2533 // c:1280
2534 assigngetset(tpm); // c:1281
2535 }
2536}
2537
2538// ---------------------------------------------------------------------------
2539// Utility functions
2540// ---------------------------------------------------------------------------
2541
2542/// Check if string is valid identifier (from params.c isident)
2543// Return 1 if the string s is a valid identifier, else return 0. // c:1288
2544/// `isident` — see implementation.
2545pub fn isident(s: &str) -> bool {
2546 // c:1288
2547 if s.is_empty() {
2548 return false;
2549 }
2550 let mut chars = s.chars().peekable();
2551
2552 // Handle namespace prefix (e.g. "ns.var")
2553 if chars.peek() == Some(&'.') {
2554 chars.next();
2555 if chars.peek().is_none_or(|c| c.is_ascii_digit()) {
2556 return false;
2557 }
2558 }
2559
2560 let first = match chars.next() {
2561 Some(c) => c,
2562 None => return false,
2563 };
2564
2565 if first.is_ascii_digit() {
2566 // All-digit names are valid (positional params)
2567 return chars.all(|c| c.is_ascii_digit());
2568 }
2569
2570 if !first.is_alphabetic() && first != '_' {
2571 return false;
2572 }
2573
2574 for c in chars {
2575 if c == '[' {
2576 // c:1326
2577 // c:1329-1330 — `if (*ss != '[') return 0; if (!(ss =
2578 // parse_subscript(++ss, 1, ']'))) return 0;`
2579 // Subscript MUST be balanced — `foo[` (missing `]`)
2580 // is NOT a valid identifier. The previous Rust port
2581 // accepted `[` at the end unconditionally, missing
2582 // the balanced-pair requirement.
2583 //
2584 // Routing through the full `parse_subscript` (which
2585 // drives a nested lex context) would be overkill at
2586 // this site — a simple bracket-balance walk over the
2587 // remaining bytes suffices. Count `[` / `]` and require
2588 // the depth to return to 0 before end-of-string.
2589 let mut depth = 1i32;
2590 // c:Src/params.c:1334 — `if (!(ss = parse_subscript(++ss,
2591 // 1, ']'))) return 0;`. C's parse_subscript rejects empty
2592 // subscripts: `h[]` returns NULL. Mirror by tracking
2593 // whether ANY non-bracket char appears before the depth
2594 // returns to 0. Without this, `h[]=val` was accepted as a
2595 // valid assoc element write — but zsh errors
2596 // `not an identifier: h[]` (bug #288 in docs/BUGS.md).
2597 //
2598 // c:Src/params.c parse_subscript (params.c:1480+) also
2599 // recognises backslash-escaped brackets: `A[\[k\]]` is a
2600 // single subscript with key `[k]`. The `\[` doesn't count
2601 // toward bracket depth and `\]` doesn't close. Track a
2602 // bslash flag so the depth walk matches C semantics.
2603 let mut saw_content = false;
2604 // Bug fix: `s.split('[').skip(1).next()` only returned
2605 // the segment between the first and second `[`, dropping
2606 // everything after. Use find()+slice to get the entire
2607 // tail starting after the first `[`.
2608 let tail_start = s
2609 .char_indices()
2610 .find(|(_, c)| *c == '[')
2611 .map(|(i, _)| i + 1);
2612 let saw_close = tail_start.map(|start| &s[start..]).is_some_and(|tail| {
2613 let mut bslash = false;
2614 for ch in tail.chars() {
2615 if bslash {
2616 // c:parse_subscript — escaped char is
2617 // content, doesn't affect depth.
2618 saw_content = true;
2619 bslash = false;
2620 continue;
2621 }
2622 match ch {
2623 '\\' => {
2624 bslash = true;
2625 saw_content = true;
2626 }
2627 '[' => {
2628 depth += 1;
2629 saw_content = true;
2630 }
2631 ']' => {
2632 depth -= 1;
2633 if depth == 0 {
2634 return true;
2635 }
2636 saw_content = true;
2637 }
2638 _ => saw_content = true,
2639 }
2640 }
2641 false
2642 });
2643 if !saw_content {
2644 return false; // c:1334 empty subscript rejected
2645 }
2646 return saw_close;
2647 }
2648 if !c.is_alphanumeric() && c != '_' && c != '.' {
2649 return false;
2650 }
2651 }
2652 true
2653}
2654
2655/// Subscript-argument parser.
2656///
2657/// Port of `getarg(char **str, int *inv, Value v, int a2, zlong *w, int *prevcharlen, int *nextcharlen, int scanflags)` from Src/params.c:1367. The C function is a
2658/// 618-line monolith handling the entire `[...]` body of a
2659/// subscripted parameter expansion.
2660///
2661/// Ported phases:
2662/// - Flag-block parse (c:1389-1480) — extract `(...)` chars.
2663/// - Hash pattern search (c:1581-1660) when `assoc` is `Some`.
2664/// - Array pattern search (c:1672-1719) when `arr` is `Some`.
2665/// - Scalar word-mode arm (c:1761-1797) when `scalar` is `Some`.
2666///
2667/// Later C phases not yet exercised by this entry point:
2668/// - Brace-depth walk to closing `]` (c:1507-1535)
2669/// - parsestr + singsub on subscript body (c:1545-1580)
2670/// - mathevalarg integer parse (c:1601-1604)
2671/// - Multibyte char-search arm (c:1798-1985)
2672pub(crate) fn getarg<'a>(
2673 idx: &'a str,
2674 arr: Option<&[String]>,
2675 assoc: Option<&IndexMap<String, String>>,
2676 scalar: Option<&str>,
2677) -> Option<getarg_out<'a>> {
2678 let rest = idx.strip_prefix('(')?;
2679 // Reject anything that looks like a char-class subscript: `[abc]`
2680 // doesn't match this prefix, but `(...)` containing brackets is
2681 // probably alternation — let it fall through to runtime instead.
2682 if rest.starts_with(')') || rest.contains('[') {
2683 return None;
2684 }
2685 // Flag scanner per zshparam(1) "Subscript Flags" /
2686 // params.c:1389-1480 switch:
2687 // r/R (reverse value-search → value/all values),
2688 // i/I (value-search → key/all keys),
2689 // k/K (key-search → value/all values),
2690 // e (exact match — disables glob),
2691 // n<DELIM>NUM<DELIM> (Nth match — params.c:1431-1442),
2692 // b<DELIM>NUM<DELIM> (begin offset — params.c:1443-1454),
2693 // w (word index on scalar),
2694 // f (word index split by newline; alias for `w` + sep="\n"),
2695 // p (escapes for next get_strarg),
2696 // s<DELIM>SEP<DELIM> (split-by-separator).
2697 // The `n` / `b` / `s` forms use `get_strarg`'s balanced-delimiter
2698 // pair: any non-flag char closes its pair (`(n.5.)`, `(n:5:)` etc.).
2699 let bytes = rest.as_bytes();
2700 let mut i: usize = 0;
2701 let mut num: i64 = 1;
2702 let mut beg: i64 = 0;
2703 let mut has_beg = false;
2704 let flags_start = 0_usize;
2705 let mut flags_end = 0_usize;
2706 let mut bad = false;
2707 while i < bytes.len() && bytes[i] != b')' {
2708 let c = bytes[i] as char;
2709 match c {
2710 'r' | 'R' | 'i' | 'I' | 'e' | 'k' | 'K' | 'w' | 'f' | 'p' => {
2711 i += 1;
2712 flags_end = i;
2713 }
2714 'n' | 'b' => {
2715 // Consume `n<DELIM>NUM<DELIM>` per c:1432 get_strarg.
2716 if i + 1 >= bytes.len() {
2717 bad = true;
2718 break;
2719 }
2720 let delim = bytes[i + 1];
2721 let arg_start = i + 2;
2722 let mut arg_end = arg_start;
2723 while arg_end < bytes.len() && bytes[arg_end] != delim {
2724 arg_end += 1;
2725 }
2726 if arg_end >= bytes.len() {
2727 bad = true;
2728 break;
2729 }
2730 // Parse the argument as a signed decimal integer.
2731 let arg = std::str::from_utf8(&bytes[arg_start..arg_end]).ok()?;
2732 let parsed: i64 = arg.trim().parse().ok()?;
2733 if c == 'n' {
2734 num = if parsed == 0 { 1 } else { parsed };
2735 } else {
2736 has_beg = true;
2737 beg = if parsed > 0 { parsed - 1 } else { parsed };
2738 }
2739 i = arg_end + 1;
2740 flags_end = i;
2741 }
2742 's' => {
2743 // (s:SEP:) — pass through with raw flag block.
2744 let close = match rest[i..].find(')') {
2745 Some(p) => i + p,
2746 None => return None,
2747 };
2748 let flags = &rest[flags_start..close];
2749 return Some(getarg_out::Flags {
2750 flags,
2751 rest: &rest[close + 1..],
2752 });
2753 }
2754 _ => {
2755 bad = true;
2756 break;
2757 }
2758 }
2759 }
2760 // c:1477-1483 — flag-error fallback: reset all flags, treat as no
2761 // subscript flags.
2762 if bad {
2763 return None;
2764 }
2765 if i >= bytes.len() || bytes[i] != b')' {
2766 return None;
2767 }
2768 if flags_end == flags_start {
2769 return None;
2770 }
2771 let flags = &rest[flags_start..flags_end];
2772 let pat = &rest[i + 1..];
2773
2774 // c:1488-1491 — negative `num` flips the search direction.
2775 let neg_num_flips = num < 0;
2776 if neg_num_flips {
2777 num = -num;
2778 }
2779
2780 // Phase 3 — hash pattern search arm (c:1581-1660 / 1672-1734).
2781 // Per C source case-arms:
2782 // `r`: rev=1 → match against VALUES, return matching VALUE
2783 // `R`: rev+down=1 → match VALUES, return ALL matching VALUEs
2784 // `i`: rev+ind=1 → match VALUES, return KEY of first match
2785 // `I`: rev+ind+down=1 → match VALUES, return ALL matching KEYs
2786 // `k`: keymatch+rev=1 → match KEYS, return VALUE of first match
2787 // `K`: keymatch+rev+down=1 → match KEYS, return ALL matching VALUEs
2788 if let Some(map) = assoc {
2789 let exact = flags.contains('e');
2790 let key_match = flags.contains('k') || flags.contains('K');
2791 let return_index = flags.contains('i') || flags.contains('I');
2792 // c:Src/params.c — on a HASH, i/I/k/K all match against KEYS while
2793 // r/R match against VALUES (zsh 5.9: `${h[(i)KEY]}`→KEY,
2794 // `${h[(r)VAL]}`→VAL, `${h[(i)VAL]}`→empty). k/K are exact key
2795 // compares (pprog=NULL, c:1707-1708); i/I glob the key; r/R glob the
2796 // value; (e) forces exact. Return: i/I→KEY, k/K→VALUE, r/R→VALUE.
2797 let match_against_key = key_match || return_index;
2798 let key_glob = return_index; // i/I glob the key; k/K are exact-only
2799 // c:Src/params.c:1689,1708-1729 — the value/key SCAN only runs when
2800 // `v->scanflags` is set, which requires a search-direction flag
2801 // (i/I/r/R/k/K). With none of those (e.g. a bare `(e)key`), C never
2802 // builds scanflags: getindex falls back to a plain `gethashnode`
2803 // exact KEY lookup. `(e)` (quote_arg, c:1450) only makes that key
2804 // literal (no glob). Without this, `(e)*` wrongly scanned VALUES for
2805 // an exact "*" and returned empty instead of the value at key "*".
2806 let has_search = flags.contains('i')
2807 || flags.contains('I')
2808 || flags.contains('r')
2809 || flags.contains('R')
2810 || flags.contains('k')
2811 || flags.contains('K');
2812 if !has_search {
2813 return Some(getarg_out::Value(Value::str(
2814 map.get(pat).cloned().unwrap_or_default(),
2815 )));
2816 }
2817 // C params.c:1488-1491 — negative `num` flips `down`. Since
2818 // R/I/K already set down=1, neg_num XORs the bit (r/i/k +
2819 // neg → return_all; R/I/K + neg → single-match again).
2820 let is_uppercase = flags.contains('I') || flags.contains('R') || flags.contains('K');
2821 let return_all = is_uppercase ^ neg_num_flips;
2822
2823 // c:1740-1747 — `b<NUM>` start offset on the values array. The
2824 // hash is iterated in insertion order (IndexMap); skip first
2825 // `beg` entries before counting matches.
2826 let len = map.len() as i64;
2827 let mut start = beg;
2828 if start < 0 {
2829 start += len;
2830 }
2831 if !return_all && start >= len {
2832 return Some(getarg_out::Value(Value::str("")));
2833 }
2834 let skip = if start < 0 { 0 } else { start as usize };
2835
2836 // Per C params.c:1707-1709 + zsh 5.9 empirical:
2837 // k/K — keymatch path: pprog=NULL, no glob; exact key
2838 // lookup. `(K)*` returns "" because there's no key
2839 // literally named "*".
2840 // r/R/i/I — value path: pprog=patcompile, glob/exact.
2841 // c:params.c:1697 — `pprog = patcompile(s, 0, NULL)` runs ONCE
2842 // before the scan loop. Compiling inside the per-entry compare
2843 // re-tokenized + re-compiled the pattern for EVERY element —
2844 // `${history[(R)(#i)*pat*]}` over a 566k-entry history ran
2845 // ~4 CPU-minutes per hsmw ^R (the reported freeze) instead of
2846 // one compile + 566k pattry calls.
2847 let compiled_pat = if exact || (match_against_key && !key_glob) {
2848 None
2849 } else {
2850 patcompile(
2851 &{
2852 let mut __pat_tok = (pat).to_string();
2853 crate::ported::glob::tokenize(&mut __pat_tok);
2854 __pat_tok
2855 },
2856 PAT_HEAPDUP as i32,
2857 None,
2858 )
2859 };
2860 let key_compare = |target: &str| -> bool {
2861 if exact || (match_against_key && !key_glob) {
2862 // (e) literal, and k/K exact key compare (no glob).
2863 target == pat
2864 } else {
2865 // i/I glob the key; r/R glob the value.
2866 compiled_pat.as_ref().map_or(false, |p| pattry(p, target))
2867 }
2868 };
2869 if return_all {
2870 let mut out: Vec<String> = Vec::new();
2871 for (k, v) in map.iter().skip(skip) {
2872 let target = if match_against_key {
2873 k.as_str()
2874 } else {
2875 v.as_str()
2876 };
2877 if key_compare(target) {
2878 // i/I → KEY; k/K → VALUE; r/R → VALUE.
2879 out.push(if return_index { k.clone() } else { v.clone() });
2880 }
2881 }
2882 return Some(getarg_out::Value(Value::str(out.join(" "))));
2883 }
2884 // c:1753 — `!--num` skips matches until the Nth.
2885 let mut remaining = num;
2886 for (k, v) in map.iter().skip(skip) {
2887 let target = if match_against_key {
2888 k.as_str()
2889 } else {
2890 v.as_str()
2891 };
2892 if key_compare(target) {
2893 remaining -= 1;
2894 if remaining == 0 {
2895 return Some(getarg_out::Value(Value::str(if return_index {
2896 k.clone()
2897 } else {
2898 v.clone()
2899 })));
2900 }
2901 }
2902 }
2903 return Some(getarg_out::Value(Value::str("")));
2904 }
2905
2906 // Phase 2 — array pattern search arm (c:1672-1719). The C body
2907 // does `pprog = patcompile(s, 0, NULL)` then forward/reverse
2908 // `for (r = 1 + beg, p = ta + beg; *p; r++, p++) if (pprog &&
2909 // pattry(pprog, *p)) return r`.
2910 if let Some(arr) = arr {
2911 // C params.c:1761-1797 — `(w)N` / `(f)N` word-mode arm.
2912 // `getstrvalue(v)` joins the array; `sepsplit` re-splits by
2913 // sep (`f` → "\n", `w` → IFS-default whitespace, `s:SEP:`
2914 // → user sep), then the Nth split word is returned. So
2915 // `arr=("a b" "c d"); ${arr[(w)2]}` → "b" (joined "a b c d",
2916 // split → ["a","b","c","d"], pick idx 1).
2917 if flags.contains('w') || flags.contains('f') {
2918 if let Ok(n) = pat.parse::<i64>() {
2919 let sep_chars: &[char] = if flags.contains('f') {
2920 &['\n']
2921 } else {
2922 &[' ', '\t', '\n']
2923 };
2924 let joined = arr.join(" ");
2925 let words: Vec<&str> = joined
2926 .split(|c: char| sep_chars.contains(&c))
2927 .filter(|w| !w.is_empty())
2928 .collect();
2929 let len = words.len() as i64;
2930 let idx_into = if n > 0 {
2931 (n - 1) as usize
2932 } else if n < 0 {
2933 let off = len + n;
2934 if off < 0 {
2935 return Some(getarg_out::Value(Value::str("")));
2936 }
2937 off as usize
2938 } else {
2939 return Some(getarg_out::Value(Value::str("")));
2940 };
2941 return Some(getarg_out::Value(Value::str(
2942 words
2943 .get(idx_into)
2944 .map(|s| s.to_string())
2945 .unwrap_or_default(),
2946 )));
2947 }
2948 }
2949 let exact = flags.contains('e');
2950 let word = flags.contains('w') || flags.contains('f');
2951 let _ = word;
2952 let return_index = flags.contains('i') || flags.contains('I');
2953 // C params.c:1575 `if (!rev)` — without a direction flag
2954 // (r/R/i/I/k/K), getarg does NOT enter the search loop on
2955 // arrays; pat is mathevalarg'd as an integer index instead.
2956 // Verified empirically: `arr=(foo bar); ${arr[(e)foo]}`
2957 // returns empty in real zsh (mathevalarg fails, no element).
2958 let any_search_flag = flags.contains('r')
2959 || flags.contains('R')
2960 || flags.contains('i')
2961 || flags.contains('I')
2962 || flags.contains('k')
2963 || flags.contains('K');
2964 if !any_search_flag {
2965 return None;
2966 }
2967 // c:1488-1491 — negative `num` flips reverse direction.
2968 let reverse = (flags.contains('R') || flags.contains('I')) ^ neg_num_flips;
2969 // C params.c:1668-1685 implicit `*` wrap fires only when
2970 // `v->scanflags` is unset; in standard subscript callsites
2971 // scanflags IS set, so the wrap does NOT engage. Verified
2972 // empirically: `arr=(foobar baz); ${arr[(r)foo]}` returns
2973 // empty in real zsh (exact match), not "foobar". Pattern is
2974 // used verbatim — globbing only when user supplies `*`.
2975 let pat_used: &str = pat;
2976
2977 // c:1740-1760 — `b<NUM>` starting offset + bounds checks.
2978 // beg is already 0-based after parse (parsed-1 for positive).
2979 let len = arr.len() as i64;
2980 let mut start = beg;
2981 if start < 0 {
2982 start += len;
2983 }
2984 // c:1743-1747 — out-of-bounds returns.
2985 if reverse {
2986 if start < 0 {
2987 return Some(getarg_out::Value(if return_index {
2988 Value::str("0")
2989 } else {
2990 Value::str("")
2991 }));
2992 }
2993 } else if start >= len {
2994 return Some(getarg_out::Value(if return_index {
2995 Value::str((arr.len() + 1).to_string())
2996 } else {
2997 Value::str("")
2998 }));
2999 }
3000 // c:1750-1751 — reverse w/o explicit b starts from len-1.
3001 if reverse && !has_beg {
3002 start = len - 1;
3003 }
3004 // c:1748 — `if (beg >= 0 && beg < len) { …search… }`
3005 if start >= len {
3006 return Some(getarg_out::Value(if return_index {
3007 Value::str("0")
3008 } else {
3009 Value::str("")
3010 }));
3011 }
3012 let iter: Box<dyn Iterator<Item = (usize, &String)>> = if reverse {
3013 // c:1752 — `for (p = ta + beg; p >= ta; p--)`: clamp start
3014 // into the valid range then walk backwards. An empty array
3015 // has no element to walk (C's loop body never dereferences a
3016 // valid `p`), so yield an empty iterator rather than indexing
3017 // `arr[..=0]` on a zero-length slice.
3018 if arr.is_empty() {
3019 Box::new(std::iter::empty())
3020 } else {
3021 let s_idx = if start < 0 { 0 } else { start as usize };
3022 let s_idx = s_idx.min(arr.len() - 1);
3023 Box::new(arr[..=s_idx].iter().enumerate().rev())
3024 }
3025 } else {
3026 // c:1757 — `for (p = ta + beg; *p; p++)`: skip first beg.
3027 let s_idx = if start < 0 { 0 } else { start as usize };
3028 Box::new(arr.iter().enumerate().skip(s_idx))
3029 };
3030 // c:1758 — `!--num` skips matches until the Nth.
3031 // c:1697 — pattern compiled ONCE before the scan (see the
3032 // assoc arm above; same per-element recompile hazard).
3033 let compiled_arr_pat = if exact {
3034 None
3035 } else {
3036 patcompile(
3037 &{
3038 let mut __pat_tok = (pat_used).to_string();
3039 crate::ported::glob::tokenize(&mut __pat_tok);
3040 __pat_tok
3041 },
3042 PAT_HEAPDUP as i32,
3043 None,
3044 )
3045 };
3046 let mut remaining = num;
3047 for (i, s) in iter {
3048 let hit = if exact {
3049 s == pat
3050 } else {
3051 compiled_arr_pat.as_ref().map_or(false, |p| pattry(p, s))
3052 };
3053 if hit {
3054 remaining -= 1;
3055 if remaining == 0 {
3056 return Some(getarg_out::Value(if return_index {
3057 Value::str((i + 1).to_string())
3058 } else {
3059 Value::str(s.clone())
3060 }));
3061 }
3062 }
3063 }
3064 return Some(getarg_out::Value(if return_index {
3065 // zsh: `i` returns len+1 if not found, `I` returns 0.
3066 if flags.contains('I') {
3067 Value::str("0")
3068 } else {
3069 Value::str((arr.len() + 1).to_string())
3070 }
3071 } else {
3072 Value::str("")
3073 }));
3074 }
3075
3076 // C params.c:1761-1797 — scalar word-mode arm. `(w)N` joins
3077 // the source string and re-splits by sep (whitespace by default
3078 // for `w`, "\n" for `f`). When `pat` is a numeric N, the Nth
3079 // word is returned. Pattern-search variants on scalars use the
3080 // c:1798-1980 char-search arm ported below.
3081 if let Some(s) = scalar {
3082 if flags.contains('w') || flags.contains('f') {
3083 if let Ok(n) = pat.parse::<i64>() {
3084 let sep_chars: &[char] = if flags.contains('f') {
3085 &['\n']
3086 } else {
3087 &[' ', '\t', '\n']
3088 };
3089 let words: Vec<&str> = s
3090 .split(|c: char| sep_chars.contains(&c))
3091 .filter(|w| !w.is_empty())
3092 .collect();
3093 let len = words.len() as i64;
3094 let idx_into = if n > 0 {
3095 (n - 1) as usize
3096 } else if n < 0 {
3097 let off = len + n;
3098 if off < 0 {
3099 return Some(getarg_out::Value(Value::str("")));
3100 }
3101 off as usize
3102 } else {
3103 return Some(getarg_out::Value(Value::str("")));
3104 };
3105 return Some(getarg_out::Value(Value::str(
3106 words
3107 .get(idx_into)
3108 .map(|s| s.to_string())
3109 .unwrap_or_default(),
3110 )));
3111 }
3112 }
3113 // C params.c:1798-1980 — scalar char-search arm. `(i)/(I)/
3114 // (r)/(R)` on a scalar runs a sliding-window glob match over
3115 // the CHARACTER array (`s_chars`), so positions are character-
3116 // based — matching C's prevcharlen/nextcharlen cursor stepping
3117 // (c:1948-1971) under MULTIBYTE. (i)/(I) return the 1-based
3118 // char position of the first/last match; (r)/(R) return the
3119 // char at the match. Verified against zsh: `s=ábc; ${s[(i)b]}`
3120 // → 2 (char pos), not 3 (byte pos).
3121 let any_search = flags.contains('r')
3122 || flags.contains('R')
3123 || flags.contains('i')
3124 || flags.contains('I');
3125 if any_search {
3126 let return_index = flags.contains('i') || flags.contains('I');
3127 let want_last = flags.contains('I') || flags.contains('R');
3128 // Negative `num` flips direction (c:1488-1491).
3129 let want_last = want_last ^ neg_num_flips;
3130 let s_chars: Vec<char> = s.chars().collect();
3131 let n = s_chars.len();
3132 let positions: Box<dyn Iterator<Item = usize>> = if want_last {
3133 Box::new((0..=n).rev())
3134 } else {
3135 Box::new(0..=n)
3136 };
3137 // c:1929+ / c:1964 — `!--num` skips matches until the Nth.
3138 // Per `b<NUM>` (c:1740-1747) — start from offset, only
3139 // when has_beg is set. Without `b`, walk all positions.
3140 let beg_idx_opt: Option<usize> = if has_beg {
3141 let beg_norm = if beg < 0 { beg + n as i64 } else { beg };
3142 Some(if beg_norm < 0 {
3143 0
3144 } else {
3145 (beg_norm as usize).min(n)
3146 })
3147 } else {
3148 None
3149 };
3150 // c:1697 — compile the search pattern ONCE (the scalar
3151 // char-search tries O(n²) spans; a per-span recompile is
3152 // quadratic × compile cost — same hazard as the assoc arm).
3153 let compiled_span_pat = if flags.contains('e') {
3154 None
3155 } else {
3156 patcompile(
3157 &{
3158 let mut __pat_tok = (pat).to_string();
3159 crate::ported::glob::tokenize(&mut __pat_tok);
3160 __pat_tok
3161 },
3162 PAT_HEAPDUP as i32,
3163 None,
3164 )
3165 };
3166 let mut found: Option<(usize, usize)> = None;
3167 let mut remaining = num;
3168 'outer: for start in positions {
3169 if let Some(b_idx) = beg_idx_opt {
3170 if want_last {
3171 if start > b_idx {
3172 continue;
3173 }
3174 } else if start < b_idx {
3175 continue;
3176 }
3177 }
3178 for span_len in 1..=(n - start) {
3179 let cand: String = s_chars[start..start + span_len].iter().collect();
3180 let hit = if flags.contains('e') {
3181 cand == pat
3182 } else {
3183 compiled_span_pat.as_ref().map_or(false, |p| pattry(p, &cand))
3184 };
3185 if hit {
3186 remaining -= 1;
3187 if remaining == 0 {
3188 found = Some((start, start + span_len));
3189 break 'outer;
3190 }
3191 // Advance past this match position to find the
3192 // next-Nth instead of repeatedly matching same
3193 // start (mirrors C's pointer increment).
3194 break;
3195 }
3196 }
3197 }
3198 return Some(getarg_out::Value(match (found, return_index) {
3199 (Some((s_pos, _)), true) => Value::str((s_pos + 1).to_string()),
3200 // C params.c:1798-1980 char-search returns the char AT
3201 // the match position, not the full matched substring.
3202 // Verified empirically: `s="barfooxyz"; ${s[(r)foo]}`
3203 // returns "f" in real zsh, not "foo".
3204 (Some((s_pos, _)), false) => Value::str(
3205 s_chars
3206 .get(s_pos)
3207 .map(|c| c.to_string())
3208 .unwrap_or_default(),
3209 ),
3210 (None, true) => Value::str(if flags.contains('i') {
3211 (n + 1).to_string()
3212 } else {
3213 "0".to_string()
3214 }),
3215 (None, false) => Value::str(String::new()),
3216 }));
3217 }
3218 }
3219
3220 // No search context — return parsed flags for caller dispatch.
3221 Some(getarg_out::Flags { flags, rest: pat })
3222}
3223
3224/// Port of `getindex(char **pptr, Value v, int scanflags)` from `Src/params.c:2001`. Returns 0 on
3225/// success, non-zero on parse error. C body parses `[N]`/`[N,M]`/
3226/// `[(flags)pat]` after a Value's name and updates v->start/end/
3227/// scanflags. Stub: needs subscript expression evaluator.
3228/// Direct port of `int getindex(char **pptr, Value v, int
3229/// scanflags)` from `Src/params.c:2001-2167`. Parses the bracket
3230/// subscript after a Value's name and updates v->start/v->end/
3231/// v->scanflags. Returns 0 on success, 1 on parse error.
3232///
3233/// Handles:
3234/// - `[*]` / `[@]` — full range, with `[@]` setting
3235/// SCANPM_ISVAR_AT (c:2027-2032).
3236/// - `[N]` / `[N,M]` — single index / slice via getarg.
3237/// - Inverse subscripts `[(I)pat]` (partial — falls back to
3238/// direct start/end without the MB_METACHAR inverse-offset
3239/// translation in c:2050-2090).
3240///
3241/// Deferred from full C body:
3242/// - MB_METACHARLEN-based inverse-offset translation
3243/// (c:2050-2090).
3244/// - KSH_ARRAYS / KSHZEROSUBSCRIPT non-strict option dispatch
3245/// (c:2130-2150).
3246/// - Flag-prefixed subscript forms `[(r)val]` / `[(i)val]` /
3247/// `[(I)pat]` route through getarg's separate dispatcher
3248/// because the Rust getarg has a different signature from C.
3249pub fn getindex(pptr: &mut &str, v: &mut value, scanflags: i32) -> i32 {
3250 // c:2001
3251
3252 let s = *pptr;
3253 // c:2006 — `*s++ = '['`. Caller asserts s[0] is '[' (or its
3254 // tokenised form Inbrack); skip it.
3255 if s.is_empty() || (s.as_bytes()[0] != b'[' && s.as_bytes()[0] != 0xa9) {
3256 return 1;
3257 }
3258 let after_lbrack = &s[1..];
3259
3260 // c:2008 — `parse_subscript(s, dq, ']')`. Routes through the
3261 // existing lex-layer port at `crate::ported::lex::parse_subscript`
3262 // which honours `[...]` / `(...)` / `{...}` nesting and single/
3263 // double quoting (parse/src/lex.rs:3074).
3264 let close_pos = parse_subscript(after_lbrack, ']');
3265 let close_pos = match close_pos {
3266 Some(p) => p,
3267 None => {
3268 // c:2020 — `zerr("invalid subscript")`.
3269 zerr("invalid subscript");
3270 *pptr = ""; // c:2021
3271 return 1; // c:2022
3272 }
3273 };
3274 let body = &after_lbrack[..close_pos];
3275
3276 // c:2027 — special-case `[*]` / `[@]`.
3277 if body == "*" || body == "@" {
3278 if body == "@" && (v.scanflags != 0 || v.pm.is_none()) {
3279 // c:2028
3280 v.scanflags |= SCANPM_ISVAR_AT as i32; // c:2029
3281 }
3282 v.start = 0; // c:2030
3283 v.end = -1; // c:2031
3284 // c:2156 — `*tbrack = ']'; *pptr = s` (s points past `]`).
3285 *pptr = &after_lbrack[close_pos + 1..];
3286 return 0; // c:2160
3287 }
3288
3289 let _ = scanflags;
3290 // c:2058-2114 — flag subscripts `[(i)pat]` / `[(I)pat]` on an
3291 // array. The numeric parser below can't handle these; route
3292 // through getarg (c:2058 `getarg(&s, &inv, v, 0, ...)`). The
3293 // index-returning reverse flags (i/I) yield the 1-based array
3294 // index of the match (len+1 / 0 when not found). C's getindex
3295 // inv branch (c:2110-2114) turns that into a single-element
3296 // access: `v->valflags |= VALFLAG_INV; v->scanflags = 0;
3297 // v->start = start; v->end = start + 1`. issetvar then range-
3298 // checks `end` against the array length, so `arr[(i)x]` (index
3299 // = len+1, out of range) correctly reports unset. Non-index
3300 // flags (r/R/k/K) still fall through to the substitution
3301 // pipeline's getarg.
3302 // c:1597-1615 — hash subscript resolution. For a PM_HASHED param
3303 // the subscript names a key (or a flag search over keys/values).
3304 // C resolves it to the element's own pm (`ht->getnode` / a fresh
3305 // PM_SCALAR|PM_UNSET on a miss) so issetvar's PM_UNSET check
3306 // (c:765) reports the *element's* set-ness, not the hash's. zshrs
3307 // keeps assoc values in `paramtab_hashed_storage` keyed by name;
3308 // resolve existence here and shape `v` so issetvar returns the
3309 // right answer: found → leave the (set) hash pm with no slice
3310 // (c:765 `!slice && !PM_UNSET` → 1); missing → clear `v.pm` so
3311 // issetvar's `getvalue` NULL-pm guard (c:761) returns 0.
3312 if let Some(p) = v.pm.as_ref() {
3313 if PM_TYPE(p.node.flags as u32) == PM_HASHED {
3314 let name = p.node.nam.clone();
3315 let map: IndexMap<String, String> = paramtab_hashed_storage()
3316 .lock()
3317 .unwrap()
3318 .get(&name)
3319 .cloned()
3320 .unwrap_or_default();
3321 let exists = if body.starts_with('(') {
3322 // Flag search (i/I over keys, etc.) — getarg returns the
3323 // matched key/value, empty when nothing matched.
3324 match getarg(body, None, Some(&map), None) {
3325 Some(getarg_out::Value(val)) => !val.to_str().is_empty(),
3326 _ => false,
3327 }
3328 } else {
3329 map.contains_key(body)
3330 };
3331 *pptr = &after_lbrack[close_pos + 1..];
3332 if exists {
3333 v.scanflags = 0;
3334 v.start = 0;
3335 v.end = -1;
3336 } else {
3337 v.pm = None;
3338 }
3339 return 0;
3340 }
3341 }
3342 if body.starts_with('(') {
3343 let pm_is_array =
3344 v.pm.as_ref()
3345 .map_or(false, |p| PM_TYPE(p.node.flags as u32) == PM_ARRAY);
3346 if pm_is_array {
3347 let flag_part = &body[1..body.find(')').unwrap_or(1)];
3348 let return_index = flag_part.contains('i') || flag_part.contains('I');
3349 if return_index {
3350 let arr: Vec<String> = v.pm.as_ref().map(|p| arrgetfn(p)).unwrap_or_default();
3351 if let Some(getarg_out::Value(val)) = getarg(body, Some(&arr), None, None) {
3352 if let Ok(idx) = val.to_str().parse::<i64>() {
3353 // c:2110-2114 — inv branch single-element access.
3354 v.valflags |= VALFLAG_INV;
3355 v.scanflags = 0;
3356 v.start = idx as i32;
3357 v.end = (idx + 1) as i32;
3358 *pptr = &after_lbrack[close_pos + 1..];
3359 return 0;
3360 }
3361 }
3362 }
3363 }
3364 }
3365 // c:2035-2040 — general path: getarg() would parse the start
3366 // index. The Rust `getarg` has a different signature (flag
3367 // dispatcher returning getarg_out, not C's char**+int*+zlong
3368 // out-params), so the bracket-subscript here inline-parses
3369 // the simple cases: `N`, `N,M`, `-N`. Flag-based subscripts
3370 // (`[(I)pat]`, `[(r)val]`) still route through getarg
3371 // separately when called by the substitution pipeline.
3372
3373 let (start_str, end_str) = match body.split_once(',') {
3374 Some((a, b)) => (a, Some(b)),
3375 None => (body, None),
3376 };
3377 let start: i64 = match start_str.parse() {
3378 Ok(n) => n,
3379 Err(_) => {
3380 // Non-numeric subscript — leave v unchanged, advance past `]`.
3381 *pptr = &after_lbrack[close_pos + 1..];
3382 return 0;
3383 }
3384 };
3385 let mut end: i64 = match end_str {
3386 Some(s) => match s.parse() {
3387 Ok(n) => n,
3388 Err(_) => {
3389 *pptr = &after_lbrack[close_pos + 1..];
3390 return 0;
3391 }
3392 },
3393 None => start,
3394 };
3395
3396 // c:2125 — `if (start > 0) start -= startprevlen`. Without
3397 // multibyte support this is a no-op for ASCII.
3398 let mut start = start;
3399 let com = end_str.is_some() || start != end;
3400
3401 if start == 0 && end == 0 {
3402 // c:2126
3403 // c:2134 — `if (isset(KSHZEROSUBSCRIPT))` non-strict mode.
3404 // Treats `a[0]` as the first element (end = startnextlen,
3405 // which is 1 for ASCII). c:2141-2150 strict mode keeps the
3406 // VALFLAG_EMPTY + start=-1 sentinel for empty access.
3407 if crate::ported::zsh_h::isset(crate::ported::zsh_h::KSHZEROSUBSCRIPT) {
3408 end = 1; // c:2140 — `end = startnextlen` (1 for ASCII)
3409 } else {
3410 v.valflags |= VALFLAG_EMPTY; // c:2147
3411 start = -1; // c:2148
3412 }
3413 }
3414 // c:2156-2158 — clear scanflags for non-comma simple subscript
3415 // when match flags absent.
3416 if v.scanflags != 0
3417 && !com
3418 && (v.scanflags as u32 & SCANPM_MATCHMANY == 0
3419 || v.scanflags as u32 & (SCANPM_MATCHKEY | SCANPM_MATCHVAL | SCANPM_KEYMATCH) == 0)
3420 {
3421 v.scanflags = 0;
3422 }
3423 let _ = (SCANPM_ISVAR_AT, SCANPM_WANTINDEX, VALFLAG_INV);
3424 v.start = start as i32; // c:2159
3425 v.end = end as i32; // c:2160
3426
3427 // c:2164-2165 — advance `*pptr` past the close bracket.
3428 *pptr = &after_lbrack[close_pos + 1..];
3429 0 // c:2166
3430}
3431
3432/// Port of `getvalue(Value v, char **pptr, int bracks)` from `Src/params.c:2173`. C body:
3433/// `return fetchvalue(v, pptr, bracks, SCANPM_CHECKING);` — pure
3434/// wrapper around `fetchvalue` with the SCANPM_CHECKING flag set
3435/// so unset params don't trigger creation.
3436pub fn getvalue<'a>(
3437 v: Option<&'a mut value>,
3438 pptr: &mut &str,
3439 bracks: i32,
3440) -> Option<&'a mut value> {
3441 fetchvalue(v, pptr, bracks, SCANPM_CHECKING as i32)
3442}
3443
3444/// Direct port of `Value fetchvalue(Value v, char **pptr,
3445/// int bracks, int scanflags)` from `Src/params.c:2180-2282`.
3446///
3447/// Walks the parameter expression starting at `*pptr`, consuming
3448/// the identifier (or special-char like `?`/`#`/`$`/`!`/`@`/`*`/
3449/// `-`) and updating `*pptr` to point past the name. Looks up the
3450/// param in paramtab and populates the Value's pm/start/end/
3451/// scanflags fields.
3452///
3453/// Currently a partial port: identifier + special-char + digit
3454/// names are parsed and looked up. Nameref resolution
3455/// (PM_NAMEREF path at c:2246-2270), bracket subscripts
3456/// (`getindex` at c:2288), and the SCANPM_ARRONLY scanflags
3457/// promotion for hash/array params are handled. The
3458/// REFSLICE/upscope path for nameref-of-array-element is deferred
3459/// pending the GETREFNAME/upscope ports.
3460pub fn fetchvalue<'a>(
3461 // c:2180
3462 v: Option<&'a mut value>,
3463 pptr: &mut &str,
3464 bracks: i32,
3465 scanflags: i32,
3466) -> Option<&'a mut value> {
3467 let s = *pptr;
3468 let bytes = s.as_bytes();
3469 if bytes.is_empty() {
3470 return None; // c:2214 fall-through
3471 }
3472 let c = bytes[0];
3473 let mut ppar: i32 = 0;
3474 let mut end_pos = 0usize;
3475
3476 if c.is_ascii_digit() {
3477 // c:2190
3478 // c:2191-2194 — zstrtol parse of positional parameter index.
3479 if bracks >= 0 {
3480 let mut idx = 0;
3481 while idx < bytes.len() && bytes[idx].is_ascii_digit() {
3482 ppar = ppar * 10 + (bytes[idx] - b'0') as i32;
3483 idx += 1;
3484 }
3485 end_pos = idx;
3486 } else {
3487 // c:2194 — single-digit positional ($0..$9 short form).
3488 ppar = (c - b'0') as i32;
3489 end_pos = 1;
3490 }
3491 } else if itype_end(s, crate::ported::ztype_h::IIDENT as u32, false) > 0 {
3492 // c:2196 — `itype_end(s, IIDENT, 0)` — walk identifier chars.
3493 end_pos = itype_end(s, crate::ported::ztype_h::IIDENT as u32, false);
3494 } else if matches!(c, b'?' | b'#' | b'$' | b'!' | b'@' | b'*' | b'-') {
3495 // c:2198-2210
3496 end_pos = 1;
3497 } else {
3498 return None; // c:2213
3499 }
3500
3501 let name = &s[..end_pos];
3502 *pptr = &s[end_pos..];
3503
3504 if ppar > 0 {
3505 // c:2217-2225 positional
3506 if let Some(v) = v {
3507 *v = value {
3508 pm: None,
3509 arr: Vec::new(),
3510 scanflags: 0,
3511 valflags: 0,
3512 start: ppar - 1,
3513 end: ppar,
3514 };
3515 return Some(v);
3516 }
3517 return None;
3518 }
3519
3520 // c:2227-2236 — paramtab lookup honouring SCANPM_NONAMEREF for
3521 // getnode vs getnode2 (the second skips nameref resolution).
3522 let pm = {
3523 let tab = paramtab().read().unwrap();
3524 let key = if name == "0" { "0" } else { name };
3525 tab.get(key).cloned()
3526 };
3527 let pm = pm?; // c:2237-2241
3528
3529 // c:2241-2243 — `if (PM_UNSET && !PM_DECLARED) return NULL`.
3530 if pm.node.flags & PM_UNSET as i32 != 0 && pm.node.flags & PM_DECLARED as i32 == 0 {
3531 return None;
3532 }
3533
3534 // c:2246-2270 — nameref deref. Partially handled: we route
3535 // through resolve_nameref if PM_NAMEREF is set and the caller
3536 // didn't pass SCANPM_NONAMEREF.
3537 let pm = if pm.node.flags & PM_NAMEREF as i32 != 0 && (scanflags as u32) & SCANPM_NONAMEREF == 0
3538 {
3539 resolve_nameref(Some(pm))?
3540 } else {
3541 pm
3542 };
3543
3544 if let Some(v) = v {
3545 // c:2274-2282 — populate Value from pm.
3546 *v = value {
3547 pm: Some(pm.clone()),
3548 arr: Vec::new(),
3549 scanflags: 0,
3550 valflags: 0,
3551 start: 0,
3552 end: -1,
3553 };
3554 let pmflags = pm.node.flags;
3555 let isvar_at = name == "@";
3556 if PM_TYPE(pmflags as u32) & (PM_ARRAY | PM_HASHED) != 0 {
3557 // c:2274-2280 — scanflags overload for hashed arrays.
3558 let mut sf = scanflags;
3559 if isvar_at {
3560 sf |= SCANPM_ISVAR_AT as i32;
3561 }
3562 if sf == 0 {
3563 sf = SCANPM_ARRONLY as i32;
3564 }
3565 v.scanflags = sf;
3566 }
3567 // c:2289-2293 — bracket-subscript dispatch. When the unparsed
3568 // remainder starts with `[` (or the lexer's `Inbrack` token),
3569 // hand off to `getindex` which fills `v.start`/`v.end`/
3570 // `v.scanflags` and advances `pptr`.
3571 if bracks > 0 && (pptr.starts_with('[') || pptr.starts_with(Inbrack)) {
3572 if getindex(pptr, v, scanflags) != 0 {
3573 // c:2290
3574 return Some(v); // c:2292
3575 }
3576 } else if (scanflags & SCANPM_ASSIGNING as i32) == 0 && v.scanflags != 0 && isset(KSHARRAYS)
3577 {
3578 // c:2294-2296 — KSHARRAYS implicit `[0]` for bare arr.
3579 v.end = 1;
3580 v.scanflags = 0;
3581 } else {
3582 }
3583 return Some(v);
3584 }
3585 None
3586}
3587
3588/// Port of `getstrvalue(Value v)` from `Src/params.c:2335`.
3589/// Full C body dispatches on `PM_TYPE(v->pm->node.flags)`:
3590/// PM_HASHED (KSH path: `[0]` index lookup), PM_ARRAY (sepjoin
3591/// when v->scanflags else `ss[v->start]`), PM_INTEGER (`convbase`),
3592/// PM_EFLOAT|PM_FFLOAT (`convfloat`), PM_SCALAR|PM_NAMEREF
3593/// (`pm->gsu.s->getfn(pm)`). Then PM_LEFT/PM_RIGHT_B/PM_RIGHT_Z
3594/// padding when VALFLAG_SUBST is set.
3595pub fn getstrvalue(v: Option<&mut value>) -> String {
3596 let v = match v {
3597 Some(v) => v,
3598 None => return String::new(),
3599 };
3600 // c:2344-2348 — `if (VALFLAG_INV && !PM_HASHED) return sprintf("%d", v->start)`.
3601 if (v.valflags & VALFLAG_INV) != 0 {
3602 let hashed =
3603 v.pm.as_ref()
3604 .map(|p| (p.node.flags as u32 & PM_HASHED) != 0)
3605 .unwrap_or(false);
3606 if !hashed {
3607 return v.start.to_string();
3608 }
3609 }
3610 let pm = match v.pm.as_mut() {
3611 Some(p) => p,
3612 None => return String::new(),
3613 };
3614 let t = PM_TYPE(pm.node.flags as u32);
3615 let pmflags = pm.node.flags as u32;
3616
3617 // c:2350-2370 — PM_TYPE dispatch.
3618 let mut s: String = if t == PM_HASHED && v.scanflags == 0 && EMULATION(EMULATE_KSH) {
3619 // c:Src/params.c:2351-2358 — `case PM_HASHED: if (!v->scanflags &&
3620 // EMULATION(EMULATE_KSH))` — a bare `$assoc` (no subscript) under
3621 // KSH EMULATION is `${assoc[0]}`: it builds `s = "[0]"`, does a
3622 // KEY-"0" `getindex`, and returns that element's value. So it is
3623 // EMPTY unless the hash actually holds a key "0". This is
3624 // emulation-gated, NOT KSHARRAYS-option-gated: `emulate -L ksh;
3625 // typeset -A h=(a 1 b 2); print $h` is empty, whereas `setopt
3626 // ksharrays; …; print $h` falls through below to the first bucket
3627 // value `1`. (`setopt ksharrays` keeps v->scanflags non-zero for a
3628 // bare assoc, so it never reaches this arm; the `scanflags==0`
3629 // guard mirrors the C `!v->scanflags` "impossible unless emulating
3630 // ksh" invariant.)
3631 paramtab_hashed_storage()
3632 .lock()
3633 .ok()
3634 .and_then(|store| store.get(&pm.node.nam).and_then(|m| m.get("0").cloned()))
3635 .unwrap_or_default()
3636 } else if t == PM_HASHED || t == PM_ARRAY {
3637 // c:2359-2369 (PM_ARRAY, and the ksh-emulation PM_HASHED fall-through)
3638 let arr = arrgetfn(pm);
3639 if v.scanflags != 0 {
3640 // c:2361
3641 arr.join(" ")
3642 } else {
3643 let mut start = v.start;
3644 if start < 0 {
3645 start += arr.len() as i32;
3646 } // c:2364
3647 if start < 0 || (start as usize) >= arr.len() {
3648 // c:2365-2366
3649 String::new()
3650 } else {
3651 arr[start as usize].clone()
3652 }
3653 }
3654 } else if t == PM_INTEGER {
3655 // c:2371
3656 // c:2373 — `convbase(buf, pm->gsu.i->getfn(pm), pm->base)`.
3657 // The previous Rust port used `intgetfn(pm).to_string()` (naked
3658 // base-10). With `convbase` now ported (params.rs:6577), honor
3659 // `pm.base` so `typeset -i 16 x=255` renders as `0xff` rather
3660 // than `255` per zsh's `$x`-expansion + `typeset -p`.
3661 convbase_underscore(
3662 intgetfn(pm),
3663 if pm.base > 0 { pm.base } else { 10 }, // c:2373 pm->base
3664 pm.width, // c:2373 pm->width for underscore grouping
3665 )
3666 } else if t == PM_EFLOAT || t == PM_FFLOAT {
3667 // c:2366-2370 — `convfloat(getfn(pm), pm->base, pm->node.flags,
3668 // NULL)`. The format flag (PM_EFLOAT → e-notation, PM_FFLOAT →
3669 // fixed) and the precision (`pm->base` holds digits for floats)
3670 // BOTH come straight from the param. The previous Rust port used
3671 // `convfloat_underscore(floatgetfn(pm), pm.width)`, which drops
3672 // the format flag (convfloat_underscore calls `convfloat(d,0,0)`
3673 // → 'g' default) and wrongly applies width grouping that C does
3674 // NOT do here (underscore grouping happens in typeset -p output
3675 // c:2866, not getstrvalue). That made getstrvalue render a
3676 // default `float f=2.5` as "2.5" instead of "2.500000000e+00",
3677 // surfacing when `assignaparam`'s AUGMENT prepend (c:3350) routes
3678 // an old float value through getstrvalue.
3679 convfloat(floatgetfn(pm), pm.base, pm.node.flags as u32)
3680 } else if t == PM_SCALAR || t == PM_NAMEREF {
3681 // c:2380
3682 strgetfn(pm)
3683 } else {
3684 // c:2384
3685 DPUTS!(true, "BUG: param node without valid type"); // c:2385
3686 String::new() // c:2386 s = "" (line c:2384)
3687 };
3688
3689 // c:2390-2538 — VALFLAG_SUBST padding (PM_LEFT / PM_RIGHT_B /
3690 // PM_RIGHT_Z). Multibyte is approximated via `chars().count()`
3691 // (codepoint count) since the Rust port stores strings as
3692 // UTF-8 rather than the C meta-byte encoding.
3693 if v.valflags & VALFLAG_SUBST != 0 {
3694 let pad_flags = pmflags & (PM_LEFT | PM_RIGHT_B | PM_RIGHT_Z);
3695 if pad_flags != 0 {
3696 let fwidth = if pm.width > 0 {
3697 pm.width as usize
3698 } else {
3699 s.chars().count()
3700 };
3701 if pad_flags == PM_LEFT || pad_flags == (PM_LEFT | PM_RIGHT_Z) {
3702 // c:2393-2424 — left-justify: optional zero/blank trim,
3703 // truncate to fwidth, right-pad with spaces.
3704 let trimmed: &str = if pad_flags & PM_RIGHT_Z != 0 {
3705 s.trim_start_matches('0')
3706 } else {
3707 s.trim_start_matches(|c: char| c == ' ' || c == '\t')
3708 };
3709 let len = trimmed.chars().count();
3710 let take = len.min(fwidth);
3711 let mut out: String = trimmed.chars().take(take).collect();
3712 if fwidth > take {
3713 out.extend(std::iter::repeat(' ').take(fwidth - take));
3714 }
3715 s = out;
3716 } else if pad_flags & (PM_RIGHT_B | PM_RIGHT_Z) != 0 {
3717 // c:2426-2510 — right-justify with optional zero-padding
3718 // honouring leading-blank/minus/0x/base# prefix
3719 // detection for numeric values.
3720 let charlen = s.chars().count();
3721 if charlen < fwidth {
3722 let mut zero = true;
3723 let mut valprefend: usize = 0;
3724 let numeric_pm = (pmflags & (PM_INTEGER | PM_EFLOAT | PM_FFLOAT)) != 0;
3725 if pad_flags & PM_RIGHT_Z != 0 {
3726 // c:2446-2466 — find the prefix to keep
3727 // (blanks → minus → 0x / base#).
3728 let bytes = s.as_bytes();
3729 let mut t = 0usize;
3730 while t < bytes.len() && (bytes[t] == b' ' || bytes[t] == b'\t') {
3731 t += 1; // c:2446-2447
3732 }
3733 if numeric_pm && t < bytes.len() && bytes[t] == b'-' {
3734 t += 1; // c:2454-2455
3735 }
3736 if (pmflags & PM_INTEGER) != 0 {
3737 let cbases = optlookup("cbases") > 0;
3738 if cbases
3739 && t + 1 < bytes.len()
3740 && bytes[t] == b'0'
3741 && bytes[t + 1] == b'x'
3742 {
3743 t += 2; // c:2462-2463
3744 } else if let Some(hash_off) =
3745 bytes[t..].iter().position(|&b| b == b'#')
3746 {
3747 t += hash_off + 1; // c:2464-2465
3748 }
3749 }
3750 valprefend = t;
3751 if t == bytes.len() {
3752 zero = false; // c:2468-2469
3753 } else if !numeric_pm && !bytes[t].is_ascii_digit() {
3754 zero = false; // c:2473-2474
3755 }
3756 }
3757 // c:2483 — pad char picks: ' ' if PM_RIGHT_B or
3758 // numeric-prefix detection failed, else '0'.
3759 let pad_char = if (pad_flags & PM_RIGHT_B) != 0 || !zero {
3760 ' '
3761 } else {
3762 '0'
3763 };
3764 let need = fwidth - charlen;
3765 let prefix = &s[..valprefend];
3766 let rest = &s[valprefend..];
3767 let mut out = String::with_capacity(need + s.len());
3768 out.push_str(prefix); // c:2491
3769 out.extend(std::iter::repeat(pad_char).take(need)); // c:2483-2485
3770 out.push_str(rest); // c:2492-2493
3771 s = out;
3772 } else if charlen > fwidth {
3773 // c:2496-2500 — truncate from the front to fit fwidth
3774 // codepoints (C uses MB_METACHARLEN; Rust uses chars).
3775 let skip = charlen - fwidth;
3776 s = s.chars().skip(skip).collect();
3777 }
3778 }
3779 }
3780 }
3781
3782 s
3783}
3784
3785/// Slice an indexed array using zsh 1-based inclusive semantics.
3786/// Port of `getarrvalue(Value v)` from Src/params.c:2548 — the slice
3787/// branch that resolves the start/end pair into a Vec. Negative
3788/// indices count from the end (`-1` is the last element);
3789/// out-of-range bounds collapse to empty (`${a[5,10]}` on len=3
3790/// returns empty, not clamped); `start > end` returns empty.
3791///
3792/// 0 has asymmetric meaning per C source's getarrvalue:
3793/// start=0 → "before first element" → resolved to 1
3794/// end=0 → "before first element" → empty slice
3795/// WARNING: param names don't match C — Rust=(arr, start, end) vs C=(v)
3796pub fn getarrvalue(arr: &[String], start: i64, end: i64) -> Vec<String> {
3797 let len = arr.len() as i64;
3798 // c:Src/params.c:2570-2585 getarrvalue — a slice whose START is at or
3799 // beyond the array end does NOT collapse to an empty array; it returns
3800 // EMPTY-STRING padding from the global `nular` (a one-element `{"", NULL}`
3801 // array), so the result is capped at a SINGLE empty element. The C
3802 // branch order (after `v->start += arrlen` / `v->end += arrlen + 1` for
3803 // negatives) is: `v->end <= v->start` → 0 elems; else `v->start < 0` → 1
3804 // elem; else `arrlen_le(s, v->start)` → `v->end - (v->start + 1)` elems
3805 // (capped by nular's length to ≤ 1). Only observable via `${#}` (counts
3806 // the empty element → `${#arr[1,2]}` is 1 for `arr=()`) and quoted splat;
3807 // unquoted array-capture elides the empty word. Mirror it here so the
3808 // out-of-range cases that previously returned `[]` pad correctly.
3809 let nular_pad = || -> Vec<String> {
3810 let v_start0 = start - 1; // C v->start is 0-based (subscript - 1)
3811 let v_end = if end < 0 { end + len + 1 } else { end }; // c:2565
3812 let v_start = if v_start0 < 0 { v_start0 + len } else { v_start0 }; // c:2563
3813 if v_end <= v_start {
3814 Vec::new() // c:2578 (empty/inverted range)
3815 } else if v_start < 0 {
3816 vec![String::new()] // c:2580 (arrdup_max(nular, 1))
3817 } else if v_start >= len {
3818 // c:2582 — arrdup_max(nular, v->end - (v->start + 1)); nular has a
3819 // single element, so the count is capped at 1.
3820 if v_end - (v_start + 1) >= 1 {
3821 vec![String::new()]
3822 } else {
3823 Vec::new()
3824 }
3825 } else {
3826 Vec::new()
3827 }
3828 };
3829 if len == 0 {
3830 return nular_pad();
3831 }
3832 // Out-of-range starts (positive past len, or negative below
3833 // -len) return nular padding per Src/params.c getarrvalue's
3834 // slice-resolution branches (c:2570-2585).
3835 if start > len {
3836 return nular_pad();
3837 }
3838 if end < 0 && (len + end + 1) < 1 {
3839 return nular_pad();
3840 }
3841 if start < 0 && end < 0 && start > end {
3842 return nular_pad();
3843 }
3844 if start < 0 && start < -len {
3845 return nular_pad();
3846 }
3847 let resolve_start = |i: i64| -> i64 {
3848 if i < 0 {
3849 (len + i + 1).max(1)
3850 } else if i == 0 {
3851 1
3852 } else {
3853 i.min(len)
3854 }
3855 };
3856 let resolve_end = |i: i64| -> i64 {
3857 if i < 0 {
3858 (len + i + 1).max(0)
3859 } else if i == 0 {
3860 0
3861 } else {
3862 i.min(len)
3863 }
3864 };
3865 let s = resolve_start(start);
3866 let e = resolve_end(end);
3867 if e < 1 || s > e {
3868 return Vec::new();
3869 }
3870 let s_idx = (s - 1) as usize;
3871 let e_idx = e as usize;
3872 arr[s_idx..e_idx.min(arr.len())].to_vec()
3873}
3874
3875// ---------------------------------------------------------------------------
3876// Parameter table
3877// ---------------------------------------------------------------------------
3878
3879/// Parameter table.
3880/// Port of the `paramtab` HashTable Src/params.c maintains —
3881/// `createparamtable()` (line 817) initializes it with all the
3882/// IPDEF*-declared special params; `createparam()` (line 1030)
3883/// adds user variables.
3884// ---------------------------------------------------------------------------
3885// Free functions matching the C API
3886// ---------------------------------------------------------------------------
3887
3888/// Port of `getintvalue(Value v)` from `Src/params.c:2601`.
3889/// C body:
3890/// ```c
3891/// if (!v) return 0;
3892/// if (v->valflags & VALFLAG_INV) return v->start;
3893/// if (v->scanflags) {
3894/// char **arr = getarrvalue(v);
3895/// if (arr) { char *scal = sepjoin(arr, NULL, 1); return mathevali(scal); }
3896/// return 0;
3897/// }
3898/// if (PM_TYPE(v->pm->node.flags) == PM_INTEGER)
3899/// return v->pm->gsu.i->getfn(v->pm);
3900/// if (v->pm->node.flags & (PM_EFLOAT|PM_FFLOAT))
3901/// return (zlong)v->pm->gsu.f->getfn(v->pm);
3902/// return mathevali(getstrvalue(v));
3903/// ```
3904pub fn getintvalue(v: Option<&mut value>) -> i64 {
3905 let v = match v {
3906 Some(v) => v,
3907 None => return 0,
3908 };
3909 if (v.valflags & VALFLAG_INV) != 0 {
3910 return v.start as i64;
3911 }
3912 if v.scanflags != 0 {
3913 // sepjoin(arr, NULL, 1) → mathevali(scal); arr backend missing.
3914 return 0;
3915 }
3916 let pm = match v.pm.as_mut() {
3917 Some(p) => p,
3918 None => return 0,
3919 };
3920 if PM_TYPE(pm.node.flags as u32) == PM_INTEGER {
3921 return intgetfn(pm);
3922 }
3923 if (pm.node.flags as u32 & (PM_EFLOAT | PM_FFLOAT)) != 0 {
3924 return floatgetfn(pm) as i64;
3925 }
3926 // c:2618 — `return mathevali(getstrvalue(v));`. The previous
3927 // Rust port used `s.parse::<i64>().unwrap_or(0)` which silently
3928 // returned 0 for any non-trivial arithmetic on the scalar
3929 // value side (e.g. `typeset x="1+2"; ((y = x))` would yield
3930 // y=0 instead of 3). Route through `math::mathevali` to
3931 // match C's arithmetic-expression evaluation.
3932 let pm = v.pm.as_mut().unwrap();
3933 let s = strgetfn(pm);
3934 mathevali(&s).unwrap_or(0) // c:2618 mathevali(...)
3935}
3936
3937/// Port of `getnumvalue(Value v)` from `Src/params.c:2624`. Returns an
3938/// `mnumber` (tagged int/float). C body dispatches on `valflags &
3939/// VALFLAG_INV` (returns start as int), `scanflags` (sepjoin →
3940/// matheval), then PM_TYPE: PM_INTEGER → mn.l = pm->gsu.i->getfn,
3941/// PM_EFLOAT|PM_FFLOAT → mn.type=MN_FLOAT; mn.d = pm->gsu.f->getfn,
3942/// else matheval(getstrvalue(v)).
3943pub fn getnumvalue(v: Option<&mut value>) -> mnumber {
3944 let v = match v {
3945 Some(v) => v,
3946 None => {
3947 return mnumber {
3948 l: 0,
3949 d: 0.0,
3950 type_: MN_INTEGER,
3951 }
3952 }
3953 };
3954 if (v.valflags & VALFLAG_INV) != 0 {
3955 return mnumber {
3956 l: v.start as i64,
3957 d: 0.0,
3958 type_: MN_INTEGER,
3959 };
3960 }
3961 if v.scanflags != 0 {
3962 return mnumber {
3963 l: 0,
3964 d: 0.0,
3965 type_: MN_INTEGER,
3966 };
3967 }
3968 let pm = match v.pm.as_mut() {
3969 Some(p) => p,
3970 None => {
3971 return mnumber {
3972 l: 0,
3973 d: 0.0,
3974 type_: MN_INTEGER,
3975 }
3976 }
3977 };
3978 let t = PM_TYPE(pm.node.flags as u32);
3979 if t == PM_INTEGER {
3980 return mnumber {
3981 l: intgetfn(pm),
3982 d: 0.0,
3983 type_: MN_INTEGER,
3984 };
3985 }
3986 if t == PM_EFLOAT || t == PM_FFLOAT {
3987 return mnumber {
3988 l: 0,
3989 d: floatgetfn(pm),
3990 type_: MN_FLOAT,
3991 };
3992 }
3993 // c:2640 — `return matheval(getstrvalue(v));`. The previous
3994 // Rust port used `parse::<i64>()` / `parse::<f64>()` directly
3995 // on the scalar string, which silently failed for any non-
3996 // trivial arithmetic. Route through `math::matheval` to match
3997 // C's arithmetic-expression evaluation; matheval returns an
3998 // mnumber tag matching the C output type.
3999 let s = strgetfn(pm);
4000 matheval(&s) // c:2640 matheval(...)
4001 .unwrap_or(mnumber {
4002 l: 0,
4003 d: 0.0,
4004 type_: MN_INTEGER,
4005 })
4006}
4007
4008/// Port of `export_param(Param pm)` from `Src/params.c:2653`.
4009///
4010/// C body converts `pm`'s value to its scalar form per `PM_TYPE`:
4011/// PM_INTEGER: convbase(buf, getfn, pm->base)
4012/// PM_EFLOAT/FFLOAT: convfloat(getfn, pm->base, pm->node.flags, NULL)
4013/// PM_SCALAR/etc.: gsu.s->getfn(pm)
4014/// Then calls `addenv(pm, val)`. PM_ARRAY/PM_HASHED early-return.
4015///
4016/// The previous Rust port used `format!("{}", intgetfn(pm))` for
4017/// integers and `format!("{}", floatgetfn(pm))` for floats — Rust's
4018/// DEFAULT formatting. C uses convbase/convfloat which respect
4019/// `pm.base` and `pm.flags`:
4020/// - `typeset -i16 x=255; export x` should put "16#FF" in the
4021/// env (per pm.base==16). The previous Rust port wrote "255".
4022/// - `typeset -F3 y=3.14; export y` should put "3.140" (per
4023/// pm.base==3 precision + PM_FFLOAT flag). Rust wrote "3.14".
4024///
4025/// Both formatter ports exist (`params::convbase`, `utils::convfloat`).
4026/// Wire them so the env-side representation matches C.
4027pub fn export_param(pm: &mut param) {
4028 // c:2653
4029 let t = PM_TYPE(pm.node.flags as u32);
4030 if (t & (PM_ARRAY | PM_HASHED)) != 0 {
4031 // c:2659 array/hash skip
4032 return;
4033 }
4034 let val: String = if t == PM_INTEGER {
4035 // c:2664 — `convbase(buf, pm->gsu.i->getfn(pm), pm->base)`.
4036 let base = if pm.base > 0 { pm.base } else { 10 };
4037 convbase(intgetfn(pm), base as u32) // c:2664
4038 } else if (pm.node.flags as u32 & (PM_EFLOAT | PM_FFLOAT)) != 0 {
4039 // c:2668 — `convfloat(pm->gsu.f->getfn(pm), pm->base,
4040 // pm->node.flags, NULL)`.
4041 convfloat(floatgetfn(pm), pm.base, pm.node.flags as u32)
4042 // c:2668
4043 } else {
4044 strgetfn(pm)
4045 };
4046 addenv(&pm.node.nam, &val);
4047 pm.env = Some(val);
4048}
4049
4050/// Port of `setstrvalue(Value v, char *val)` from `Src/params.c:2685`. C body is a
4051/// one-liner: `assignstrvalue(v, val, 0);` — the real workhorse
4052/// is `assignstrvalue` (params.c:2692).
4053pub fn setstrvalue(v: Option<&mut value>, val: &str) {
4054 assignstrvalue(v, Some(val.to_string()), 0);
4055}
4056
4057/// 1:1 port of the C body covering: EXECOPT short-circuit,
4058/// PM_READONLY/PM_HASHED/VALFLAG_EMPTY guards, PM_UNSET clear,
4059/// per-PM_TYPE dispatch including the SCALAR/NAMEREF subscript
4060/// splice (KSHARRAYS-aware index normalization, MULTIBYTE end
4061/// adjust, full-string overwrite vs in-place memcpy fast path,
4062/// AUTONAMEDIRS/PM_NAMEDDIR re-registration), PM_INTEGER (with
4063/// ASSPM_ENV_IMPORT → `zstrtol_underscore`, else `mathevali`,
4064/// `lastbase` propagation), PM_EFLOAT/PM_FFLOAT (env vs `matheval`,
4065/// MN_FLOAT/MN_INTEGER coercion), PM_ARRAY (single-element wrap
4066/// via `setarrvalue`), PM_HASHED (`foundparam` indirection); then
4067/// `setscope(pm)`, errflag/env/ALLEXPORT/PM_ARRAY/ename gate, and
4068/// `export_param`. Width tracking for PM_LEFT/PM_RIGHT_B/PM_RIGHT_Z
4069/// preserved.
4070/// Port of `assignstrvalue(Value v, char *val, int flags)` from `Src/params.c:2692`.
4071pub fn assignstrvalue(v: Option<&mut value>, val: Option<String>, flags: i32) {
4072 if unset(EXECOPT) {
4073 return;
4074 }
4075
4076 let v = match v {
4077 Some(v) => v,
4078 None => return,
4079 };
4080 let pm = match v.pm.as_mut() {
4081 Some(p) => p,
4082 None => return,
4083 };
4084
4085 if (pm.node.flags as u32 & PM_READONLY) != 0 {
4086 // c:2701 — `zerr("read-only variable: %s", pm->node.nam)`.
4087 zerr(&format!("read-only variable: {}", pm.node.nam)); // c:2701
4088 return;
4089 }
4090 if (pm.node.flags as u32 & PM_HASHED) != 0
4091 && (v.scanflags as u32 & (SCANPM_MATCHMANY | SCANPM_ARRONLY)) != 0
4092 {
4093 // c:2706 — `zerr("%s: attempt to set slice of associative array", ...)`.
4094 zerr(&format!(
4095 "{}: attempt to set slice of associative array",
4096 pm.node.nam
4097 )); // c:2706
4098 return;
4099 }
4100 if (v.valflags & VALFLAG_EMPTY) != 0 {
4101 // c:2710 — `zerr("%s: assignment to invalid subscript range", ...)`.
4102 zerr(&format!(
4103 "{}: assignment to invalid subscript range",
4104 pm.node.nam
4105 )); // c:2710
4106 return;
4107 }
4108 pm.node.flags &= !(PM_UNSET as i32);
4109
4110 let mut val = val;
4111 match PM_TYPE(pm.node.flags as u32) {
4112 t if t == PM_SCALAR || t == PM_NAMEREF => {
4113 let mut v_str = val.take().unwrap_or_default();
4114 // c:Src/params.c — PM_LOWER / PM_UPPER case fold on
4115 // assignment. zsh applies these flags both when writing
4116 // the in-memory scalar and when exporting to env; the
4117 // copyenvstr path handles the export side, but the
4118 // scalar set path also needs to fold so `echo $X`
4119 // reads the lowercased value. Without this, `typeset -l
4120 // X; X=MixedCase; echo $X` printed "MixedCase".
4121 let pf = pm.node.flags as u32;
4122 if pf & PM_LOWER != 0 {
4123 v_str = v_str.to_ascii_lowercase();
4124 } else if pf & PM_UPPER != 0 {
4125 v_str = v_str.to_ascii_uppercase();
4126 }
4127 if v.start == 0 && v.end == -1 {
4128 // c:2748 — `v->pm->gsu.s->setfn(v->pm, val);`. C
4129 // dispatches through the param's GSU vtable so
4130 // PM_SPECIAL params route to their canonical setfn
4131 // (homesetfn, ifssetfn, ...). The Rust port stores
4132 // the vtable on `pm.gsu_s` (set in `createparamtable`
4133 // via `gsu_scalar_for_special`); when set, dispatch
4134 // through it. When unset, fall back to the default
4135 // `strsetfn` path (C's stdscalar_gsu.setfn).
4136 // c:2742-2746 — ASSPM_AUGMENT: prepend the existing
4137 // value before storing. Without this, `PATH+=":/foo"`
4138 // would replace PATH instead of appending.
4139 let final_str = if (flags & ASSPM_AUGMENT) != 0 {
4140 let prev = pm.u_str.clone().unwrap_or_default();
4141 format!("{}{}", prev, v_str)
4142 } else {
4143 v_str
4144 };
4145 let len = final_str.len();
4146 let setfn_ptr = pm.gsu_s.as_ref().map(|g| g.setfn);
4147 if let Some(setfn) = setfn_ptr {
4148 setfn(pm, final_str); // c:2748
4149 } else {
4150 strsetfn(pm, final_str); // c:2748 (default)
4151 }
4152 if (pm.node.flags as u32 & (PM_LEFT | PM_RIGHT_B | PM_RIGHT_Z)) != 0
4153 && pm.width == 0
4154 {
4155 pm.width = len as i32;
4156 }
4157 } else {
4158 // Subscript splice (c:Src/params.c:2748+ assignstrvalue). In C
4159 // `v->start`/`v->end` are BYTE offsets into the metafied string
4160 // (getindex already converted the character subscript to a byte
4161 // offset via MB_METACHARLEN). zshrs's subscript resolution
4162 // instead yields CHARACTER positions, so operate on the char
4163 // sequence throughout — otherwise a multibyte scalar value
4164 // (e.g. a Powerline glyph U+E0B0, 3 bytes) panics when the byte
4165 // slice `z[..start]` lands inside a codepoint. p10k's
4166 // `_p9k_get_icon` hit exactly this (crashing the shell).
4167 let z = strgetfn(pm);
4168 let z_chars: Vec<char> = z.chars().collect();
4169 let zlen = z_chars.len() as i32;
4170 let mut start = v.start;
4171 let mut end = v.end;
4172 if (v.valflags & VALFLAG_INV) != 0 && !isset(KSHARRAYS) {
4173 start -= 1;
4174 end -= 1;
4175 }
4176 if start < 0 {
4177 start += zlen;
4178 if start < 0 {
4179 start = 0;
4180 }
4181 }
4182 if start > zlen {
4183 start = zlen;
4184 }
4185 if end < 0 {
4186 end += zlen;
4187 if end < 0 {
4188 end = 0;
4189 } else if end >= zlen {
4190 end = zlen;
4191 } else {
4192 // MULTIBYTE branch: increment by metachar length;
4193 // single-byte path increments by 1.
4194 end += 1;
4195 }
4196 } else if end > zlen {
4197 end = zlen;
4198 }
4199 // Slice by CHARACTER index into z_chars (never a raw byte
4200 // slice of z, which would split a multibyte codepoint).
4201 let s = (start.max(0) as usize).min(z_chars.len());
4202 let e = (end.max(0) as usize).min(z_chars.len());
4203 let mut x = String::with_capacity(z.len() + v_str.len());
4204 x.extend(z_chars[..s].iter());
4205 x.push_str(&v_str);
4206 if e <= z_chars.len() {
4207 x.extend(z_chars[e..].iter());
4208 }
4209 strsetfn(pm, x);
4210 if (pm.node.flags as u32 & PM_HASHELEM) == 0
4211 && ((pm.node.flags as u32 & PM_NAMEDDIR) != 0 || isset(AUTONAMEDIRS))
4212 {
4213 pm.node.flags |= PM_NAMEDDIR as i32;
4214 // adduserdir(pm.node.nam, &z, 0, 0); -- userdirs not ported
4215 }
4216 }
4217 }
4218 t if t == PM_INTEGER => {
4219 if let Some(ref s) = val {
4220 let ival: i64 = if (flags & ASSPM_ENV_IMPORT) != 0 {
4221 s.parse::<i64>().unwrap_or(0)
4222 } else {
4223 // c:Src/params.c:2774 — `mathevali(val)`. C's
4224 // matheval calls zerr (Src/math.c:1462+) on parse
4225 // failures, which sets `errflag |= ERRFLAG_ERROR`
4226 // and the caller propagates the abort. The Rust
4227 // port returns Result<i64,String>; `unwrap_or(0)`
4228 // silently swallowed "operator expected at 'def'"
4229 // and stored 0 instead. Bug #75 in docs/BUGS.md.
4230 //
4231 // Mirror the C semantic: surface the error via
4232 // zerr (which sets errflag), then fall back to 0
4233 // for the stored value (C also stores whatever the
4234 // partial parse computed, which is typically 0).
4235 match mathevali(s) {
4236 Ok(v) => v,
4237 Err(msg) => {
4238 zerr(&msg);
4239 0
4240 }
4241 }
4242 };
4243 // c:2775-2778 — `if (flags & ASSPM_AUGMENT) pm->u.val += val.l;
4244 // else pm->u.val = val.l;`. The augment
4245 // path is what makes `integer x=42; x+=8` store 50 instead
4246 // of 8. Without this the integer `+=` operator silently
4247 // replaced.
4248 let final_val = if (flags & ASSPM_AUGMENT) != 0 {
4249 pm.u_val.wrapping_add(ival)
4250 } else {
4251 ival
4252 };
4253 intsetfn(pm, final_val);
4254 if (pm.node.flags as u32 & (PM_LEFT | PM_RIGHT_B | PM_RIGHT_Z)) != 0
4255 && pm.width == 0
4256 {
4257 pm.width = s.len() as i32;
4258 }
4259 if pm.base == 0 {
4260 let lb = lastbase();
4261 if lb != -1 {
4262 pm.base = lb;
4263 }
4264 }
4265 }
4266 }
4267 t if t == PM_EFLOAT || t == PM_FFLOAT => {
4268 if let Some(ref s) = val {
4269 let mn = if (flags & ASSPM_ENV_IMPORT) != 0 {
4270 mnumber {
4271 l: 0,
4272 d: s.parse::<f64>().unwrap_or(0.0),
4273 type_: MN_FLOAT,
4274 }
4275 } else {
4276 // c:Src/params.c — float assignment runs the RHS
4277 // through matheval; on parse failure, zsh emits the
4278 // engine's diagnostic ("bad math expression: operator
4279 // expected at `abc'") via zerr. Mirror the integer
4280 // arm above which already calls zerr on Err. Bug
4281 // #506 — zshrs previously swallowed the error and
4282 // stored 0.0 silently for `float f="3.14abc"`.
4283 match matheval(s) {
4284 Ok(v) => v,
4285 Err(msg) => {
4286 zerr(&msg);
4287 mnumber {
4288 l: 0,
4289 d: 0.0,
4290 type_: MN_FLOAT,
4291 }
4292 }
4293 }
4294 };
4295 let d = if (mn.type_ & MN_FLOAT) != 0 {
4296 mn.d
4297 } else {
4298 mn.l as f64
4299 };
4300 // c:2775-2778 — ASSPM_AUGMENT path: `float x=1.5; x+=0.25`
4301 // adds 0.25 to the current u_dval rather than replacing.
4302 // Mirrors the integer-augment block above; without it
4303 // `+=` was a plain `=`.
4304 let final_d = if (flags & ASSPM_AUGMENT) != 0 {
4305 pm.u_dval + d
4306 } else {
4307 d
4308 };
4309 floatsetfn(pm, final_d);
4310 if (pm.node.flags as u32 & (PM_LEFT | PM_RIGHT_B | PM_RIGHT_Z)) != 0
4311 && pm.width == 0
4312 {
4313 pm.width = s.len() as i32;
4314 }
4315 }
4316 }
4317 t if t == PM_ARRAY => {
4318 // c:2826-2828 — `char **ss = zalloc(2*sizeof(char*));
4319 // ss[0]=val; ss[1]=NULL; setarrvalue(v, ss);` — wrap the
4320 // single value in a 1-element array. The C-faithful
4321 // setarrvalue takes &mut Value; we already hold a &mut
4322 // borrow of pm from v.pm.as_mut() higher up, so inline
4323 // the dispatch directly against pm here to avoid the
4324 // double-borrow.
4325 let one = vec![val.take().unwrap_or_default()];
4326 if v.start == 0 && v.end == -1 {
4327 if isset(KSHARRAYS) {
4328 // c:Src/params.c getvalue — under KSHARRAYS a bare
4329 // array name addresses element 0 (ksh semantics), so
4330 // a SCALAR assignment `a=X` to an existing array sets
4331 // the FIRST element keeping the rest (a=(X second)),
4332 // not a whole-array replace. The non-KSHARRAYS path
4333 // already reset the array to a scalar before reaching
4334 // here (assignsparam c:3179-3192, gated on
4335 // unset(KSHARRAYS)); only the KSHARRAYS case arrives
4336 // with the param still PM_ARRAY.
4337 let arr = pm.u_arr.get_or_insert_with(Vec::new);
4338 let first = one.into_iter().next().unwrap_or_default();
4339 if arr.is_empty() {
4340 arr.push(first);
4341 } else {
4342 arr[0] = first;
4343 }
4344 } else {
4345 // c:2922 — full replace.
4346 pm.u_arr = Some(one);
4347 }
4348 } else {
4349 // c:2933+ — slice splice path with bounds adjust.
4350 let arr = pm.u_arr.get_or_insert_with(Vec::new);
4351 let len = arr.len() as i64;
4352 let start_raw = v.start as i64;
4353 let end_raw = v.end as i64;
4354 let start = if start_raw < 0 {
4355 (len + start_raw + 1).max(0)
4356 } else {
4357 start_raw
4358 };
4359 let end = if end_raw < 0 {
4360 (len + end_raw + 1).max(0)
4361 } else {
4362 end_raw
4363 };
4364 let start_idx = (start.max(1) - 1) as usize;
4365 let end_idx = end.max(0) as usize;
4366 while arr.len() < start_idx {
4367 arr.push(String::new());
4368 }
4369 let end_idx = end_idx.min(arr.len());
4370 if start_idx <= end_idx {
4371 arr.splice(start_idx..end_idx, one);
4372 } else {
4373 for (i, x) in one.into_iter().enumerate() {
4374 if start_idx + i < arr.len() {
4375 arr[start_idx + i] = x;
4376 } else {
4377 arr.push(x);
4378 }
4379 }
4380 }
4381 }
4382 }
4383 t if t == PM_HASHED => {
4384 // Element-assignment path: the C source does
4385 // `setstrvalue(&((Param)foundparam)->u, val)` to update the
4386 // member found by an earlier `scanparamvals` lookup.
4387 if let Some(nam) = foundparam() {
4388 if let Some(ref h) = pm.u_hash {
4389 let _ = (nam, h);
4390 }
4391 }
4392 set_foundparam(None);
4393 }
4394 _ => {}
4395 }
4396 setscope(pm);
4397 if errflag.load(Ordering::Relaxed) != 0
4398 || ((pm.env.is_none()
4399 && (pm.node.flags as u32 & PM_EXPORTED) == 0
4400 && !(isset(ALLEXPORT) && (pm.node.flags as u32 & PM_HASHELEM) == 0))
4401 || (pm.node.flags as u32 & PM_ARRAY) != 0
4402 || pm.ename.is_some())
4403 {
4404 return;
4405 }
4406 export_param(pm);
4407}
4408
4409/// Port of `setnumvalue(Value v, mnumber val)` from `Src/params.c:2856`. C body
4410/// dispatches on `PM_TYPE(v->pm->node.flags)`:
4411/// PM_SCALAR/PM_NAMEREF/PM_ARRAY → convbase_underscore /
4412/// convfloat_underscore + setstrvalue; PM_INTEGER →
4413/// `pm->gsu.i->setfn(pm, val.u.l)`; PM_EFLOAT|PM_FFLOAT →
4414/// `pm->gsu.f->setfn(pm, val.u.d)`. EXECOPT/PM_READONLY checks
4415/// at top.
4416pub fn setnumvalue(v: Option<&mut value>, val: mnumber) {
4417 // c:2860 — `if (unset(EXECOPT)) return;`. In NO_EXEC mode, param
4418 // mutations must be skipped so dry-run shell evaluation doesn't
4419 // leak state into the param table. The previous Rust port skipped
4420 // this check; `zsh -n -c '(( x=5 ))'` would mutate $x silently.
4421 if unset(EXECOPT) {
4422 // c:2860
4423 return;
4424 }
4425 let v = match v {
4426 Some(v) => v,
4427 None => return,
4428 };
4429 let pm = match v.pm.as_mut() {
4430 Some(p) => p,
4431 None => return,
4432 };
4433 if (pm.node.flags as u32 & PM_READONLY) != 0 {
4434 zerr(&format!("read-only variable: {}", pm.node.nam)); // c:2862
4435 return;
4436 }
4437 let t = PM_TYPE(pm.node.flags as u32);
4438 if t == PM_SCALAR || t == PM_NAMEREF || t == PM_ARRAY {
4439 // c:2862-2872 — convbase_underscore for integers (honors
4440 // pm.base for the radix prefix + pm.width for underscore
4441 // grouping), convfloat_underscore for floats. The previous
4442 // Rust port computed `val.l.to_string()` then DROPPED the
4443 // result via `let _ = s;` — meaning a numeric assignment
4444 // to a SCALAR param stored NOTHING. `typeset s; (( s = 42 ))`
4445 // would leave $s empty.
4446 let s = if (val.type_ & MN_INTEGER) != 0 {
4447 // c:2862
4448 // c:2864 — `convbase_underscore(val.u.l, pm->base, pm->width)`.
4449 convbase_underscore(val.l, if pm.base > 0 { pm.base } else { 10 }, pm.width)
4450 } else {
4451 // c:2867
4452 // c:2869 — `convfloat_underscore(val.u.d, pm->width)`.
4453 convfloat_underscore(val.d, pm.width)
4454 };
4455 pm.u_str = Some(s); // c:2871 setstrvalue → store
4456 } else if t == PM_INTEGER {
4457 // c:2874 — `pm->gsu.i->setfn(pm, val.u.l)`. For MN_FLOAT
4458 // input, C truncates to integer via `(zlong)val.u.d`.
4459 pm.u_val = if (val.type_ & MN_INTEGER) != 0 {
4460 val.l
4461 } else {
4462 val.d as i64
4463 };
4464 } else if t == PM_EFLOAT || t == PM_FFLOAT {
4465 // c:2878 — `pm->gsu.f->setfn(pm, val.u.d)`. MN_INTEGER input
4466 // gets promoted via `(double)val.u.l`.
4467 pm.u_dval = if (val.type_ & MN_INTEGER) != 0 {
4468 val.l as f64
4469 } else {
4470 val.d
4471 };
4472 }
4473}
4474
4475/// Direct port of `void setarrvalue(Value v, char **val)` from
4476/// `Src/params.c:2895-3037`. Sets an array (or assoc-array via
4477/// arrhashsetfn) into the param identified by v.pm, honouring
4478/// PM_READONLY / type-guards / VALFLAG_EMPTY rejections and the
4479/// slice-bounds adjust for `[N,M]` subscripts.
4480///
4481/// C dispatch:
4482/// - !EXECOPT → silent return (c:2897-2898)
4483/// - PM_READONLY → zerr + return (c:2899-2904)
4484/// - !PM_ARRAY && !PM_HASHED → zerr (c:2905-2911)
4485/// - VALFLAG_EMPTY → zerr (c:2913-2917)
4486/// - start==0,end==-1 && PM_HASHED → arrhashsetfn(0) (c:2919-2922)
4487/// - start==0,end==-1 && PM_ARRAY → gsu.a->setfn (c:2922-2923)
4488/// - start==-1,end==0 && PM_HASHED → arrhashsetfn(AUGMENT) (c:2925-2928)
4489/// - PM_HASHED with other bounds → zerr slice-of-assoc (c:2929-2932)
4490/// - PM_ARRAY with slice → bounds adjust + splice (c:2933+)
4491///
4492/// VALFLAG_INV + !KSHARRAYS off-by-one (c:2938-2942) is ported below.
4493/// PM_UNIQUE dedupe (c:2966-2967) is ported at the tail. ASSPM_AUGMENT
4494/// prepend (c:2945-2954) for slice-AUGMENT remains deferred — rare path
4495/// (`a[i,j]+=...` array+=) that requires snapshotting the pre-existing
4496/// slice value before splice.
4497pub fn setarrvalue(v: &mut value, val: Vec<String>) {
4498 // c:2895
4499 // c:2897-2898 — `if (unset(EXECOPT)) return;`. Match the same
4500 // NO_EXEC bail as setnumvalue at c:2860. Without it,
4501 // `zsh -n -c 'arr=(a b c)'` would mutate arr during a parse-
4502 // only run.
4503 if unset(EXECOPT) {
4504 // c:2897
4505 return;
4506 }
4507
4508 let pm = match v.pm.as_mut() {
4509 Some(p) => p,
4510 None => return,
4511 };
4512
4513 // c:2899-2904 — PM_READONLY rejection.
4514 if pm.node.flags & PM_READONLY as i32 != 0 {
4515 zerr(&format!("read-only variable: {}", pm.node.nam));
4516 return;
4517 }
4518 // c:2905-2911 — type guard.
4519 let t = PM_TYPE(pm.node.flags as u32);
4520 if t & (PM_ARRAY | PM_HASHED) == 0 {
4521 zerr(&format!(
4522 "{}: attempt to assign array value to non-array",
4523 pm.node.nam
4524 ));
4525 return;
4526 }
4527 // c:2913-2917 — VALFLAG_EMPTY rejection.
4528 if v.valflags & VALFLAG_EMPTY != 0 {
4529 zerr(&format!(
4530 "{}: assignment to invalid subscript range",
4531 pm.node.nam
4532 ));
4533 return;
4534 }
4535
4536 // c:2919-2932 — full-replace / AUGMENT / hash-slice-reject paths.
4537 if v.start == 0 && v.end == -1 {
4538 if t == PM_HASHED {
4539 // c:2920 — arrhashsetfn(pm, val, 0).
4540 arrhashsetfn(pm, val, 0);
4541 } else {
4542 // c:2922 — `pm->gsu.a->setfn(pm, val)`. Route through
4543 // arrsetfn so PM_UNIQUE dedupe + arrfixenv side-effects
4544 // fire (params.c:4066-4076).
4545 arrsetfn(pm, val);
4546 }
4547 return;
4548 }
4549 if v.start == -1 && v.end == 0 && t == PM_HASHED {
4550 arrhashsetfn(pm, val, ASSPM_AUGMENT);
4551 return;
4552 }
4553 if t == PM_HASHED {
4554 zerr(&format!(
4555 "{}: attempt to set slice of associative array",
4556 pm.node.nam
4557 ));
4558 return;
4559 }
4560
4561 // c:2938-2942 — VALFLAG_INV + !KSHARRAYS off-by-one. Inverse
4562 // subscripts (`a[(i)pat]=val`) are 1-based when KSHARRAYS is
4563 // off; shift start/end down by 1 to match the 0-based slice
4564 // arithmetic below.
4565 if v.valflags & VALFLAG_INV != 0 && !isset(KSHARRAYS) {
4566 if v.start > 0 {
4567 v.start -= 1;
4568 }
4569 v.end -= 1;
4570 }
4571
4572 // c:2933+ — PM_ARRAY slice path.
4573 let arr = pm.u_arr.get_or_insert_with(Vec::new);
4574 let len = arr.len() as i64;
4575 // c:2944-2949 — negative start: add pre_assignment_length; clamp to 0.
4576 let start = if v.start < 0 {
4577 (len + v.start as i64).max(0)
4578 } else {
4579 v.start as i64
4580 };
4581 // c:2950-2953 — negative end: add pre_assignment_length + 1; clamp to 0.
4582 let end = if v.end < 0 {
4583 (len + v.end as i64 + 1).max(0)
4584 } else {
4585 v.end as i64
4586 };
4587 let start_idx = (start.max(1) - 1) as usize;
4588 let end_idx = end.max(0) as usize;
4589
4590 // c:2980 — pad with empty strings up to start.
4591 while arr.len() < start_idx {
4592 arr.push(String::new());
4593 }
4594
4595 // c:2940-2943 — `if (v->end < v->start || v->start > oldlen)
4596 // v->end = v->start; else if (v->end > oldlen)
4597 // v->end = oldlen;`. A REVERSED range (start > end,
4598 // e.g. `a[4,2]=(42 43 44)`) collapses to an EMPTY range at start, so
4599 // the splice INSERTS the value there keeping every element
4600 // ("1 2 3 42 43 44 4 5") — it does NOT overwrite from start. The
4601 // previous Rust port took a custom `start_idx > end_idx` else-branch
4602 // that overwrote/extended from start, dropping the tail. `clamp`
4603 // implements both C arms: lower-bound start_idx (reversed → empty),
4604 // upper-bound arr.len() (end past tail → clamp). After the pad above
4605 // arr.len() >= start_idx, so the clamp range is always valid.
4606 let end_idx = end_idx.clamp(start_idx, arr.len());
4607 let val_len = val.len(); // c:3030 post-assign sanity
4608 let pre_len = arr.len(); // c:3030 (snapshot)
4609 // c:2989-2998 — splice val into [start..end] range.
4610 arr.splice(start_idx..end_idx, val);
4611 // c:3030 — DPUTS2(p - new != post_assignment_length, …). The
4612 // post-splice arr.len() is C's `p - new`; expected follows C's
4613 // post_assignment_length = start + vallen + MAX(0, oldlen - end).
4614 let expected = pre_len - (end_idx - start_idx) + val_len; // c:3030
4615 DPUTS2!(
4616 // c:3030
4617 arr.len() != expected, // c:3030
4618 "setarrvalue: wrong allocation: {} 1= {}",
4619 expected,
4620 arr.len() // c:3030-3031
4621 );
4622
4623 // c:2966-2967 — `if (pm->node.flags & PM_UNIQUE) arrunique(pm->u.arr);`
4624 // Dedupe in-place preserving first occurrence. Without this,
4625 // `typeset -U arr; arr[2]=foo` leaves duplicates that violate the
4626 // PM_UNIQUE invariant.
4627 if (pm.node.flags as u32 & PM_UNIQUE) != 0 {
4628 let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
4629 arr.retain(|s| seen.insert(s.clone())); // c:2967
4630 }
4631 // c:Src/params.c:2922 — a SUBSCRIPT-RANGE assignment (`path[i,j]=(…)`,
4632 // `path[N]=(…)`, `path[i,j]=()`) reaches here (the whole-replace form
4633 // returned early via arrsetfn above). For a tied colon-array the scalar
4634 // side must re-derive, just like the single-element path in
4635 // assignsparam; see TIED_COLON_ARRAYS. Snapshot after the splice.
4636 let tie_sync: Option<(&'static str, String)> = TIED_COLON_ARRAYS
4637 .iter()
4638 .find(|(a, _)| *a == pm.node.nam)
4639 .map(|(_, scalar)| (*scalar, pm.u_arr.as_deref().unwrap_or(&[]).join(":")));
4640 if let Some((scalar, joined)) = tie_sync {
4641 assignsparam(scalar, &joined, 0);
4642 }
4643}
4644
4645/// Retrieve integer parameter.
4646/// Port of `getiparam(char *s)` from Src/params.c:3044. C: getvalue +
4647/// getintvalue. Our adaptation reads the scalar string and parses;
4648/// returns 0 on missing or unparseable, matching getintvalue's
4649/// failure-returns-0 convention (params.c:2601).
4650pub fn getiparam(s: &str) -> i64 {
4651 // C also honours PM_INTEGER's `pm->u.val` payload directly when
4652 // the param is typed numeric; check paramtab first for that case.
4653 if let Ok(tab) = paramtab().read() {
4654 if let Some(pm) = tab.get(s) {
4655 if (pm.node.flags as u32 & PM_INTEGER) != 0 {
4656 return pm.u_val;
4657 }
4658 }
4659 }
4660 getsparam(s)
4661 .and_then(|s| s.parse::<i64>().ok())
4662 .unwrap_or(0)
4663}
4664
4665/// Retrieve numeric (int-or-float) parameter.
4666/// Port of `getnparam(char *s)` from Src/params.c:3058. C returns an
4667/// `mnumber` (tagged int/float union); our adaptation returns
4668/// `(i64, f64, bool)` where the bool is true for float. Unset
4669/// returns `(0, 0.0, false)`, matching the MN_INTEGER zero
4670/// fallback in the C source's not-found branch.
4671pub fn getnparam(s: &str) -> (i64, f64, bool) {
4672 if let Ok(tab) = paramtab().read() {
4673 if let Some(pm) = tab.get(s) {
4674 let fl = pm.node.flags as u32;
4675 if (fl & (PM_EFLOAT | PM_FFLOAT)) != 0 {
4676 return (pm.u_dval as i64, pm.u_dval, true);
4677 }
4678 if (fl & PM_INTEGER) != 0 {
4679 return (pm.u_val, pm.u_val as f64, false);
4680 }
4681 }
4682 }
4683 let s = match getsparam(s) {
4684 Some(s) => s,
4685 None => return (0, 0.0, false),
4686 };
4687 if s.contains('.') || s.contains('e') || s.contains('E') {
4688 if let Ok(f) = s.parse::<f64>() {
4689 return (f as i64, f, true);
4690 }
4691 }
4692 if let Ok(i) = s.parse::<i64>() {
4693 return (i, i as f64, false);
4694 }
4695 (0, 0.0, false)
4696}
4697
4698/// Port of `getsparam(char *s)` from `Src/params.c:3076`.
4699///
4700/// C body:
4701/// ```c
4702/// char *getsparam(char *s) {
4703/// struct value vbuf;
4704/// Value v = getvalue(&vbuf, &s, 0);
4705/// if (!v) return NULL;
4706/// return getstrvalue(v);
4707/// }
4708/// ```
4709///
4710/// `getvalue` (params.c:2173) builds a `Value` for the parameter,
4711/// dispatching through `Param.gsu->getfn` for special parameters.
4712/// `getstrvalue` (params.c:2335) extracts the scalar form: for
4713/// PM_INTEGER calls `pm->gsu.i->getfn(pm)` and convbase's the
4714/// result; for PM_SCALAR calls `pm->gsu.s->getfn(pm)`; for
4715/// PM_ARRAY joins the elements.
4716///
4717/// **Sole funnel.** Every scalar parameter read in zshrs routes
4718/// through this fn — `subst.rs` parameter expansion AND
4719/// `fusevm_bridge::expand_param` both call `getsparam`. The
4720/// dispatch chain lives in exactly one place, mirroring C's
4721/// "every read goes through getsparam" architecture.
4722///
4723/// Lookup order (mirrors C's `getvalue` → `getstrvalue` cascade):
4724/// 1. **GSU dispatch** via [`lookup_special_var`] — special
4725/// parameters route through their getfn callback (`uidgetfn` /
4726/// `randomgetfn` / `usernamegetfn` / etc.). Same role as
4727/// C's `Param.gsu->getfn` virtual dispatch.
4728/// 2. **Local variable** — `variables[name]`. C reads `pm->u.str`
4729/// for PM_SCALAR; here we hold the scalar in the variables
4730/// HashMap.
4731/// 3. **Environment fallback** — `std::env::var(name)`. C imports
4732/// env vars into the param table at startup so they go through
4733/// the same dispatch as everything else; zshrs reads from the
4734/// OS env on miss to match.
4735/// 4. **Array → scalar** — `arrays[name].join(" ")`. Mirrors
4736/// C's PM_ARRAY case in getstrvalue (params.c:2358) which
4737/// joins via `sepjoin(ss, NULL, 1)`.
4738///
4739// Retrieve a scalar (string) parameter // c:3076
4740/// Returns `None` only if all four paths miss (parameter genuinely
4741/// unset).
4742pub fn getsparam(name: &str) -> Option<String> {
4743 // c:3076
4744 // 1. GSU dispatch — `Param.gsu->getfn(pm)` equivalent. Special
4745 // parameters (UID/RANDOM/USERNAME/...) live behind getfn
4746 // hooks that the table read below would otherwise miss.
4747 if let Some(v) = lookup_special_var(name) {
4748 return Some(v);
4749 }
4750 // 1b. c:Src/params.c:570-575 — getparamnode resolves PM_NAMEREF
4751 // chains before the value read (`pm = resolve_nameref(pm)`).
4752 // Redirect the lookup to the resolved target.
4753 // c:570-575 getparamnode → resolve_nameref: scalar-read
4754 // redirection through a nameref chain, INLINE (former
4755 // nameref_read_redirect, single-caller fold 2026-06-12).
4756 if let Some(res) = (|name: &str| -> Option<Option<String>> {
4757 if !is_nameref(name) {
4758 return None;
4759 }
4760 match resolve_nameref_name(name, None) {
4761 nameref_resolution::NotRef => None,
4762 nameref_resolution::Placeholder(_)
4763 | nameref_resolution::SelfRef
4764 | nameref_resolution::OutOfScope => Some(None),
4765 nameref_resolution::Target {
4766 name: t,
4767 subscript,
4768 pm,
4769 level,
4770 } => {
4771 let pm = match pm {
4772 Some(p) => p,
4773 None => return Some(None), // dangling target
4774 };
4775 if (pm.node.flags as u32 & PM_UNSET) != 0 {
4776 return Some(None);
4777 }
4778 match subscript {
4779 None => {
4780 // Visible binding → recurse through the normal
4781 // read path (padding, gsu, specials all apply).
4782 let visible_level = paramtab()
4783 .read()
4784 .ok()
4785 .and_then(|tab| tab.get(&t).map(|p| p.level));
4786 if visible_level == Some(level) {
4787 Some(getsparam(&t))
4788 } else {
4789 // Hidden (old-chain) binding — read the clone.
4790 Some(param_scalar_value(&pm))
4791 }
4792 }
4793 Some(k) => Some(nameref_element_read(&pm, &t, &k)),
4794 }
4795 }
4796 }
4797 })(name)
4798 {
4799 return res;
4800 }
4801 // 2. Paramtab read — `(Value)gethashnode2(paramtab, name)`.
4802 // Walk the global paramtab for the named param, returning
4803 // `pm->u.str` for PM_SCALAR/PM_NAMEREF or `sepjoin(pm->u.arr)`
4804 // for PM_ARRAY (matches `getstrvalue` at params.c:2358).
4805 if let Ok(tab) = paramtab().read() {
4806 if let Some(pm) = tab.get(name) {
4807 // c:Src/Modules/param_private.c:568-617 + c:678 — C swaps
4808 // `realparamtab->getnode` to getprivatenode when
4809 // zsh/param/private is active, so every lookup skips
4810 // private params belonging to an OUTER scope when read
4811 // from a DEEPER one (`private x` in f is invisible to g
4812 // called by f; g sees the global). zshrs's HashMap
4813 // paramtab has no getnode vtable, so call the canonical
4814 // walk at the lookup site. SAFETY: getprivatenode only
4815 // follows the `pm->old` chain read-only; the chain is
4816 // owned by this entry, which stays alive under the read
4817 // guard held for the rest of this block.
4818 let visible =
4819 crate::ported::modules::param_private::getprivatenode(&**pm as *const param); // c:678 getnode hook
4820 if visible.is_null() {
4821 return None; // c:609 walk exhausted — no visible node
4822 }
4823 let pm: ¶m = unsafe { &*visible };
4824 // c:Src/params.c:2335-2358 — `if (pm->node.flags & PM_UNSET)
4825 // return ""`. Unset specials kept in paramtab (PM_SPECIAL
4826 // retention path, c:3911) read as empty, not as their stale
4827 // u.val / u.str. Without this, `unset SECONDS; echo
4828 // "[$SECONDS]"` printed `[0]` instead of `[]` — the
4829 // PM_UNSET pm fell through to the integer path and
4830 // returned convbase(0). Bug #418 in docs/BUGS.md.
4831 if (pm.node.flags as u32 & PM_UNSET) != 0 {
4832 return None;
4833 }
4834 let t = PM_TYPE(pm.node.flags as u32);
4835 // c:2390-2538 — when PM_LEFT/PM_RIGHT_B/PM_RIGHT_Z + width
4836 // are set, getstrvalue (called from getsparam in C) applies
4837 // padding. Mirror that here so `$var` expansion respects
4838 // `typeset -Z N` / `-L N` / `-R N` even when the caller
4839 // didn't go through getstrvalue. Numeric prefix detection
4840 // (leading blanks/minus/0x/base#) follows params.rs:3156.
4841 let pad_flags = (pm.node.flags as u32) & (PM_LEFT | PM_RIGHT_B | PM_RIGHT_Z);
4842 // c:Src/params.c:2358 — getstrvalue for a PM_SCALAR
4843 // dispatches through `pm->gsu.s->getfn(pm)` so tied /
4844 // colon-array / special-callback scalars (PATH/path,
4845 // user `typeset -T` pairs) return their live computed
4846 // value rather than a stale cached `u_str`. Reads of
4847 // `u_str` for scalars without a GSU vtable (the common
4848 // case) fall through unchanged. Bug #24 in docs/BUGS.md.
4849 let raw: Option<String> = if t == PM_INTEGER {
4850 let base = if pm.base > 0 { pm.base } else { 10 };
4851 Some(convbase(pm.u_val, base as u32))
4852 } else if t == PM_EFLOAT || t == PM_FFLOAT {
4853 Some(convfloat(pm.u_dval, pm.base, pm.node.flags as u32))
4854 } else if t == PM_SCALAR && pm.gsu_s.is_some() {
4855 pm.gsu_s.as_ref().map(|gsu| (gsu.getfn)(pm))
4856 } else if let Some(s) = pm.u_str.as_ref() {
4857 Some(s.clone())
4858 } else {
4859 pm.u_arr.as_ref().map(|arr| arr.join(" "))
4860 };
4861 if let Some(mut s) = raw {
4862 if pad_flags != 0 && pm.width > 0 {
4863 let fwidth = pm.width as usize;
4864 let numeric_pm =
4865 (pm.node.flags as u32 & (PM_INTEGER | PM_EFLOAT | PM_FFLOAT)) != 0;
4866 if pad_flags == PM_LEFT || pad_flags == (PM_LEFT | PM_RIGHT_Z) {
4867 let trimmed: &str = if pad_flags & PM_RIGHT_Z != 0 {
4868 s.trim_start_matches('0')
4869 } else {
4870 s.trim_start_matches(|c: char| c == ' ' || c == '\t')
4871 };
4872 let len = trimmed.chars().count();
4873 let take = len.min(fwidth);
4874 let mut out: String = trimmed.chars().take(take).collect();
4875 if fwidth > take {
4876 out.extend(std::iter::repeat(' ').take(fwidth - take));
4877 }
4878 s = out;
4879 } else if pad_flags & (PM_RIGHT_B | PM_RIGHT_Z) != 0 {
4880 let charlen = s.chars().count();
4881 if charlen < fwidth {
4882 let mut zero = true;
4883 let mut valprefend: usize = 0;
4884 if pad_flags & PM_RIGHT_Z != 0 {
4885 let bytes = s.as_bytes();
4886 let mut tpos = 0usize;
4887 while tpos < bytes.len()
4888 && (bytes[tpos] == b' ' || bytes[tpos] == b'\t')
4889 {
4890 tpos += 1;
4891 }
4892 if numeric_pm && tpos < bytes.len() && bytes[tpos] == b'-' {
4893 tpos += 1;
4894 }
4895 if (pm.node.flags as u32 & PM_INTEGER) != 0
4896 && tpos + 1 < bytes.len()
4897 && bytes[tpos] == b'0'
4898 && bytes[tpos + 1] == b'x'
4899 {
4900 tpos += 2;
4901 } else if (pm.node.flags as u32 & PM_INTEGER) != 0 {
4902 if let Some(hash_off) =
4903 bytes[tpos..].iter().position(|&b| b == b'#')
4904 {
4905 tpos += hash_off + 1;
4906 }
4907 }
4908 valprefend = tpos;
4909 if tpos == bytes.len() {
4910 zero = false;
4911 } else if !numeric_pm && !bytes[tpos].is_ascii_digit() {
4912 zero = false;
4913 }
4914 }
4915 let pad_char = if (pad_flags & PM_RIGHT_B) != 0 || !zero {
4916 ' '
4917 } else {
4918 '0'
4919 };
4920 let pad_count = fwidth - charlen;
4921 let prefix = &s[..valprefend];
4922 let rest = &s[valprefend..];
4923 let mut out = String::with_capacity(fwidth);
4924 out.push_str(prefix);
4925 out.extend(std::iter::repeat(pad_char).take(pad_count));
4926 out.push_str(rest);
4927 s = out;
4928 } else if charlen > fwidth {
4929 // c:Src/params.c:2496-2500 — right-justify
4930 // with charlen > fwidth: truncate from the
4931 // FRONT to fit fwidth codepoints. Bug #100
4932 // in docs/BUGS.md — the getsparam path had
4933 // the pad arm but missed the truncation
4934 // arm. The parallel getstrvalue path at
4935 // ~line 3355 has it; this is the
4936 // missing-symmetry fix.
4937 let skip = charlen - fwidth;
4938 s = s.chars().skip(skip).collect();
4939 }
4940 }
4941 }
4942 return Some(s);
4943 }
4944 }
4945 }
4946 // 3. Env fallback — C imports env into paramtab at init so the
4947 // read above would hit. If the import hasn't happened yet
4948 // (e.g. during very early init) fall back to the live env.
4949 env::var(name).ok()
4950}
4951
4952/// Port of `getsparam_u(char *s)` from `Src/params.c:3089`. C body
4953/// (c:3091-3094):
4954/// ```c
4955/// /* getsparam() returns pointer into global params table, so ... */
4956/// if ((s = getsparam(s)))
4957/// return unmeta(s); /* returns static pointer to copy */
4958/// return s;
4959/// ```
4960///
4961/// The previous Rust "port" was an entirely fabricated impl — it
4962/// took `Option<&mut value>` and gated on `PM_TYPE == PM_SCALAR`,
4963/// which matches no part of the C body. C just calls getsparam(s)
4964/// and unmeta's the resulting string. No callers existed because
4965/// no caller's type fit the bogus signature.
4966///
4967/// Real use case: locale setters (c:4847, c:4867, c:4882, c:4917)
4968/// call `getsparam_u("LC_ALL")` / `getsparam_u("LANG")` to read the
4969/// param as a Meta-stripped C string suitable for `setlocale`.
4970pub fn getsparam_u(s: &str) -> Option<String> {
4971 // c:3089
4972 // c:3092 — `if ((s = getsparam(s))) return unmeta(s);`
4973 getsparam(s).map(|v| unmeta(&v))
4974}
4975
4976/// Port of `char **getaparam(char *s)` from `Src/params.c:3101-3110`.
4977///
4978/// C body:
4979/// ```c
4980/// struct value vbuf;
4981/// Value v;
4982/// if (!idigit(*s) && (v = getvalue(&vbuf, &s, 0)) &&
4983/// PM_TYPE(v->pm->node.flags) == PM_ARRAY)
4984/// return v->pm->gsu.a->getfn(v->pm);
4985/// return NULL;
4986/// ```
4987///
4988/// The previous Rust port was a fabrication: signature was
4989/// `Option<&mut value> -> Option<Vec<String>>`, taking an already-
4990/// resolved Value pointer rather than the C-canonical name string.
4991/// No caller used it because the bogus signature fit nothing — and
4992/// the in-tree `savematch` at modules/zutil.rs:30 hardcoded `a = None`
4993/// because the existing API couldn't be threaded through.
4994///
4995/// Real C use: name lookup. e.g. `getaparam("match")` returns the
4996/// `$match` array from the regex-match callouts (Modules/zutil.c:45).
4997pub fn getaparam(name: &str) -> Option<Vec<String>> {
4998 // c:3101
4999 // c:3107 — `if (idigit(*s))` reject digit-first names. C
5000 // `getvalue` would also reject these later, but the explicit
5001 // check matches C's flow.
5002 if name.starts_with(|c: char| c.is_ascii_digit()) {
5003 // c:3107
5004 return None;
5005 }
5006 // c:Src/params.c:570-575 — getvalue→fetchvalue→getparamnode
5007 // resolves PM_NAMEREF before the type check.
5008 // c:570-575 getparamnode → resolve_nameref: array-read
5009 // redirection through a nameref chain, INLINE (former
5010 // nameref_aread_redirect, single-caller fold 2026-06-12).
5011 if let Some(res) = (|name: &str| -> Option<Option<Vec<String>>> {
5012 if !is_nameref(name) {
5013 return None;
5014 }
5015 match resolve_nameref_name(name, None) {
5016 nameref_resolution::NotRef => None,
5017 nameref_resolution::Placeholder(_)
5018 | nameref_resolution::SelfRef
5019 | nameref_resolution::OutOfScope => Some(None),
5020 nameref_resolution::Target {
5021 name: t,
5022 subscript,
5023 pm,
5024 level,
5025 } => {
5026 let pm = match pm {
5027 Some(p) => p,
5028 None => return Some(None),
5029 };
5030 if (pm.node.flags as u32 & PM_UNSET) != 0 {
5031 return Some(None);
5032 }
5033 if let Some(k) = subscript {
5034 // single element reads back as a 1-element array view
5035 return Some(nameref_element_read(&pm, &t, &k).map(|v| vec![v]));
5036 }
5037 if PM_TYPE(pm.node.flags as u32) != PM_ARRAY {
5038 return Some(None);
5039 }
5040 let visible_level = paramtab()
5041 .read()
5042 .ok()
5043 .and_then(|tab| tab.get(&t).map(|p| p.level));
5044 if visible_level == Some(level) {
5045 Some(getaparam(&t))
5046 } else {
5047 Some(pm.u_arr.clone())
5048 }
5049 }
5050 }
5051 })(name)
5052 {
5053 return res;
5054 }
5055 // c:3107-3109 — `getvalue(&vbuf, &s, 0)` resolves the name to a
5056 // paramtab entry. Then PM_TYPE check + `pm->u.arr` return.
5057 if let Ok(tab) = paramtab().read() {
5058 if let Some(pm) = tab.get(name) {
5059 // c:Src/Modules/param_private.c:678 — the getnode hook
5060 // applies to every paramtab lookup; same canonical walk
5061 // as getsparam (SAFETY: read-only old-chain walk under
5062 // the held read guard).
5063 let visible =
5064 crate::ported::modules::param_private::getprivatenode(&**pm as *const param);
5065 if visible.is_null() {
5066 return None;
5067 }
5068 let pm: ¶m = unsafe { &*visible };
5069 if PM_TYPE(pm.node.flags as u32) == PM_ARRAY {
5070 // c:3108
5071 if let Some(arr) = pm.u_arr.as_ref() {
5072 // c:3109
5073 return Some(arr.clone());
5074 }
5075 }
5076 }
5077 }
5078 None // c:3110
5079}
5080
5081/// Port of `char **gethparam(char *s)` from `Src/params.c:3117-3126`.
5082///
5083/// C body:
5084/// ```c
5085/// struct value vbuf;
5086/// Value v;
5087/// if (!idigit(*s) && (v = getvalue(&vbuf, &s, 0)) &&
5088/// PM_TYPE(v->pm->node.flags) == PM_HASHED)
5089/// return paramvalarr(v->pm->gsu.h->getfn(v->pm), SCANPM_WANTVALS);
5090/// return NULL;
5091/// ```
5092///
5093/// Same fabricated-port family as the prior `getaparam`/`getsparam_u`
5094/// fixes: previous Rust sig took `Option<&mut value>` instead of the
5095/// canonical name string, with no real callers. Fixed sig + body
5096/// that resolves the name through paramtab and returns the values
5097/// vector when PM_HASHED.
5098///
5099/// NOTE: zshrs's paramtab stores hash-params via `pm->u_hash` (a
5100/// `HashTable` struct that's a generic bucket-array container). The
5101/// canonical C path threads through `gsu.h->getfn(pm)` → `paramvalarr`
5102/// which extracts the value side of each key-value pair. zshrs's
5103/// canonical assoc backing lives in `paramtab_hashed_storage` (an
5104/// IndexMap<String, String> keyed by param name); read the values
5105/// directly from there as the C macro's
5106/// `paramvalarr(hashgetfn(pm), SCANPM_WANTVALS)` resolves to a
5107/// values walk over the same backing.
5108pub fn gethparam(name: &str) -> Option<Vec<String>> {
5109 // c:3117
5110 if name.starts_with(|c: char| c.is_ascii_digit()) {
5111 // c:3122
5112 return None;
5113 }
5114 // c:Src/params.c:570-575 — nameref deref before the type check.
5115 let resolved = match crate::ported::params::resolve_nameref_name(name, None) {
5116 crate::ported::params::nameref_resolution::Target { name: t_, .. } => t_,
5117 _ => name.to_string(),
5118 };
5119 let name: &str = &resolved;
5120 if let Ok(tab) = paramtab().read() {
5121 if let Some(pm) = tab.get(name) {
5122 // c:Src/Modules/param_private.c:678 — getnode hook; same
5123 // canonical walk as getsparam/getaparam. (Values below
5124 // still come from the name-keyed hashed storage — a
5125 // single slot per name; per-scope assoc shadowing is a
5126 // storage-model limitation predating this walk.)
5127 let visible =
5128 crate::ported::modules::param_private::getprivatenode(&**pm as *const param);
5129 if visible.is_null() {
5130 return None;
5131 }
5132 let pm: ¶m = unsafe { &*visible };
5133 if PM_TYPE(pm.node.flags as u32) == PM_HASHED {
5134 // c:3123
5135 // c:3124 — `paramvalarr(hashgetfn(pm), SCANPM_WANTVALS)`.
5136 // Read values directly from the canonical hashed-storage
5137 // backing — IndexMap iteration matches C's hashtable
5138 // walk order (insertion-stable). When the storage hasn't
5139 // been populated (empty hash, fresh declaration), return
5140 // an empty Vec so the C "param exists, no entries" shape
5141 // is preserved (vs returning None which means "param
5142 // doesn't exist").
5143 let store = paramtab_hashed_storage().lock().ok()?;
5144 let vals = store
5145 .get(name)
5146 .map(|m| m.values().cloned().collect())
5147 .unwrap_or_default();
5148 return Some(vals); // c:3124
5149 }
5150 }
5151 }
5152 None // c:3125
5153}
5154
5155/// Port of `char **gethkparam(char *s)` from `Src/params.c:3131-3140`.
5156/// Same as `gethparam` but `paramvalarr(..., SCANPM_WANTKEYS)`.
5157pub fn gethkparam(name: &str) -> Option<Vec<String>> {
5158 // c:3131
5159 if name.starts_with(|c: char| c.is_ascii_digit()) {
5160 // c:3136
5161 return None;
5162 }
5163 if let Ok(tab) = paramtab().read() {
5164 if let Some(pm) = tab.get(name) {
5165 if PM_TYPE(pm.node.flags as u32) == PM_HASHED {
5166 // c:3137
5167 // c:3138 — `paramvalarr(pm->gsu.h->getfn(pm),
5168 // SCANPM_WANTKEYS)`. Same backing as gethparam —
5169 // return keys instead of values. Empty-storage
5170 // fallback identical: Some(empty Vec) for "exists,
5171 // no entries" shape.
5172 {
5173 let store = paramtab_hashed_storage().lock().ok()?;
5174 if let Some(m) = store.get(name) {
5175 return Some(m.keys().cloned().collect()); // c:3138
5176 }
5177 }
5178 // c:3138 — for SPECIALPMDEF magic hashes (parameters/
5179 // options/commands/…, Src/Modules/parameter.c) the
5180 // `getfn` is the module's scan fn, not hashgetfn;
5181 // there's no paramtab_hashed_storage backing. The
5182 // partab_scan_keys port (vm_helper.rs:3486) is that
5183 // scan. Without this, `${(k)parameters[PATH]}` saw an
5184 // empty key set and returned "" where zsh prints PATH.
5185 if let Some(keys) = (|| -> Option<Vec<String>> {
5186 // c:Src/Modules/parameter.c — special-hash scantab
5187 // key enumeration via the partab[] row's scanfn
5188 // (former bridge partab_scan_keys, inlined; the
5189 // captureless callback is the C ScanFunc ABI).
5190 let e_ = crate::ported::modules::parameter::PARTAB
5191 .iter()
5192 .find(|e_| e_.name == name)?;
5193 if let Some(m_) = e_.module {
5194 if !crate::ported::module::MODULESTAB
5195 .lock()
5196 .map(|t| t.is_loaded(m_))
5197 .unwrap_or(false)
5198 {
5199 return None;
5200 }
5201 }
5202 thread_local! {
5203 static KEYS_: std::cell::RefCell<Vec<String>> =
5204 const { std::cell::RefCell::new(Vec::new()) };
5205 }
5206 fn cb_(node: &crate::ported::zsh_h::HashNode, _f: i32) {
5207 KEYS_.with(|k| k.borrow_mut().push(node.nam.clone()));
5208 }
5209 KEYS_.with(|k| k.borrow_mut().clear());
5210 (e_.scanfn)(std::ptr::null_mut(), Some(cb_), 0);
5211 Some(KEYS_.with(|k| k.borrow().clone()))
5212 })() {
5213 return Some(keys); // c:3138
5214 }
5215 return Some(Vec::new()); // c:3138
5216 }
5217 }
5218 }
5219 None // c:3139
5220}
5221
5222/// Port of `check_warn_pm(Param pm, const char *pmtype, int created, int may_warn_about_nested_vars)` from `Src/params.c:3160`.
5223///
5224/// C body emits the WARN_CREATE_GLOBAL / WARN_NESTED_VAR
5225/// diagnostic when a function-local creates/passes a non-local
5226/// param with the matching shell options set.
5227///
5228/// The previous Rust port handled the GATE logic correctly but
5229/// SKIPPED the diagnostic emit, claiming the `funcstack` global
5230/// wasn't ported. But `FUNCSTACK`
5231/// IS ported (`Mutex<Vec<funcstack>>`). Wire the walk:
5232/// for (i = funcstack; i; i = i->prev)
5233/// if (i->tp == FS_FUNC) {
5234/// msg = created ?
5235/// "%s parameter %s created globally in function %s" :
5236/// "%s parameter %s set in enclosing scope in function %s";
5237/// zwarn(msg, pmtype, pm->node.nam, i->name);
5238/// break;
5239/// }
5240///
5241/// Without the diagnostic, `setopt WARN_CREATE_GLOBAL` had no
5242/// observable effect — the whole point of the option is the
5243/// user-visible warning.
5244pub fn check_warn_pm(pm: ¶m, pmtype: &str, created: i32, may_warn_about_nested_vars: i32) {
5245 // c:3160
5246 if may_warn_about_nested_vars == 0 && created == 0 {
5247 // c:3165
5248 return;
5249 }
5250 // `locallevel` is the canonical `pub static` above (port of
5251 // params.c:54). `forklevel` is the ported global at vm_helper
5252 // (port of exec.c:1052) set to locallevel at every entersubsh().
5253 let cur_local: i32 = locallevel.load(Ordering::Relaxed);
5254 let forklevel: i32 = FORKLEVEL.load(Ordering::Relaxed); // c:1052 (Src/exec.c)
5255 if created != 0 && isset(WARNCREATEGLOBAL) {
5256 // c:3168
5257 if cur_local <= forklevel || pm.level != 0 {
5258 // c:3169
5259 return;
5260 }
5261 } else if created == 0 && isset(WARNNESTEDVAR) {
5262 // c:3171
5263 if pm.level >= cur_local {
5264 // c:3172
5265 return;
5266 }
5267 } else {
5268 return;
5269 }
5270 if (pm.node.flags as u32 & (PM_SPECIAL | PM_NAMEREF)) != 0 {
5271 // c:3177
5272 return;
5273 }
5274 // c:3180-3190 — walk funcstack, emit zwarn at first FS_FUNC.
5275 let stack = match FUNCSTACK.lock() {
5276 Ok(s) => s,
5277 Err(_) => return,
5278 };
5279 for frame in stack.iter().rev() {
5280 // c:3180 walk most-recent-first
5281 if frame.tp == FS_FUNC {
5282 // c:3181 FS_FUNC
5283 let msg = if created != 0 {
5284 // c:3185
5285 format!(
5286 "{} parameter {} created globally in function {}",
5287 pmtype, pm.node.nam, frame.name
5288 )
5289 } else {
5290 // c:3187
5291 format!(
5292 "{} parameter {} set in enclosing scope in function {}",
5293 pmtype, pm.node.nam, frame.name
5294 )
5295 };
5296 zwarn(&msg); // c:3189
5297 break; // c:3190
5298 }
5299 }
5300}
5301
5302// intgetfn / strgetfn drift wrappers removed — replaced below with
5303// real C-shape ports `intgetfn(pm: ¶m) -> i64` (Src/params.c:3993)
5304// and `strgetfn(pm: ¶m) -> String` (Src/params.c:4029) that read
5305// directly from the union fields `pm->u.val` / `pm->u.str`.
5306
5307// ---------------------------------------------------------------------------
5308// Tests
5309// ---------------------------------------------------------------------------
5310
5311// ===========================================================
5312// Methods moved verbatim from src/ported/vm_helper because their
5313// C counterpart's source file maps 1:1 to this Rust module.
5314// Phase: params
5315// ===========================================================
5316
5317// BEGIN moved-from-exec-rs
5318// (impl ShellExecutor block moved to src/exec_shims.rs — see file marker)
5319
5320// END moved-from-exec-rs
5321
5322// ===========================================================
5323// Free ported moved verbatim from src/ported/vm_helper.
5324// ===========================================================
5325// BEGIN moved-from-exec-rs (free ported)
5326/// Subscript-argument result.
5327///
5328/// `Flags` carries the parsed flag chars and the remaining subscript
5329/// text (the pattern after `(...)`); the caller dispatches the
5330/// search itself. `Value` is the result of an in-getarg array/hash
5331/// pattern search — direct port of getarg's pprog/pattry arm at
5332/// Src/params.c:1672-1719 (array) and 1581-1660 (hash).
5333// `enum getarg_out` is a Rust extension to express the dual-mode
5334// return of `getarg`. C `getarg` (`Src/params.c:1367`) writes back
5335// via out-pointers (`int *inv`, `Value v`, `zlong *w`, ...) and
5336// returns `int`. The Rust port collapses those into one sum-typed
5337// return: `Flags` carries the parsed flag chars + remaining
5338// subscript when no search ran; `Value` carries the search result
5339// from the pprog/pattry arms at c:1581-1660 (hash) / c:1672-1719
5340// (array). Naming kept lowercase to mark this as a port-shape helper
5341// rather than a C-mirrored struct.
5342/// `getarg_out` — see variants.
5343#[allow(non_camel_case_types)]
5344pub enum getarg_out<'a> {
5345 Flags { flags: &'a str, rest: &'a str },
5346 Value(Value),
5347}
5348
5349/// Port of `assignsparam(char *s, char *val, int flags)` from `Src/params.c:3193`. C signature:
5350/// `mod_export Param assignsparam(char *s, char *val, int flags)`.
5351///
5352/// `s` may carry an embedded `[...]` subscript (matching C's
5353/// `strchr(s, '[')` parse). The function operates on the global
5354/// `paramtab` (Src/params.c:515), creating/mutating `Param`
5355/// entries in place. Branches preserved 1:1 with C:
5356/// - c:3203 `isident(s)` — reject non-identifier names.
5357/// - c:3209 `queue_signals()`.
5358/// - c:3210 subscripted path: c:3212 `getvalue` lookup,
5359/// c:3213 `createparam(t, PM_ARRAY)` on miss, c:3216
5360/// PM_READONLY guard, c:3227 ASSPM_WARN drop, c:3228 clear
5361/// PM_DEFAULTED, c:3231 `v = NULL` then re-dispatch by type.
5362/// - c:3232 non-subscripted: c:3233 `getvalue` → c:3234
5363/// `createparam(t, PM_SCALAR)`; c:3236-3250 array/hash type-flip
5364/// to PM_SCALAR (when not PM_SPECIAL|PM_TIED, not KSHARRAYS,
5365/// not ASSPM_AUGMENT) via `resetparam(v->pm, PM_SCALAR)`.
5366/// - c:3258 PM_NAMEREF → c:3259 `valid_refname(val, flags)` guard.
5367/// - c:3269 clear PM_DEFAULTED.
5368/// - c:3343 `assignstrvalue(v, val, flags)`.
5369/// - c:3344 `unqueue_signals()`; c:3345 return v->pm.
5370///
5371/// The full HashTable substrate (vtable callbacks, scope-stacked
5372/// iterators) is not yet wired; non-essential branches such as
5373/// `+= AUGMENT` numeric/array slice append and `check_warn_pm`
5374/// are documented but elided where unreachable from current
5375/// callers — none of those code paths are exercised by zshrs's
5376/// existing call sites.
5377/// c:Src/params.c:395-422 — the IPDEF8 PM_TIED colon-array pairs. A
5378/// SUBSCRIPT modification of the ARRAY side (`path[N]=x`, `path[i,j]=(…)`,
5379/// `unset path[N]`) must re-derive the tied SCALAR side, exactly as C's
5380/// array setfn does after setarrvalue rebuilds the array. zshrs lacks the
5381/// per-name GSU vtable, so the subscript paths carry the tie explicitly via
5382/// this const map (the mirror of the scalar→array cascade); each site joins
5383/// the updated array with `:` and writes the scalar through assignsparam.
5384pub(crate) const TIED_COLON_ARRAYS: &[(&str, &str)] = &[
5385 ("path", "PATH"),
5386 ("fpath", "FPATH"),
5387 ("manpath", "MANPATH"),
5388 ("cdpath", "CDPATH"),
5389 ("psvar", "PSVAR"),
5390 ("module_path", "MODULE_PATH"),
5391 ("fignore", "FIGNORE"),
5392 ("mailpath", "MAILPATH"),
5393];
5394
5395pub fn assignsparam(s: &str, val: &str, flags: i32) -> Option<Param> {
5396 // c:3203 `if (!isident(s)) { zerr; errflag |= ERRFLAG_ERROR; return NULL; }`
5397 if !isident(s) {
5398 zerr(&format!("not an identifier: {}", s)); // c:3204
5399 errflag.fetch_or(
5400 // c:3206
5401 ERRFLAG_ERROR,
5402 Ordering::Relaxed,
5403 );
5404 return None; // c:3207
5405 }
5406 // !!! WARNING: RUST-ONLY ARM — ZLE GSU adapter !!!
5407 // C's makezleparams (Src/Zle/zle_params.c:194) installs LIVE GSU
5408 // setters for the editing specials; zshrs snapshots copies into
5409 // the paramtab (extensions/zle_param_sync.rs), so sequential
5410 // widget mutations read STALE peers (`LBUFFER=tpl; CURSOR=n;
5411 // RBUFFER=${RBUFFER:len}` scrambled zsh-expand's multiline
5412 // snippet). Route these writes through the live editor and let
5413 // live_write re-publish the derived family. Only fires inside an
5414 // active widget scope; live_write's guard blocks its own
5415 // re-publish writes.
5416 // ASSPM_AUGMENT must be resolved BEFORE the live write: `+=`
5417 // routes here with only the APPENDED text as `val`, and writing
5418 // that raw would ASSIGN it (`LBUFFER+=\"` in zsh-autopair's
5419 // _ap-self-insert nuked everything typed before the quote —
5420 // `echo hi "` collapsed to `""`). C never has this problem: its
5421 // augment concatenation happens before gsu.s->setfn dispatch
5422 // (Src/params.c:2742-2748). Concatenate against the LIVE editor
5423 // value (CURSOR augments arithmetically, matching PM_INTEGER).
5424 let zle_special = matches!(s, "BUFFER" | "LBUFFER" | "RBUFFER" | "CURSOR");
5425 let zle_augmented: String;
5426 let zle_val: &str = if zle_special && (flags & ASSPM_AUGMENT) != 0 {
5427 use crate::ported::zle::zle_params as zp;
5428 zle_augmented = match s {
5429 "BUFFER" => format!("{}{}", zp::get_buffer(), val),
5430 "LBUFFER" => format!("{}{}", zp::get_lbuffer(), val),
5431 "RBUFFER" => format!("{}{}", zp::get_rbuffer(), val),
5432 // c:2775-2778 — integer augment is arithmetic add.
5433 _ => {
5434 let old = zp::get_cursor() as i64;
5435 let add = val.trim().parse::<i64>().unwrap_or(0);
5436 (old + add).to_string()
5437 }
5438 };
5439 &zle_augmented
5440 } else {
5441 val
5442 };
5443 if zle_special && crate::zle_param_sync::live_write(s, zle_val) {
5444 return getsparam(s).map(|_| {
5445 Box::new(crate::ported::zsh_h::param {
5446 node: crate::ported::zsh_h::hashnode {
5447 next: None,
5448 nam: s.to_string(),
5449 flags: 0,
5450 },
5451 u_data: 0,
5452 u_tied: None,
5453 u_arr: None,
5454 u_str: Some(zle_val.to_string()),
5455 u_val: 0,
5456 u_dval: 0.0,
5457 u_hash: None,
5458 gsu_s: None,
5459 gsu_i: None,
5460 gsu_f: None,
5461 gsu_a: None,
5462 gsu_h: None,
5463 base: 0,
5464 width: 0,
5465 env: None,
5466 ename: None,
5467 old: None,
5468 level: 0,
5469 })
5470 });
5471 }
5472 // c:3233/3252 — `getvalue(&vbuf, &s, 1)` routes through
5473 // fetchvalue which resolves PM_NAMEREF chains (c:2247-2270).
5474 // Mirror by redirecting the assignment to the resolved target.
5475 {
5476 let base = s.split('[').next().unwrap_or(s);
5477 if crate::ported::params::is_nameref(base) {
5478 match crate::ported::params::resolve_nameref_name(base, None) {
5479 crate::ported::params::nameref_resolution::SelfRef => {
5480 // zerr emitted inside the walk (c:6341-6343).
5481 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
5482 return None;
5483 }
5484 crate::ported::params::nameref_resolution::OutOfScope => {
5485 // c:1108-1118 — createparam refuses the existing
5486 // ref; assignment fails (status 1, no errflag —
5487 // c:3255 is commented out in the C source).
5488 return None;
5489 }
5490 crate::ported::params::nameref_resolution::Placeholder(last) => {
5491 // Chain ends at a placeholder/unset ref: the value
5492 // becomes its new refname (assignstrvalue
5493 // PM_NAMEREF arm, c:2712-2717 + c:3258).
5494 // assignstrvalue PM_NAMEREF arm, INLINE per Src/params.c:2690-2720
5495 // (former vm_helper::nameref_assign_refname,
5496 // single-caller inline 2026-06-12):
5497 // c:2696-2697 read-only; c:3258-3264 valid_refname;
5498 // c:2717 SETREFNAME; c:2845 setscope.
5499 return (|| -> Option<Param> {
5500 let last_n: &str = &last;
5501 let snap = {
5502 let tab = paramtab().read().ok()?;
5503 tab.get(last_n).map(|p| (p.node.flags, p.node.nam.clone()))
5504 };
5505 let (flags, nam) = snap?;
5506 if (flags as u32 & PM_READONLY) != 0 {
5507 // c:2696-2697
5508 zerr(&format!("read-only variable: {}", nam));
5509 return None;
5510 }
5511 if !val.is_empty() && !valid_refname(val, flags) {
5512 // c:3258-3264
5513 zerr(&format!("invalid name reference: {}", val));
5514 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
5515 return None;
5516 }
5517 {
5518 let mut tab = paramtab().write().ok()?;
5519 let pm = tab.get_mut(last_n)?;
5520 pm.base = 0; // c:6376 shape (rebind resets scope info)
5521 pm.width = 0;
5522 pm.u_str = Some(val.to_string()); // c:2717 SETREFNAME
5523 pm.node.flags &= !(PM_DEFAULTED as i32); // c:3269 + c:2712
5524 }
5525 // c:2845 — setscope(v->pm): self-reference detection + base.
5526 if setscope_by_name(last_n) != 0 {
5527 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
5528 return None;
5529 }
5530 paramtab().read().ok()?.get(last_n).cloned()
5531 })();
5532 }
5533 crate::ported::params::nameref_resolution::Target {
5534 name: t,
5535 subscript: rsub,
5536 pm: rpm,
5537 level,
5538 } => {
5539 // Rebuild the target expression: ref subscript
5540 // first (REFSLICE, c:2264-2268), then any caller
5541 // subscript.
5542 let user_sub = s.find('[').map(|i| &s[i..]);
5543 // Hidden (old-chain) binding — the upscope walk
5544 // (c:6455) resolved past the visible binding;
5545 // write through the chain node directly.
5546 if rsub.is_none() && user_sub.is_none() && rpm.is_some() {
5547 let visible_level = paramtab()
5548 .read()
5549 .ok()
5550 .and_then(|tb| tb.get(&t).map(|p| p.level));
5551 if visible_level != Some(level) {
5552 // upscope write (c:6455 landed on a pm->old
5553 // node), INLINE (former nameref_hidden_scalar_assign).
5554 return (|name: &str, level: i32, val: &str| -> Option<Param> {
5555 let mut tab = paramtab().write().ok()?;
5556 let mut node: &mut param = tab.get_mut(name)?.as_mut();
5557 while node.level != level {
5558 node = node.old.as_mut()?.as_mut();
5559 }
5560 if (node.node.flags as u32 & PM_READONLY) != 0 {
5561 zerr(&format!("read-only variable: {}", name)); // c:2696
5562 return None;
5563 }
5564 let t = PM_TYPE(node.node.flags as u32);
5565 if t == PM_INTEGER {
5566 // c:Src/params.c:4007 intsetfn via mathevali.
5567 node.u_val = crate::ported::math::mathevali(val).unwrap_or(0);
5568 } else if t == PM_EFLOAT || t == PM_FFLOAT {
5569 node.u_dval = val.trim().parse().unwrap_or(0.0);
5570 } else {
5571 // c:3236-3250 — array/hash → scalar type flip through ref.
5572 let type_mask = PM_TYPE(u32::MAX) as i32;
5573 node.node.flags =
5574 (node.node.flags & !type_mask) | PM_SCALAR as i32;
5575 node.u_arr = None;
5576 node.u_str = Some(val.to_string());
5577 }
5578 node.node.flags &= !((PM_UNSET | PM_DECLARED) as i32); // c:2712 + c:3269
5579 Some(Box::new(node.clone()))
5580 })(&t, level, val);
5581 }
5582 }
5583 let mut new_s = t.clone();
5584 if let Some(rs) = &rsub {
5585 new_s.push('[');
5586 new_s.push_str(rs);
5587 new_s.push(']');
5588 }
5589 if let Some(us) = user_sub {
5590 new_s.push_str(us);
5591 }
5592 if new_s != s {
5593 return assignsparam(&new_s, val, flags);
5594 }
5595 }
5596 crate::ported::params::nameref_resolution::NotRef => {}
5597 }
5598 }
5599 }
5600 // c:Src/params.c randomsetfn / secondssetfn — re-assigning a
5601 // regenerator-style special clears the PM_UNSET flag so the
5602 // getfn becomes live again. zshrs's lookup_special_var uses a
5603 // side-set for this (no real pm node), so clear it here. Bug #417.
5604 if matches!(
5605 s,
5606 "RANDOM" | "SECONDS" | "EPOCHSECONDS" | "EPOCHREALTIME" | "TTYIDLE" | "ERRNO"
5607 ) {
5608 clear_unset_special(s);
5609 }
5610 queue_signals(); // c:3209
5611
5612 // c:3210 — `strchr(s, '[')`. Split the leading name from the
5613 // subscript while preserving C's `*ss = '\0'` / `*ss = '['`
5614 // restore semantics: the Rust port works on `&str` slices so
5615 // there's no in-place null-terminator dance, but the parse
5616 // shape is identical.
5617 // c:Src/params.c:3210 — `strchr(s, '[')` finds the subscript
5618 // opener; C's parse_subscript (params.c:1480+) then walks to the
5619 // matching `]` respecting backslash-escapes and nesting. zshrs's
5620 // previous `s.rfind(']')` picked the LAST `]` which works for
5621 // simple `name[key]` but mis-bounds `A[\[k\]]` (where the key
5622 // contains escaped brackets).
5623 let (name, subscript) = match s.find('[') {
5624 Some(i) => {
5625 // Walk forward from `i + 1` matching brackets with
5626 // escape-awareness per parse_subscript semantics.
5627 let mut depth = 1i32;
5628 let mut close_byte: Option<usize> = None;
5629 let mut bslash = false;
5630 let mut byte_off = i + 1;
5631 for ch in s[i + 1..].chars() {
5632 if bslash {
5633 bslash = false;
5634 byte_off += ch.len_utf8();
5635 continue;
5636 }
5637 match ch {
5638 '\\' => bslash = true,
5639 '[' => depth += 1,
5640 ']' => {
5641 depth -= 1;
5642 if depth == 0 {
5643 close_byte = Some(byte_off);
5644 break;
5645 }
5646 }
5647 _ => {}
5648 }
5649 byte_off += ch.len_utf8();
5650 }
5651 let close = close_byte.unwrap_or(s.len());
5652 let key_end = if close > i { close } else { s.len() };
5653 (&s[..i], Some(&s[i + 1..key_end]))
5654 }
5655 None => (s, None),
5656 };
5657 // c:Src/params.c parse_subscript — backslash-escapes in the
5658 // subscript body (`\[`, `\]`, `\\`) are stripped to their literal
5659 // form for the actual key value. `A[\[k\]]=v` stores under key
5660 // `[k]`. zshrs's subscript extractor above preserved the escapes
5661 // verbatim, so the stored key was `\[k\]` and the matching lookup
5662 // `${A[[k]]}` couldn't find it.
5663 let subscript_owned: Option<String> = subscript.map(|key| {
5664 let mut out = String::with_capacity(key.len());
5665 let mut bslash = false;
5666 for ch in key.chars() {
5667 if bslash {
5668 out.push(ch);
5669 bslash = false;
5670 } else if ch == '\\' {
5671 bslash = true;
5672 } else {
5673 out.push(ch);
5674 }
5675 }
5676 if bslash {
5677 out.push('\\');
5678 }
5679 out
5680 });
5681 let subscript: Option<&str> = subscript_owned.as_deref();
5682
5683 // c:Src/params.c — a bare all-digit parameter name (`2=X`,
5684 // `2+=end`) addresses the positional parameter $N (1-based). C maps
5685 // digit names onto the special `argv` value (isident accepts them,
5686 // c:1336-1340); zshrs keeps positionals in PPARAMS, so store there
5687 // directly — the same backing the `argv[N]=`/`@[N]=` path uses
5688 // below. `$0` (n==0) is the shell/script name, not a positional, so
5689 // it falls through to the normal param path. `+=` concatenates onto
5690 // the existing positional (ASSPM_AUGMENT scalar append, c:3270-3276).
5691 if subscript.is_none() && !name.is_empty() && name.bytes().all(|b| b.is_ascii_digit()) {
5692 if let Ok(n) = name.parse::<usize>() {
5693 if n >= 1 {
5694 if let Ok(mut pp) = crate::ported::builtin::PPARAMS.lock() {
5695 while pp.len() < n {
5696 pp.push(String::new());
5697 }
5698 let idx = n - 1;
5699 if (flags & ASSPM_AUGMENT) != 0 {
5700 let cur = pp[idx].clone();
5701 pp[idx] = format!("{}{}", cur, val);
5702 } else {
5703 pp[idx] = val.to_string();
5704 }
5705 }
5706 unqueue_signals();
5707 return Some(Box::new(param {
5708 node: hashnode {
5709 next: None,
5710 nam: name.to_string(),
5711 flags: PM_SCALAR as i32,
5712 },
5713 u_data: 0,
5714 u_tied: None,
5715 u_arr: None,
5716 u_str: Some(val.to_string()),
5717 u_val: 0,
5718 u_dval: 0.0,
5719 u_hash: None,
5720 gsu_s: None,
5721 gsu_i: None,
5722 gsu_f: None,
5723 gsu_a: None,
5724 gsu_h: None,
5725 base: 0,
5726 width: 0,
5727 env: None,
5728 ename: None,
5729 old: None,
5730 level: 0,
5731 }));
5732 }
5733 }
5734 }
5735
5736 // c:Src/Modules/parameter.c — magic associative-array assignment
5737 // forms: `functions[name]=body`, `aliases[name]=value`,
5738 // `dis_functions[name]=body`, `saliases[name]=value`,
5739 // `galiases[name]=value`, `dis_aliases[name]=value`, etc.
5740 // These reach assignsparam as `name[key]=value` shape and must
5741 // dispatch to the canonical setpmfunction / setpmalias hooks
5742 // BEFORE the generic paramtab subscript store. Without this
5743 // route, the assignment lands in paramtab_hashed_storage as
5744 // a normal assoc element and the corresponding function/alias
5745 // is never actually defined in shfunctab/aliastab.
5746 if let Some(key) = subscript {
5747 match name {
5748 "functions" => {
5749 use crate::ported::zsh_h::hashnode;
5750 use crate::ported::zsh_h::param as ParamStruct;
5751 // c:Src/params.c:3270-3276 — ASSPM_AUGMENT on PM_SCALAR
5752 // appends raw text to the existing value. The
5753 // `functions[name]` magic-assoc is scalar-typed under
5754 // the hood (each entry is a function-body string). For
5755 // `functions[f]+="echo b"`, fetch the existing body
5756 // from shfunctab and concatenate before calling
5757 // setpmfunction. Without this, `+=` silently no-ops.
5758 // Bug #323 in docs/BUGS.md.
5759 let final_body = if (flags & ASSPM_AUGMENT) != 0 {
5760 let existing = crate::ported::hashtable::shfunctab_lock()
5761 .read()
5762 .ok()
5763 .and_then(|tab| tab.get(key).and_then(|shf| shf.body.clone()))
5764 .unwrap_or_default();
5765 format!("{}{}", existing, val)
5766 } else {
5767 val.to_string()
5768 };
5769 let pm: Box<ParamStruct> = Box::new(ParamStruct {
5770 node: hashnode {
5771 next: None,
5772 nam: key.to_string(),
5773 flags: 0,
5774 },
5775 u_data: 0,
5776 u_tied: None,
5777 u_arr: None,
5778 u_str: None,
5779 u_val: 0,
5780 u_dval: 0.0,
5781 u_hash: None,
5782 gsu_s: None,
5783 gsu_i: None,
5784 gsu_f: None,
5785 gsu_a: None,
5786 gsu_h: None,
5787 base: 0,
5788 width: 0,
5789 env: None,
5790 ename: None,
5791 old: None,
5792 level: 0,
5793 });
5794 crate::ported::modules::parameter::setpmfunction(pm.clone(), final_body);
5795 unqueue_signals();
5796 return Some(pm);
5797 }
5798 "dis_functions" => {
5799 use crate::ported::zsh_h::hashnode;
5800 use crate::ported::zsh_h::param as ParamStruct;
5801 let pm: Box<ParamStruct> = Box::new(ParamStruct {
5802 node: hashnode {
5803 next: None,
5804 nam: key.to_string(),
5805 flags: 0,
5806 },
5807 u_data: 0,
5808 u_tied: None,
5809 u_arr: None,
5810 u_str: None,
5811 u_val: 0,
5812 u_dval: 0.0,
5813 u_hash: None,
5814 gsu_s: None,
5815 gsu_i: None,
5816 gsu_f: None,
5817 gsu_a: None,
5818 gsu_h: None,
5819 base: 0,
5820 width: 0,
5821 env: None,
5822 ename: None,
5823 old: None,
5824 level: 0,
5825 });
5826 crate::ported::modules::parameter::setpmdisfunction(pm.clone(), val.to_string());
5827 unqueue_signals();
5828 return Some(pm);
5829 }
5830 "aliases" => {
5831 // c:Src/Modules/parameter.c — install a regular
5832 // alias via canonical aliastab. Use createaliasnode
5833 // with default flags (no ALIAS_GLOBAL / ALIAS_SUFFIX).
5834 if let Ok(mut tab) = crate::ported::hashtable::aliastab_lock().write() {
5835 let node = crate::ported::hashtable::createaliasnode(key, val, 0u32);
5836 tab.add(node);
5837 }
5838 unqueue_signals();
5839 return Some(Box::new(crate::ported::zsh_h::param {
5840 node: crate::ported::zsh_h::hashnode {
5841 next: None,
5842 nam: key.to_string(),
5843 flags: 0,
5844 },
5845 u_data: 0,
5846 u_tied: None,
5847 u_arr: None,
5848 u_str: Some(val.to_string()),
5849 u_val: 0,
5850 u_dval: 0.0,
5851 u_hash: None,
5852 gsu_s: None,
5853 gsu_i: None,
5854 gsu_f: None,
5855 gsu_a: None,
5856 gsu_h: None,
5857 base: 0,
5858 width: 0,
5859 env: None,
5860 ename: None,
5861 old: None,
5862 level: 0,
5863 }));
5864 }
5865 "galiases" => {
5866 // c:Src/Modules/parameter.c — global alias.
5867 if let Ok(mut tab) = crate::ported::hashtable::aliastab_lock().write() {
5868 let node = crate::ported::hashtable::createaliasnode(
5869 key,
5870 val,
5871 crate::ported::zsh_h::ALIAS_GLOBAL as u32,
5872 );
5873 tab.add(node);
5874 }
5875 unqueue_signals();
5876 return Some(Box::new(crate::ported::zsh_h::param {
5877 node: crate::ported::zsh_h::hashnode {
5878 next: None,
5879 nam: key.to_string(),
5880 flags: 0,
5881 },
5882 u_data: 0,
5883 u_tied: None,
5884 u_arr: None,
5885 u_str: Some(val.to_string()),
5886 u_val: 0,
5887 u_dval: 0.0,
5888 u_hash: None,
5889 gsu_s: None,
5890 gsu_i: None,
5891 gsu_f: None,
5892 gsu_a: None,
5893 gsu_h: None,
5894 base: 0,
5895 width: 0,
5896 env: None,
5897 ename: None,
5898 old: None,
5899 level: 0,
5900 }));
5901 }
5902 "saliases" => {
5903 // c:Src/Modules/parameter.c — suffix alias.
5904 if let Ok(mut tab) = crate::ported::hashtable::sufaliastab_lock().write() {
5905 let node = crate::ported::hashtable::createaliasnode(
5906 key,
5907 val,
5908 crate::ported::zsh_h::ALIAS_SUFFIX as u32,
5909 );
5910 tab.add(node);
5911 }
5912 unqueue_signals();
5913 return Some(Box::new(crate::ported::zsh_h::param {
5914 node: crate::ported::zsh_h::hashnode {
5915 next: None,
5916 nam: key.to_string(),
5917 flags: 0,
5918 },
5919 u_data: 0,
5920 u_tied: None,
5921 u_arr: None,
5922 u_str: Some(val.to_string()),
5923 u_val: 0,
5924 u_dval: 0.0,
5925 u_hash: None,
5926 gsu_s: None,
5927 gsu_i: None,
5928 gsu_f: None,
5929 gsu_a: None,
5930 gsu_h: None,
5931 base: 0,
5932 width: 0,
5933 env: None,
5934 ename: None,
5935 old: None,
5936 level: 0,
5937 }));
5938 }
5939 "options" => {
5940 // c:Src/Modules/parameter.c:926 setpmoption — userspace
5941 // `options[X]=on|off` toggles option X via dosetopt, not
5942 // a generic assoc-write. Build a synthetic Param whose
5943 // node.nam carries the option name and dispatch to the
5944 // canonical setpmoption port (`src/ported/modules/
5945 // parameter.rs:1620`), which calls optlookup + dosetopt.
5946 use crate::ported::zsh_h::hashnode;
5947 use crate::ported::zsh_h::param as ParamStruct;
5948 let pm: Box<ParamStruct> = Box::new(ParamStruct {
5949 node: hashnode {
5950 next: None,
5951 nam: key.to_string(),
5952 flags: 0,
5953 },
5954 u_data: 0,
5955 u_tied: None,
5956 u_arr: None,
5957 u_str: None,
5958 u_val: 0,
5959 u_dval: 0.0,
5960 u_hash: None,
5961 gsu_s: None,
5962 gsu_i: None,
5963 gsu_f: None,
5964 gsu_a: None,
5965 gsu_h: None,
5966 base: 0,
5967 width: 0,
5968 env: None,
5969 ename: None,
5970 old: None,
5971 level: 0,
5972 });
5973 crate::ported::modules::parameter::setpmoption(pm.clone(), val.to_string());
5974 unqueue_signals();
5975 return Some(pm);
5976 }
5977 "commands" => {
5978 // c:Src/Modules/parameter.c:151-160 setpmcommand —
5979 // `commands[name]=path` installs a HASHED Cmdnam
5980 // node in cmdnamtab (`cn->node.flags = HASHED;
5981 // cn->u.cmd = ztrdup(value); cmdnamtab->addnode`),
5982 // exactly like `hash name=path`. zsh ACCEPTS the
5983 // write (verified vs zsh 5.9: rc=0, readback works,
5984 // whence/hash see the entry). Build a synthetic
5985 // Param whose node.nam carries the command name and
5986 // dispatch to the canonical setpmcommand port.
5987 // Bug #375.
5988 use crate::ported::zsh_h::hashnode;
5989 use crate::ported::zsh_h::param as ParamStruct;
5990 let pm: Box<ParamStruct> = Box::new(ParamStruct {
5991 node: hashnode {
5992 next: None,
5993 nam: key.to_string(),
5994 flags: 0,
5995 },
5996 u_data: 0,
5997 u_tied: None,
5998 u_arr: None,
5999 u_str: None,
6000 u_val: 0,
6001 u_dval: 0.0,
6002 u_hash: None,
6003 gsu_s: None,
6004 gsu_i: None,
6005 gsu_f: None,
6006 gsu_a: None,
6007 gsu_h: None,
6008 base: 0,
6009 width: 0,
6010 env: None,
6011 ename: None,
6012 old: None,
6013 level: 0,
6014 });
6015 crate::ported::modules::parameter::setpmcommand(pm.clone(), val.to_string());
6016 unqueue_signals();
6017 return Some(pm);
6018 }
6019 "mapfile" => {
6020 // c:Src/Modules/mapfile.c:68 setpmmapfile —
6021 // `mapfile[fname]=value` WRITES `value` to the file
6022 // named by the key (creating it if needed). The setfn
6023 // is ported; route the element-assign to it (the
6024 // PartabHashEntry dispatch carries only get/scan).
6025 use crate::ported::zsh_h::hashnode;
6026 use crate::ported::zsh_h::param as ParamStruct;
6027 crate::ported::modules::mapfile::setpmmapfile(key, val, false);
6028 let pm: Box<ParamStruct> = Box::new(ParamStruct {
6029 node: hashnode {
6030 next: None,
6031 nam: key.to_string(),
6032 flags: 0,
6033 },
6034 u_data: 0,
6035 u_tied: None,
6036 u_arr: None,
6037 u_str: None,
6038 u_val: 0,
6039 u_dval: 0.0,
6040 u_hash: None,
6041 gsu_s: None,
6042 gsu_i: None,
6043 gsu_f: None,
6044 gsu_a: None,
6045 gsu_h: None,
6046 base: 0,
6047 width: 0,
6048 env: None,
6049 ename: None,
6050 old: None,
6051 level: 0,
6052 });
6053 unqueue_signals();
6054 return Some(pm);
6055 }
6056 _ => {}
6057 }
6058 }
6059
6060 // c:Src/params.c:3262 IPDEF9 — `argv`, `@`, `*` are aliases for
6061 // the global `pparams` (positional parameter vector). Whole-
6062 // array writes (`argv=(...)`) route through assignaparam at
6063 // params.rs:5487 which updates PPARAMS. Subscripted writes
6064 // (`argv[N]=val`) must do the same — without this, the value
6065 // landed in paramtab's u_arr but `$@`/`$1`/`$2`/... continued
6066 // reading from PPARAMS, so the update was invisible. Bug #281
6067 // in docs/BUGS.md.
6068 if let Some(key) = subscript {
6069 if matches!(name, "argv" | "@" | "*") {
6070 if let Ok(idx) = key.trim().parse::<i64>() {
6071 if let Ok(mut pp) = crate::ported::builtin::PPARAMS.lock() {
6072 let len = pp.len() as i64;
6073 let kshzero =
6074 crate::ported::zsh_h::isset(crate::ported::zsh_h::KSHZEROSUBSCRIPT);
6075 if idx == 0 && !isset(KSHARRAYS) && !kshzero {
6076 zerr(&format!("{}: assignment to invalid subscript range", name));
6077 drop(pp);
6078 unqueue_signals();
6079 return None;
6080 }
6081 let real_idx = if idx < 0 {
6082 (len + idx).max(0) as usize
6083 } else if isset(KSHARRAYS) {
6084 idx.max(0) as usize
6085 } else {
6086 (idx - 1).max(0) as usize
6087 };
6088 while pp.len() <= real_idx {
6089 pp.push(String::new());
6090 }
6091 pp[real_idx] = val.to_string();
6092 }
6093 unqueue_signals();
6094 return Some(Box::new(param {
6095 node: hashnode {
6096 next: None,
6097 nam: name.to_string(),
6098 flags: PM_ARRAY as i32,
6099 },
6100 u_data: 0,
6101 u_tied: None,
6102 u_arr: None,
6103 u_str: None,
6104 u_val: 0,
6105 u_dval: 0.0,
6106 u_hash: None,
6107 gsu_s: None,
6108 gsu_i: None,
6109 gsu_f: None,
6110 gsu_a: None,
6111 gsu_h: None,
6112 base: 0,
6113 width: 0,
6114 env: None,
6115 ename: None,
6116 old: None,
6117 level: 0,
6118 }));
6119 }
6120 }
6121 }
6122
6123 // c:Src/Modules/parameter.c — read-only magic-assoc names. C
6124 // zsh's SPECIALPMDEF / createspecialhash sets PM_READONLY on
6125 // these so userspace assignment errors with `read-only
6126 // variable: NAME`. `vm_helper::init_partab_params` strips
6127 // PM_READONLY off the paramtab stub to allow INTERNAL writes
6128 // (function-call funcstack push, etc.) — see comment at
6129 // vm_helper.rs:2799. The trade-off is that USERSPACE writes
6130 // now slip through too. Intercept here, AFTER the writable-
6131 // magic-assoc dispatch arms above (functions, aliases,
6132 // dis_aliases, galiases, saliases, dis_functions) but BEFORE
6133 // the generic paramtab subscript store, so the userspace path
6134 // emits the canonical diagnostic and exits non-zero. Internal
6135 // table mutations that bypass `assignsparam` stay free. Bug
6136 // #242 in docs/BUGS.md.
6137 if subscript.is_some() {
6138 // `options` is intentionally NOT in this list — C zsh's
6139 // `setpmoption`/`setpmoptions` (Src/Modules/parameter.c:926-979)
6140 // accept "on"/"off" writes and translate them to dosetopt calls.
6141 // The `options` arm above this readonly-list check routes
6142 // `options[X]=on|off` writes through the canonical setpmoption
6143 // port, so by the time we reach here `options` has already
6144 // returned. Bug #342.
6145 // `commands` is intentionally NOT in this list — C zsh's
6146 // `setpmcommand` (Src/Modules/parameter.c:151-160) ACCEPTS
6147 // `commands[name]=path` and installs a HASHED Cmdnam node in
6148 // cmdnamtab (same effect as `hash name=path`). Verified vs
6149 // zsh 5.9: `commands[x]=/y` → rc=0, ${commands[x]} → /y,
6150 // `whence x` → /y. The `commands` arm above routes through
6151 // the canonical setpmcommand port. Bug #375.
6152 let is_readonly_magic = matches!(
6153 name,
6154 "builtins"
6155 | "modules"
6156 | "parameters"
6157 | "dis_builtins"
6158 | "history"
6159 | "historywords"
6160 | "jobtexts"
6161 | "jobstates"
6162 | "jobdirs"
6163 | "nameddirs"
6164 | "userdirs"
6165 | "usergroups"
6166 | "widgets"
6167 | "functions_source"
6168 | "dis_functions_source"
6169 | "terminfo"
6170 | "termcap"
6171 );
6172 if is_readonly_magic {
6173 zerr(&format!("read-only variable: {}", name));
6174 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
6175 unqueue_signals();
6176 return None;
6177 }
6178 }
6179
6180 // Subscripted path (c:3210-3231).
6181 if let Some(key) = subscript {
6182 // c:params.c:1601 (getarg) — a non-hashed parameter's subscript is an
6183 // ARITHMETIC expression. Resolve it to a numeric index HERE, before
6184 // the paramtab write lock below, because mathevalarg reads parameters
6185 // and would deadlock against a held write lock. Associative arrays use
6186 // the raw string key instead, so only pre-evaluate for non-hashed.
6187 let is_hashed = {
6188 let flagged = {
6189 let tab = paramtab().read().unwrap();
6190 tab.get(name)
6191 .map(|pm| (pm.node.flags as u32 & PM_HASHED) != 0)
6192 .unwrap_or(false)
6193 };
6194 // c:Src/Zle/complete.c — `$compstate` is a special ASSOCIATIVE
6195 // parameter of the completion system. zshrs does not wire it as a
6196 // real PM_HASHED paramtab node (complete.rs:1682 defers that); its
6197 // values live in the parallel `paramtab_hashed_storage()` store fed
6198 // by set_compstate_str. A subscript WRITE like `compstate[insert]=
6199 // menu` from a completion widget must take the ASSOCIATIVE path so
6200 // the string key `insert` is NOT arithmetic-evaluated to 0 (which
6201 // then errored `assignment to invalid subscript range` — flooding
6202 // every tab-completion). Recognise it as hashed whenever the name
6203 // is a known special assoc OR already has hash-storage backing.
6204 // Removing the undeclared-subscript auto-vivify (correct for user
6205 // params like `as[k1]=`) regressed this because compstate relied on
6206 // that vivify; a truly-undeclared user param still errors.
6207 // `$compstate` is the completion system's special assoc (the only
6208 // special assoc user completers write with string keys).
6209 flagged
6210 || name == "compstate"
6211 || paramtab_hashed_storage()
6212 .lock()
6213 .map(|s| s.contains_key(name))
6214 .unwrap_or(false)
6215 };
6216 let resolved_idx: i64 = if is_hashed {
6217 0 // unused for hashed params
6218 } else {
6219 key.parse::<i64>().unwrap_or_else(|_| {
6220 // c:Src/params.c:2058 getarg → c:1585-1593 — a subscript that
6221 // is not a plain literal is parameter-expanded (singsub) BEFORE
6222 // arithmetic evaluation. When assignsparam is reached with a
6223 // raw, UNEXPANDED subscript — e.g. `setsparam("pgid[$#pgid+1]",
6224 // …)` from the sysread/read builtins, whose target string the
6225 // command compiler never pre-expanded — `$#pgid` / `$n` /
6226 // `${#a}` must be resolved here, or mathevalarg sees a literal
6227 // `$` and yields 0 → "invalid subscript range" (gitstatus's
6228 // `_gitstatus_daemon_p9k_` hit this on every prompt). The
6229 // shell-level `a[$n+1]=v` form already arrives expanded, so its
6230 // numeric key takes the parse fast-path above and skips this.
6231 let expanded = crate::ported::subst::singsub(&key);
6232 crate::ported::math::mathevalarg(&expanded)
6233 })
6234 };
6235 let mut tab = paramtab().write().unwrap();
6236 let exists = tab.contains_key(name); // c:3212
6237 if !exists {
6238 // c:3213 `createparam(t, PM_ARRAY); created = 1;` — but when the
6239 // name is a hash-storage-backed associative (e.g. `$compstate`),
6240 // create it PM_HASHED so the dispatch below routes to the assoc
6241 // element store instead of the numeric-subscript array path.
6242 let (create_flags, create_arr) = if is_hashed {
6243 (PM_HASHED as i32, None)
6244 } else {
6245 (PM_ARRAY as i32, Some(Vec::new()))
6246 };
6247 let pm: Param = Box::new(param {
6248 node: hashnode {
6249 next: None,
6250 nam: name.to_string(),
6251 flags: create_flags,
6252 },
6253 u_data: 0,
6254 u_tied: None,
6255 u_arr: create_arr,
6256 u_str: None,
6257 u_val: 0,
6258 u_dval: 0.0,
6259 u_hash: None,
6260 gsu_s: None,
6261 gsu_i: None,
6262 gsu_f: None,
6263 gsu_a: None,
6264 gsu_h: None,
6265 base: 0,
6266 width: 0,
6267 env: None,
6268 ename: None,
6269 old: None,
6270 level: 0,
6271 });
6272 tab.insert(name.to_string(), pm);
6273 } else {
6274 // c:3216 `if (v->pm->node.flags & PM_READONLY)`.
6275 let pm = tab.get(name).unwrap();
6276 if (pm.node.flags as u32 & PM_READONLY) != 0 {
6277 zerr(&format!("read-only variable: {}", pm.node.nam)); // c:3217
6278 drop(tab);
6279 unqueue_signals(); // c:3220
6280 return None; // c:3221
6281 }
6282 }
6283 // c:3231 `v = NULL;` — re-dispatch by storage type.
6284 let pm = tab.get_mut(name).unwrap();
6285 pm.node.flags &= !(PM_DEFAULTED as i32); // c:3228
6286 if (pm.node.flags as u32 & PM_HASHED) != 0 {
6287 // PM_HASHED element store. `param.u_hash` is typed
6288 // `Option<HashTable>` per Src/zsh.h:1841 but the
6289 // HashTable runtime backing isn't wired; the assoc-array
6290 // values live in a parallel storage keyed on param name
6291 // (`paramtab_hashed_storage()`).
6292 let mut store = paramtab_hashed_storage().lock().unwrap();
6293 store
6294 .entry(name.to_string())
6295 .or_default()
6296 .insert(key.to_string(), val.to_string());
6297 } else {
6298 // Non-hashed param: subscript already arithmetic-resolved above
6299 // (c:params.c:1601). zsh never auto-creates an assoc here, so
6300 // `as[k1]=bb` on undeclared `as` uses idx 0 → invalid range.
6301 let idx = resolved_idx;
6302 // c:Src/params.c:2748-2789 — PM_SCALAR + numeric subscript
6303 // SPLICES the value into the scalar's char string
6304 // (`a=hello; a[2]=X` → `hXllo`). Only PM_ARRAY does
6305 // element-store at this subscript. Bug #589 in
6306 // docs/BUGS.md: zshrs's subscript store always treated
6307 // numeric subscript as array-store, so `a[2]=X` on a
6308 // scalar cleared `u_str` and put "X" at array slot 1,
6309 // leaving `$a` displaying as `" X"` (empty + space + X)
6310 // when joined.
6311 let pm_type = PM_TYPE(pm.node.flags as u32);
6312 if pm_type == PM_SCALAR {
6313 // c:2748+ scalar splice — replace chars at index.
6314 let s = pm.u_str.clone().unwrap_or_default();
6315 let chars: Vec<char> = s.chars().collect();
6316 let len = chars.len() as i64;
6317 let kshzero = crate::ported::zsh_h::isset(crate::ported::zsh_h::KSHZEROSUBSCRIPT);
6318 if idx == 0 && !isset(KSHARRAYS) && !kshzero {
6319 zerr(&format!("{}: assignment to invalid subscript range", name));
6320 drop(tab);
6321 unqueue_signals();
6322 return None;
6323 }
6324 // 1-based forward, negative-from-end. KSHARRAYS = 0-based.
6325 let raw = if idx < 0 {
6326 len + idx
6327 } else if isset(KSHARRAYS) {
6328 idx
6329 } else {
6330 idx - 1
6331 };
6332 let mut out = String::with_capacity(s.len() + val.len());
6333 if raw < 0 {
6334 // c:Src/params.c:2721-2733 (assignstrvalue) — a negative
6335 // subscript that points PAST the start of the string
6336 // (`a[-4]` on "abc") clamps BOTH start and end to 0, so
6337 // the slice old[0..0] is empty and val is INSERTED at the
6338 // front (overwriting nothing): `a[-4]="x"` → "xabc". The
6339 // prior port clamped to index 0 and OVERWROTE char 0.
6340 out.push_str(val);
6341 out.extend(chars.iter());
6342 } else {
6343 // Replace one char at real_idx with val. If real_idx is
6344 // past the end, append (C clamps end to zlen and concats).
6345 let real_idx = (raw as usize).min(chars.len());
6346 out.extend(chars[..real_idx].iter());
6347 out.push_str(val);
6348 if real_idx < chars.len() {
6349 out.extend(chars[real_idx + 1..].iter());
6350 }
6351 }
6352 pm.u_str = Some(out);
6353 } else {
6354 // PM_ARRAY + numeric subscript (c:3357 `assignaparam`).
6355 // c:Src/params.c:2125-2150 + c:2911 — `a[0]=val` under
6356 // default zsh semantics (no KSHARRAYS, no KSHZEROSUBSCRIPT)
6357 // produces VALFLAG_EMPTY in getarg, which setarrvalue
6358 // then rejects with "assignment to invalid subscript
6359 // range".
6360 let kshzero = crate::ported::zsh_h::isset(crate::ported::zsh_h::KSHZEROSUBSCRIPT);
6361 if idx == 0 && !isset(KSHARRAYS) && !kshzero {
6362 zerr(&format!("{}: assignment to invalid subscript range", name)); // c:2911 (effective)
6363 drop(tab);
6364 unqueue_signals();
6365 return None;
6366 }
6367 // c:2966-2967 — capture PM_UNIQUE before the mutable
6368 // borrow so the post-splice dedup below can fire.
6369 let uniq = (pm.node.flags as u32 & PM_UNIQUE) != 0;
6370 let arr = pm.u_arr.get_or_insert_with(Vec::new);
6371 let len = arr.len() as i64;
6372 let real_idx = if idx < 0 {
6373 len + idx
6374 } else if isset(KSHARRAYS) {
6375 idx
6376 } else {
6377 idx - 1
6378 };
6379 if real_idx < 0 {
6380 // c:Src/params.c:2930-2946 (setarrvalue) — a negative
6381 // subscript that points PAST the start of the array
6382 // (`a[-7]` on a 5-element array) clamps BOTH start and
6383 // end to 0, so the splice `old[start..end]` =
6384 // `old[0..0]` replaces nothing and the value is
6385 // INSERTED at the front. The prior port clamped to
6386 // index 0 and OVERWROTE element 0 (`42 2 3 4 5`);
6387 // zsh prepends keeping every element (`42 1 2 3 4 5`).
6388 arr.insert(0, val.to_string());
6389 } else {
6390 let real_idx = real_idx as usize;
6391 while arr.len() <= real_idx {
6392 arr.push(String::new());
6393 }
6394 arr[real_idx] = val.to_string();
6395 }
6396 // c:2966-2967 — `if (pm->node.flags & PM_UNIQUE)
6397 // arrunique(pm->u.arr)`. A subscript element assignment to a
6398 // PM_UNIQUE array dedupes in-place (first occurrence wins):
6399 // `typeset -aU u=(a b c); u[2]=c` → (a c). setarrvalue
6400 // already does this at its tail; the single-index element
6401 // path bypassed it.
6402 if uniq {
6403 let mut seen: std::collections::HashSet<String> =
6404 std::collections::HashSet::new();
6405 arr.retain(|s| seen.insert(s.clone())); // c:2967
6406 }
6407 pm.u_str = None;
6408 }
6409 }
6410 let cloned = pm.clone();
6411 drop(tab);
6412 unqueue_signals(); // c:3344
6413 // c:Src/params.c:2922 — `pm->gsu.a->setfn(pm, val)`. In C a
6414 // SUBSCRIPT assignment (`path[2]=x`) rebuilds the whole array
6415 // and hands it to the param's array setfn, so a tied
6416 // colon-array (path→PATH, fpath→FPATH, …) re-derives its
6417 // scalar/env side. zshrs's element path above wrote `u_arr`
6418 // directly and skipped the setfn, so `path[2]=/NEW` left
6419 // $PATH stale (`echo $PATH` showed the pre-edit value). Mirror
6420 // `arrsetfn`'s ename sync here. zshrs carries the tie via the
6421 // explicit colon-array name map (mirror of the scalar→array
6422 // cascade at params.rs:~6396); rejoin the array with `:` and
6423 // write the scalar side so `echo $PATH` sees the edit.
6424 // c:Src/params.c:2922 — a tied colon-array element assignment
6425 // re-derives the scalar side (path→PATH etc.); see
6426 // TIED_COLON_ARRAYS. The direct-u_arr element path above skipped it.
6427 let base = name.split('[').next().unwrap_or(name);
6428 if let Some((_, scalar)) = TIED_COLON_ARRAYS.iter().find(|(a, _)| *a == base) {
6429 let joined = cloned.u_arr.as_deref().unwrap_or(&[]).join(":");
6430 assignsparam(scalar, &joined, 0);
6431 }
6432 return Some(cloned); // c:3345
6433 }
6434
6435 // c:3232 non-subscripted branch.
6436 let mut tab = paramtab().write().unwrap();
6437 let existing = tab.contains_key(name);
6438 let created_now = !existing; // c:3232 createparam path sets `created = 1`
6439 if !existing {
6440 // c:3234 `createparam(t, PM_SCALAR); created = 1;`
6441 let mut pm_flags = PM_SCALAR as i32;
6442 if isset(ALLEXPORT) {
6443 // c:1149-1150 (ALLEXPORT path)
6444 pm_flags |= PM_EXPORTED as i32;
6445 }
6446 // c:1135-1160 — `createparam` installs gsu via the special-
6447 // params table when the name matches a PM_SPECIAL entry. C
6448 // walks `paramtab->getnode(name)` first, but for fresh
6449 // creations the gsu pointer comes from the IPDEF macro the
6450 // paramdef ships with. Mirror by looking up the name in the
6451 // special-scalar GSU table — same end-state as if
6452 // createparamtable had run.
6453 let gsu_s: Option<Box<gsu_scalar>> = match name {
6454 "0" => {
6455 pm_flags |= PM_SPECIAL as i32;
6456 Some(Box::new(ARGZERO_GSU.clone())) // c:225-226 / IPDEF2("0", argzero_gsu, 0)
6457 }
6458 "HOME" => {
6459 pm_flags |= PM_SPECIAL as i32;
6460 Some(Box::new(HOME_GSU.clone())) // c:248
6461 }
6462 "IFS" => {
6463 pm_flags |= PM_SPECIAL as i32;
6464 Some(Box::new(IFS_GSU.clone())) // c:245
6465 }
6466 "TERM" => {
6467 pm_flags |= PM_SPECIAL as i32;
6468 Some(Box::new(TERM_GSU.clone())) // c:250
6469 }
6470 "TERMINFO" => {
6471 pm_flags |= PM_SPECIAL as i32;
6472 Some(Box::new(TERMINFO_GSU.clone())) // c:251
6473 }
6474 "TERMINFO_DIRS" => {
6475 pm_flags |= PM_SPECIAL as i32;
6476 Some(Box::new(TERMINFODIRS_GSU.clone())) // c:252
6477 }
6478 "WORDCHARS" => {
6479 pm_flags |= PM_SPECIAL as i32;
6480 Some(Box::new(WORDCHARS_GSU.clone())) // c:249
6481 }
6482 "USERNAME" => {
6483 pm_flags |= PM_SPECIAL as i32;
6484 Some(Box::new(USERNAME_GSU.clone())) // c:247
6485 }
6486 "KEYBOARD_HACK" => {
6487 pm_flags |= PM_SPECIAL as i32;
6488 Some(Box::new(KEYBOARDHACK_GSU.clone())) // c:253
6489 }
6490 "HISTCHARS" | "histchars" => {
6491 pm_flags |= PM_SPECIAL as i32;
6492 Some(Box::new(HISTCHARS_GSU.clone())) // c:246
6493 }
6494 _ => None,
6495 };
6496 let pm: Param = Box::new(param {
6497 node: hashnode {
6498 next: None,
6499 nam: name.to_string(),
6500 flags: pm_flags,
6501 },
6502 u_data: 0,
6503 u_tied: None,
6504 u_arr: None,
6505 u_str: Some(String::new()),
6506 u_val: 0,
6507 u_dval: 0.0,
6508 u_hash: None,
6509 gsu_s, // c:1149 special gsu wired
6510 gsu_i: None,
6511 gsu_f: None,
6512 gsu_a: None,
6513 gsu_h: None,
6514 base: 0,
6515 width: 0,
6516 env: None,
6517 ename: None,
6518 old: None,
6519 level: 0,
6520 });
6521 tab.insert(name.to_string(), pm);
6522 } else {
6523 let pm = tab.get(name).unwrap();
6524 // c:3216 PM_READONLY guard for an existing param.
6525 if (pm.node.flags as u32 & PM_READONLY) != 0 {
6526 zerr(&format!("read-only variable: {}", pm.node.nam)); // c:3217
6527 drop(tab);
6528 unqueue_signals(); // c:3220
6529 return None; // c:3221
6530 }
6531 // c:1135-1160 — back-fill the gsu_s vtable on existing
6532 // entries that pre-dated `createparamtable` (e.g. env-import
6533 // path inserted HOME/IFS/etc via the non-special branch
6534 // before the special-param table was populated). Without
6535 // this back-fill, those params stay None-gsu forever and
6536 // assignstrvalue falls through to the default `strsetfn`
6537 // path, bypassing homesetfn/ifssetfn — so the canonical
6538 // C globals (`home`, `ifs`, ...) drift from paramtab.
6539 let pm = tab.get_mut(name).unwrap();
6540 if pm.gsu_s.is_none() {
6541 let new_gsu: Option<Box<gsu_scalar>> = match name {
6542 "0" => Some(Box::new(ARGZERO_GSU.clone())), // c:225-226
6543 "HOME" => Some(Box::new(HOME_GSU.clone())), // c:248
6544 "IFS" => Some(Box::new(IFS_GSU.clone())), // c:245
6545 "TERM" => Some(Box::new(TERM_GSU.clone())), // c:250
6546 "TERMINFO" => Some(Box::new(TERMINFO_GSU.clone())), // c:251
6547 "TERMINFO_DIRS" => Some(Box::new(TERMINFODIRS_GSU.clone())), // c:252
6548 "WORDCHARS" => Some(Box::new(WORDCHARS_GSU.clone())), // c:249
6549 "USERNAME" => Some(Box::new(USERNAME_GSU.clone())), // c:247
6550 "KEYBOARD_HACK" => Some(Box::new(KEYBOARDHACK_GSU.clone())), // c:253
6551 "HISTCHARS" | "histchars" => Some(Box::new(HISTCHARS_GSU.clone())), // c:246
6552 _ => None,
6553 };
6554 if new_gsu.is_some() {
6555 pm.gsu_s = new_gsu;
6556 pm.node.flags |= PM_SPECIAL as i32;
6557 }
6558 }
6559 let pm = tab.get(name).unwrap();
6560 // c:3236-3250 — existing PM_ARRAY/PM_HASHED on a non-special,
6561 // non-tied, non-KSHARRAYS, non-AUGMENT scalar assignment →
6562 // `resetparam(v->pm, PM_SCALAR)`.
6563 let f = pm.node.flags as u32;
6564 let is_array_or_hash = (f & PM_ARRAY) != 0 || (f & PM_HASHED) != 0;
6565 let is_special_or_tied = (f & (PM_SPECIAL | PM_TIED)) != 0;
6566 let augment_bit = (flags & ASSPM_AUGMENT) != 0;
6567 if is_array_or_hash && !is_special_or_tied && !augment_bit && !isset(KSHARRAYS) {
6568 // c:3242 — flip type to PM_SCALAR, drop array/hash slots.
6569 let pm_mut = tab.get_mut(name).unwrap();
6570 pm_mut.node.flags =
6571 (pm_mut.node.flags & !(PM_TYPE(u32::MAX) as i32)) | PM_SCALAR as i32;
6572 pm_mut.u_arr = None;
6573 paramtab_hashed_storage().lock().unwrap().remove(name);
6574 }
6575 }
6576
6577 // c:3258-3266 `if (*val && (v->pm->node.flags & PM_NAMEREF))`.
6578 let pm = tab.get(name).unwrap();
6579 if !val.is_empty() && (pm.node.flags as u32 & PM_NAMEREF) != 0 {
6580 if !valid_refname(val, pm.node.flags) {
6581 // c:3259
6582 zerr(&format!("invalid name reference: {}", val)); // c:3260
6583 drop(tab);
6584 errflag.fetch_or(
6585 // c:3263
6586 ERRFLAG_ERROR,
6587 Ordering::Relaxed,
6588 );
6589 unqueue_signals(); // c:3262
6590 return None; // c:3264
6591 }
6592 }
6593
6594 // c:3266-3268 `if (flags & ASSPM_WARN) check_warn_pm(v->pm, "scalar", created, 1);`
6595 if (flags & ASSPM_WARN) != 0 {
6596 if let Some(pm_ref) = tab.get(name) {
6597 check_warn_pm(pm_ref, "scalar", created_now as i32, 1); // c:3268
6598 }
6599 }
6600 // c:3269 `v->pm->node.flags &= ~PM_DEFAULTED;`
6601 let pm = tab.get_mut(name).unwrap(); // c:3269
6602 pm.node.flags &= !(PM_DEFAULTED as i32); // c:3269
6603
6604 // c:3343 `assignstrvalue(v, val, flags)`. C aliases `v->pm`
6605 // through to the param in the hash table; Rust's borrow rules
6606 // forbid holding `&mut Param` and wrapping it in `value.pm:
6607 // Option<Param>` at once. Previous port used `tab.remove(name)`
6608 // to take ownership during dispatch, then re-insert — but
6609 // assignstrvalue's PM_INTEGER arm calls `mathevali(val)` which
6610 // looks up identifiers via paramtab. With the param removed,
6611 // `X=X+1` evaluated `X` as unset (=0) and stored `0+1=1`.
6612 // Symptom: `typeset -gi X=5; X=X+1; echo $X` printed 1 instead
6613 // of 6 — broke self-referential integer arithmetic which p10k
6614 // uses for counters, frame depth, hook chain index, etc.
6615 //
6616 // Fix: clone the Param into the value struct so the original
6617 // stays in the table during mathevali's identifier lookup,
6618 // then overwrite the table entry with the mutated clone.
6619 let pm_clone = tab.get(name).unwrap().clone(); // c:3343
6620 drop(tab); // release write lock — assignstrvalue may take it
6621 let mut v = value {
6622 // c:3343
6623 pm: Some(pm_clone), // c:3343
6624 arr: Vec::new(), // c:3343
6625 scanflags: 0, // c:3343
6626 valflags: 0, // c:3343
6627 start: 0, // c:3343
6628 end: -1, // c:3343
6629 }; // c:3343
6630 assignstrvalue(Some(&mut v), Some(val.to_string()), flags); // c:3343
6631 let cloned = v.pm.as_ref().cloned(); // c:3345
6632 if let Some(pm_back) = v.pm {
6633 // c:3343
6634 paramtab()
6635 .write()
6636 .unwrap()
6637 .insert(name.to_string(), pm_back); // c:3343
6638 } // c:3343
6639 // c:Src/params.c pathsetfn / fpathsetfn / manpathsetfn /
6640 // cdpathsetfn — when the SCALAR side of a tied colon-array
6641 // pair is assigned, the canonical setfn split-rebuilds the
6642 // ARRAY side via `splitstring(value, ":", &globalarr)`. zshrs
6643 // lacks per-name GSU setfns for these, so the array stayed
6644 // stale after `PATH=/a:/b`. Mirror the split cascade
6645 // explicitly for the full IPDEF8 PM_TIED colonarr list
6646 // (c:Src/params.c:395-422): PATH↔path, FPATH↔fpath,
6647 // MANPATH↔manpath, CDPATH↔cdpath, PSVAR↔psvar,
6648 // MODULE_PATH↔module_path, FIGNORE↔fignore,
6649 // MAILPATH↔mailpath. Bug #423/#424.
6650 let alt: Option<&str> = match name {
6651 "PATH" => Some("path"),
6652 "FPATH" => Some("fpath"),
6653 "MANPATH" => Some("manpath"),
6654 "CDPATH" => Some("cdpath"),
6655 "PSVAR" => Some("psvar"),
6656 "MODULE_PATH" => Some("module_path"),
6657 "FIGNORE" => Some("fignore"),
6658 "MAILPATH" => Some("mailpath"),
6659 _ => None,
6660 };
6661 if let Some(alt_name) = alt {
6662 let parts: Vec<String> = val.split(':').map(String::from).collect();
6663 if let Ok(mut tab) = paramtab().write() {
6664 let entry = tab.entry(alt_name.to_string()).or_insert_with(|| {
6665 Box::new(param {
6666 node: hashnode {
6667 next: None,
6668 nam: alt_name.to_string(),
6669 flags: (PM_ARRAY | PM_SPECIAL) as i32,
6670 },
6671 u_data: 0,
6672 u_tied: None,
6673 u_arr: Some(Vec::new()),
6674 u_str: None,
6675 u_val: 0,
6676 u_dval: 0.0,
6677 u_hash: None,
6678 gsu_s: None,
6679 gsu_i: None,
6680 gsu_f: None,
6681 gsu_a: None,
6682 gsu_h: None,
6683 base: 0,
6684 width: 0,
6685 env: None,
6686 ename: None,
6687 old: None,
6688 level: 0,
6689 })
6690 });
6691 entry.u_arr = Some(parts);
6692 entry.u_str = None;
6693 }
6694 // c:Src/params.c:5291 — `if (t == path) cmdnamtab->emptytable
6695 // (cmdnamtab)`. PATH change invalidates the command-name cache.
6696 if name == "PATH" {
6697 crate::ported::hashtable::emptycmdnamtable();
6698 }
6699 }
6700 // c:Src/params.c colonarrsetfn (the REVERSE of the split-cascade above) —
6701 // a SCALAR assignment to a tied colon-ARRAY name (`path=/x`) coerces to a
6702 // 1-element array and must re-derive its tied env SCALAR (PATH). The array
6703 // assign (`path=(...)`, params.rs:4548) and element assign
6704 // (`path[2]=x`, :6230) already sync; the plain scalar-assign path did not,
6705 // so `path=/x; echo $PATH` left PATH stale while `typeset -T` ties worked.
6706 let tied_scalar: Option<&str> = TIED_COLON_ARRAYS
6707 .iter()
6708 .find(|(arr, _)| *arr == name)
6709 .map(|(_, sc)| *sc);
6710 if let Some(sc) = tied_scalar {
6711 // The freshly stored value joined with ':' (a scalar assign is a
6712 // 1-element array → just the value).
6713 let joined = {
6714 let tab = paramtab().read().unwrap();
6715 tab.get(name)
6716 .map(|p| match p.u_arr.as_deref() {
6717 Some(a) => a.join(":"),
6718 None => p.u_str.clone().unwrap_or_default(),
6719 })
6720 .unwrap_or_else(|| val.to_string())
6721 };
6722 if let Ok(mut tab) = paramtab().write() {
6723 let entry = tab.entry(sc.to_string()).or_insert_with(|| {
6724 Box::new(param {
6725 node: hashnode {
6726 next: None,
6727 nam: sc.to_string(),
6728 flags: (PM_SCALAR | PM_SPECIAL) as i32,
6729 },
6730 u_data: 0,
6731 u_tied: None,
6732 u_arr: None,
6733 u_str: None,
6734 u_val: 0,
6735 u_dval: 0.0,
6736 u_hash: None,
6737 gsu_s: None,
6738 gsu_i: None,
6739 gsu_f: None,
6740 gsu_a: None,
6741 gsu_h: None,
6742 base: 0,
6743 width: 0,
6744 env: None,
6745 ename: None,
6746 old: None,
6747 level: 0,
6748 })
6749 });
6750 entry.u_str = Some(joined.clone());
6751 entry.u_arr = None;
6752 }
6753 // Keep the process environment in sync so command resolution and
6754 // child processes see the new PATH/FPATH/etc.
6755 std::env::set_var(sc, &joined);
6756 if sc == "PATH" {
6757 crate::ported::hashtable::emptycmdnamtable();
6758 }
6759 }
6760 // c:Src/params.c — `{ "PROMPT", PM_SCALAR|PM_ALIAS, &ps1, ... }` and the
6761 // matching `{ "PS1", PM_SCALAR, &ps1, ... }` share the same backing
6762 // pointer; assigning to PROMPT updates the byte that PS1 reads (and
6763 // vice versa). Same for PROMPT2↔PS2, PROMPT3↔PS3, PROMPT4↔PS4.
6764 // zshrs lacks PM_ALIAS — mirror the scalar write to the alias. Bug #518.
6765 let alias_pair: Option<&str> = match name {
6766 "PROMPT" => Some("PS1"),
6767 "PS1" => Some("PROMPT"),
6768 "PROMPT2" => Some("PS2"),
6769 "PS2" => Some("PROMPT2"),
6770 "PROMPT3" => Some("PS3"),
6771 "PS3" => Some("PROMPT3"),
6772 "PROMPT4" => Some("PS4"),
6773 "PS4" => Some("PROMPT4"),
6774 _ => None,
6775 };
6776 if let Some(other) = alias_pair {
6777 if let Ok(mut tab) = paramtab().write() {
6778 let entry = tab.entry(other.to_string()).or_insert_with(|| {
6779 Box::new(param {
6780 node: hashnode {
6781 next: None,
6782 nam: other.to_string(),
6783 flags: PM_SCALAR as i32,
6784 },
6785 u_data: 0,
6786 u_tied: None,
6787 u_arr: None,
6788 u_str: Some(String::new()),
6789 u_val: 0,
6790 u_dval: 0.0,
6791 u_hash: None,
6792 gsu_s: None,
6793 gsu_i: None,
6794 gsu_f: None,
6795 gsu_a: None,
6796 gsu_h: None,
6797 base: 0,
6798 width: 0,
6799 env: None,
6800 ename: None,
6801 old: None,
6802 level: 0,
6803 })
6804 });
6805 entry.u_str = Some(val.to_string());
6806 entry.u_arr = None;
6807 }
6808 }
6809 unqueue_signals(); // c:3344
6810 cloned // c:3345
6811}
6812
6813// `VarAttr` struct + `VarKind` enum + `impl VarAttr::format_zsh`
6814// DELETED. C zsh stores typeset attributes as bare `PM_*` bit
6815// flags on `Param.node.flags` (`Src/zsh.h` PM_* + `Src/params.c`
6816// flag tests); the `${(t)var}` flag report (`typeprintparam` at
6817// `Src/builtin.c:3050+`) writes those bits to a string directly
6818// against the `Param.node.flags` int.
6819//
6820// Both types had zero external use sites — pure dead-code carryover
6821// from an earlier vm_helper refactor. The PM_* bit constants are at
6822// `zsh_h.rs:1340+` and the `(t)` formatting routes through
6823// `typeset_print_flags` (when wired) reading bare `Param.node.flags`.
6824
6825// ===========================================================
6826// Special-parameter GSU (get/set/unset) callbacks ported from
6827// Src/params.c.
6828//
6829// C zsh stores per-special-param state in file-static globals
6830// (`ifs`, `home`, `term`, `histsiz`, etc.) and dispatches getfn/
6831// setfn/unsetfn callbacks through `Param.gsu->getfn` etc. zshrs's
6832// param storage is per-evaluator HashMaps on `ShellExecutor`, so
6833// the C globals are reproduced as `OnceLock<Mutex<…>>` module
6834// statics here, with the get/set ported mutating the static.
6835//
6836// Functions that genuinely need a `Param *` (the GSU dispatch
6837// callbacks for non-special arr/hash/int/float/str params, the
6838// param-table mutators, scope helpers, etc.) cannot be properly
6839// ported until zshrs gains a Param struct + callback-table ABI;
6840// those keep their C signatures but the body is a WARNING-stub
6841// that does nothing.
6842// ===========================================================
6843
6844// -----------------------------------------------------------
6845// Module statics — one per C global referenced by the special-
6846// param callbacks below. All initialised lazily on first read.
6847// -----------------------------------------------------------
6848
6849// `Src/params.c:515 mod_export HashTable paramtab, realparamtab;`
6850//
6851// `realparamtab` always points to the shell's global parameter
6852// table. `paramtab` normally aliases it; it is temporarily
6853// redirected during associative-array key iteration
6854// (`Src/params.c:508-513` — "paramtab is sometimes temporarily
6855// changed to point at another table").
6856//
6857// Per PORT_PLAN.md Phase 3, bucket-2 read-mostly tables use
6858// `RwLock` so parallel readers (every `$VAR` expansion, every
6859// completion lookup) don't serialize. Writers (assignments,
6860// scope pushes/pops, function-local declarations) take the
6861// exclusive write lock. `OnceLock` provides the single-static
6862// guarantee without an `Arc` allocation since the table lives
6863// for the process lifetime.
6864//
6865// Entries are keyed on `node.nam` (the canonical `param` struct
6866// lives in `zsh_h.rs`). The full `HashTable` substrate (vtable
6867// callbacks, intrusive `next` chain, scope-stacked iterators) is
6868// not yet wired; until it is, the typed map is the operative
6869// storage.
6870static PARAMTAB_INNER: OnceLock<RwLock<crate::fast_hash::FastMap<String, Param>>> = OnceLock::new();
6871static REALPARAMTAB_INNER: OnceLock<RwLock<crate::fast_hash::FastMap<String, Param>>> = OnceLock::new();
6872
6873/// Array parameter assignment (no subscript).
6874///
6875/// Direct port of `Param assignaparam(char *s, char **val, int flags)`
6876/// from `Src/params.c:3357`. Writes an array value into paramtab
6877/// and returns the new/updated Param.
6878///
6879/// Ported semantics:
6880/// - PM_READONLY rejection (c:3370-3381)
6881/// - PM_NAMEREF type-change reject (c:3395-3398)
6882/// - ASSPM_AUGMENT (`a+=val`) preserve-old prepend (c:3404-3412)
6883/// - PM_UNIQUE dedupe (c:3401)
6884/// - element-wise `a[k]=v` slice path pre-check (c:3373-3389)
6885/// - PM_HASHED slice rejection (c:3384-3391)
6886///
6887/// Pending (rare paths):
6888/// - resetparam from non-array (c:3415-3420) — handled implicitly
6889/// by the type-mask rewrite below; matches C observable behavior.
6890pub fn assignaparam(name: &str, val: Vec<String>, flags: i32) -> Option<Param> {
6891 // c:3357
6892 // c:3366-3370 — `if (!isident(s)) { zerr; return NULL }`.
6893 if !isident(name) {
6894 zerr(&format!("not an identifier: {}", name));
6895 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
6896 return None;
6897 }
6898 // c:Src/params.c:2922-2923 — `pm->gsu.a->setfn(pm, val)`: a special
6899 // PM_ARRAY param (e.g. `dirstack`) routes a whole-array assignment
6900 // through its GSU setter instead of plain paramtab storage. zshrs
6901 // keeps these magic arrays in PARTAB_ARRAY (not paramtab), so a
6902 // bare `dirstack=(...)` would otherwise land in the executor's
6903 // generic array store and never reach `dirssetfn` (so the dir stack
6904 // stayed empty). Dispatch writable PARTAB_ARRAY specials here. Only
6905 // whole-name (non-subscripted) assignment; `dirstack[1]=x` keeps the
6906 // existing subscript path.
6907 if !name.contains('[') {
6908 if let Some(entry) = crate::ported::modules::parameter::PARTAB_ARRAY
6909 .iter()
6910 .find(|e| e.name == name)
6911 {
6912 if let Some(setfn) = entry.setfn {
6913 setfn(std::ptr::null_mut(), val.clone()); // c:2922 gsu.a->setfn
6914 let mut pm = param::default();
6915 pm.node.nam = name.to_string();
6916 pm.node.flags = (PM_ARRAY | PM_SPECIAL) as i32;
6917 pm.u_arr = Some(val);
6918 return Some(Box::new(pm));
6919 }
6920 }
6921 }
6922 // c:3392 — `fetchvalue(&vbuf, &s, 1, SCANPM_ASSIGNING)` resolves
6923 // PM_NAMEREF chains; c:3395-3398 rejects an unresolvable ref.
6924 {
6925 let base = name.split('[').next().unwrap_or(name);
6926 if crate::ported::params::is_nameref(base) {
6927 match crate::ported::params::resolve_nameref_name(base, None) {
6928 crate::ported::params::nameref_resolution::SelfRef => {
6929 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
6930 return None;
6931 }
6932 crate::ported::params::nameref_resolution::OutOfScope => {
6933 // see assignsparam — silent failure, status 1.
6934 return None;
6935 }
6936 crate::ported::params::nameref_resolution::Placeholder(_) => {
6937 // c:3396 — message uses the ORIGINAL assigned name
6938 // (`t = s` saved at c:3361).
6939 zwarn(&format!("{}: can't change type of a named reference", base));
6940 return None;
6941 }
6942 crate::ported::params::nameref_resolution::Target {
6943 name: t,
6944 subscript: rsub,
6945 pm: rpm,
6946 level,
6947 } => {
6948 let user_sub = name.find('[').map(|i| &name[i..]);
6949 // Hidden (old-chain) binding — upscope write.
6950 if rsub.is_none() && user_sub.is_none() && rpm.is_some() {
6951 let visible_level = paramtab()
6952 .read()
6953 .ok()
6954 .and_then(|tb| tb.get(&t).map(|p| p.level));
6955 if visible_level != Some(level) {
6956 // upscope write, INLINE (former
6957 // nameref_hidden_array_assign).
6958 return (|name: &str, level: i32, val: Vec<String>| -> Option<Param> {
6959 let mut tab = paramtab().write().ok()?;
6960 let mut node: &mut param = tab.get_mut(name)?.as_mut();
6961 while node.level != level {
6962 node = node.old.as_mut()?.as_mut();
6963 }
6964 if (node.node.flags as u32 & PM_READONLY) != 0 {
6965 zerr(&format!("read-only variable: {}", name)); // c:3370-3381
6966 return None;
6967 }
6968 let type_mask = PM_TYPE(u32::MAX) as i32;
6969 node.node.flags = (node.node.flags & !type_mask) | PM_ARRAY as i32;
6970 node.u_str = None;
6971 node.u_arr = Some(val);
6972 node.node.flags &= !((PM_UNSET | PM_DECLARED) as i32);
6973 Some(Box::new(node.clone()))
6974 })(&t, level, val);
6975 }
6976 }
6977 let mut new_s = t.clone();
6978 if let Some(rs) = &rsub {
6979 new_s.push('[');
6980 new_s.push_str(rs);
6981 new_s.push(']');
6982 }
6983 if let Some(us) = user_sub {
6984 new_s.push_str(us);
6985 }
6986 if new_s != name {
6987 return assignaparam(&new_s, val, flags);
6988 }
6989 }
6990 crate::ported::params::nameref_resolution::NotRef => {}
6991 }
6992 }
6993 }
6994
6995 // c:3375 — `if ((ss = strchr(s, '['))) { ... slice path ... }`
6996 // Extract the base name when there's a subscript and
6997 // dispatch to the slice-rejection / slice-write path.
6998 if let Some(_bracket) = name.find('[') {
6999 let base = name.split('[').next().unwrap_or(name);
7000 // c:3384-3391 — slice into a HASHED → zerr.
7001 let is_hashed = {
7002 let tab = paramtab().read().unwrap();
7003 tab.get(base)
7004 .map(|pm| (pm.node.flags as u32 & PM_HASHED) != 0)
7005 .unwrap_or(false)
7006 };
7007 if is_hashed {
7008 zerr(&format!(
7009 "{}: attempt to set slice of associative array",
7010 base
7011 ));
7012 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
7013 return None;
7014 }
7015 // Slice into a non-existent param → create as PM_ARRAY (c:3377-3382).
7016 let exists = paramtab().read().unwrap().contains_key(base);
7017 if !exists {
7018 createparam(base, PM_ARRAY as i32)?;
7019 }
7020 // Subscript-write itself (a[k]=v) is handled at the caller's
7021 // SubscriptArith dispatch; reaching here means the slice
7022 // pre-check has passed and the param exists.
7023 return paramtab().read().unwrap().get(base).cloned();
7024 }
7025
7026 // c:3391-3394 — fetchvalue / createparam(PM_ARRAY) if missing.
7027 let (existed, prior_scalar, prior_flags) = {
7028 let tab = paramtab().read().unwrap();
7029 match tab.get(name) {
7030 Some(pm) => {
7031 // c:3350 — the ASSPM_AUGMENT prepend uses
7032 // `ztrdup(getstrvalue(v))`, the type-aware string form of
7033 // the old value (PM_INTEGER → base-formatted, PM_FFLOAT/
7034 // PM_EFLOAT → convfloat, PM_SCALAR → strgetfn). Reading
7035 // `u_str` alone loses it for numeric params — u_str is
7036 // None for `integer`/`float`, so `integer i=1; i+=(2 3)`
7037 // dropped the "1". Route scalar-type targets through
7038 // getstrvalue so the conversion-to-array keeps the value.
7039 let f = pm.node.flags as u32;
7040 let ps = if f & (PM_ARRAY | PM_HASHED | PM_SPECIAL) == 0 {
7041 let mut pv = value {
7042 pm: Some(pm.clone()),
7043 arr: Vec::new(),
7044 scanflags: 0,
7045 valflags: 0,
7046 start: 0,
7047 end: -1,
7048 };
7049 Some(getstrvalue(Some(&mut pv))) // c:3350
7050 } else {
7051 pm.u_str.clone()
7052 };
7053 (true, ps, pm.node.flags)
7054 }
7055 None => (false, None, 0),
7056 }
7057 };
7058 // c:3397-3400 — PM_NAMEREF: can't change type of a named reference.
7059 if existed && (prior_flags as u32 & PM_NAMEREF) != 0 {
7060 zwarn(&format!("{}: can't change type of a named reference", name));
7061 return None;
7062 }
7063 let created_now = !existed; // c:3393 createparam path sets `created = 1`
7064 if !existed {
7065 createparam(name, PM_ARRAY as i32)?;
7066 }
7067
7068 // c:3370-3381 PM_READONLY rejection — C routes through setarrvalue
7069 // → arrsetfn which emits "read-only variable: X" and returns NULL.
7070 // Rust write path bypasses gsu setfn, so mirror the check here.
7071 if existed && (prior_flags as u32 & PM_READONLY) != 0 {
7072 zerr(&format!("read-only variable: {}", name));
7073 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
7074 return None;
7075 }
7076 // c:3395-3401 — the type-reset arm only fires for params that are
7077 // NOT (PM_ARRAY|PM_HASHED) and NOT PM_SPECIAL|PM_TIED. A special
7078 // param falls THROUGH the reset: `v` stays set and the assignment
7079 // reaches setarrvalue (c:3585), which dispatches:
7080 // - PM_HASHED whole-assign (v->start==0, v->end==-1) →
7081 // arrhashsetfn (c:2918-2920) → pm->gsu.h->setfn(pm, ht) — for
7082 // the zsh/parameter specials that's setpmoptions /
7083 // setpmcommands / setaliases / setfunctions / setpmnameddirs
7084 // (Src/Modules/parameter.c).
7085 // - non-array, non-hash special (e.g. $SECONDS) → zerr
7086 // "%s: attempt to assign array value to non-array" (c:2905).
7087 // The previous Rust arm rejected EVERY non-array special with
7088 // "can't change type of a special parameter" — a message C's
7089 // assignaparam never emits. Gap #2 2026-06-12.
7090 if existed {
7091 let pm_type = prior_flags as u32 & PM_TYPE(u32::MAX);
7092 if pm_type != PM_ARRAY && (prior_flags as u32 & PM_SPECIAL) != 0 {
7093 if pm_type == PM_HASHED {
7094 // c:3544-3560 — under ASSPM_KEY_VALUE, associative
7095 // arrays strictly enforce `[key]=value` syntax: walk
7096 // in strides of 3; every stride must start with a
7097 // Marker (a Marker can only introduce a
7098 // Marker/key/value triad, never appear by accident).
7099 if (flags & ASSPM_KEY_VALUE) != 0 {
7100 let mut i = 0usize;
7101 while i < val.len() {
7102 if !val[i].starts_with(Marker) {
7103 zerr("bad [key]=value syntax for associative array");
7104 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
7105 return None;
7106 }
7107 i += 3;
7108 }
7109 }
7110 // c:4124-4131 (arrhashsetfn) — count non-Marker
7111 // entries; odd → zerr + abort. zsh 5.9 truth:
7112 // `options=(noglob)` → "bad set of key/value pairs
7113 // for associative array", rc=1, script aborts.
7114 let alen = val
7115 .iter()
7116 .filter(|s| !s.starts_with(Marker as char))
7117 .count();
7118 if alen % 2 != 0 {
7119 zerr("bad set of key/value pairs for associative array");
7120 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
7121 return None;
7122 }
7123 // c:4141-4166 (arrhashsetfn pair walk) — flatten the
7124 // value list to (key, value) pairs; `[k]=v` triples
7125 // arrive as [Marker, k, v].
7126 let mut pairs: Vec<(String, String)> = Vec::with_capacity(alen / 2);
7127 let mut it = val.iter();
7128 while let Some(first) = it.next() {
7129 let key = if first.starts_with(Marker as char) {
7130 match it.next() {
7131 Some(k) => k.clone(),
7132 None => break,
7133 }
7134 } else {
7135 first.clone()
7136 };
7137 let v = it.next().cloned().unwrap_or_default();
7138 pairs.push((key, v));
7139 }
7140 // pm->gsu.h->setfn(pm, ht) — per-name dispatch to the
7141 // canonical Src/Modules/parameter.c setfn ports. The
7142 // synthetic Param mirrors the established per-element
7143 // pattern (assignsparam "options"/"commands" arms).
7144 let synth: Param = Box::new(param {
7145 node: hashnode {
7146 next: None,
7147 nam: name.to_string(),
7148 flags: prior_flags,
7149 },
7150 u_data: 0,
7151 u_tied: None,
7152 u_arr: None,
7153 u_str: None,
7154 u_val: 0,
7155 u_dval: 0.0,
7156 u_hash: None,
7157 gsu_s: None,
7158 gsu_i: None,
7159 gsu_f: None,
7160 gsu_a: None,
7161 gsu_h: None,
7162 base: 0,
7163 width: 0,
7164 env: None,
7165 ename: None,
7166 old: None,
7167 level: 0,
7168 });
7169 use crate::ported::modules::parameter as pmod;
7170 match name {
7171 // SPECIALPMDEF entries with writable hash gsu
7172 // (Src/Modules/parameter.c:2235-2298, no
7173 // PM_READONLY_SPECIAL):
7174 "options" => pmod::setpmoptions(synth.clone(), &pairs), // c:2285
7175 "commands" => pmod::setpmcommands(synth.clone(), &pairs), // c:2238
7176 "functions" => pmod::setpmfunctions(synth.clone(), &pairs), // c:2263
7177 "dis_functions" => pmod::setpmdisfunctions(synth.clone(), &pairs), // c:2245
7178 "aliases" => pmod::setpmraliases(synth.clone(), &pairs), // c:2235
7179 "dis_aliases" => pmod::setpmdisraliases(synth.clone(), &pairs), // c:2241
7180 "galiases" => pmod::setpmgaliases(synth.clone(), &pairs), // c:2269
7181 "dis_galiases" => pmod::setpmdisgaliases(synth.clone(), &pairs), // c:2249
7182 "saliases" => pmod::setpmsaliases(synth.clone(), &pairs), // c:2293
7183 "dis_saliases" => pmod::setpmdissaliases(synth.clone(), &pairs), // c:2255
7184 "nameddirs" => pmod::setpmnameddirs(synth.clone(), &pairs), // c:2283
7185 _ => {
7186 // PM_READONLY_SPECIAL hashed specials (builtins,
7187 // modules, parameters, history, jobtexts, ...) —
7188 // C's PM_READONLY check (setarrvalue c:2900-2903)
7189 // rejects before any setfn. zsh 5.9 truth:
7190 // `modules=(a b)` → "read-only variable: modules",
7191 // rc=1. zshrs strips PM_READONLY off the paramtab
7192 // stubs (vm_helper init), so match by family.
7193 zerr(&format!("read-only variable: {}", name));
7194 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
7195 return None;
7196 }
7197 }
7198 return Some(synth);
7199 }
7200 // c:2905-2909 (setarrvalue) — non-hash special target.
7201 // zsh 5.9 truth: `SECONDS=(1 2)` → "SECONDS: attempt to
7202 // assign array value to non-array", rc=1.
7203 zerr(&format!(
7204 "{}: attempt to assign array value to non-array",
7205 name
7206 ));
7207 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
7208 return None;
7209 }
7210 }
7211
7212 // c:3436-3541 — ASSPM_KEY_VALUE on an ordinary array: the value
7213 // list contains Marker / index / value triads (from
7214 // keyvalpairelement, Src/subst.c:49) interleaved with plain
7215 // elements. Resolve them into a dense array:
7216 // - First pass (c:3465-3486): matheval each index; reject < 0,
7217 // and 0 unless KSH_ARRAYS (zerr "bad subscript for direct
7218 // array assignment"); KSH_ARRAYS keeps 0-based, otherwise
7219 // 1-based → decrement; track `nextind` (a plain element lands
7220 // just after the previously placed one) and `maxlen`.
7221 // - Allocate `fullval` of maxlen (c:3487-3495), pre-filled with
7222 // the existing elements under ASSPM_AUGMENT (c:3496-3502).
7223 // - Second pass (c:3503-3525): place each value; a `Marker +`
7224 // triad concatenates onto the slot's current value (c:3510-
7225 // 3515 bicat); plain elements advance nextind.
7226 // - Unset slots become "" — no sparse arrays (c:3530-3537).
7227 // C returns straight after `setarrvalue(v, fullval)` (c:3538-
7228 // 3540): the resolved vec REPLACES the whole array, so the tail
7229 // AUGMENT prepend below must not run again — clear the flag.
7230 let mut val = val;
7231 let mut flags = flags;
7232 if (flags & ASSPM_KEY_VALUE) != 0 {
7233 let ksh = crate::ported::zsh_h::isset(crate::ported::zsh_h::KSHARRAYS);
7234 let orig: Vec<String> = if (flags & ASSPM_AUGMENT) != 0 && existed {
7235 let tab = paramtab().read().unwrap();
7236 tab.get(name)
7237 .and_then(|pm| pm.u_arr.clone())
7238 .unwrap_or_default()
7239 } else {
7240 Vec::new()
7241 };
7242 let mut maxlen = orig.len(); // c:3460-3462
7243 let mut nextind: i64 = 0; // c:3464
7244 let mut subscripts: Vec<i64> = Vec::new(); // c:3455-3456
7245 let mut i = 0usize;
7246 while i < val.len() {
7247 // c:3466-3481
7248 if val[i].starts_with(Marker) {
7249 let key_str = val.get(i + 1).cloned().unwrap_or_default();
7250 let idx = match crate::ported::math::mathevali(&key_str) {
7251 Ok(n) => n,
7252 Err(e) => {
7253 // C mathevali zerr's internally (Src/math.c);
7254 // surface the message and abort the assign.
7255 zerr(&e);
7256 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
7257 return None;
7258 }
7259 };
7260 if idx < 0 || (!ksh && idx == 0) {
7261 // c:3468-3474
7262 zerr(&format!(
7263 "bad subscript for direct array assignment: {}",
7264 key_str
7265 ));
7266 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
7267 return None;
7268 }
7269 let ix = if ksh { idx } else { idx - 1 }; // c:3475-3476
7270 subscripts.push(ix);
7271 nextind = ix + 1; // c:3477
7272 i += 3; // c:3479
7273 } else {
7274 nextind += 1; // c:3481-3482
7275 i += 1;
7276 }
7277 if nextind > maxlen as i64 {
7278 // c:3484-3485
7279 maxlen = nextind as usize;
7280 }
7281 }
7282 // c:3487-3495 fullval = zshcalloc((maxlen+1) * sizeof(char*)).
7283 let mut fullval: Vec<Option<String>> = vec![None; maxlen];
7284 // c:3496-3502 — AUGMENT: copy the original elements in first.
7285 for (j, o) in orig.iter().enumerate() {
7286 fullval[j] = Some(o.clone());
7287 }
7288 // Second pass — c:3503-3525.
7289 let mut si = 0usize;
7290 let mut nextind = 0usize;
7291 let mut i = 0usize;
7292 while i < val.len() {
7293 if val[i].starts_with(Marker) {
7294 let augment_elt = val[i].len() > Marker.len_utf8(); // c:3507 `(*aptr)[1] == '+'`
7295 let ix = subscripts[si] as usize;
7296 si += 1;
7297 let old = fullval[ix].take();
7298 fullval[ix] = Some(match old {
7299 // c:3510-3512 bicat(old, value)
7300 Some(o) if augment_elt => format!("{}{}", o, val[i + 2]),
7301 _ => val[i + 2].clone(),
7302 });
7303 nextind = ix + 1; // c:3516
7304 i += 3;
7305 } else {
7306 fullval[nextind] = Some(val[i].clone()); // c:3519-3520
7307 nextind += 1;
7308 i += 1;
7309 }
7310 }
7311 // c:3530-3537 — unfilled slots become "".
7312 val = fullval.into_iter().map(|o| o.unwrap_or_default()).collect();
7313 // c:3538-3540 — setarrvalue(v, fullval); return. The orig
7314 // elements are already merged in; don't prepend them again.
7315 flags &= !ASSPM_AUGMENT;
7316 }
7317
7318 // c:3402-3412 — ASSPM_AUGMENT preserve-old prepend. When the
7319 // previous value was a scalar (not array/hashed) and we're
7320 // augmenting (`a+=val`), prepend that scalar's string form as
7321 // val[0]. Only fires when the existing param is not PM_UNSET.
7322 let was_scalar_array_target = existed
7323 && prior_flags & (PM_ARRAY | PM_HASHED) as i32 == 0
7324 && prior_flags & PM_SPECIAL as i32 == 0;
7325 if (flags & ASSPM_AUGMENT) != 0 && was_scalar_array_target && prior_flags & PM_UNSET as i32 == 0
7326 {
7327 if let Some(old_scalar) = prior_scalar {
7328 val.insert(0, old_scalar); // c:3408-3411
7329 }
7330 }
7331
7332 // c:3570-3585 — ASSPM_AUGMENT on an existing PM_ARRAY target:
7333 // append rather than replace. C bumps v->start to arrlen(existing)
7334 // and v->end to start+1 so setarrvalue writes past the tail.
7335 // zshrs writes through pm.u_arr without the value struct, so do
7336 // the equivalent here: prepend the existing array elements to the
7337 // new val so the final stored vec is [old..., new...].
7338 if (flags & ASSPM_AUGMENT) != 0
7339 && existed
7340 && (prior_flags as u32 & PM_ARRAY) != 0
7341 && (prior_flags as u32 & PM_UNSET) == 0
7342 {
7343 let prior_arr = {
7344 let tab = paramtab().read().unwrap();
7345 tab.get(name)
7346 .and_then(|pm| pm.u_arr.clone())
7347 .unwrap_or_default()
7348 };
7349 let appended: Vec<String> = prior_arr.into_iter().chain(val.into_iter()).collect();
7350 val = appended;
7351 }
7352
7353 // c:3432 `if (flags & ASSPM_WARN) check_warn_pm(v->pm, "array", created, may_warn_about_nested_vars);`
7354 // c:3372 — `may_warn_about_nested_vars = !(flags & ASSPM_AUGMENT)`.
7355 if (flags & ASSPM_WARN) != 0 {
7356 let may_nested = if (flags & ASSPM_AUGMENT) != 0 { 0 } else { 1 };
7357 if let Some(pm_ref) = paramtab().read().unwrap().get(name) {
7358 check_warn_pm(pm_ref, "array", created_now as i32, may_nested); // c:3432
7359 }
7360 }
7361
7362 // c:3434 — setarrvalue(v, val): store array in pm.u_arr.
7363 let mut tab = paramtab().write().unwrap();
7364 let pm = tab.get_mut(name)?;
7365 let uniq = pm.node.flags & PM_UNIQUE as i32 != 0; // c:3401
7366 if pm.node.flags & PM_SPECIAL as i32 == 0 {
7367 let type_mask = PM_ARRAY | PM_INTEGER | PM_EFLOAT | PM_FFLOAT | PM_HASHED | PM_NAMEREF;
7368 pm.node.flags = (pm.node.flags & !type_mask as i32) | PM_ARRAY as i32;
7369 }
7370 // c:3401 — preserve PM_UNIQUE through the type change, then let
7371 // arrsetfn dedupe via the actual write.
7372 if uniq {
7373 pm.node.flags |= PM_UNIQUE as i32;
7374 }
7375 let val_final = if uniq { simple_arrayuniq(val) } else { val };
7376 // c:3434 — `setarrvalue(v, val);` → `gsu.a->setfn(pm, val)` for
7377 // PM_SPECIAL params (PATH/path → pathsetfn rebuilds $PATH cstring;
7378 // FPATH/fpath → fpathsetfn rebuilds; MANPATH etc.). Without this
7379 // dispatch, env var pairs (PATH vs path) drift apart on array
7380 // assignment. Fall back to direct u_arr write when no gsu_a is
7381 // wired (regular non-special arrays).
7382 let setfn_ptr = pm.gsu_a.as_ref().map(|g| g.setfn);
7383 // c:Src/params.c:3434 — `setarrvalue(v, val)` calls `gsu.a->setfn(pm, val)`.
7384 // The canonical arrsetfn (params.rs:6584) writes `pm->u_arr` then,
7385 // if `pm->ename` is set, calls `arrfixenv` which re-acquires the
7386 // paramtab lock. We're holding the WRITE lock here — that would
7387 // deadlock the RWLock. Inline the storage write under the held
7388 // lock and defer arrfixenv to AFTER drop(tab). Bug #600.
7389 pm.u_arr = Some(val_final.clone());
7390 pm.u_str = None;
7391 pm.u_hash = None;
7392 // c:2712 (setarrvalue head) — `v->pm->node.flags &= ~PM_UNSET;`
7393 // a declared-but-unset (TYPESET_TO_UNSET) array becomes set on
7394 // its first assignment.
7395 pm.node.flags &= !((PM_UNSET | PM_DECLARED) as i32);
7396 let ename_for_envsync: Option<String> = if setfn_ptr.is_some() {
7397 pm.ename.clone()
7398 } else {
7399 None
7400 };
7401 let cloned = pm.clone();
7402 drop(tab);
7403 // c:Src/params.c:5285 arrfixenv — deferred from inside the lock
7404 // (see Bug #600 above). Acquires its own paramtab read+write
7405 // locks; safe to call now that we've dropped the write lock.
7406 if let Some(ename) = ename_for_envsync {
7407 arrfixenv(&ename, Some(&val_final));
7408 }
7409 // c:Src/params.c:3262 IPDEF9 — \`argv\`/\`@\`/\`*\` are aliases for
7410 // the C global \`pparams\` (the positional parameter vector).
7411 // assignaparam("argv", [...]) in C writes through the array's
7412 // setfn which mutates \`pparams\` directly. zshrs's pparams lives
7413 // in builtin::PPARAMS; mirror the write so \`argv=(...)\` updates
7414 // \$1/\$2/.../\$# correctly.
7415 if name == "argv" || name == "@" || name == "*" {
7416 if let Ok(mut pp) = crate::ported::builtin::PPARAMS.lock() {
7417 *pp = val_final.clone();
7418 }
7419 }
7420 let _ = val_final;
7421 Some(cloned)
7422}
7423
7424/// Set array parameter.
7425/// Port of `setaparam(char *s, char **aval)` from `Src/params.c:3595` — single-line wrapper
7426/// around `assignaparam(s, val, ASSPM_WARN)`. C body:
7427/// ```c
7428/// mod_export Param setaparam(char *s, char **val) {
7429/// return assignaparam(s, val, ASSPM_WARN);
7430/// }
7431/// ```
7432///
7433/// `ASSPM_WARN` (params.c:104) drives the WARN_CREATE_GLOBAL /
7434/// WARN_NESTED_VAR diagnostics inside `assignaparam` →
7435/// `check_warn_pm` (params.rs:4428).
7436/// WARNING: param names don't match C — Rust=() vs C=(s, val)
7437pub fn setaparam(name: &str, val: Vec<String>) -> Option<Param> {
7438 // c:3766 — `return assignaparam(s, val, ASSPM_WARN)`.
7439 assignaparam(name, val, ASSPM_WARN)
7440}
7441
7442/// Direct port of `Param sethparam(char *s, char **val)` from
7443/// `Src/params.c:3602`. Writes an associative array (flat
7444/// alternating key,value list) into paramtab + the parallel
7445/// `paramtab_hashed_storage` table; returns the new Param.
7446///
7447/// Ported C semantics:
7448/// - PM_READONLY rejection (c:3625 via setarrvalue chain in C; here inline)
7449/// - PM_SPECIAL type-change reject (c:3637)
7450/// Pending:
7451/// - resetparam(PM_HASHED) for non-special type-change (rare)
7452pub fn sethparam(name: &str, val: Vec<String>) -> Option<Param> {
7453 // c:3611-3615 — `if (!isident(s)) { zerr; return NULL }`.
7454 if !isident(name) {
7455 zerr(&format!("not an identifier: {}", name));
7456 return None;
7457 }
7458 // c:3630 — `fetchvalue(&vbuf, &s, 1, SCANPM_ASSIGNING)` resolves
7459 // PM_NAMEREF chains (same shape as assignaparam c:3392-3398).
7460 if crate::ported::params::is_nameref(name) {
7461 match crate::ported::params::resolve_nameref_name(name, None) {
7462 crate::ported::params::nameref_resolution::SelfRef => {
7463 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
7464 return None;
7465 }
7466 crate::ported::params::nameref_resolution::OutOfScope => {
7467 return None;
7468 }
7469 crate::ported::params::nameref_resolution::Placeholder(_) => {
7470 zwarn(&format!("{}: can't change type of a named reference", name));
7471 return None;
7472 }
7473 crate::ported::params::nameref_resolution::Target {
7474 name: t,
7475 subscript: None,
7476 ..
7477 } => {
7478 if t != name {
7479 return sethparam(&t, val);
7480 }
7481 }
7482 _ => {}
7483 }
7484 }
7485 // c:3617-3621 — `if (strchr(s, '[')) { zerr; return NULL }`.
7486 if name.contains('[') {
7487 zerr("nested associative arrays not yet supported");
7488 return None;
7489 }
7490
7491 // c:3625 — PM_READONLY rejection. C routes through gsu.h->setfn
7492 // which checks readonly inside hashsetfn / arrhashsetfn; Rust
7493 // bypasses that path, so check explicitly here.
7494 {
7495 let tab = paramtab().read().unwrap();
7496 if let Some(pm) = tab.get(name) {
7497 if (pm.node.flags as u32 & PM_READONLY) != 0 {
7498 zerr(&format!("read-only variable: {}", name));
7499 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
7500 return None;
7501 }
7502 // c:3637 — `if (PM_TYPE(pm->node.flags) != PM_HASHED &&
7503 // (pm->node.flags & PM_SPECIAL)) { zerr; return; }`
7504 // Can't change type of a PM_SPECIAL non-hashed param.
7505 let pm_type = pm.node.flags as u32 & PM_TYPE(u32::MAX);
7506 if pm_type != PM_HASHED && (pm.node.flags as u32 & PM_SPECIAL) != 0 {
7507 zerr(&format!(
7508 "{}: can't change type of a special parameter",
7509 name
7510 ));
7511 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
7512 return None;
7513 }
7514 }
7515 }
7516
7517 // c:3625 — fetchvalue / createparam(PM_HASHED) if missing.
7518 let exists = paramtab().read().unwrap().contains_key(name);
7519 let checkcreate = !exists; // c:3626 `checkcreate = 1;`
7520 if !exists {
7521 createparam(name, PM_HASHED as i32)?;
7522 }
7523
7524 // c:3649 `check_warn_pm(v->pm, "associative array", checkcreate, 1);`
7525 // — sethparam always warns (no ASSPM_WARN gate in C).
7526 if let Some(pm_ref) = paramtab().read().unwrap().get(name) {
7527 check_warn_pm(pm_ref, "associative array", checkcreate as i32, 1); // c:3649
7528 }
7529
7530 // c:3651 — `setarrvalue(v, val);` — full-replace dispatch for a
7531 // PM_HASHED param (setarrvalue c:2919-2920) collapses to
7532 // `arrhashsetfn(v->pm, val, 0)`, which owns the odd-count gate
7533 // (c:4128-4131, zerr + ERRFLAG_ERROR) and the pair walk.
7534 let mut tab = paramtab().write().unwrap();
7535 let pm = tab.get_mut(name)?;
7536 if pm.node.flags & PM_SPECIAL as i32 == 0 {
7537 let type_mask = PM_ARRAY | PM_INTEGER | PM_EFLOAT | PM_FFLOAT | PM_HASHED | PM_NAMEREF;
7538 pm.node.flags = (pm.node.flags & !type_mask as i32) | PM_HASHED as i32;
7539 }
7540 pm.u_arr = None;
7541 pm.u_str = None;
7542 arrhashsetfn(pm, val, 0); // c:3651 via setarrvalue c:2920
7543 let cloned = pm.clone();
7544 drop(tab);
7545
7546 // c:3652-3653 — `unqueue_signals(); return v->pm;` — C returns
7547 // the param even when arrhashsetfn errored; the failure travels
7548 // via errflag (callers like the SET_ARRAY bridge check it).
7549 Some(cloned)
7550}
7551
7552// -----------------------------------------------------------
7553// Param-table mutators / scope / nameref helpers.
7554// `Src/params.c` calls these against the global `paramtab`
7555// HashTable; until our HashTable vtable (`Box<hashtable>` in
7556// zsh_h.rs:285) is wired, these remain no-op shims with the
7557// real C signatures.
7558// -----------------------------------------------------------
7559
7560/// Port of `assignnparam(char *s, mnumber val, int flags)` from `Src/params.c:3664`. C body
7561/// looks up the param via `gethashnode2(realparamtab, s)`,
7562/// dispatches on PM_TYPE: PM_INTEGER → `intsetfn(pm, val.u.l)`;
7563/// PM_FFLOAT/EFLOAT → `floatsetfn(pm, val.u.d)`; otherwise
7564/// `assignstrvalue(&v, conv_to_string(val), flags)`. Stub
7565/// pending HashTable backend; signature mirrors C `mnumber val`.
7566/// flow: isident guard → unset(EXECOPT) bail → `getvalue(&vbuf,&s,1)`
7567/// → if existing array/hashed (non-special, non-tied, non-KSHARRAYS,
7568/// no subscript) → unsetparam_pm + recreate → else if no value →
7569/// `createparam(t, type)` (POSIXIDENTIFIERS gates SCALAR vs
7570/// MN_INTEGER→PM_INTEGER else PM_FFLOAT) → second `getvalue` →
7571/// `check_warn_pm` if ASSPM_WARN → clear PM_DEFAULTED → `setnumvalue`
7572/// → return pm. This port wires the structural flow against the
7573/// already-ported helpers; the createparam/paramtab backend is
7574/// still stubbed elsewhere so the create-new-param branch returns
7575/// None until `createparam` lands.
7576pub fn assignnparam(s: &str, val: mnumber, flags: i32) -> Option<Box<param>> {
7577 // c:3666 `if (!isident(s)) { zerr; errflag |= ERRFLAG_ERROR; return NULL; }`
7578 if !isident(s) {
7579 zerr(&format!("not an identifier: {}", s)); // c:3667
7580 errflag.fetch_or(
7581 // c:3669
7582 ERRFLAG_ERROR,
7583 Ordering::Relaxed,
7584 );
7585 return None; // c:3670
7586 }
7587 if unset(EXECOPT) {
7588 return None;
7589 }
7590 let mut vbuf = value {
7591 pm: None,
7592 arr: Vec::new(),
7593 scanflags: 0,
7594 valflags: 0,
7595 start: 0,
7596 end: -1,
7597 };
7598 let mut cursor: &str = s;
7599 let has_sub = s.contains('[');
7600 let mut was_unset = false;
7601 let v = getvalue(Some(&mut vbuf), &mut cursor, 1);
7602 let need_create = match v {
7603 Some(ref vv) => {
7604 if let Some(pm) = vv.pm.as_ref() {
7605 let f = pm.node.flags as u32;
7606 if (f & (PM_ARRAY | PM_HASHED)) != 0
7607 && (f & (PM_SPECIAL | PM_TIED)) == 0
7608 && unset(KSHARRAYS)
7609 && !has_sub
7610 {
7611 // unsetparam_pm(vv.pm, 0, 1);
7612 was_unset = true;
7613 true
7614 } else {
7615 false
7616 }
7617 } else {
7618 true
7619 }
7620 }
7621 None => true,
7622 };
7623 if need_create {
7624 // c:3686-3691 — `createparam(t, val.type & MN_FLOAT ? PM_FFLOAT
7625 // : PM_INTEGER); second getvalue;`. Synthesize a fresh
7626 // numeric param in paramtab matching the C body. Without
7627 // this branch wired, callers like `setiparam` silently
7628 // dropped the create (returned None) — every new integer
7629 // param assignment was a no-op.
7630 let _ = was_unset;
7631 let new_type = if val.type_ == MN_FLOAT {
7632 PM_FFLOAT // c:3687
7633 } else {
7634 PM_INTEGER // c:3688
7635 };
7636 // c:Src/params.c:3690 — newly created PM_INTEGER param
7637 // inherits the source numeric base from `lastbase` (set by
7638 // the math parser when consuming a `N#NNN` or `0x..` literal).
7639 // Mirror the assignstrvalue path at c:3714 so `(( X = 16#ff ))`
7640 // creates X as `typeset -i16 X=255` (displays as `16#FF`)
7641 // rather than naked decimal `255`.
7642 let inherited_base = if val.type_ == MN_FLOAT {
7643 0
7644 } else {
7645 let lb = crate::ported::math::lastbase();
7646 if lb > 0 {
7647 lb
7648 } else {
7649 0
7650 }
7651 };
7652 let pm: Param = Box::new(param {
7653 node: hashnode {
7654 next: None,
7655 nam: s.to_string(),
7656 flags: new_type as i32,
7657 },
7658 u_data: 0,
7659 u_tied: None,
7660 u_arr: None,
7661 u_str: None,
7662 // c:3690 — `setnumvalue(...)` stores the value. For
7663 // PM_INTEGER → u.l; for PM_FFLOAT → u.dval.
7664 u_val: if val.type_ == MN_FLOAT { 0 } else { val.l },
7665 u_dval: if val.type_ == MN_FLOAT { val.d } else { 0.0 },
7666 u_hash: None,
7667 gsu_s: None,
7668 gsu_i: None,
7669 gsu_f: None,
7670 gsu_a: None,
7671 gsu_h: None,
7672 base: inherited_base,
7673 width: 0,
7674 env: None,
7675 ename: None,
7676 old: None,
7677 level: 0,
7678 });
7679 if let Ok(mut tab) = paramtab().write() {
7680 tab.insert(s.to_string(), pm.clone());
7681 }
7682 return Some(pm);
7683 }
7684 if (flags & ASSPM_WARN) != 0 {
7685 if let Some(ref vv) = v {
7686 if let Some(ref pm) = vv.pm {
7687 check_warn_pm(pm, "numeric", 0, 1);
7688 }
7689 }
7690 }
7691 // The reassign path: getvalue gave us a cloned pm inside the value
7692 // buffer. setnumvalue mutates that clone but the write doesn't
7693 // propagate back to paramtab. Write through paramtab directly so
7694 // reassignments stick — same shape as `assignsparam`'s c:3343
7695 // `assignstrvalue(v, val, flags)` path which mutates paramtab in
7696 // place.
7697 if let Ok(mut tab) = paramtab().write() {
7698 if let Some(pm) = tab.get_mut(s) {
7699 // c:Src/params.c — setnumvalue (the C function this
7700 // reassign-path mirrors) eventually calls setfn which
7701 // goes through assignstrvalue (params.c:2899-2904) where
7702 // PM_READONLY rejection lives. The Rust port writes
7703 // u_val / u_dval / u_str directly, bypassing that path,
7704 // so the readonly check has to happen here. Without it,
7705 // `(( x++ ))` / `let "x = ..."` could mutate readonly
7706 // params silently. Bug #154 in docs/BUGS.md.
7707 if (pm.node.flags as u32 & PM_READONLY) != 0 {
7708 // zerr internally sets ERRFLAG_ERROR via the
7709 // c:194 path in Src/utils.c. Match the existing
7710 // readonly check at params.rs:3951 (assignstrvalue
7711 // arm) — same call shape, same behavior.
7712 zerr(&format!("read-only variable: {}", pm.node.nam));
7713 return None;
7714 }
7715 pm.node.flags &= !(PM_DEFAULTED as i32);
7716 let t = PM_TYPE(pm.node.flags as u32);
7717 if t == PM_INTEGER {
7718 // c:2874 — `pm->gsu.i->setfn(pm, val.u.l)`. MN_FLOAT
7719 // input truncates to integer.
7720 pm.u_val = if val.type_ == MN_FLOAT {
7721 val.d as i64
7722 } else {
7723 val.l
7724 };
7725 // c:Src/params.c:2801 — `if (!v->pm->base && lastbase
7726 // != -1) v->pm->base = lastbase;`. After setfn the C
7727 // path falls through `setstrvalue(v, NULL)` which
7728 // inherits the source numeric base from `lastbase`
7729 // when the param doesn't yet have an explicit base.
7730 // Mirror that here so `(( x = 0xFF ))` on an existing
7731 // integer param updates pm.base to 16, displaying as
7732 // `16#FF` instead of decimal `255`. Bug #175 in
7733 // docs/BUGS.md. The create path already inherits at
7734 // params.rs:5759-5768; this is the reassign-path
7735 // equivalent.
7736 if pm.base == 0 {
7737 let lb = crate::ported::math::lastbase();
7738 if lb > 0 {
7739 pm.base = lb;
7740 }
7741 }
7742 } else if t == PM_EFLOAT || t == PM_FFLOAT {
7743 // c:2878 — MN_INTEGER input promotes to f64.
7744 pm.u_dval = if val.type_ == MN_FLOAT {
7745 val.d
7746 } else {
7747 val.l as f64
7748 };
7749 } else if t == PM_SCALAR || t == PM_NAMEREF || t == PM_ARRAY {
7750 // c:2862-2871 — convbase/convfloat → u_str.
7751 let s_rendered = if val.type_ == MN_FLOAT {
7752 convfloat_underscore(val.d, pm.width)
7753 } else {
7754 convbase_underscore(val.l, if pm.base > 0 { pm.base } else { 10 }, pm.width)
7755 };
7756 pm.u_str = Some(s_rendered);
7757 }
7758 let cloned = pm.clone();
7759 return Some(cloned);
7760 }
7761 }
7762 None
7763}
7764
7765/// Port of `Param setnparam(char *s, mnumber val)` from `Src/params.c:3745-3749`.
7766///
7767/// C body (c:3747-3748):
7768/// ```c
7769/// return assignnparam(s, val, ASSPM_WARN);
7770/// ```
7771///
7772/// Single-line wrapper around `assignnparam` with ASSPM_WARN flags.
7773///
7774/// The previous Rust port took `(s: &str, val: f64) -> ()` — losing
7775/// the integer branch (callers couldn't set integer params via
7776/// `setnparam`) AND the Param return. No real callers existed because
7777/// the fabricated sig fit nothing. Match C exactly: `(s, val)` where
7778/// `val` is the canonical `mnumber` tagged union, returning the
7779/// resulting Param.
7780pub fn setnparam(s: &str, val: mnumber) -> Option<Param> {
7781 assignnparam(s, val, ASSPM_WARN as i32) // c:3748
7782}
7783
7784/// Port of `Param assigniparam(char *s, zlong val, int flags)` from
7785/// `Src/params.c:3754-3761`.
7786///
7787/// C body (c:3757-3760):
7788/// ```c
7789/// mnumber mnval;
7790/// mnval.type = MN_INTEGER;
7791/// mnval.u.l = val;
7792/// return assignnparam(s, mnval, flags);
7793/// ```
7794///
7795/// Two divergences in the previous Rust port:
7796/// 1. Dropped the `flags` arg — caller-supplied flags (e.g.
7797/// ASSPM_AUGMENT for `+= int`) couldn't be threaded through;
7798/// every call hardcoded ASSPM_WARN regardless.
7799/// 2. Returned void instead of Param — losing the new param
7800/// pointer the caller may want to read back.
7801pub fn assigniparam(s: &str, val: i64, flags: i32) -> Option<Param> {
7802 // c:3757-3759 — `mnumber{ .type = MN_INTEGER, .u.l = val }`.
7803 let mnval = mnumber {
7804 l: val,
7805 d: 0.0,
7806 type_: MN_INTEGER,
7807 };
7808 // c:3760 — `return assignnparam(s, mnval, flags);`
7809 assignnparam(s, mnval, flags) // c:3760
7810}
7811
7812/// Port of `Param setiparam(char *s, zlong val)` from `Src/params.c:3767-3773`.
7813///
7814/// C body (c:3769-3772):
7815/// ```c
7816/// mnumber mnval;
7817/// mnval.type = MN_INTEGER;
7818/// mnval.u.l = val;
7819/// return assignnparam(s, mnval, ASSPM_WARN);
7820/// ```
7821///
7822/// The previous Rust port stringified to decimal and routed through
7823/// `assignsparam` — which CREATES THE PARAM AS PM_SCALAR. C creates
7824/// as PM_INTEGER. `setiparam("x", 5)` followed by `typeset -p x`:
7825/// - C: \`typeset -i x=5\`
7826/// - Old Rust: \`typeset x=5\`
7827///
7828/// `assignnparam` IS now ported (params.rs:4403). Route through it
7829/// matching C exactly so integer-typed params get created with the
7830/// right PM_INTEGER flag.
7831pub fn setiparam(s: &str, val: i64) -> Option<Param> {
7832 // c:3770-3771 — `mnumber{ .type = MN_INTEGER, .u.l = val }`.
7833 let mnval = mnumber {
7834 l: val,
7835 d: 0.0,
7836 type_: MN_INTEGER,
7837 };
7838 // c:3772 — `return assignnparam(s, mnval, ASSPM_WARN);`
7839 assignnparam(s, mnval, ASSPM_WARN as i32) // c:3772
7840}
7841
7842/// Port of `setiparam_no_convert(char *s, zlong val)` from Src/params.c:3781. C
7843/// source comment: "If the target is already an integer, this
7844/// gets converted back. Low technology rules." It uses convbase
7845/// to render decimal then calls assignsparam.
7846/// WARNING: param names don't match C — Rust=() vs C=(s, val)
7847pub fn setiparam_no_convert(s: &str, val: i64) -> Option<Param> {
7848 assignsparam(s, &val.to_string(), ASSPM_WARN as i32)
7849}
7850
7851/// Port of `resetparam(Param pm, int flags)` from `Src/params.c:3796`. C body:
7852/// ```c
7853/// char *s = pm->node.nam;
7854/// queue_signals();
7855/// if (pm != (Param)(paramtab == realparamtab ?
7856/// paramtab->getnode2(paramtab, s) :
7857/// paramtab->getnode(paramtab, s))) {
7858/// unqueue_signals();
7859/// zerr("can't change type of hidden variable: %s", s);
7860/// return 1;
7861/// }
7862/// s = dupstring(s);
7863/// unsetparam_pm(pm, 0, 1);
7864/// unqueue_signals();
7865/// createparam(s, flags);
7866/// return 0;
7867/// ```
7868/// Tears `pm` down + recreates it with `flags` so the next
7869/// assignment lands in a fresh slot of the requested type. Used
7870/// by `assignsparam` when the type-flag of an existing param
7871/// changes (e.g. `typeset -i x; x="abc"` resets x back to scalar).
7872///
7873/// The `paramtab->getnode` reachability check at c:3800 catches
7874/// the hidden-shadow case (a local var hiding the global `pm` we
7875/// were handed) — without the paramtab vtable we skip the check
7876/// and proceed to unset+create.
7877pub fn resetparam(pm: &mut param, flags: i32) -> i32 {
7878 // c:3796
7879 let s = pm.node.nam.clone(); // c:3796
7880 queue_signals(); // c:3799
7881 // c:3800-3807 — paramtab->getnode2 / getnode reachability check.
7882 // Without paramtab vtable wired we cannot detect the hidden-
7883 // variable case, so we proceed; a future port of paramtab
7884 // adds the check at this site.
7885 unsetparam_pm(pm, 0, 1); // c:3819
7886 unqueue_signals(); // c:3819
7887 let _ = createparam(&s, flags); // c:3819
7888 0 // c:3819
7889}
7890
7891/// Port of `void unsetparam(char *s)` from `Src/params.c:3819`.
7892///
7893/// C body:
7894/// ```c
7895/// Param pm;
7896/// queue_signals();
7897/// if ((pm = (Param)(paramtab == realparamtab ?
7898/// paramtab->getnode2(paramtab, s) :
7899/// paramtab->getnode(paramtab, s))))
7900/// unsetparam_pm(pm, 0, 1);
7901/// unqueue_signals();
7902/// ```
7903///
7904/// The previous Rust port took `(variables, arrays, assoc_arrays,
7905/// name)` operating on EXTERNAL HashMap storage — a SubstState-
7906/// era stale signature. C operates on the canonical `paramtab`
7907/// global. No live callers used the old 4-arg form (all use
7908/// `paramtab().write().remove(...)` directly), so renaming is
7909/// safe.
7910pub fn unsetparam(name: &str) -> i32 {
7911 // c:3819 — C's unsetparam is void and discards unsetparam_pm's
7912 // status; bin_unset (c:Src/builtin.c:3952-3953) does the
7913 // paramtab lookup itself and calls `if (unsetparam_pm(pm, 0, 1))
7914 // returnval = 1;`. The Rust bin_unset routes through this
7915 // wrapper for the bridge plumbing (tied names, hashed-storage
7916 // shadow, special regenerators), so the rejection status is
7917 // surfaced here instead: 1 = readonly rejection, 0 = unset ok.
7918 // c:Src/params.c:3853-3935 — unsetparam_pm's tied-alt-name
7919 // removal block. zsh's PATH/path, FPATH/fpath, MANPATH/manpath,
7920 // CDPATH/cdpath, PSVAR/psvar pairs are tied (`pm->ename` points
7921 // to the alt name) — unsetting one must clear the other or
7922 // command lookup keeps finding binaries via the surviving `path`
7923 // array even after `unset PATH`. The full ename machinery is
7924 // deferred until the gsu vtable lands; until then, mirror the
7925 // tie explicitly for the canonical pairs so `unset PATH` is
7926 // actually a security boundary. Bug #416.
7927 let tied_alt: Option<&str> = match name {
7928 "PATH" => Some("path"),
7929 "path" => Some("PATH"),
7930 "FPATH" => Some("fpath"),
7931 "fpath" => Some("FPATH"),
7932 "MANPATH" => Some("manpath"),
7933 "manpath" => Some("MANPATH"),
7934 "CDPATH" => Some("cdpath"),
7935 "cdpath" => Some("CDPATH"),
7936 "PSVAR" => Some("psvar"),
7937 "psvar" => Some("PSVAR"),
7938 "MODULE_PATH" => Some("module_path"),
7939 "module_path" => Some("MODULE_PATH"),
7940 "FIGNORE" => Some("fignore"),
7941 "fignore" => Some("FIGNORE"),
7942 "MAILPATH" => Some("mailpath"),
7943 "mailpath" => Some("MAILPATH"),
7944 _ => None,
7945 };
7946 queue_signals(); // c:3825
7947 // c:3826-3831 — `if ((pm = ... getnode2 ...) && !(pm->node.flags
7948 // & PM_NAMEREF)) unsetparam_pm(pm, 0, 1);`.
7949 //
7950 // Two divergences in the previous Rust port:
7951 // 1. Missing PM_NAMEREF check — `unsetparam("ref")` where `ref`
7952 // is a nameref would remove the ref alias itself. C explicitly
7953 // skips nameref params here (they're cleared via the
7954 // ref-specific path, not the value-side unset).
7955 // 2. Bypassed `unsetparam_pm` — removed the entry directly from
7956 // paramtab without running the readonly-guard at c:3850, the
7957 // stdunsetfn dispatch at c:3870, or the `pm->old` scope
7958 // restore. `typeset -r x=foo; unset x` would silently succeed
7959 // in Rust where C rejects with `read-only variable: x`.
7960 // c:Src/params.c:3853 — flag regenerator-style specials as unset
7961 // so subsequent reads via lookup_special_var skip the getfn.
7962 // RANDOM/SECONDS/EPOCH*/TTYIDLE/ERRNO have no paramtab pm node in
7963 // zshrs (they're lookup_special_var libc shims), so the standard
7964 // unsetparam_pm path below doesn't catch them. Bug #417/#418.
7965 if matches!(
7966 name,
7967 "RANDOM" | "SECONDS" | "EPOCHSECONDS" | "EPOCHREALTIME" | "TTYIDLE" | "ERRNO"
7968 ) {
7969 mark_unset_special(name);
7970 }
7971 // c:Src/params.c:3850 — `if (pm->node.flags & PM_READONLY) { zerr;
7972 // return 1; }`. Read-only specials (LINENO, HISTCMD, PPID, etc.)
7973 // have PM_READONLY in their special_paramdef entry but no paramtab
7974 // pm node by default, so the standard PM_READONLY check inside
7975 // unsetparam_pm never fires for them. Walk the special_params
7976 // table directly to catch these. Bug #419.
7977 let is_readonly_special = special_params
7978 .iter()
7979 .any(|ip| ip.name == name && (ip.pm_flags & PM_READONLY) != 0);
7980 if is_readonly_special {
7981 zerr(&format!("read-only variable: {}", name));
7982 unqueue_signals();
7983 return 1; // c:3854 — unsetparam_pm's readonly rejection status
7984 }
7985 let mut retval = 0i32;
7986 // c:Src/params.c:3855 — `if (pm->ename && !altflag) altremove =
7987 // ztrdup(pm->ename)`. User ties created by `typeset -T FOO foo`
7988 // cross-link the two params via `pm.ename` (each side names the
7989 // other); the static `tied_alt` map above only covers the
7990 // canonical PM_SPECIAL pairs (PATH/path …). Capture the unset
7991 // param's ename here so the cascade below can clear the tied
7992 // partner for user ties too.
7993 let (found, is_nameref, param_ename) = {
7994 let tab = paramtab().read().unwrap();
7995 match tab.get(name) {
7996 Some(pm) => (
7997 true,
7998 (pm.node.flags as u32 & PM_NAMEREF) != 0,
7999 pm.ename.clone(),
8000 ),
8001 None => (false, false, None),
8002 }
8003 };
8004 if found && !is_nameref {
8005 // c:3826-3830
8006 // c:3831 — `unsetparam_pm(pm, 0, 1)`. Take an owned copy out
8007 // of paramtab so we can mutate it (unsetparam_pm wants
8008 // &mut), run the readonly-guard + env teardown, then re-insert
8009 // or fully remove based on the readonly path.
8010 let mut pm_owned = paramtab().write().unwrap().remove(name).unwrap();
8011 let rejected = unsetparam_pm(&mut pm_owned, 0, 1); // c:3831
8012 if rejected != 0 {
8013 retval = 1; // c:Src/builtin.c:3952-3953 surfaced to bin_unset
8014 // Readonly rejection — restore the entry so the state
8015 // is unchanged.
8016 paramtab()
8017 .write()
8018 .unwrap()
8019 .insert(name.to_string(), pm_owned);
8020 } else if pm_owned.old.is_some()
8021 || (pm_owned.level > 0 && locallevel.load(Ordering::Relaxed) as i32 >= pm_owned.level)
8022 {
8023 // c:Src/params.c:3892-3925 — when the unset'd pm is a
8024 // local that shadowed an outer binding (chained via
8025 // pm.old by `addparam` at c:1137), the local pm STAYS
8026 // in paramtab with PM_UNSET set so the current scope's
8027 // reads see "unset" (empty). The pm.old chain is
8028 // preserved so endparamscope can uncover the outer
8029 // when the local scope ends. Without this re-insert,
8030 // either:
8031 // - the outer would be uncovered immediately (wrong:
8032 // zsh hides the outer until scope end), or
8033 // - pm.old would be dropped (wrong: outer never
8034 // comes back).
8035 // c:3911-3913 — `if ((pm->level && locallevel >= pm->level)
8036 // ...) return 0;` — locals are kept in the table marked
8037 // PM_UNSET even WITHOUT an outer binding ("foo() { local
8038 // bar; unset bar; } makes the global bar available? The
8039 // following makes the answer no"), and `typeset -p bar`
8040 // still finds the node (prints nothing, status 0).
8041 paramtab()
8042 .write()
8043 .unwrap()
8044 .insert(name.to_string(), pm_owned);
8045 } else if (pm_owned.node.flags as u32 & PM_SPECIAL) != 0
8046 && (pm_owned.node.flags as u32 & PM_REMOVABLE) == 0
8047 {
8048 // c:Src/params.c:3911-3913 — `if ((pm->flags &
8049 // (PM_SPECIAL|PM_REMOVABLE)) == PM_SPECIAL) return 0;`.
8050 // PM_SPECIAL params (SECONDS, RANDOM, HOME, IFS, ...)
8051 // stay in paramtab with PM_UNSET set after unset. A
8052 // subsequent re-assign (`SECONDS=100`) finds the same
8053 // pm via createparam's `oldpm` lookup, hits the reuse
8054 // arm (c:1132), preserves the PM_INTEGER|PM_SPECIAL
8055 // flags + gsu vtable, so intsetfn's name-dispatch
8056 // fires and routes through intsecondssetfn. Without
8057 // this, the special pm was dropped, a fresh PM_SCALAR
8058 // pm was created for the re-assignment, the value
8059 // landed in pm.u_str, and lookup_special_var kept
8060 // reading the time-since-shtimer delta. Bug #418 in
8061 // docs/BUGS.md.
8062 //
8063 // PM_UNSET is already stamped by unsetparam_pm at
8064 // line 6491; keep it on the re-inserted pm so reads
8065 // still see "unset" until re-assignment clears it
8066 // (clear_unset_special at line 4756 handles the
8067 // regenerator-style unset_specials set; the pm flag
8068 // is cleared inside assignsparam's value-write arm
8069 // via stdunsetfn's symmetric set/clear convention).
8070 paramtab()
8071 .write()
8072 .unwrap()
8073 .insert(name.to_string(), pm_owned);
8074 }
8075 // No pm.old + no rejection + not PM_SPECIAL → drop entirely
8076 // (matches the C path at c:3935 where the node is removed
8077 // from paramtab).
8078 }
8079 // c:Src/params.c:3905-3935 — tied-alt removal. Cascade the
8080 // unset to the paired name (PATH↔path etc.). Also clear the OS
8081 // env mirror since command lookup at the syscall level reads
8082 // the inherited libc environ.
8083 // Static canonical pair (PATH↔path …) takes precedence; otherwise
8084 // fall back to the unset param's `ename` (user `typeset -T` ties).
8085 let effective_alt: Option<String> = tied_alt.map(|s| s.to_string()).or(param_ename);
8086 if let Some(alt) = effective_alt.as_deref() {
8087 let alt_present = paramtab()
8088 .read()
8089 .map(|t| t.contains_key(alt))
8090 .unwrap_or(false);
8091 if alt_present {
8092 if let Some(mut alt_pm) = paramtab().write().ok().and_then(|mut t| t.remove(alt)) {
8093 let _ = unsetparam_pm(&mut alt_pm, 1, 1);
8094 }
8095 }
8096 env::remove_var(alt);
8097 env::remove_var(name);
8098 // c:Src/params.c:5291 — `if (t == path) cmdnamtab->emptytable
8099 // (cmdnamtab)`. The hashed-cmdnam cache holds absolute paths
8100 // resolved via the prior PATH search; without clearing,
8101 // `unset PATH; ls` still hits the cached `/bin/ls` entry and
8102 // exec succeeds — defeating the security boundary the unset
8103 // is supposed to establish. Bug #416.
8104 if matches!(name, "PATH" | "path") {
8105 crate::ported::hashtable::emptycmdnamtable();
8106 }
8107 }
8108 unqueue_signals(); // c:3832
8109 retval
8110}
8111
8112/// Unset parameter (from params.c unsetparam_pm)
8113/// Port of `unsetparam_pm(Param pm, int altflag, int exp)` from `Src/params.c:3841`. Full body
8114/// removes `pm` from `paramtab` (after invoking
8115/// `pm->gsu.s->unsetfn(pm, exp)`), tears down the tied alternate
8116/// (`pm->ename`) when `!altflag`, deletes the env entry, and
8117/// resurrects `pm->old` at the right scope. Stub: needs paramtab
8118/// HashTable backend (`paramtab->removenode/addnode`) plus the
8119/// `delenv`/`adduserdir` helpers — direct port retains only the
8120/// in-memory mutation of `pm` that doesn't touch the table.
8121#[allow(unused_variables)]
8122pub fn unsetparam_pm(pm: &mut param, altflag: i32, exp: i32) -> i32 {
8123 // c:3850 — `if ((pm->node.flags & PM_READONLY) && pm->level <= locallevel)`.
8124 let cur_ll = locallevel.load(Ordering::Relaxed) as i32; // c:3850 locallevel
8125 if (pm.node.flags as u32 & PM_READONLY) != 0 && pm.level <= cur_ll {
8126 // c:3850
8127 // c:3852 — `zerr("read-only %s: %s", ...)`. Emit diagnostic
8128 // so users see why the unset failed.
8129 let kind = if (pm.node.flags as u32 & PM_NAMEREF) != 0 {
8130 // c:3852
8131 "reference"
8132 } else {
8133 "variable"
8134 };
8135 zerr(&format!("read-only {}: {}", kind, pm.node.nam));
8136 return 1; // c:3854
8137 }
8138 pm.node.flags &= !(PM_DECLARED as i32); // c:3868
8139 if (pm.node.flags as u32 & PM_UNSET) == 0 || (pm.node.flags as u32 & PM_REMOVABLE) != 0 {
8140 // c:3870 — `pm->gsu.s->unsetfn(pm, exp)` — open-coded to stdunsetfn.
8141 stdunsetfn(pm, exp);
8142 }
8143 if pm.env.is_some() {
8144 delenv(&pm.node.nam); // c:3872 delenv(pm)
8145 pm.env = None;
8146 }
8147 // Tied alt-name removal + paramtab restore-from-old not yet
8148 // possible without HashTable backend; the C postlude (lines
8149 // 3853-3935) is a paramtab->removenode + addnode dance that
8150 // requires the missing vtable.
8151 pm.node.flags |= PM_UNSET as i32;
8152 0
8153}
8154
8155// -----------------------------------------------------------
8156// GSU dispatch callbacks — direct ports against `param.u_*`
8157// fields. C source in Src/params.c:4002.
8158// -----------------------------------------------------------
8159
8160/// Port of `intgetfn(Param pm)` from `Src/params.c:3993`. C body:
8161/// `return pm->u.val;`
8162pub fn intgetfn(pm: ¶m) -> i64 {
8163 pm.u_val
8164}
8165
8166/// Port of `intsetfn(Param pm, zlong x)` from `Src/params.c:4002`. C body:
8167/// `pm->u.val = x;`
8168pub fn intsetfn(pm: &mut param, x: i64) {
8169 // c:Src/params.c:4575 — PM_SPECIAL integers have per-name gsu_i->setfn
8170 // hooks: SECONDS routes through intsecondssetfn (shtimer math),
8171 // RANDOM seeds the PRNG, etc. The default intsetfn is the fallback
8172 // for non-special integers (pm->u.val write).
8173 //
8174 // Rust port lookup_special_var dispatches GETTERS by name; the
8175 // setters need symmetric dispatch so `SECONDS=N` actually moves
8176 // shtimer instead of writing u.val which intsecondsgetfn never
8177 // reads. Without this, `$SECONDS` always read the time-since-shtimer
8178 // delta regardless of writes.
8179 // Name-based dispatch (not flag-based): some assignment paths
8180 // construct a fresh param shell for tc and lose PM_SPECIAL.
8181 match pm.node.nam.as_str() {
8182 "SECONDS" => {
8183 intsecondssetfn(x);
8184 return;
8185 }
8186 // c:Src/params.c:364-365 —
8187 // IPDEF6("TRY_BLOCK_ERROR", &try_errflag, varinteger_gsu)
8188 // IPDEF6("TRY_BLOCK_INTERRUPT", &try_interrupt, varinteger_gsu)
8189 // varinteger_gsu's setfn is `intvarsetfn` (c:4213), whose body is
8190 // `*pm->u.valptr = x;` — i.e. the assignment writes the loop.c
8191 // GLOBAL itself, not just `pm->u.val`. That is the entire
8192 // mechanism behind `TRY_BLOCK_ERROR=0` inside an `always` block:
8193 // `exectry` re-reads the global after the always-list runs
8194 // (c:Src/loop.c:774 `if (try_errflag) errflag |= ERRFLAG_ERROR;`)
8195 // and a user-zeroed global swallows the try-block's error.
8196 //
8197 // The Rust port cannot carry C's `u.valptr` raw pointer, so the
8198 // bound global is reached by name — the same asymmetry the
8199 // SECONDS / RANDOM arms below already resolve this way. Without
8200 // it, `TRY_BLOCK_ERROR=0` wrote `pm.u_val` while every reader
8201 // (params.rs's special-var getter, exectry) kept consulting the
8202 // atomic, so the assignment was a no-op.
8203 "TRY_BLOCK_ERROR" => {
8204 crate::ported::r#loop::try_errflag.store(x, Ordering::Relaxed); // c:4213
8205 intvarsetfn(pm, x); // c:4213 `*pm->u.valptr = x;`
8206 return;
8207 }
8208 "TRY_BLOCK_INTERRUPT" => {
8209 crate::ported::r#loop::try_interrupt.store(x, Ordering::Relaxed); // c:4213
8210 intvarsetfn(pm, x); // c:4213
8211 return;
8212 }
8213 // c:Src/params.c:4552 randomsetfn — `RANDOM=N` calls
8214 // srand(N). Without this dispatch, $RANDOM writes only u.val
8215 // and the next read returns rand()'s next value from the
8216 // PROCESS-START seed, not the user-requested seed. Same
8217 // name-based dispatch shape as SECONDS above.
8218 "RANDOM" => {
8219 randomsetfn(x);
8220 return;
8221 }
8222 // c:Src/params.c:4698 uidsetfn / c:4719 euidsetfn / c:4740
8223 // gidsetfn / c:4761 egidsetfn — `UID=N` / `EUID=N` /
8224 // `GID=N` / `EGID=N` attempt the corresponding setuid /
8225 // seteuid / setgid / setegid syscall and emit
8226 // `failed to change [effective ]{user,group} ID: ERRNO`
8227 // on failure. Bug #254 in docs/BUGS.md. Same name-based
8228 // dispatch shape as SECONDS/RANDOM above.
8229 "UID" => {
8230 uidsetfn(x);
8231 return;
8232 }
8233 "EUID" => {
8234 euidsetfn(x);
8235 return;
8236 }
8237 "GID" => {
8238 gidsetfn(x);
8239 return;
8240 }
8241 "EGID" => {
8242 egidsetfn(x);
8243 return;
8244 }
8245 // c:Src/params.c:4974 histsizesetfn / c:4998 savehistsizesetfn —
8246 // `HISTSIZE=N` / `SAVEHIST=N` must update the canonical
8247 // `histsiz` / `savehistsiz` globals AND clamp to >= 1 +
8248 // call `resizehistents()` so the in-memory history buffer
8249 // shrinks/grows. Without this dispatch the assignment
8250 // lands in `pm.u_val` and `histsizegetfn` (which reads the
8251 // global) keeps returning the un-touched default. Bug #520.
8252 "HISTSIZE" => {
8253 histsizesetfn(x);
8254 return;
8255 }
8256 "SAVEHIST" => {
8257 savehistsizesetfn(x);
8258 return;
8259 }
8260 _ => {}
8261 }
8262 pm.u_val = x;
8263}
8264
8265/// Port of `floatgetfn(Param pm)` from `Src/params.c:4011`. C body:
8266/// `return pm->u.dval;`
8267pub fn floatgetfn(pm: ¶m) -> f64 {
8268 pm.u_dval
8269}
8270
8271/// Port of `floatsetfn(Param pm, double x)` from `Src/params.c:4020`. C body:
8272/// `pm->u.dval = x;`
8273pub fn floatsetfn(pm: &mut param, x: f64) {
8274 // c:Src/params.c:4603 floatsecondssetfn — PM_SPECIAL float SECONDS
8275 // routes through shtimer math. Symmetric with intsetfn's SECONDS
8276 // dispatch above and lookup_special_var's getter.
8277 // c:Src/params.c:4603 floatsecondssetfn — PM_SPECIAL float SECONDS
8278 // routes through shtimer math. Symmetric with intsetfn's SECONDS
8279 // dispatch above and lookup_special_var's getter. Some assignment
8280 // paths construct a fresh `param` shell for tc (type-conversion)
8281 // and lose PM_SPECIAL, so name-only dispatch is the safe fallback.
8282 if pm.node.nam == "SECONDS" {
8283 floatsecondssetfn(x);
8284 return;
8285 }
8286 pm.u_dval = x;
8287}
8288
8289/// Port of `strgetfn(Param pm)` from `Src/params.c:4029`. C body:
8290/// `return pm->u.str ? pm->u.str : (char *) hcalloc(1);`
8291pub fn strgetfn(pm: ¶m) -> String {
8292 pm.u_str.clone().unwrap_or_default()
8293}
8294
8295/// Port of `strsetfn(Param pm, char *x)` from `Src/params.c:4040`.
8296///
8297/// C body (c:4043-4051):
8298/// ```c
8299/// zsfree(pm->u.str); pm->u.str = x;
8300/// if (!(pm->node.flags & PM_HASHELEM) &&
8301/// ((pm->node.flags & PM_NAMEDDIR) || isset(AUTONAMEDIRS))) {
8302/// pm->node.flags |= PM_NAMEDDIR;
8303/// adduserdir(pm->node.nam, x, 0, 0);
8304/// }
8305/// ```
8306///
8307/// The C body fires the `adduserdir` path when EITHER `PM_NAMEDDIR`
8308/// is already set OR the `AUTONAMEDIRS` option is on. The previous
8309/// Rust port only fired when PM_NAMEDDIR was already set, missing
8310/// the AUTONAMEDIRS auto-create branch entirely. With `setopt
8311/// AUTONAMEDIRS`, every scalar assignment to a path-shaped value
8312/// should register a named-directory entry for `~name` expansion;
8313/// the Rust port silently dropped that behavior.
8314pub fn strsetfn(pm: &mut param, x: String) {
8315 // c:4040
8316 pm.u_str = Some(x.clone()); // c:4044 pm->u.str = x
8317 // c:4045-4046 — `if (!(PM_HASHELEM) && (PM_NAMEDDIR || isset(AUTONAMEDIRS)))`.
8318 if (pm.node.flags as u32 & PM_HASHELEM) == 0
8319 && ((pm.node.flags as u32 & PM_NAMEDDIR) != 0 || isset(AUTONAMEDIRS))
8320 // c:4046 isset(AUTONAMEDIRS)
8321 {
8322 pm.node.flags |= PM_NAMEDDIR as i32; // c:4047
8323 adduserdir(&pm.node.nam, &x, 0, false); // c:4048
8324 }
8325}
8326
8327/// Port of `arrgetfn(Param pm)` from `Src/params.c:4057`. C body:
8328/// `return pm->u.arr ? pm->u.arr : &nullarray;`
8329pub fn arrgetfn(pm: ¶m) -> Vec<String> {
8330 pm.u_arr.clone().unwrap_or_default()
8331}
8332
8333/// Port of `arrsetfn(Param pm, char **x)` from `Src/params.c:4066`. C body frees
8334/// the old array, applies PM_UNIQUE filter via `uniqarray()`, then
8335/// stores. Calls `arrfixenv(ename, x)` for tied colon-arrays.
8336pub fn arrsetfn(pm: &mut param, x: Vec<String>) {
8337 let val = if (pm.node.flags as u32 & PM_UNIQUE) != 0 {
8338 simple_arrayuniq(x)
8339 } else {
8340 x
8341 };
8342 pm.u_arr = Some(val.clone());
8343 if let Some(ename) = pm.ename.clone() {
8344 arrfixenv(&ename, Some(&val));
8345 }
8346}
8347
8348/// Port of `hashgetfn(Param pm)` from `Src/params.c:4084`. C body:
8349/// `return pm->u.hash;`
8350pub fn hashgetfn(pm: ¶m) -> Option<&HashTable> {
8351 pm.u_hash.as_ref()
8352}
8353
8354/// Port of `hashsetfn(Param pm, HashTable x)` from `Src/params.c:4093`. C body:
8355/// `if (pm->u.hash && pm->u.hash != x) deleteparamtable(pm->u.hash);
8356/// pm->u.hash = x;`
8357pub fn hashsetfn(pm: &mut param, x: HashTable) {
8358 pm.u_hash = Some(x);
8359}
8360
8361/// Direct port of `static void arrhashsetfn(Param pm, char **val,
8362/// int flags)` from `Src/params.c:4113-4170`. Set callback for
8363/// assoc arrays: takes a flat `[k1, v1, k2, v2, ...]` value list
8364/// and turns it into a hash.
8365///
8366/// C body:
8367/// 1. Count non-Marker entries; if odd, error c:4128-4131.
8368/// 2. Under ASSPM_AUGMENT, fetch existing hash via getfn
8369/// (c:4134-4137); otherwise allocate fresh via
8370/// newparamtable(17, name).
8371/// 3. Walk pairs: each value (k, v) becomes a PM_SCALAR|PM_UNSET
8372/// child param `createparam(k)`, then `assignstrvalue(v->pm,
8373/// val, eltflags)` (c:4140-4166).
8374/// 4. `pm->gsu.h->setfn(pm, ht)` to install (c:4168).
8375///
8376/// Storage model: C's per-pair `createparam(k)` + `assignstrvalue`
8377/// builds child Params inside a fresh `newparamtable`; zshrs's assoc
8378/// values live in the `paramtab_hashed_storage` IndexMap keyed by the
8379/// owning param's name, so the pair walk writes there. `pm.u_hash`
8380/// stays untouched — the IndexMap is the authoritative store (same
8381/// contract sethparam/gethparam already use).
8382pub fn arrhashsetfn(
8383 // c:4113
8384 pm: &mut param,
8385 val: Vec<String>,
8386 flags: i32,
8387) {
8388 // c:4124-4127 — count non-Marker entries.
8389 let alen: usize = val
8390 .iter()
8391 .filter(|s| !s.starts_with(Marker as char))
8392 .count();
8393
8394 // c:4129-4131 — odd count → error.
8395 if alen % 2 != 0 {
8396 zerr("bad set of key/value pairs for associative array");
8397 return;
8398 }
8399
8400 // c:4135-4139 — ASSPM_AUGMENT starts from the existing hash
8401 // (`ht = paramtab = pm->gsu.h->getfn(pm)`); otherwise a fresh
8402 // table (`newparamtable(17, pm->node.nam)`).
8403 let mut map: IndexMap<String, String> = if (flags & ASSPM_AUGMENT) != 0 {
8404 paramtab_hashed_storage()
8405 .lock()
8406 .unwrap()
8407 .get(&pm.node.nam)
8408 .cloned()
8409 .unwrap_or_default() // c:4136
8410 } else {
8411 IndexMap::new() // c:4138-4139
8412 };
8413
8414 // c:4141-4166 — pair walk. keyvalpairelement (Src/subst.c:49)
8415 // emits `[Marker, key, value]` triples for `[k]=v` / `[k]+=v`
8416 // forms ("Either all elements have Marker or none. Checked in
8417 // caller." c:4144); plain input is flat `[k, v, ...]` pairs.
8418 let mut it = val.into_iter();
8419 while let Some(first) = it.next() {
8420 let (elt_augment, key) = if first.starts_with(Marker as char) {
8421 // c:4145-4151 — `(*aptr)[1] == '+'` → per-element append
8422 // (ASSPM_AUGMENT via the setsparam INT_MAX trick).
8423 let aug = first[Marker.len_utf8()..].starts_with('+');
8424 match it.next() {
8425 Some(k) => (aug, k),
8426 None => break,
8427 }
8428 } else {
8429 (false, first)
8430 };
8431 let v = it.next().unwrap_or_default(); // c:4166 assignstrvalue value
8432 if elt_augment {
8433 // c:4147-4150 — `[k]+=v` appends to the existing element.
8434 map.entry(key).or_default().push_str(&v);
8435 } else {
8436 // c:4156-4166 — createparam(k, PM_SCALAR|PM_UNSET) +
8437 // assignstrvalue: plain insert in the IndexMap model.
8438 map.insert(key, v);
8439 }
8440 }
8441
8442 // c:4168-4169 — `pm->gsu.h->setfn(pm, ht)` installs the table.
8443 paramtab_hashed_storage()
8444 .lock()
8445 .unwrap()
8446 .insert(pm.node.nam.clone(), map);
8447 // c:4170 — free(val). Rust drops automatically.
8448}
8449
8450/// Port of `nullstrsetfn(UNUSED(Param pm), char *x)` from `Src/params.c:4180`. C body:
8451/// `zsfree(x);` — frees but doesn't store. Rust drop handles free.
8452#[allow(unused_variables)]
8453pub fn nullstrsetfn(pm: &mut param, x: String) {}
8454
8455/// Port of `nullunsetfn(UNUSED(Param pm), UNUSED(int exp))` from `Src/params.c:4192`. C body: empty.
8456#[allow(unused_variables)]
8457pub fn nullunsetfn(pm: &mut param, exp: i32) {}
8458
8459/// Port of `stdunsetfn(Param pm, UNUSED(int exp))` from `Src/params.c:3955`. C body:
8460/// dispatches `pm->gsu->setfn(pm, NULL)` per `PM_TYPE`, clears
8461/// `PM_TIED`/frees ename for tied params, sets PM_UNSET.
8462///
8463/// Rust port mirrors C semantics: clears the union slot and sets
8464/// PM_UNSET. The GSU vtable callbacks are stored on `param` as
8465/// `Option<Gsu*>` (zsh_h:760-764) but the dispatch uses callback
8466/// fn-ptrs that aren't generally registered yet, so we open-code
8467/// the "setfn(pm, NULL)" effect by zeroing the matching union
8468/// member instead of calling through the vtable.
8469#[allow(unused_variables)]
8470pub fn stdunsetfn(pm: &mut param, exp: i32) {
8471 match PM_TYPE(pm.node.flags as u32) {
8472 PM_SCALAR | PM_NAMEREF => {
8473 pm.u_str = None;
8474 }
8475 PM_ARRAY => {
8476 pm.u_arr = None;
8477 }
8478 PM_HASHED => {
8479 pm.u_hash = None;
8480 }
8481 _ => {
8482 if (pm.node.flags as u32 & PM_SPECIAL) == 0 {
8483 pm.u_str = None;
8484 }
8485 }
8486 }
8487 if (pm.node.flags as u32 & (PM_SPECIAL | PM_TIED)) == PM_TIED {
8488 pm.ename = None;
8489 pm.node.flags &= !(PM_TIED as i32);
8490 }
8491 pm.node.flags |= PM_UNSET as i32;
8492}
8493
8494// -----------------------------------------------------------
8495// "Null" callbacks — no-op getfn/setfn/unsetfn slots used for
8496// read-only or write-only special params.
8497// -----------------------------------------------------------
8498
8499/// Port of `nullintsetfn(UNUSED(Param pm), UNUSED(zlong x))` from `Src/params.c:4187`. C body:
8500/// empty (no-op setter for read-only int params).
8501#[allow(unused_variables)]
8502pub fn nullintsetfn(pm: &mut param, x: i64) {}
8503
8504/// Port of `nullsethashfn(UNUSED(Param pm), HashTable x)` from `Src/params.c:4104`. C body:
8505/// `deleteparamtable(x);` — frees the supplied table, doesn't store.
8506#[allow(unused_variables)]
8507pub fn nullsethashfn(pm: &mut param, x: HashTable) {
8508 // Rust drop semantics free `x` when this scope ends.
8509}
8510
8511// -----------------------------------------------------------
8512// Generic special-param GSU callbacks (`u.valptr` / `u.data`).
8513// C source uses raw pointer indirection through `pm->u.data`/
8514// `pm->u.valptr` — Rust port stores the global's name in `u_str`
8515// (lookup key) since we can't carry raw pointers across an FFI
8516// boundary safely. The lookup-table integration ships with the
8517// special-params init code (Src/params.c:4213 createparamtable).
8518// -----------------------------------------------------------
8519
8520/// Port of `intvargetfn(Param pm)` from `Src/params.c:4202`. C body:
8521/// `return *pm->u.valptr;`
8522pub fn intvargetfn(pm: ¶m) -> i64 {
8523 pm.u_val
8524}
8525
8526/// Port of `intvarsetfn(Param pm, zlong x)` from `Src/params.c:4213`. C body:
8527/// `*pm->u.valptr = x;`
8528pub fn intvarsetfn(pm: &mut param, x: i64) {
8529 pm.u_val = x;
8530}
8531
8532/// Port of `zlevarsetfn(Param pm, zlong x)` from `Src/params.c:4224`. C body sets
8533/// the int and triggers `adjustwinsize` for LINES/COLUMNS.
8534/// Port of `zlevarsetfn(Param pm, zlong x)` from `Src/params.c:4226`.
8535/// C body: `*p = x; if (p == &zterm_lines || p == &zterm_columns)
8536/// adjustwinsize(2 + (p == &zterm_columns));`
8537///
8538/// The `from` argument to `adjustwinsize` is documented at
8539/// `Src/utils.c:1883-1887`: 0=signal, 1=manual, 2=LINES callback,
8540/// 3=COLUMNS callback. Each value selects a different code path
8541/// inside `adjustwinsize` — for example, `from=2` skips the
8542/// COLUMNS-specific ioctl, and `from=3` skips the LINES path.
8543///
8544/// The previous Rust port passed `0` for both LINES and COLUMNS,
8545/// which triggered the FULL `getwinsz` ioctl + both adjustlines
8546/// AND adjustcolumns calls AND the potential setiparam recursion
8547/// — diverging from C's narrow "just adjust the one axis we
8548/// changed" semantics. Effect: setting `LINES=80` would re-issue
8549/// `setiparam("COLUMNS", ...)` recursively, churning the
8550/// paramtab for no reason.
8551pub fn zlevarsetfn(pm: &mut param, x: i64) {
8552 // c:4226
8553 pm.u_val = x; // c:4230 *p = x;
8554 // c:4231-4232 — `2 + (p == &zterm_columns)` selects 2 for LINES
8555 // (zterm_lines) and 3 for COLUMNS (zterm_columns).
8556 if pm.node.nam == "LINES" {
8557 let _ = adjustwinsize(2); // c:4232 LINES path
8558 } else if pm.node.nam == "COLUMNS" {
8559 let _ = adjustwinsize(3); // c:4232 COLUMNS path
8560 }
8561}
8562
8563/// Port of `strvarsetfn(Param pm, char *x)` from `Src/params.c:4249`. C body:
8564/// `zsfree(*q); *q = x;` where `q = (char **)pm->u.data`.
8565pub fn strvarsetfn(pm: &mut param, x: Option<String>) {
8566 pm.u_str = x;
8567}
8568
8569/// Port of `strvargetfn(Param pm)` from `Src/params.c:4263`. C body:
8570/// `s = *((char **)pm->u.data); return s ? s : hcalloc(1);`
8571pub fn strvargetfn(pm: ¶m) -> String {
8572 pm.u_str.clone().unwrap_or_default()
8573}
8574
8575/// Port of `arrvargetfn(Param pm)` from `Src/params.c:4279`. C body:
8576/// `arrptr = *((char ***)pm->u.data); return arrptr ?: &nullarray;`
8577pub fn arrvargetfn(pm: ¶m) -> Vec<String> {
8578 pm.u_arr.clone().unwrap_or_default()
8579}
8580
8581/// Direct port of `mod_export void arrvarsetfn(Param pm, char **x)`
8582/// from `Src/params.c:4292-4317`. The previous body skipped three of
8583/// the four canonical C arms:
8584/// 1. PM_UNIQUE → uniqarray (was ported via simple_arrayuniq).
8585/// 2. PM_SPECIAL + null x → `*dptr = mkarray(NULL)` so a tied
8586/// array set to NULL becomes a writable empty array, not a
8587/// dangling null (was missing).
8588/// 3. `pm->ename` set → `arrfixenv(ename, x)` syncs the colon-
8589/// joined env var partner (was missing — breaks PATH/path,
8590/// FPATH/fpath, etc. when set via the array form).
8591/// 4. `pm->ename` set + null x + `*dptr == path` → invalidate
8592/// pathchecked so the next path resolution re-walks (was
8593/// missing).
8594pub fn arrvarsetfn(pm: &mut param, x: Option<Vec<String>>) {
8595 // c:4296 `char ***dptr = (char ***)pm->u.data;`
8596 // c:4298-4299 — `if (*dptr != x) freearray(*dptr);` Rust Vec drop
8597 // on reassignment handles freeing automatically.
8598 // c:4300-4301 — `if (pm->node.flags & PM_UNIQUE) uniqarray(x);`
8599 let uniq_applied: Option<Vec<String>> = match x {
8600 Some(v) if (pm.node.flags as u32 & PM_UNIQUE) != 0 => Some(simple_arrayuniq(v)),
8601 other => other,
8602 };
8603 // c:4302-4310 — PM_SPECIAL + NULL → mkarray(NULL); else assign.
8604 let final_val: Vec<String> = match uniq_applied {
8605 Some(v) => v, // c:4310 `*dptr = x;`
8606 None => {
8607 if (pm.node.flags as u32 & PM_SPECIAL) != 0 {
8608 crate::ported::utils::mkarray(None) // c:4308 `mkarray(NULL)`
8609 } else {
8610 Vec::new() // c:4310 — null case for non-special: empty.
8611 }
8612 }
8613 };
8614 // c:4311-4316 — ename sync.
8615 if let Some(ename) = pm.ename.clone() {
8616 // c:4311 `if (pm->ename)`
8617 if !final_val.is_empty() || pm.u_arr.is_some() {
8618 // c:4312-4313 — `if (x) arrfixenv(pm->ename, x);`
8619 arrfixenv(&ename, Some(&final_val));
8620 } else if pm.node.nam == "path" {
8621 // c:4314-4315 — `else if (*dptr == path) pathchecked = path;`
8622 // — invalidate the path-resolver cache. Rust port uses an
8623 // AtomicUsize sentinel; storing 0 marks "must re-walk".
8624 crate::ported::hashtable::pathchecked.store(0, Ordering::SeqCst);
8625 }
8626 }
8627 pm.u_arr = Some(final_val);
8628}
8629
8630/// Array to colon-separated path — inverse of `colonsplit`.
8631/// Port of `colonarrgetfn(Param pm)` from Src/params.c (joins the array
8632/// stored in `pm->u.colon` back into the `:`-form for env).
8633/// WARNING: param names don't match C — Rust=(arr) vs C=(pm)
8634pub fn colonarrgetfn(arr: &[String]) -> String {
8635 arr.join(":")
8636}
8637
8638/// Port of `colonarrsetfn(Param pm, char *x)` from `Src/params.c:4329`. C body
8639/// splits the colon-string into an array and stores via the
8640/// generic arrvarsetfn.
8641pub fn colonarrsetfn(pm: &mut param, x: Option<String>) {
8642 let uniq = (pm.node.flags as u32 & PM_UNIQUE) != 0; // c:4339
8643 // c:4339-4341 — `arrvarsetfn(pm, x ? colonsplit(...) : NULL);`
8644 // The None branch must pass `None` (not `Some(Vec::new())`) so the
8645 // PM_SPECIAL + NULL → mkarray(NULL) arm in arrvarsetfn fires.
8646 let arr = x.map(|s| colonsplit(&s, uniq)); // c:4339
8647 arrvarsetfn(pm, arr);
8648}
8649
8650/// Port of `tiedarrgetfn(Param pm)` from `Src/params.c:4348`. C body:
8651/// `struct tieddata *dptr = (struct tieddata *)pm->u.data;`
8652/// `return *dptr->arrptr ? zjoin(*dptr->arrptr, …) : "";`
8653///
8654/// C's `pm->u.data->arrptr` is a raw pointer into the partner array
8655/// param's storage. The Rust port can't hold a pointer into another
8656/// paramtab entry's heap, so the partner lookup goes via
8657/// `pm.ename` → paramtab → `apm.u_arr`. For backwards compatibility
8658/// with callers that set `pm.u_arr` directly (the pre-fix code path
8659/// at c:4348 that's still in some Rust call sites), fall back to
8660/// the scalar's own `u_arr` when `ename` is None or the partner is
8661/// missing. Bug #24 in docs/BUGS.md.
8662pub fn tiedarrgetfn(pm: ¶m) -> Vec<String> {
8663 if let Some(ename) = pm.ename.as_deref() {
8664 if let Ok(tab) = paramtab().read() {
8665 if let Some(apm) = tab.get(ename) {
8666 if let Some(arr) = apm.u_arr.as_ref() {
8667 return arr.clone();
8668 }
8669 }
8670 }
8671 }
8672 pm.u_arr.clone().unwrap_or_default()
8673}
8674
8675/// Direct port of `void tiedarrsetfn(Param pm, char *x)` from
8676/// `Src/params.c:4357-4389`. Setter for a colon-array-tied
8677/// scalar (PATH/CDPATH/MAILPATH/etc.).
8678///
8679/// C body:
8680/// 1. Free the existing tied array (`*dptr->arrptr`) at c:4363.
8681/// 2. If no array but an `ename` exists, clear PM_DEFAULTED on
8682/// the tied array param (c:4365-4368).
8683/// 3. If `x` is non-null: build a 1-or-2-byte separator from
8684/// `dptr->joinchar` (Meta-quoting if needed, c:4371-4380),
8685/// `sepsplit(x, sepbuf, 0, 0)` into the array (c:4381), and
8686/// uniqarray() if PM_UNIQUE (c:4382-4383). Free `x` (c:4384).
8687/// 4. Else: `*dptr->arrptr = NULL` (c:4385-4386).
8688/// 5. If `pm->ename` is set, call `arrfixenv(pm->name, arrptr)`
8689/// to sync env (c:4387-4388).
8690///
8691/// The Rust port treats `u_arr` as the tied array storage and
8692/// uses `':'` as the joinchar default (matches PATH/CDPATH/FPATH
8693/// /MAILPATH/PSVAR/MODULE_PATH which all use colon separators —
8694/// the joinchar field on the C-side tieddata wasn't ported to the
8695/// Rust Param struct yet).
8696pub fn tiedarrsetfn(pm: &mut param, x: Option<String>) {
8697 // c:4357
8698
8699 // c:4361-4368 — free old / clear PM_DEFAULTED on tied counterpart.
8700 if pm.u_arr.is_none() {
8701 if let Some(ename) = pm.ename.clone() {
8702 // c:4365
8703 let mut tab = paramtab().write().unwrap();
8704 if let Some(altpm) = tab.get_mut(&ename) {
8705 // c:4366
8706 altpm.node.flags &= !(PM_DEFAULTED as i32); // c:4367
8707 }
8708 }
8709 }
8710
8711 // c:4369-4386 — split + assign. C writes through `*dptr->arrptr`
8712 // which is a pointer INTO THE PARTNER ARRAY param's storage. The
8713 // Rust port can't hold raw pointers across paramtab entries, so
8714 // when `pm.ename` is set, write the split result to the partner's
8715 // `u_arr` via paramtab (bug #24). When `pm.ename` is None (older
8716 // call sites or non-tied use), keep the scalar's own `u_arr`
8717 // up-to-date for legacy callers.
8718 // c:4370-4380 — single-byte separator built from `dptr->joinchar`
8719 // on the tieddata riding `pm->u.data` (Rust: typed `u_tied` view);
8720 // joinchar==0 → empty sepbuf; no tieddata → `:` default (the
8721 // PM_SPECIAL colon-tied params, c:5314-5315).
8722 let sepbuf: String = match pm.u_tied.as_deref() {
8723 Some(td) if td.joinchar == 0 => String::new(), // c:4376-4377
8724 Some(td) => ((td.joinchar as u8) as char).to_string(), // c:4378-4379
8725 None => ":".to_string(), // c:5314-5315
8726 };
8727 let arr_opt: Option<Vec<String>> = if let Some(s) = x {
8728 // c:4369
8729 // c:4381 — `sepsplit(x, sepbuf, 0, 0)`.
8730 // joinchar==0 (typeset -T s a ''): the zsh 5.9.1 release
8731 // binary keeps the whole string as ONE element on assignment
8732 // (measured: `typeset -T S s ""; S=abc; typeset -p s` →
8733 // `s=( abc )`), diverging from a literal char-split reading
8734 // of sepsplit("") in the C source; match the release binary
8735 // (parity floor).
8736 let split: Vec<String> = if sepbuf.is_empty() {
8737 vec![s.clone()]
8738 } else {
8739 crate::ported::utils::sepsplit(&s, Some(&sepbuf), true)
8740 };
8741 // c:4382-4383 — uniqarray if PM_UNIQUE.
8742 let split = if pm.node.flags & PM_UNIQUE as i32 != 0 {
8743 // c:4382
8744 uniqarray(split) // c:4383
8745 } else {
8746 split
8747 };
8748 Some(split)
8749 } else {
8750 None
8751 };
8752 if let Some(ename) = pm.ename.clone() {
8753 if let Ok(mut tab) = paramtab().write() {
8754 if let Some(apm) = tab.get_mut(&ename) {
8755 apm.u_arr = arr_opt.clone(); // c:4381
8756 }
8757 }
8758 // c:4352 — zjoin writes the raw joinchar byte; joinchar==0
8759 // joins with NUL (measured on 5.9.1: `s=(x y); print -rn
8760 // "$S"` → `x\0y`), not with the empty split-sepbuf.
8761 let joinsep = if sepbuf.is_empty() {
8762 "\0"
8763 } else {
8764 sepbuf.as_str()
8765 };
8766 pm.u_str = arr_opt.as_ref().map(|a| a.join(joinsep));
8767 } else {
8768 pm.u_arr = arr_opt;
8769 }
8770
8771 // c:4387-4388 — `if (pm->ename) arrfixenv(pm->name, *dptr->arrptr)`.
8772 if pm.ename.is_some() {
8773 let nam = pm.node.nam.clone();
8774 // Pull the live array out of the partner for env sync.
8775 let snap = paramtab().read().ok().and_then(|t| {
8776 t.get(pm.ename.as_deref().unwrap())
8777 .and_then(|p| p.u_arr.clone())
8778 });
8779 arrfixenv(&nam, snap.as_deref());
8780 }
8781}
8782
8783/// Port of `tiedarrunsetfn(Param pm, UNUSED(int exp))` from `Src/params.c:4393`. C body
8784/// frees the tied storage and calls stdunsetfn.
8785/// Direct port of `void tiedarrunsetfn(Param pm, UNUSED(int exp))`
8786/// from `Src/params.c:4393`. Special unset for tied arrays:
8787/// frees tieddata, ename, clears PM_TIED, sets PM_UNSET.
8788///
8789/// C body:
8790/// pm->gsu.s->setfn(pm, NULL); // c:4393
8791/// zfree(pm->u.data, sizeof(tieddata)); // c:4393
8792/// pm->u.data = NULL; // c:4393
8793/// zsfree(pm->ename); // c:4393
8794/// pm->ename = NULL; // c:4393
8795/// pm->flags &= ~PM_TIED; // c:4393
8796/// pm->flags |= PM_UNSET; // c:4393
8797pub fn tiedarrunsetfn(pm: &mut param, _exp: i32) {
8798 // c:4393
8799 // c:4400 — invoke the scalar setfn with NULL (frees backing array).
8800 tiedarrsetfn(pm, None);
8801 // c:4401-4403 — drop tieddata.
8802 pm.u_data = 0;
8803 pm.u_arr = None;
8804 // c:4404-4405 — `zsfree(pm->ename); pm->ename = NULL`.
8805 pm.ename = None;
8806 // c:4406-4407 — flag toggles.
8807 pm.node.flags &= !(PM_TIED as i32);
8808 pm.node.flags |= PM_UNSET as i32;
8809}
8810
8811// -----------------------------------------------------------
8812// Array uniq helpers.
8813// -----------------------------------------------------------
8814
8815/// Port of `simple_arrayuniq(char **x, int freeok)` from `Src/params.c:4412`. C body:
8816/// O(n^2) dedupe in place — first occurrence wins.
8817/// WARNING: param names don't match C — Rust=(x) vs C=(x, freeok)
8818pub fn simple_arrayuniq(x: Vec<String>) -> Vec<String> {
8819 let mut seen: HashSet<String> = HashSet::new();
8820 let mut out = Vec::with_capacity(x.len());
8821 for s in x {
8822 if seen.insert(s.clone()) {
8823 out.push(s);
8824 }
8825 }
8826 out
8827}
8828
8829/// Port of `arrayuniq_freenode(HashNode hn)` from `Src/params.c:4443`. C
8830/// body: `zsfree(((Pathnode)hn)->name); zfree(hn, sizeof…);` —
8831/// the freenode callback for the temporary HashTable `arrayuniq`
8832/// builds. Rust drop semantics handle this; no-op shim.
8833/// is `(void)hn;` — intentional no-op; passed as freenode callback
8834/// to scratch hashtable used by `arrayuniq` so existing entries
8835/// aren't freed when the table is torn down.
8836/// WARNING: param names don't match C — Rust=() vs C=(hn)
8837/// WARNING: param names don't match C — Rust=() vs C=(pm, x)
8838pub fn arrayuniq_freenode() {}
8839
8840/// Direct port of `HashTable newuniqtable(zlong size)` from
8841/// `Src/params.c:4450`. C body allocates a `HashTable`
8842/// named "arrayuniq" with the standard hasher/cmpnodes/
8843/// add/get/remove/disable/enable function pointers plus
8844/// `arrayuniq_freenode` as the freenode callback (which is a
8845/// no-op — see c:4443). Rust returns a `HashSet<String>` with
8846/// the size hint pre-allocated; the freenode-callback role is
8847/// implicit (Drop runs on HashSet teardown without freeing
8848/// borrowed strings).
8849pub fn newuniqtable(size: i64) -> HashSet<String> {
8850 // c:4450
8851 HashSet::with_capacity(size.max(0) as usize) // c:4450 newhashtable(size, ...)
8852}
8853
8854/// Direct port of `static void arrayuniq(char **x, int freeok)`
8855/// from `Src/params.c:4473`. First-wins dedupe of `x`,
8856/// in-place. C uses simple O(n²) scan for arrays under 10
8857/// entries, switching to a HashTable for larger arrays. `freeok`
8858/// controls whether to `zsfree()` duplicates (only safe when
8859/// caller owns the strings — Rust drop semantics handle it).
8860///
8861/// Signature note: C takes `char **x` + in-place mutation; Rust
8862/// takes owned `Vec<String>` and returns the deduped result.
8863/// `freeok` is preserved but is a no-op in Rust (drops free
8864/// automatically). The hashtable / simple-loop tiering follows
8865/// the same threshold (10) as C.
8866pub fn arrayuniq(x: Vec<String>, freeok: i32) -> Vec<String> {
8867 // c:4473
8868 let _ = freeok;
8869 let array_size = x.len();
8870 if array_size == 0 {
8871 // c:4481
8872 return x;
8873 }
8874 // c:4482-4486 — small-array fallback to simple_arrayuniq.
8875 if array_size < 10 {
8876 // c:4482
8877 return simple_arrayuniq(x); // c:4484
8878 }
8879 // c:4483 — `if (!(ht = newuniqtable(array_size + 1)))` — Rust
8880 // newuniqtable never fails, but mirror the C order of allocation.
8881 let mut ht = newuniqtable(array_size as i64 + 1);
8882 // c:4487-4507 — walk + first-wins.
8883 let mut out: Vec<String> = Vec::with_capacity(array_size);
8884 for s in x {
8885 // c:4487 walk
8886 if ht.insert(s.clone()) {
8887 // c:4488 gethashnode2 + addhashnode2
8888 out.push(s); // c:4495 *write_it = *it
8889 }
8890 // else: dup — drop the value (c:4502 zsfree if freeok).
8891 }
8892 drop(ht); // c:4523 deletehashtable
8893 out
8894}
8895
8896/// Remove duplicate elements from array while preserving order.
8897/// Port of `uniqarray(char **x)` from Src/params.c.
8898/// WARNING: param names don't match C — Rust=(arr) vs C=(x)
8899pub fn uniqarray(arr: Vec<String>) -> Vec<String> {
8900 let mut seen = HashSet::new();
8901 arr.into_iter().filter(|s| seen.insert(s.clone())).collect()
8902}
8903
8904/// Direct port of `void zhuniqarray(char **x)` from
8905/// `Src/params.c:4523`. Wraps `arrayuniq` with `freeok=0`.
8906/// (C body is literally `arrayuniq(x, 0);`.)
8907pub fn zhuniqarray(x: Vec<String>) -> Vec<String> {
8908 // c:4523
8909 arrayuniq(x, 0) // c:4523
8910}
8911
8912/// Port of `poundgetfn(UNUSED(Param pm))` from `Src/params.c:4534`. C body:
8913/// `return arrlen(pparams);`
8914/// WARNING: param names don't match C — Rust=() vs C=(pm)
8915pub fn poundgetfn() -> i64 {
8916 pparams_lock().lock().expect("pparams poisoned").len() as i64
8917}
8918
8919/// Port of `randomgetfn(UNUSED(Param pm))` from `Src/params.c:4543`. C body:
8920/// `return rand() & 0x7fff;`
8921/// WARNING: param names don't match C — Rust=() vs C=(pm)
8922pub fn randomgetfn() -> i64 {
8923 (unsafe { libc::rand() } & 0x7fff) as i64
8924}
8925
8926/// Port of `randomsetfn(UNUSED(Param pm), zlong v)` from `Src/params.c:4552`. C body:
8927/// `srand((unsigned int)v);`
8928/// WARNING: param names don't match C — Rust=(v) vs C=(pm, v)
8929pub fn randomsetfn(v: i64) {
8930 unsafe { libc::srand(v as libc::c_uint) };
8931}
8932
8933// -----------------------------------------------------------
8934// SECONDS / EPOCHSECONDS family — backed by SHTIMER static.
8935// -----------------------------------------------------------
8936
8937/// Port of `intsecondsgetfn(UNUSED(Param pm))` from `Src/params.c:4561`. C body:
8938/// `return (zlong)(now.tv_sec - shtimer.tv_sec - …);`
8939/// WARNING: param names don't match C — Rust=() vs C=(pm)
8940pub fn intsecondsgetfn() -> i64 {
8941 // c:4563 — `shtimer` is initialized at shell startup (zsh.h
8942 // mod_export). Force shtimer init BEFORE reading `now` so the
8943 // lazy-init race doesn't make `now < shtimer` on first call
8944 // (which produced -1 from the nsec borrow-from-sec adjustment).
8945 let timer = *shtimer_lock().lock().expect("shtimer poisoned");
8946 let now = SystemTime::now()
8947 .duration_since(UNIX_EPOCH)
8948 .unwrap_or_default();
8949 let now_sec = now.as_secs() as i64;
8950 let timer_sec = timer.as_secs() as i64;
8951 let now_nsec = now.subsec_nanos() as i64;
8952 let timer_nsec = timer.subsec_nanos() as i64;
8953 let diff = now_sec - timer_sec - i64::from(now_nsec < timer_nsec);
8954 // c:4565 — clamp negative-diff (lazy-init or clock skew) to 0
8955 // so \$SECONDS reads as a non-negative count of elapsed seconds
8956 // from shell start. zsh's shtimer is set in main() before any
8957 // user code runs, guaranteeing now >= shtimer; the Rust lazy
8958 // init makes this stricter via .max(0).
8959 diff.max(0)
8960}
8961
8962/// Port of `intsecondssetfn(UNUSED(Param pm), zlong x)` from `Src/params.c:4575`. C body:
8963/// ```c
8964/// diff = (zlong)now.tv_sec - x;
8965/// shtimer.tv_sec = diff;
8966/// if ((zlong)shtimer.tv_sec != diff)
8967/// zwarn("SECONDS truncated on assignment");
8968/// shtimer.tv_nsec = now.tv_nsec;
8969/// ```
8970/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
8971pub fn intsecondssetfn(x: i64) {
8972 let now = SystemTime::now()
8973 .duration_since(UNIX_EPOCH)
8974 .unwrap_or_default();
8975 let now_sec = now.as_secs() as i64;
8976 let new_sec = now_sec - x;
8977 // c:4587 — C uses `zwarn` (informational), NOT `zerr` (fatal).
8978 // The C body STORES `diff` unconditionally then emits the warning
8979 // if truncation lost information. Rust port previously used `zerr`
8980 // and early-returned (skipping the store) — divergent from C.
8981 if new_sec < 0 {
8982 zwarn("SECONDS truncated on assignment");
8983 // c:4585 — C still stores; Rust represents shtimer as Duration
8984 // which is non-negative. We clamp to zero to preserve the
8985 // "store-anyway" semantic for the time-display path, even
8986 // though the negative-time case is unrepresentable.
8987 *shtimer_lock().lock().expect("shtimer poisoned") = Duration::new(0, now.subsec_nanos());
8988 return;
8989 }
8990 *shtimer_lock().lock().expect("shtimer poisoned") =
8991 Duration::new(new_sec as u64, now.subsec_nanos());
8992}
8993
8994/// Port of `floatsecondsgetfn(UNUSED(Param pm))` from `Src/params.c:4591`. C body:
8995/// `return (double)(now-tv_sec - shtimer.tv_sec) + nsec/1e9;`
8996/// WARNING: param names don't match C — Rust=() vs C=(pm)
8997pub fn floatsecondsgetfn() -> f64 {
8998 // Read shtimer BEFORE `now`, forcing its lazy init first — intsecondsgetfn
8999 // (above) documents the same ordering requirement: with the opposite order
9000 // a first call can observe `now < shtimer` and go backwards.
9001 let timer = *shtimer_lock().lock().expect("shtimer poisoned");
9002 let now = SystemTime::now()
9003 .duration_since(UNIX_EPOCH)
9004 .unwrap_or_default();
9005 // c:4595-4596 — `(double)(now.tv_sec - shtimer.tv_sec) +
9006 // (double)(now.tv_nsec - shtimer.tv_nsec) / 1000000000.0`
9007 // C subtracts the two fields SEPARATELY and SIGNED, so a `now` behind
9008 // `shtimer` simply yields a negative reading. This was
9009 // `(now - timer).as_secs_f64()`, and Duration - Duration PANICS on
9010 // underflow rather than going negative — `overflow when subtracting
9011 // durations`. It had never fired because the float type was unreachable:
9012 // `typeset -F SECONDS` was silently ignored (bin_typeset never called
9013 // setsecondstype), so nothing ever selected this getter. Wiring that up
9014 // turned a silent no-op into a crash, which is how this surfaced.
9015 (now.as_secs() as f64 - timer.as_secs() as f64)
9016 + (now.subsec_nanos() as f64 - timer.subsec_nanos() as f64) / 1_000_000_000.0
9017}
9018
9019/// Port of `floatsecondssetfn(UNUSED(Param pm), double x)` from `Src/params.c:4603`. C body:
9020/// `shtimer.tv_sec = now.tv_sec - (zlong)x; shtimer.tv_nsec = now.tv_nsec - (x-int)*1e9;`
9021/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
9022pub fn floatsecondssetfn(x: f64) {
9023 let now = SystemTime::now()
9024 .duration_since(UNIX_EPOCH)
9025 .unwrap_or_default();
9026 let new = now
9027 .checked_sub(Duration::from_secs_f64(x))
9028 .unwrap_or_default();
9029 *shtimer_lock().lock().expect("shtimer poisoned") = new;
9030}
9031
9032/// Port of `getrawseconds()` from `Src/params.c:4615`. C body:
9033/// `return (double)shtimer.tv_sec + (double)shtimer.tv_nsec / 1e9;`
9034pub fn getrawseconds() -> f64 {
9035 shtimer_lock()
9036 .lock()
9037 .expect("shtimer poisoned")
9038 .as_secs_f64()
9039}
9040
9041/// Port of `setrawseconds(double x)` from `Src/params.c:4622`. C body:
9042/// `shtimer.tv_sec = (zlong)x; shtimer.tv_nsec = (x-int)*1e9;`
9043pub fn setrawseconds(x: f64) {
9044 *shtimer_lock().lock().expect("shtimer poisoned") = Duration::from_secs_f64(x);
9045}
9046
9047/// Port of `setsecondstype(Param pm, int on, int off)` from `Src/params.c:4630`. C body
9048/// flips the `gsu.f`/`gsu.i` callback pointer based on the new
9049/// param-flag bitset.
9050///
9051/// WARNING: zshrs has no Param/GSU dispatch table yet — the
9052/// "promotion between integer/float seconds" logic happens via
9053/// pm->gsu pointer swaps in C. Returns 0 to signal success;
9054/// callers can assume the type change is recorded by the caller's
9055/// own bookkeeping until the GSU table lands.
9056/// WARNING: param names don't match C — Rust=(on, off) vs C=(pm, on, off)
9057pub fn setsecondstype(
9058 // c:4630
9059 pm: &mut param,
9060 on: i32,
9061 off: i32,
9062) -> i32 {
9063 // c:4632 — `int newflags = (pm->flags | on) & ~off`.
9064 let newflags = (pm.node.flags | on) & !off;
9065 // c:4633 — `int tp = PM_TYPE(newflags)`.
9066 let tp = PM_TYPE(newflags as u32);
9067 // c:4635-4638 / 4639-4642 — float vs integer GSU pointer swap.
9068 if tp == PM_EFLOAT || tp == PM_FFLOAT {
9069 // c:4635
9070 // C: `pm->gsu.f = &floatseconds_gsu`. GSU table not yet
9071 // wired in the Rust port; record the type by clearing
9072 // any integer GSU.
9073 pm.gsu_i = None;
9074 // pm.gsu_f = Some(floatseconds_gsu) — pending GSU port.
9075 } else if tp == PM_INTEGER {
9076 // c:4639
9077 // C: `pm->gsu.i = &intseconds_gsu`.
9078 pm.gsu_f = None;
9079 // pm.gsu_i = Some(intseconds_gsu) — pending GSU port.
9080 } else {
9081 return 1; // c:4644
9082 }
9083 pm.node.flags = newflags; // c:4645
9084 0 // c:4646
9085}
9086
9087// -----------------------------------------------------------
9088// $USERNAME
9089// -----------------------------------------------------------
9090
9091/// Port of `usernamegetfn(UNUSED(Param pm))` from `Src/params.c:4653`. C body:
9092/// Port of `usernamegetfn(UNUSED(Param pm))` from Src/params.c:4655.
9093/// C body: `return get_username();`. C's `get_username()`
9094/// (Src/utils.c:1075) walks `getuid() != cached_uid` and
9095/// refreshes the cache via `getpwuid()` on mismatch — so a
9096/// USERNAME read AFTER an `setuid()` call sees the NEW
9097/// username, not the stale cache.
9098///
9099/// The previous Rust port returned `cached_username_lock()`
9100/// directly without the refresh, so a script that called
9101/// setuid(3) (or USER changed externally via setuid binary)
9102/// would keep returning the old username.
9103///
9104pub fn usernamegetfn(_pm: ¶m) -> String {
9105 // c:4655
9106 // c:4658 — `return get_username();`. Route through the
9107 // canonical refresh-on-uid-change accessor at utils.rs.
9108 get_username() // c:4658
9109}
9110
9111/// Port of `usernamesetfn(UNUSED(Param pm), char *x)` from `Src/params.c:4662`. C body:
9112/// `getpwnam(x); setgid; setuid; cached_uid = pswd->pw_uid;`
9113///
9114/// WARNING: the SUID-changing path requires getpwnam(3) which
9115/// crosses an unsafe FFI boundary not yet wrapped here. The
9116/// cached-name update is performed; uid/gid changes still need
9117/// porting of the `pwd.h` getpwnam wrapper.
9118pub fn usernamesetfn(_pm: &mut param, x: String) {
9119 // c:4662
9120 // c:4662 — `if (x && (pswd = getpwnam(x)) && pswd->pw_uid != cached_uid)`.
9121 let target = std::ffi::CString::new(x.as_bytes()).ok();
9122 if let Some(cstr) = target {
9123 unsafe {
9124 let pwd = libc::getpwnam(cstr.as_ptr()); // c:4666
9125 if !pwd.is_null() {
9126 // c:4666 — C reads `cached_uid` (a global initialized
9127 // to `getuid()` at init.c:1219 — the REAL uid, NOT
9128 // the effective one). The previous Rust port used
9129 // `geteuid()` which diverges when running setuid
9130 // (geteuid != getuid) — the shell would erroneously
9131 // try to change to a uid it's already at, or skip
9132 // a needed change. Match C exactly: use `getuid()`.
9133 let cached_uid = libc::getuid(); // c:4666 cached_uid = getuid()
9134 if (*pwd).pw_uid != cached_uid {
9135 // c:4666
9136 // c:4670-4672 — initgroups(x, pswd->pw_gid).
9137 let _ = libc::initgroups(cstr.as_ptr(), (*pwd).pw_gid as _);
9138 // c:4671 — setgid(pswd->pw_gid).
9139 if libc::setgid((*pwd).pw_gid) != 0 {
9140 // c:4673
9141 zwarn(&format!(
9142 "failed to change group ID: {}",
9143 std::io::Error::last_os_error()
9144 ));
9145 } else if libc::setuid((*pwd).pw_uid) != 0 {
9146 // c:4675
9147 // c:4675-4676 — setuid failed.
9148 zwarn(&format!(
9149 "failed to change user ID: {}",
9150 std::io::Error::last_os_error()
9151 ));
9152 } else {
9153 // c:4677-4681 — cache update.
9154 let name_cstr = std::ffi::CStr::from_ptr((*pwd).pw_name);
9155 let name_str = name_cstr.to_string_lossy().to_string();
9156 *cached_username_lock().lock().expect("username poisoned") =
9157 ztrdup_metafy(&name_str);
9158 }
9159 }
9160 }
9161 }
9162 }
9163 // c:4683 — `zsfree(x)`; Rust drop handles it.
9164 drop(x);
9165}
9166
9167// -----------------------------------------------------------
9168// libc-backed callbacks (UID/GID/EUID/EGID/errno/RANDOM/TTYIDLE).
9169// -----------------------------------------------------------
9170
9171/// Port of `uidgetfn(UNUSED(Param pm))` from `Src/params.c:4689`. C body:
9172/// `return getuid();`
9173/// WARNING: param names don't match C — Rust=() vs C=(pm)
9174pub fn uidgetfn() -> i64 {
9175 unsafe { libc::getuid() as i64 }
9176}
9177
9178// `termflags` from Src/init.c — bitmap of terminal-state flags. Set
9179// from term_reinit_from_pm and consulted by ZLE before first paint.
9180/// `TERMFLAGS` static.
9181/// Starts as TERM_UNKNOWN (0x02) — c:Src/init.c:1103 `termflags =
9182/// TERM_UNKNOWN;` in init_setup. Cleared by init_term() on success
9183/// (c:Src/init.c:802-803); promptexpand/zleread lazily call
9184/// init_term() when the bit is still set (c:Src/prompt.c:189-190,
9185/// c:Src/Zle/zle_main.c:1260-1261).
9186pub static TERMFLAGS: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0x02);
9187// `TERM_UNKNOWN` re-exported from canonical zsh_h.rs (port of
9188// `Src/zsh.h:1986`). The local declaration here had the value
9189// `1 << 0 = 0x01` — which is C's TERM_BAD (Src/zsh.h:1985), NOT
9190// TERM_UNKNOWN. The canonical TERM_UNKNOWN value is 0x02.
9191//
9192// Callers reading `crate::ported::params::TERM_UNKNOWN` got the
9193// TERM_BAD bit; the params.rs term-init path fired
9194// `TERMFLAGS.fetch_or(TERM_UNKNOWN)` which actually set TERM_BAD,
9195// while the prompt.rs guard at line 441 imported the correct
9196// (0x02) value from zsh_h.rs — so the two paths disagreed silently
9197// about which bit means "unknown terminal".
9198
9199/// Port of `uidsetfn(UNUSED(Param pm), zlong x)` from `Src/params.c:4698`. C body:
9200/// `if (setuid((uid_t)x)) zerr("failed to change user ID: %e", errno);`
9201/// C body (2 lines):
9202/// `if (setuid((uid_t)x)) zerr("failed to change user ID: %e", errno);`
9203/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
9204pub fn uidsetfn(x: i64) {
9205 // c:4698
9206 if unsafe { libc::setuid(x as libc::uid_t) } != 0 {
9207 // c:4701 — `zerr("failed to change user ID: %e", errno)`.
9208 // C's `%e` formatter consumes errno and prints
9209 // `strerror(errno)` with the system's casing (typically
9210 // lowercase on macOS/Linux). Rust's
9211 // `std::io::Error::last_os_error()` displays the same
9212 // text but with capital first letter + `(os error N)`
9213 // suffix, diverging from zsh. Mirror the C format by
9214 // calling strerror via libc directly. Bug #254 in
9215 // docs/BUGS.md.
9216 let errno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
9217 let msg = unsafe {
9218 let p = libc::strerror(errno);
9219 std::ffi::CStr::from_ptr(p).to_string_lossy().into_owned()
9220 };
9221 zerr(&format!("failed to change user ID: {}", msg.to_lowercase())); // c:4702
9222 }
9223}
9224
9225/// Port of `euidgetfn(UNUSED(Param pm))` from `Src/params.c:4710`. C body:
9226/// `return geteuid();`
9227/// WARNING: param names don't match C — Rust=() vs C=(pm)
9228pub fn euidgetfn() -> i64 {
9229 unsafe { libc::geteuid() as i64 }
9230}
9231
9232/// Port of `euidsetfn(UNUSED(Param pm), zlong x)` from `Src/params.c:4719`. C body:
9233/// `if (seteuid((uid_t)x)) zerr("failed to change effective user ID: %e", errno);`
9234/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
9235pub fn euidsetfn(x: i64) {
9236 // c:4719
9237 if unsafe { libc::seteuid(x as libc::uid_t) } != 0 {
9238 // c:4722 — strerror format to match C zerr's `%e`. See
9239 // uidsetfn above.
9240 let errno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
9241 let msg = unsafe {
9242 let p = libc::strerror(errno);
9243 std::ffi::CStr::from_ptr(p).to_string_lossy().into_owned()
9244 };
9245 zerr(&format!(
9246 "failed to change effective user ID: {}",
9247 msg.to_lowercase()
9248 )); // c:4723
9249 }
9250}
9251
9252/// Port of `gidgetfn(UNUSED(Param pm))` from `Src/params.c:4731`. C body: `return getgid();`
9253/// WARNING: param names don't match C — Rust=() vs C=(pm)
9254pub fn gidgetfn() -> i64 {
9255 unsafe { libc::getgid() as i64 }
9256}
9257
9258/// Port of `gidsetfn(UNUSED(Param pm), zlong x)` from `Src/params.c:4740`. C body:
9259/// `if (setgid((gid_t)x)) zerr("failed to change group ID: %e", errno);`
9260/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
9261pub fn gidsetfn(x: i64) {
9262 // c:4740
9263 if unsafe { libc::setgid(x as libc::gid_t) } != 0 {
9264 // c:4743 — strerror format to match C zerr's `%e`. See
9265 // uidsetfn above.
9266 let errno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
9267 let msg = unsafe {
9268 let p = libc::strerror(errno);
9269 std::ffi::CStr::from_ptr(p).to_string_lossy().into_owned()
9270 };
9271 zerr(&format!(
9272 "failed to change group ID: {}",
9273 msg.to_lowercase()
9274 )); // c:4744
9275 }
9276}
9277
9278/// Port of `egidgetfn(UNUSED(Param pm))` from `Src/params.c:4752`. C body: `return getegid();`
9279/// WARNING: param names don't match C — Rust=() vs C=(pm)
9280pub fn egidgetfn() -> i64 {
9281 unsafe { libc::getegid() as i64 }
9282}
9283
9284/// Port of `egidsetfn(UNUSED(Param pm), zlong x)` from `Src/params.c:4761`. C body:
9285/// `if (setegid((gid_t)x)) zerr("failed to change effective group ID: %e", errno);`
9286/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
9287pub fn egidsetfn(x: i64) {
9288 // c:4761
9289 if unsafe { libc::setegid(x as libc::gid_t) } != 0 {
9290 // c:4764 — strerror format to match C zerr's `%e`. See
9291 // uidsetfn above.
9292 let errno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
9293 let msg = unsafe {
9294 let p = libc::strerror(errno);
9295 std::ffi::CStr::from_ptr(p).to_string_lossy().into_owned()
9296 };
9297 zerr(&format!(
9298 "failed to change effective group ID: {}",
9299 msg.to_lowercase()
9300 )); // c:4765
9301 }
9302}
9303
9304/// Port of `ttyidlegetfn(UNUSED(Param pm))` from `Src/params.c:4771`. C body:
9305/// ```c
9306/// struct stat ttystat;
9307/// if (SHTTY == -1 || fstat(SHTTY, &ttystat)) return -1;
9308/// return time(NULL) - ttystat.st_atime;
9309/// ```
9310/// Rust port reads stdin (fd 0) — closest match to `SHTTY` the
9311/// shell tracks as the controlling-tty fd. Returns -1 if stdin is
9312/// not a tty.
9313/// WARNING: param names don't match C — Rust=() vs C=(pm)
9314pub fn ttyidlegetfn() -> i64 {
9315 // c:4776 — `if (SHTTY == -1 || fstat(SHTTY, &ttystat)) return -1;`
9316 // The previous Rust port hardcoded fd 0 (stdin) which is wrong
9317 // when SHTTY was opened on a non-stdin file descriptor (e.g.
9318 // `zsh < script` where stdin is a file but the controlling tty
9319 // was opened separately). C tracks the actual SHTTY fd.
9320 let shtty = SHTTY.load(Ordering::SeqCst);
9321 if shtty == -1 {
9322 // c:4776
9323 return -1;
9324 }
9325 let mut st: libc::stat = unsafe { std::mem::zeroed() };
9326 if unsafe { libc::fstat(shtty, &mut st) } != 0 {
9327 // c:4776
9328 return -1;
9329 }
9330 let now = SystemTime::now()
9331 .duration_since(UNIX_EPOCH)
9332 .unwrap_or_default()
9333 .as_secs() as i64;
9334 now - st.st_atime as i64 // c:4779
9335}
9336
9337// -----------------------------------------------------------
9338// $IFS / $HOME / $TERM / $WORDCHARS / $TERMINFO / $TERMINFO_DIRS
9339// $KEYBOARD_HACK / $HISTCHARS / $_ — string-state callbacks.
9340// -----------------------------------------------------------
9341
9342/// Port of `ifsgetfn(UNUSED(Param pm))` from `Src/params.c:4784`. C body: `return ifs;`
9343pub fn ifsgetfn(_pm: ¶m) -> String {
9344 ifs_lock().lock().expect("ifs poisoned").clone()
9345}
9346
9347/// Port of `ifssetfn(UNUSED(Param pm), char *x)` from `Src/params.c:4793`. C body:
9348/// `zsfree(ifs); ifs = x; inittyptab();`
9349pub fn ifssetfn(_pm: &mut param, x: String) {
9350 *ifs_lock().lock().expect("ifs poisoned") = x;
9351 // c:4795 — `inittyptab()` rebuilds the typtab[] ISEP/IWSEP bits
9352 // from the new IFS. Without this, every word-split path stays
9353 // pinned to the old separator set and silently mis-splits.
9354 inittyptab();
9355}
9356
9357// -----------------------------------------------------------
9358// Locale callbacks: $LANG, $LC_*, setlang
9359// -----------------------------------------------------------
9360
9361/// Port of `clear_mbstate()` from `Src/params.c:4831`. C body:
9362/// `mb_charinit(); clear_shiftstate();`
9363///
9364/// WARNING: zshrs uses Rust's UTF-8 native handling so multibyte
9365/// state machines aren't kept; this is a no-op pinned to the
9366/// C name for parity.
9367/// (under `MULTIBYTE_SUPPORT`):
9368/// ```c
9369/// mb_charinit(); /* utils.c */
9370/// clear_shiftstate(); /* pattern.c */
9371/// ```
9372/// Resets the mbstate_t globals after LC_CTYPE changes (NetBSD-9
9373/// requires this). Rust port forwards to the matching helpers.
9374pub fn clear_mbstate() {
9375 // c:Src/params.c:4732+ — `#ifdef MULTIBYTE_SUPPORT
9376 // mb_charinit(); /* utils.c */
9377 // clear_shiftstate(); /* pattern.c */
9378 // #endif`
9379 // Both helpers are ported (utils.rs:526 and pattern.rs:190). The
9380 // pattern.rs version is currently a no-op (shiftstate machine
9381 // not stored) and utils.rs::mb_charinit resets the mbstate_t
9382 // tracking. Wire them through so a future locale-change hook
9383 // routes through this one entry point per c:Src/params.c
9384 // setlang(c:4842) which calls clear_mbstate() between setlocale
9385 // and the per-LC_* re-apply loop.
9386 crate::ported::utils::mb_charinit(); // c:utils.c mb_charinit
9387 crate::ported::pattern::clear_shiftstate(); // c:pattern.c:327 clear_shiftstate
9388}
9389
9390/// Port of `static struct localename lc_names[]` from `Src/params.c:4805-4825`.
9391/// C body:
9392/// ```c
9393/// static struct localename {
9394/// char *name;
9395/// int category;
9396/// } lc_names[] = {
9397/// {"LC_COLLATE", LC_COLLATE},
9398/// {"LC_CTYPE", LC_CTYPE},
9399/// {"LC_MESSAGES", LC_MESSAGES},
9400/// {"LC_NUMERIC", LC_NUMERIC},
9401/// {"LC_TIME", LC_TIME},
9402/// {NULL, 0}
9403/// };
9404/// ```
9405///
9406/// The C source guards each entry under `#ifdef LC_*`; libc on
9407/// macOS/Linux defines all five so the Rust port simply lists them.
9408const LC_NAMES: &[(&str, libc::c_int)] = &[
9409 ("LC_COLLATE", libc::LC_COLLATE), // c:4810
9410 ("LC_CTYPE", libc::LC_CTYPE), // c:4813
9411 ("LC_MESSAGES", libc::LC_MESSAGES), // c:4816
9412 ("LC_NUMERIC", libc::LC_NUMERIC), // c:4819
9413 ("LC_TIME", libc::LC_TIME), // c:4822
9414];
9415
9416/// Port of `setlang(char *x)` from `Src/params.c:4842`.
9417///
9418/// C body (c:4842-4869):
9419/// ```c
9420/// if ((x2 = getsparam_u("LC_ALL")) && *x2) return;
9421/// setlocale(LC_ALL, x ? unmeta(x) : "");
9422/// clear_mbstate();
9423/// queue_signals();
9424/// for (ln = lc_names; ln->name; ln++)
9425/// if ((x = getsparam_u(ln->name)) && *x)
9426/// setlocale(ln->category, x);
9427/// unqueue_signals();
9428/// inittyptab();
9429/// ```
9430///
9431/// The previous Rust port skipped the actual `setlocale(LC_ALL, ...)`
9432/// libc call and just set the LANG env var. C invokes libc
9433/// setlocale to actually change the program's locale state —
9434/// required so any libc calls during shell execution (e.g.,
9435/// `iswctype`, `mbrtowc`) use the new locale's classification.
9436///
9437/// Also skipped: the per-LC_* override loop (c:4866-4868) which
9438/// re-applies category-specific settings after the global
9439/// LC_ALL set. The Rust port doesn't yet have the lc_names
9440/// table, but we can at least respect the canonical sequence.
9441pub fn setlang(x: Option<&str>) {
9442 // c:4842
9443 // c:4847 — `if ((x2 = getsparam_u("LC_ALL")) && *x2) return;`
9444 if let Some(lc_all) = getsparam_u("LC_ALL") {
9445 // c:4847
9446 if !lc_all.is_empty() {
9447 return;
9448 }
9449 }
9450 // c:4860 — `setlocale(LC_ALL, x ? unmeta(x) : "");`
9451 let locale_arg = match x {
9452 Some(s) => unmeta(s),
9453 None => String::new(),
9454 };
9455 // The previous Rust port skipped the libc setlocale call.
9456 // Without it, libc's locale state (used by iswctype, mbrtowc,
9457 // etc.) stays pinned to whatever the shell inherited from
9458 // its parent — diverging from C which actively changes the
9459 // running program's locale.
9460 let cstr = std::ffi::CString::new(locale_arg.as_bytes()).unwrap_or_default();
9461 unsafe {
9462 libc::setlocale(libc::LC_ALL, cstr.as_ptr()); // c:4860
9463 }
9464 // Mirror to env so subsequent `getsparam("LANG")` reads agree.
9465 if let Some(s) = x {
9466 // c:Src/params.c:5354 — setenv via the C-named zputenv
9467 let _ = zputenv(&format!("{}={}", "LANG", s));
9468 }
9469 clear_mbstate(); // c:4861
9470 // c:4863-4867 — `for (ln = lc_names; ln->name; ln++) if ((x =
9471 // getsparam_u(ln->name)) && *x) setlocale(ln->category, x);`
9472 // After the global LC_ALL setlocale, any explicitly-set LC_*
9473 // category overrides its slot. The previous Rust port skipped
9474 // this loop, so `LC_NUMERIC=tr_TR.UTF-8 LANG=C` would leave
9475 // numeric formatting on C rather than tr_TR.
9476 for (name, category) in LC_NAMES {
9477 // c:4863
9478 if let Some(val) = getsparam_u(name) {
9479 // c:4866 getsparam_u
9480 if !val.is_empty() {
9481 let cat_cstr = std::ffi::CString::new(val.as_bytes()).unwrap_or_default();
9482 unsafe {
9483 libc::setlocale(*category, cat_cstr.as_ptr()); // c:4867
9484 }
9485 }
9486 }
9487 }
9488 // c:4868 — `inittyptab();`. The locale change may shift which
9489 // bytes are isalpha/isalnum/etc under the typtab init, so the
9490 // table must be rebuilt.
9491 inittyptab();
9492}
9493
9494/// Port of `lc_allsetfn(Param pm, char *x)` from `Src/params.c:4873`.
9495///
9496/// C body (c:4873-4894):
9497/// ```c
9498/// strsetfn(pm, x);
9499/// if (!x || !*x) {
9500/// x = getsparam_u("LANG");
9501/// if (x && *x) {
9502/// queue_signals();
9503/// setlang(x);
9504/// unqueue_signals();
9505/// }
9506/// } else {
9507/// setlocale(LC_ALL, unmeta(x));
9508/// clear_mbstate();
9509/// inittyptab();
9510/// }
9511/// ```
9512///
9513/// The previous Rust port for the non-empty case set the env
9514/// var via `env::set_var("LC_ALL", &s)` but skipped THREE
9515/// pieces:
9516/// 1. `setlocale(LC_ALL, unmeta(x))` — actively changes the
9517/// program's locale per c:4890.
9518/// 2. `unmeta(x)` — strips Meta-encoded bytes before passing
9519/// to libc setlocale per c:4890.
9520/// 3. `inittyptab()` — rebuilds the typtab for the new
9521/// LC_CTYPE per c:4892.
9522///
9523/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
9524pub fn lc_allsetfn(x: Option<String>) {
9525 // c:4873
9526 match x {
9527 None => setlang(getsparam_u("LANG").as_deref()), // c:4882 getsparam_u
9528 Some(s) if s.is_empty() => {
9529 // c:4881
9530 // c:4881-4884 — empty x falls back to setlang(getsparam_u("LANG")).
9531 setlang(getsparam_u("LANG").as_deref()); // c:4882
9532 }
9533 Some(s) => {
9534 // c:4889 — `setlocale(LC_ALL, unmeta(x));`
9535 let unmeta = unmeta(&s); // c:4889 unmeta(x)
9536 let cstr = std::ffi::CString::new(unmeta.as_bytes()).unwrap_or_default();
9537 unsafe {
9538 libc::setlocale(libc::LC_ALL, cstr.as_ptr()); // c:4890
9539 }
9540 // c:Src/params.c:5354 — setenv via the C-named zputenv
9541 let _ = zputenv(&format!("{}={}", "LC_ALL", &s));
9542 clear_mbstate(); // c:4891
9543 // c:4892 — `inittyptab();` rebuild typtab for new LC_CTYPE.
9544 inittyptab(); // c:4892
9545 }
9546 }
9547}
9548
9549/// Port of `langsetfn(Param pm, char *x)` from `Src/params.c:4898`. C body:
9550/// `strsetfn(pm, x); setlang(unmeta(x));`
9551///
9552/// `unmeta(x)` strips Meta-encoding before passing to libc
9553/// `setlocale` — locale names are normally ASCII but Meta bytes
9554/// in the assigned value (from a `LANG="$value"` round-trip
9555/// through metafied param storage) would otherwise reach
9556/// setlocale literally. The previous Rust port passed raw `x`
9557/// without unmeta'ing — divergent.
9558///
9559/// `strsetfn(pm, x)` stores the value in the param slot. The Rust
9560/// adaptation doesn't have a `pm` in scope; the assign path that
9561/// reaches langsetfn already stored the value in the paramtab,
9562/// so this body only runs the post-store side effect (locale).
9563///
9564/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x).
9565pub fn langsetfn(x: String) {
9566 // c:4898
9567 // c:4901 — `setlang(unmeta(x));`. Strip Meta bytes before
9568 // passing to libc setlocale.
9569 let unmeta_x = unmeta(&x); // c:4901 unmeta(x)
9570 setlang(Some(&unmeta_x));
9571}
9572
9573/// Port of `lcsetfn(Param pm, char *x)` from `Src/params.c:4906`. C body
9574/// (c:4912-4931):
9575/// ```c
9576/// strsetfn(pm, x);
9577/// if ((x2 = getsparam("LC_ALL")) && *x2) return;
9578/// queue_signals();
9579/// if (!x || !*x) x = getsparam("LANG");
9580/// if (x && *x) {
9581/// for (ln = lc_names; ln->name; ln++)
9582/// if (!strcmp(ln->name, pm->node.nam))
9583/// setlocale(ln->category, unmeta(x));
9584/// }
9585/// unqueue_signals();
9586/// clear_mbstate();
9587/// inittyptab();
9588/// ```
9589///
9590/// Two divergences in the previous Rust port:
9591/// 1. Missed `inittyptab()` call at c:4932 — LC_CTYPE changes
9592/// shift which bytes are isalpha/iblank/isep, but the
9593/// typtab stayed pinned to the prior locale's classes.
9594/// `setopt POSIX_BUILTINS; LC_NUMERIC=tr_TR.UTF-8; ...`
9595/// would still classify with the old C locale's tables.
9596/// 2. The Meta-unmeta'ing on the value passed to setlocale
9597/// wasn't applied. C uses `setlocale(cat, unmeta(x))`.
9598pub fn lcsetfn(pm: &str, x: Option<String>) {
9599 // c:4906
9600 // c:4912-4913 — `if ((x2 = getsparam("LC_ALL")) && *x2) return;`.
9601 if let Some(lc_all) = getsparam("LC_ALL") {
9602 // c:4912
9603 if !lc_all.is_empty() {
9604 return;
9605 }
9606 }
9607 // c:4916-4917 — `if (!x || !*x) x = getsparam("LANG");`.
9608 let val = x
9609 .filter(|s| !s.is_empty())
9610 .or_else(|| getsparam("LANG").filter(|s| !s.is_empty())); // c:4917
9611 // c:4924-4928 — apply `setlocale(category, unmeta(x))` for the
9612 // matching LC_* category. The previous Rust port skipped the
9613 // actual libc setlocale call and only wrote the env var, so
9614 // assigning `LC_NUMERIC=tr_TR.UTF-8` never flipped libc's
9615 // numeric-formatting category.
9616 if let Some(v) = val {
9617 let unmeta = unmeta(&v); // c:4928 unmeta(x)
9618 // c:Src/params.c:5354 — setenv via the C-named zputenv
9619 let _ = zputenv(&format!("{}={}", pm, &unmeta));
9620 for (name, category) in LC_NAMES {
9621 // c:4925
9622 if *name == pm {
9623 // c:4926 strcmp
9624 let cstr = std::ffi::CString::new(unmeta.as_bytes()).unwrap_or_default();
9625 unsafe {
9626 libc::setlocale(*category, cstr.as_ptr()); // c:4927
9627 }
9628 break;
9629 }
9630 }
9631 }
9632 // c:4930 — `clear_mbstate();` — LC_CTYPE may have changed.
9633 clear_mbstate();
9634 // c:4931 — `inittyptab();` — rebuild typtab classifications.
9635 // The previous Rust port skipped this; char-classification
9636 // predicates would stay pinned to the prior locale's class
9637 // set even after `LC_CTYPE=` was assigned.
9638 inittyptab(); // c:4931
9639}
9640
9641/// Direct port of `static void argzerosetfn(UNUSED(Param pm),
9642/// char *x)` from `Src/params.c:4937-4946`. Setter for `$0` —
9643/// POSIX mode rejects assignment (read-only), zsh mode replaces
9644/// `argzero`.
9645///
9646/// C body:
9647/// if (x) {
9648/// if (isset(POSIXARGZERO))
9649/// zerr("read-only variable: 0");
9650/// else {
9651/// zsfree(argzero);
9652/// argzero = ztrdup(x);
9653/// }
9654/// zsfree(x);
9655/// }
9656/// Port of `argzerosetfn(UNUSED(Param pm), char *x)` from `Src/params.c:4937`.
9657/// `pm` is UNUSED in C (the `argzero` global is updated regardless of
9658/// the Param the assignment hit), but the parameter is preserved here
9659/// to match the C signature so this fn can wire into the GsuScalar.setfn
9660/// slot directly (see ARGZERO_GSU at line ~8307).
9661pub fn argzerosetfn(_pm: &mut param, x: String) {
9662 // c:4937
9663 // c:4937 — if (x).
9664 if !x.is_empty() {
9665 // c:4940 — isset(POSIXARGZERO) reject.
9666 if isset(POSIXARGZERO) {
9667 zerr("read-only variable: 0"); // c:4941
9668 } else {
9669 // c:4943-4944 — zsfree(argzero); argzero = ztrdup(x).
9670 set_argzero(Some(ztrdup(&x)));
9671 }
9672 // c:4946 — `zsfree(x)`. Rust drop handles via move.
9673 }
9674}
9675
9676// -----------------------------------------------------------
9677// $0 / $#
9678// -----------------------------------------------------------
9679
9680/// Port of `argzerogetfn(UNUSED(Param pm))` from `Src/params.c:4954`. C body:
9681/// `return isset(POSIXARGZERO) ? posixzero : argzero;`
9682///
9683/// Both `argzero` and `posixzero` live in `utils.rs` (OnceLock storage).
9684/// After `exec -a foo` or function-call argv-rewrite, `$0` under
9685/// POSIXARGZERO reports the ORIGINAL startup `argv[0]`, not the
9686/// rewritten name. `pm` is UNUSED in C; signature preserved for the
9687/// GsuScalar.getfn slot at ARGZERO_GSU (line ~8307).
9688pub fn argzerogetfn(_pm: ¶m) -> String {
9689 if isset(POSIXARGZERO) {
9690 // c:4958
9691 posixzero().unwrap_or_default() // c:4959
9692 } else {
9693 argzero().unwrap_or_default() // c:4960
9694 }
9695}
9696
9697// -----------------------------------------------------------
9698// $HISTSIZE / $SAVEHIST
9699// -----------------------------------------------------------
9700
9701/// Port of `histsizegetfn(UNUSED(Param pm))` from `Src/params.c:4965`. C body: `return histsiz;`
9702/// WARNING: param names don't match C — Rust=() vs C=(pm)
9703pub fn histsizegetfn() -> i64 {
9704 *histsiz_lock().lock().expect("histsiz poisoned")
9705}
9706
9707/// Port of `histsizesetfn(UNUSED(Param pm), zlong v)` from `Src/params.c:4974`. C body:
9708/// `if ((histsiz = v) < 1) histsiz = 1; resizehistents();`
9709///
9710/// The previous Rust port noted `resizehistents()` as "pending the
9711/// history-table port", but `resizehistents`
9712/// IS available — was a stale comment. Without the resize call,
9713/// setting HISTSIZE to a smaller value left the in-memory ring
9714/// over-sized until the next implicit prune (next entry added).
9715/// Wired the call now per c:4977.
9716/// WARNING: param names don't match C — Rust=(v) vs C=(pm, v)
9717pub fn histsizesetfn(v: i64) {
9718 *histsiz_lock().lock().expect("histsiz poisoned") = v.max(1);
9719 // c:4977 — mirror into the hist.rs atomic so resizehistents()
9720 // sees the new size, then trigger the prune.
9721 histsiz.store(v.max(1), Ordering::SeqCst);
9722 resizehistents(); // c:4977
9723}
9724
9725/// Port of `savehistsizegetfn(UNUSED(Param pm))` from `Src/params.c:4985`. C body:
9726/// `return savehistsiz;`
9727/// WARNING: param names don't match C — Rust=() vs C=(pm)
9728pub fn savehistsizegetfn() -> i64 {
9729 *savehistsiz_lock().lock().expect("savehistsiz poisoned")
9730}
9731
9732/// Port of `savehistsizesetfn(UNUSED(Param pm), zlong v)` from `Src/params.c:4994`. C body:
9733/// `if ((savehistsiz = v) < 0) savehistsiz = 0;`
9734///
9735/// The Rust port has TWO mirrors of `savehistsiz`: a `Mutex<i64>`
9736/// in params.rs (read by `savehistsizegetfn`) AND an AtomicI64
9737/// in hist.rs (read by the history-file writer at
9738/// `Src/hist.c:savehistfile` per c:3878). The previous Rust port
9739/// only wrote to the params.rs lock; `hist.rs::savehistsiz`
9740/// stayed pinned to its initial 0 value, so `SAVEHIST=10000`
9741/// would store the limit in `savehistsiz_lock` (visible to
9742/// `$SAVEHIST` reads) but the history-file writer would still
9743/// cap at the original AtomicI64 value (effectively saving zero
9744/// lines). Sync both storages so reads + writes agree.
9745///
9746/// WARNING: param names don't match C — Rust=(v) vs C=(pm, v)
9747pub fn savehistsizesetfn(v: i64) {
9748 // c:4994
9749 let clamped = v.max(0); // c:4998
9750 *savehistsiz_lock().lock().expect("savehistsiz poisoned") = clamped;
9751 // Mirror to hist.rs::savehistsiz so the writer-side cap
9752 // matches the just-assigned value. C uses a single global;
9753 // the Rust port's twin-storage requires sync writes.
9754 savehistsiz.store(clamped, Ordering::SeqCst);
9755 // c:4994
9756}
9757
9758/// Port of `errnosetfn(UNUSED(Param pm), zlong x)` from `Src/params.c:5004`. C body:
9759/// `errno = (int)x; if ((zlong)errno != x) zwarn("errno truncated on assignment");`
9760///
9761/// Rust note: `errno` is a libc thread-local; Rust uses `std::io::Error`
9762/// which captures the *last* call. To set errno for subsequent
9763/// `last_os_error()` reads on macOS / Linux, write through the libc
9764/// `__error()`/`__errno_location()` accessor.
9765/// C body (Src/params.c:5004):
9766/// `errno = (int)x;
9767/// if ((zlong)errno != x) zwarn("errno truncated on assignment");`
9768/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
9769pub fn errnosetfn(x: i64) {
9770 // c:5004
9771 let truncated = x as i32;
9772 unsafe {
9773 *errno_ptr() = truncated;
9774 } // c:5006 errno = (int)x
9775 // c:5009-5010 — C uses `zwarn` (informational), NOT `zerr`. The
9776 // store happens unconditionally; the warning fires only on
9777 // truncation. Previously used `zerr` — divergent.
9778 if truncated as i64 != x {
9779 // c:5008
9780 zwarn("errno truncated on assignment"); // c:5009
9781 }
9782}
9783
9784/// !!! RUST-ONLY HELPER — no direct C counterpart. C accesses
9785/// `errno` through the standard macro which the compiler resolves
9786/// to the per-platform getter (`__error()` on macOS, `__errno_location()`
9787/// on Linux). Rust libc exposes both as raw FFI; this helper picks
9788/// the right one per target so errnosetfn/errnogetfn stay one-liners.
9789#[inline]
9790unsafe fn errno_ptr() -> *mut libc::c_int {
9791 #[cfg(target_os = "macos")]
9792 {
9793 libc::__error()
9794 }
9795 #[cfg(target_os = "linux")]
9796 {
9797 libc::__errno_location()
9798 }
9799 #[cfg(not(any(target_os = "macos", target_os = "linux")))]
9800 {
9801 std::ptr::null_mut()
9802 }
9803}
9804
9805/// Port of `errnogetfn(UNUSED(Param pm))` from `Src/params.c:5015`. C body: `return errno;`
9806///
9807/// Reads the libc errno directly through the per-platform accessor
9808/// (matching C's `return errno;` semantics).
9809/// WARNING: param names don't match C — Rust=() vs C=(pm)
9810pub fn errnogetfn() -> i64 {
9811 let p = unsafe { errno_ptr() }; // c:5017 return errno
9812 if p.is_null() {
9813 // Non-Linux/macOS fallback: best-effort via std API.
9814 std::io::Error::last_os_error().raw_os_error().unwrap_or(0) as i64
9815 } else {
9816 unsafe { *p as i64 }
9817 }
9818}
9819
9820/// Port of `keyboardhackgetfn(UNUSED(Param pm))` from `Src/params.c:5024`. C body:
9821/// `static char buf[2]; buf[0] = keyboardhackchar; return buf;`
9822pub fn keyboardhackgetfn(_pm: ¶m) -> String {
9823 let c = *keyboardhack_lock().lock().expect("keyboardhack poisoned");
9824 if c == 0 {
9825 String::new()
9826 } else {
9827 (c as char).to_string()
9828 }
9829}
9830
9831/// Port of `keyboardhacksetfn(UNUSED(Param pm), char *x)` from `Src/params.c:5040-5060`. C body:
9832/// ```c
9833/// if (x) {
9834/// unmetafy(x, &len);
9835/// if (len > 1) { len = 1; zwarn("Only one KEYBOARD_HACK character can be defined"); }
9836/// for (i = 0; i < len; i++)
9837/// if (!isascii((unsigned char) x[i])) {
9838/// zwarn("KEYBOARD_HACK can only contain ASCII characters");
9839/// return;
9840/// }
9841/// keyboardhackchar = len ? (unsigned char) x[0] : '\0';
9842/// } else
9843/// keyboardhackchar = '\0';
9844/// ```
9845///
9846/// The C source `unmetafy(x, &len)` strips Meta-encoded prefix
9847/// bytes (collapsing every `Meta + (b^32)` pair to the original
9848/// byte) BEFORE the length and ASCII checks. The previous Rust
9849/// port skipped unmetafy, so:
9850/// - `len > 1` warning fired on every assignment of a Meta-
9851/// encoded single byte (the byte-length was 2 pre-unmetafy).
9852/// - ASCII check ran against the raw Meta byte (0x83) instead
9853/// of the demetafied result, falsely rejecting valid ASCII
9854/// characters that happened to round-trip through Meta
9855/// encoding in the assignment pipeline.
9856///
9857pub fn keyboardhacksetfn(_pm: &mut param, x: String) {
9858 // c:5040
9859 // c:5044 — `unmetafy(x, &len)` — strip Meta-encoded pairs.
9860 // Run on the byte buffer so the protocol matches C's pointer
9861 // walk; the Rust `unmeta()` helper does the same fold.
9862 let unmeta = unmeta(&x); // c:5044 unmetafy(x)
9863 let bytes = unmeta.as_bytes();
9864 // c:5046-5049 — `if (len > 1) { len = 1; zwarn(...); }`. The
9865 // length check happens AFTER unmetafy so a 2-byte Meta pair
9866 // representing a single byte doesn't trigger the warning.
9867 if bytes.len() > 1 {
9868 zwarn("Only one KEYBOARD_HACK character can be defined");
9869 }
9870 let c = bytes.first().copied().unwrap_or(0);
9871 // c:5050-5054 — ASCII check runs on the unmetafied byte, NOT
9872 // the raw Meta byte. With unmetafy now in place this works as
9873 // C intended.
9874 if c >= 0x80 {
9875 // c:5051 !isascii(...)
9876 zwarn("KEYBOARD_HACK can only contain ASCII characters");
9877 return;
9878 }
9879 // c:5056 — `keyboardhackchar = len ? (unsigned char) x[0] : '\0';`
9880 *keyboardhack_lock().lock().expect("keyboardhack poisoned") = c;
9881}
9882
9883/// Port of `histcharsgetfn(UNUSED(Param pm))` from `Src/params.c:5064`. C body:
9884/// ```c
9885/// static char buf[4];
9886/// buf[0] = bangchar; buf[1] = hatchar; buf[2] = hashchar; buf[3] = '\0';
9887/// return buf;
9888/// ```
9889/// Reads from the three canonical atomic globals
9890/// (`crate::ported::hist::{bangchar, hatchar, hashchar}`) to mirror C
9891/// which reads from three separate `unsigned char` globals.
9892pub fn histcharsgetfn(_pm: ¶m) -> String {
9893 let b = bangchar.load(Ordering::SeqCst) as u8;
9894 let h = hatchar.load(Ordering::SeqCst) as u8;
9895 let p = hashchar.load(Ordering::SeqCst) as u8;
9896 // c:5068-5073 — terminal NUL trims unset chars (default-`!^#` is
9897 // 3 non-NUL bytes); explicit NULs are skipped to match C `buf[3]
9898 // = '\0'` C-string truncation semantics.
9899 let mut s = String::new();
9900 for &byte in &[b, h, p] {
9901 if byte != 0 {
9902 s.push(byte as char);
9903 }
9904 }
9905 s
9906}
9907
9908/// Port of `histcharssetfn(UNUSED(Param pm), char *x)` from `Src/params.c:5081`. C body
9909/// validates ASCII, takes up to 3 chars; defaults `!^#` if NULL.
9910///
9911/// C `unmetafy(x, &len)` (c:5086) strips Meta-encoded pairs BEFORE
9912/// the length truncation and ASCII guard. The previous Rust port
9913/// skipped unmetafy entirely:
9914/// - `len > 3` truncation ran on raw byte length, so a Meta-pair
9915/// would inflate the byte count and skip valid chars.
9916/// - ASCII check ran against raw Meta bytes (0x83), falsely
9917/// rejecting valid round-tripped values.
9918///
9919pub fn histcharssetfn(_pm: &mut param, x: String) {
9920 // c:5081
9921 // C signature is `histcharssetfn(Param pm, char *x)`. C uses NULL
9922 // for the "reset to defaults" path; in Rust the canonical fn-ptr
9923 // type is `fn(&mut param, String)` so the empty-string sentinel
9924 // takes that role (`x.is_empty()` ≡ C `x == NULL`).
9925 let new_chars: [u8; 3] = if x.is_empty() {
9926 // c:5100-5103 — defaults `!^#` when x is NULL.
9927 [b'!', b'^', b'#']
9928 } else {
9929 let s = x;
9930 {
9931 // c:5086 — `unmetafy(x, &len)`. Strip Meta pairs first.
9932 let unmeta = unmeta(&s); // c:5086 unmetafy(x)
9933 let bytes = unmeta.as_bytes();
9934 // c:5087-5088 — `if (len > 3) len = 3;`. Truncation
9935 // applies AFTER unmetafy.
9936 let bytes = if bytes.len() > 3 { &bytes[..3] } else { bytes };
9937 for &b in bytes.iter() {
9938 if b >= 0x80 {
9939 // c:5090-5093
9940 // c:5091 — C uses `zwarn` (informational), NOT
9941 // `zerr` (fatal). Function returns early without
9942 // updating any globals.
9943 zwarn("HISTCHARS can only contain ASCII characters");
9944 return;
9945 }
9946 }
9947 // c:5095-5097 — `bangchar = x[0]; hatchar = x[1]; hashchar = x[2]`.
9948 // C uses `len ? x[0] : '\0'` etc — for short strings the
9949 // unset bytes are NUL.
9950 let mut chars = [0u8; 3];
9951 for (i, &b) in bytes.iter().enumerate() {
9952 chars[i] = b;
9953 }
9954 chars
9955 }
9956 };
9957 // c:5079 — set histchars table.
9958 *histchars_lock().lock().expect("histchars poisoned") = new_chars;
9959 // c:5095-5097 — `bangchar = x[0]; hatchar = x[1]; hashchar = x[2]`.
9960 // Sync all three per-char atomic globals so lex/hist callers
9961 // see the new HISTCHARS. (Previously hashchar was a `const char`
9962 // in lex.rs — promoted to atomic this iteration.)
9963 bangchar.store(new_chars[0] as i32, Ordering::SeqCst);
9964 hatchar.store(new_chars[1] as i32, Ordering::SeqCst);
9965 hashchar.store(new_chars[2] as i32, Ordering::SeqCst);
9966 // c:5104 — `inittyptab();`. The bangchar special bit in typtab
9967 // depends on the current `bangchar` global; reseed.
9968 inittyptab();
9969}
9970
9971// ---------------------------------------------------------------------
9972// Special-scalar GSU vtables — port of `Src/params.c:217-256` `stdscalar_gsu`,
9973// `home_gsu`, `ifs_gsu`, etc. Each entry pairs the canonical
9974// getfn/setfn/unsetfn for a PM_SPECIAL scalar. `createparamtable`
9975// copies the matching gsu into each special param's `gsu_s` slot so
9976// `assignstrvalue` dispatches via `pm->gsu.s->setfn(pm, val)`
9977// (params.c:2748) exactly like C.
9978// ---------------------------------------------------------------------
9979
9980/// Port of `static const struct gsu_scalar argzero_gsu` from `Src/params.c:225-226`.
9981/// `{ argzerogetfn, argzerosetfn, nullunsetfn }`. Both functions' Rust
9982/// signatures match C exactly (with `pm` UNUSED), so they wire into
9983/// the GSU vtable directly — no Rust-only wrappers. The $0 Param picks
9984/// this up at `add_special` (line ~1555) and `assignsparam` routes
9985/// `0=value` through the canonical setter via the gsu_s.setfn dispatch
9986/// at params.rs:3767.
9987pub const ARGZERO_GSU: gsu_scalar = gsu_scalar {
9988 // c:225-226
9989 getfn: argzerogetfn,
9990 setfn: argzerosetfn,
9991 unsetfn: nullunsetfn,
9992};
9993
9994/// Port of `static const struct gsu_scalar home_gsu` from `Src/params.c:248`.
9995pub const HOME_GSU: gsu_scalar = gsu_scalar {
9996 // c:248
9997 getfn: homegetfn,
9998 setfn: homesetfn,
9999 unsetfn: stdunsetfn,
10000};
10001
10002/// Port of `static const struct gsu_scalar ifs_gsu` from `Src/params.c:245`.
10003pub const IFS_GSU: gsu_scalar = gsu_scalar {
10004 // c:245
10005 getfn: ifsgetfn,
10006 setfn: ifssetfn,
10007 unsetfn: stdunsetfn,
10008};
10009
10010/// Port of `static const struct gsu_scalar term_gsu` from `Src/params.c:250`.
10011pub const TERM_GSU: gsu_scalar = gsu_scalar {
10012 // c:250
10013 getfn: termgetfn,
10014 setfn: termsetfn,
10015 unsetfn: stdunsetfn,
10016};
10017
10018/// Port of `static const struct gsu_scalar terminfo_gsu` from `Src/params.c:251`.
10019pub const TERMINFO_GSU: gsu_scalar = gsu_scalar {
10020 // c:251
10021 getfn: terminfogetfn,
10022 setfn: terminfosetfn,
10023 unsetfn: stdunsetfn,
10024};
10025
10026/// Port of `static const struct gsu_scalar terminfodirs_gsu`
10027/// from `Src/params.c:252`.
10028pub const TERMINFODIRS_GSU: gsu_scalar = gsu_scalar {
10029 // c:252
10030 getfn: terminfodirsgetfn,
10031 setfn: terminfodirssetfn,
10032 unsetfn: stdunsetfn,
10033};
10034
10035/// Port of `static const struct gsu_scalar wordchars_gsu`
10036/// from `Src/params.c:249`.
10037pub const WORDCHARS_GSU: gsu_scalar = gsu_scalar {
10038 // c:249
10039 getfn: wordcharsgetfn,
10040 setfn: wordcharssetfn,
10041 unsetfn: stdunsetfn,
10042};
10043
10044/// Port of `static const struct gsu_scalar username_gsu`
10045/// from `Src/params.c:247`.
10046pub const USERNAME_GSU: gsu_scalar = gsu_scalar {
10047 // c:247
10048 getfn: usernamegetfn,
10049 setfn: usernamesetfn,
10050 unsetfn: stdunsetfn,
10051};
10052
10053/// Port of `static const struct gsu_scalar keyboardhack_gsu`
10054/// from `Src/params.c:253`.
10055pub const KEYBOARDHACK_GSU: gsu_scalar = gsu_scalar {
10056 // c:253
10057 getfn: keyboardhackgetfn,
10058 setfn: keyboardhacksetfn,
10059 unsetfn: stdunsetfn,
10060};
10061
10062/// Port of `static const struct gsu_scalar histchars_gsu`
10063/// from `Src/params.c:246`.
10064pub const HISTCHARS_GSU: gsu_scalar = gsu_scalar {
10065 // c:246
10066 getfn: histcharsgetfn,
10067 setfn: histcharssetfn,
10068 unsetfn: stdunsetfn,
10069};
10070
10071/// Port of `homegetfn(UNUSED(Param pm))` from `Src/params.c:5109`. C body: `return home;`
10072pub fn homegetfn(_pm: ¶m) -> String {
10073 home_lock().lock().expect("home poisoned").clone()
10074}
10075
10076/// Port of `homesetfn(UNUSED(Param pm), char *x)` from `Src/params.c:5118`. C body:
10077/// ```c
10078/// zsfree(home);
10079/// if (x && isset(CHASELINKS) && (home = xsymlink(x, 0)))
10080/// zsfree(x);
10081/// else
10082/// home = x ? x : ztrdup("");
10083/// finddir(NULL);
10084/// ```
10085pub fn homesetfn(_pm: &mut param, x: String) {
10086 // c:5121-5126 — CHASELINKS path resolves symlinks before storing.
10087 // Falls through to the plain `x` store when CHASELINKS is off or
10088 // xsymlink fails.
10089 let resolved = if !x.is_empty() && isset(CHASELINKS) {
10090 xsymlink(&x).unwrap_or(x)
10091 } else {
10092 x
10093 };
10094 *home_lock().lock().expect("home poisoned") = resolved;
10095 // c:5127 — `finddir(NULL)` invalidates zsh's cached named-directory
10096 // lookups. zshrs's finddir port has no cache (per hashnameddir.rs
10097 // createnameddirtable note); the call is a no-op here.
10098}
10099
10100/// Port of `wordcharsgetfn(UNUSED(Param pm))` from `Src/params.c:5132`. C body:
10101/// `return wordchars;`
10102pub fn wordcharsgetfn(_pm: ¶m) -> String {
10103 wordchars_lock().lock().expect("wordchars poisoned").clone()
10104}
10105
10106/// Port of `wordcharssetfn(UNUSED(Param pm), char *x)` from `Src/params.c:5141`. C body:
10107/// `zsfree(wordchars); wordchars = x; inittyptab();`
10108pub fn wordcharssetfn(_pm: &mut param, x: String) {
10109 *wordchars_lock().lock().expect("wordchars poisoned") = x;
10110 // c:5143 — `inittyptab()` rebuilds typtab IWORD bits from the
10111 // new WORDCHARS. Without this, every IWORD lookup stays pinned
10112 // to the old set and silently mis-classifies word boundaries.
10113 inittyptab();
10114}
10115
10116/// Port of `underscoregetfn(UNUSED(Param pm))` from `Src/params.c:5152`. C body:
10117/// `char *u = dupstring(zunderscore); untokenize(u); return u;`
10118///
10119/// C runs `untokenize(u)` on the cloned string before returning, so
10120/// ITOK bytes (Pound..Nularg per `Src/zsh.h:159-194`) in `$_` get
10121/// replaced/dropped via the canonical `ztokens[]` table. The previous
10122/// Rust port skipped untokenize entirely — every `$_` read that
10123/// included a lexer-injected token byte exposed the raw token in user
10124/// output (e.g. `$_` containing `$cmd` would surface as raw Stringg
10125/// instead of the literal `$`).
10126/// WARNING: param names don't match C — Rust=() vs C=(pm)
10127pub fn underscoregetfn() -> String {
10128 let u = zunderscore_lock()
10129 .lock()
10130 .expect("zunderscore poisoned")
10131 .clone();
10132 untokenize(&u) // c:5156 untokenize(u)
10133}
10134
10135/// Port of `term_reinit_from_pm()` from `Src/params.c:5163`.
10136/// C: `static void term_reinit_from_pm(void)` →
10137/// `if (unset(INTERACTIVE) || !*term) termflags |= TERM_UNKNOWN;
10138/// else init_term();`
10139pub fn term_reinit_from_pm() {
10140 // c:5163
10141 // c:5167 — `if (unset(INTERACTIVE) || !*term) termflags |= TERM_UNKNOWN;`
10142 let interactive = isset(INTERACTIVE);
10143 let term = term_lock().lock().map(|s| s.clone()).unwrap_or_default();
10144 if !interactive || term.is_empty() {
10145 // c:5167
10146 TERMFLAGS.fetch_or(TERM_UNKNOWN, Ordering::Relaxed); // c:5168
10147 } else {
10148 crate::ported::init::init_term(); // c:5170
10149 }
10150}
10151
10152/// Port of `termgetfn(UNUSED(Param pm))` from `Src/params.c:5176`. C body: `return term;`
10153pub fn termgetfn(_pm: ¶m) -> String {
10154 term_lock().lock().expect("term poisoned").clone()
10155}
10156
10157/// Port of `termsetfn(UNUSED(Param pm), char *x)` from `Src/params.c:5185`. C body:
10158/// `zsfree(term); term = x ? x : ""; term_reinit_from_pm();`
10159pub fn termsetfn(_pm: &mut param, x: String) {
10160 *term_lock().lock().expect("term poisoned") = x;
10161 term_reinit_from_pm();
10162}
10163
10164/// Port of `terminfogetfn(UNUSED(Param pm))` from `Src/params.c:5196`. C body:
10165/// `return zsh_terminfo ? zsh_terminfo : "";`
10166pub fn terminfogetfn(_pm: ¶m) -> String {
10167 zsh_terminfo_lock()
10168 .lock()
10169 .expect("zsh_terminfo poisoned")
10170 .clone()
10171}
10172
10173/// Port of `int rprompt_indent` from `Src/init.c`. Set to 1 by
10174/// `init_term()` and reset by `rprompt_indent_unsetfn` when the
10175/// `RPROMPT_INDENT` parameter is unset.
10176pub static RPROMPT_INDENT: Mutex<i32> = Mutex::new(1);
10177
10178/// Port of `void terminfosetfn(Param pm, char *x)` from `Src/params.c:5205`.
10179///
10180/// Frees the old `zsh_terminfo`, stores `x`, and — only when the
10181/// parameter is exported — pushes `TERMINFO=x` into the environment
10182/// (terminfo reads the value from the env before the terminal is
10183/// reinitialised), then reinitialises the terminal.
10184pub fn terminfosetfn(pm: &mut param, x: String) {
10185 // c:5205
10186 // c:5207-5208 — `zsfree(zsh_terminfo); zsh_terminfo = x;`
10187 *zsh_terminfo_lock().lock().expect("zsh_terminfo poisoned") = x.clone();
10188 // c:5214-5215 — `if ((pm->node.flags & PM_EXPORTED) && x) addenv(pm, x);`
10189 // Only leak into the environment when $TERMINFO is actually
10190 // exported; a bare `TERMINFO=x` (no export) must not reach children.
10191 if pm.node.flags & PM_EXPORTED as i32 != 0 {
10192 let _ = zputenv(&format!("TERMINFO={}", &x)); // c:5215 addenv(pm, x)
10193 }
10194 term_reinit_from_pm(); // c:5217
10195}
10196
10197/// Port of `terminfodirsgetfn(UNUSED(Param pm))` from `Src/params.c:5224`. C body:
10198/// `return zsh_terminfodirs ? zsh_terminfodirs : "";`
10199pub fn terminfodirsgetfn(_pm: ¶m) -> String {
10200 zsh_terminfodirs_lock()
10201 .lock()
10202 .expect("zsh_terminfodirs poisoned")
10203 .clone()
10204}
10205
10206/// Port of `terminfodirssetfn(Param pm, char *x)` from `Src/params.c:5233`. C body
10207/// mirrors `terminfosetfn` for the TERMINFO_DIRS env var.
10208pub fn terminfodirssetfn(pm: &mut param, x: String) {
10209 // c:5233
10210 // c:5235-5236 — `zsfree(zsh_terminfodirs); zsh_terminfodirs = x;`
10211 *zsh_terminfodirs_lock()
10212 .lock()
10213 .expect("zsh_terminfodirs poisoned") = x.clone();
10214 // c:5242-5243 — `if ((pm->node.flags & PM_EXPORTED) && x) addenv(pm, x);`
10215 // Only export when $TERMINFO_DIRS is exported; a bare assignment
10216 // must not reach child processes.
10217 if pm.node.flags & PM_EXPORTED as i32 != 0 {
10218 let _ = zputenv(&format!("TERMINFO_DIRS={}", &x)); // c:5243 addenv(pm, x)
10219 }
10220 term_reinit_from_pm(); // c:5245
10221}
10222
10223// -----------------------------------------------------------
10224// $pipestatus
10225// -----------------------------------------------------------
10226
10227/// Port of `pipestatgetfn(UNUSED(Param pm))` from `Src/params.c:5251`. C body
10228/// snapshots the `pipestats[]` C array as a heap-allocated
10229/// `char **`. Rust port returns the cloned snapshot.
10230/// WARNING: param names don't match C — Rust=() vs C=(pm)
10231pub fn pipestatgetfn() -> Vec<String> {
10232 pipestats_lock()
10233 .lock()
10234 .expect("pipestats poisoned")
10235 .iter()
10236 .map(|n| n.to_string())
10237 .collect()
10238}
10239
10240/// Port of `pipestatsetfn(UNUSED(Param pm), char **x)` from `Src/params.c:5270`. C body:
10241/// `for (i=0; *x && i<MAX_PIPESTATS; i++) pipestats[i] = atoi(*x++); numpipestats = i;`
10242/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
10243pub fn pipestatsetfn(x: Option<Vec<String>>) {
10244 const MAX_PIPESTATS: usize = 256;
10245 let mut guard = pipestats_lock().lock().expect("pipestats poisoned");
10246 guard.clear();
10247 if let Some(v) = x {
10248 for s in v.iter().take(MAX_PIPESTATS) {
10249 guard.push(s.parse::<i32>().unwrap_or(0));
10250 }
10251 }
10252}
10253
10254/// Port of `arrfixenv(char *s, char **t)` from `Src/params.c:5285`. C body re-syncs
10255/// the env entry for an array param after mutation, joining with
10256/// the param's `joinchar`. Rust port joins with ':' (the default
10257/// for PATH-style arrays) and updates the env var.
10258/// Direct port of `void arrfixenv(char *s, char **t)` from
10259/// `Src/params.c:5285`. Re-syncs the env-side entry for an
10260/// array parameter after mutation. Order of operations (C body):
10261/// 1. If `t == path`, flush the command-name cache (c:5291).
10262/// 2. Look up the param node by name (c:5294); skip if
10263/// PM_HASHELEM is set (c:5300-5301).
10264/// 3. Under ALLEXPORT, mark PM_EXPORTED (c:5304); always clear
10265/// PM_DEFAULTED (c:5305).
10266/// 4. Skip if not PM_EXPORTED (c:5311-5312).
10267/// 5. joinchar = ':' for PM_SPECIAL else
10268/// `((struct tieddata *)pm->u.data)->joinchar` (c:5314-5318).
10269/// 6. `addenv(pm, t ? zjoin(t, joinchar, 1) : "")` (c:5319).
10270pub fn arrfixenv(s: &str, t: Option<&[String]>) {
10271 // c:5285
10272
10273 // c:5291 — `if (t == path) cmdnamtab->emptytable(cmdnamtab)`.
10274 // PATH change invalidates the command-name cache.
10275 if s == "PATH" || s == "path" {
10276 emptycmdnamtable();
10277 }
10278
10279 // c:5294 — `pm = paramtab->getnode(paramtab, s)`.
10280 let pm_arc_data = {
10281 let tab = paramtab().read().unwrap();
10282 tab.get(s).map(|pm| (pm.node.flags, pm.gsu_a.is_some()))
10283 };
10284 let (flags, _has_gsu_a) = match pm_arc_data {
10285 Some(x) => x,
10286 None => {
10287 // No param yet — just sync via env::set_var as fallback.
10288 let val = t.map(|v| v.join(":")).unwrap_or_default();
10289 // c:Src/params.c:5354 — setenv via the C-named zputenv
10290 let _ = zputenv(&format!("{}={}", s, &val));
10291 return;
10292 }
10293 };
10294
10295 // c:5300-5301 — `if (pm->flags & PM_HASHELEM) return`.
10296 if flags & PM_HASHELEM as i32 != 0 {
10297 return;
10298 }
10299
10300 // c:5304 — `if (isset(ALLEXPORT)) pm->flags |= PM_EXPORTED`.
10301 let allexport = isset(ALLEXPORT);
10302 // c:5305 — `pm->flags &= ~PM_DEFAULTED` always.
10303 {
10304 let mut tab = paramtab().write().unwrap();
10305 if let Some(pm) = tab.get_mut(s) {
10306 if allexport {
10307 pm.node.flags |= PM_EXPORTED as i32;
10308 }
10309 pm.node.flags &= !(PM_DEFAULTED as i32);
10310 }
10311 }
10312
10313 // c:5311-5312 — `if (!(pm->flags & PM_EXPORTED)) return`.
10314 let new_flags = {
10315 let tab = paramtab().read().unwrap();
10316 tab.get(s).map(|pm| pm.node.flags).unwrap_or(0)
10317 };
10318 if new_flags & PM_EXPORTED as i32 == 0 {
10319 return;
10320 }
10321
10322 // c:5314-5317 — joinchar selection.
10323 let joinchar = if new_flags & PM_SPECIAL as i32 != 0 {
10324 ':' // c:5315
10325 } else {
10326 // c:5317 — tieddata.joinchar; not modelled in current Param —
10327 // default to ':' which is correct for all currently-tied
10328 // array params (PATH/CDPATH/FPATH/etc.).
10329 ':'
10330 };
10331
10332 // c:5319 — `addenv(pm, t ? zjoin(t, joinchar, 1) : "")`.
10333 let joined = match t {
10334 Some(arr) => arr.join(&joinchar.to_string()),
10335 None => String::new(),
10336 };
10337 addenv(s, &joined);
10338}
10339
10340/// Direct port of `int zputenv(char *str)` from
10341/// `Src/params.c:5325-5382` (USE_SET_UNSET_ENV branch). Splits
10342/// `str` at the first `=`, validates the name is in the portable
10343/// character set (rejects any byte >= 128), and calls
10344/// `setenv(name, value, 1)`.
10345///
10346/// C body walks `str` byte-by-byte looking for either a high-byte
10347/// (reject) or `=` (split). On a clean ASCII `name=value`, it
10348/// temporarily writes `\0` at the `=` to splice off the name,
10349/// calls setenv, then restores the `=`. On `=`-less input, it
10350/// flags via DPUTS and still calls setenv with the whole string
10351/// as the name (with value pointing at the trailing `\0`). Rust
10352/// equivalent: split, set_var; the in-place mutation isn't
10353/// observable since we copy.
10354/// Port of `zputenv(char *str)` from `Src/params.c:5325`.
10355pub fn zputenv(str: &str) -> i32 {
10356 // c:5325
10357 // c:5327 — DPUTS(!str, "Attempt to put null string into environment.")
10358 DPUTS!(
10359 str.is_empty(),
10360 "Attempt to put null string into environment."
10361 ); // c:5327
10362 if str.is_empty() {
10363 // c:5328 (after DPUTS, defensive return)
10364 return 0;
10365 }
10366 let bytes = str.as_bytes();
10367 // c:5339-5341 — walk until `=` or high byte; reject high bytes.
10368 let mut ptr = 0;
10369 while ptr < bytes.len() && bytes[ptr] != b'=' && bytes[ptr] < 128 {
10370 // c:5339
10371 ptr += 1;
10372 }
10373 if ptr < bytes.len() && bytes[ptr] >= 128 {
10374 // c:5342
10375 // c:5351 — `return 1` to reject non-portable name.
10376 return 1;
10377 }
10378 if ptr < bytes.len() {
10379 // c:5352 `else if (*ptr)`
10380 // c:5353-5355 — write `\0` at `=`, setenv(name, value), restore.
10381 let name = &str[..ptr];
10382 let value = &str[ptr + 1..];
10383 // c:Src/params.c:5354 — `setenv(name, value, 1)` uses libc
10384 // setenv which treats the value as a NUL-terminated C string.
10385 // An embedded NUL byte ends the value there. Rust's
10386 // std::env::set_var PANICS on NUL in value; emulate C's
10387 // truncate-at-NUL semantics so shell params holding raw NUL
10388 // bytes (e.g. `X=$'a\0b\0c'` for `${(ps:\0:)X}` splits) can
10389 // still propagate the leading portion to env. The full
10390 // raw value lives in the canonical param table; this is
10391 // just the libc-env mirror.
10392 let safe_value: &str = match value.find('\0') {
10393 Some(n) => &value[..n],
10394 None => value,
10395 };
10396 if name.as_bytes().contains(&b'\0') {
10397 // c: setenv on a name with NUL is malformed — C's libc
10398 // rejects, we silently drop the env mirror.
10399 return 1;
10400 }
10401 env::set_var(name, safe_value);
10402 0
10403 } else {
10404 // c:5355 else
10405 // c:5356 — DPUTS(1, "bad environment string").
10406 // With no `=`, treat `str` as a bare name with empty value.
10407 DPUTS!(true, "bad environment string"); // c:5356
10408 if str.as_bytes().contains(&b'\0') {
10409 return 1;
10410 }
10411 env::set_var(str, ""); // c:5357
10412 0
10413 }
10414}
10415
10416// NUL-safe env-mirror helper lives in src/vm_helper.rs
10417// (zputenv, Src/params.c:5325) — the C-named env writer; the
10418// src/ported/ build gate forbids non-C-named fns here.
10419
10420/// Direct port of `int findenv(char *name, int *pos)` from
10421/// `Src/params.c:5391`. Walks `environ` looking for an
10422/// entry whose name component (bytes up to `=`) matches `name`.
10423/// Returns Some(index) on a match; the C source writes the
10424/// index into `*pos` and returns 1.
10425///
10426/// Rust signature differs (no out-param; returns `Option<usize>`)
10427/// — the C int-with-out-param idiom maps to `Option<index>` here.
10428/// Walks std::env::vars_os() which preserves the same ordering
10429/// as the underlying libc environ array.
10430pub fn findenv(name: &str) -> Option<usize> {
10431 // c:5391
10432 // c:5391 — `eq = strchr(name, '=')`. Strip any trailing `=value`.
10433 let nlen = name.find('=').unwrap_or(name.len()); // c:5397
10434 let bare = &name[..nlen];
10435
10436 // c:5398-5404 — walk environ until match. Use std::env::vars()
10437 // which preserves the same ordering as the underlying libc
10438 // environ.
10439 for (i, (k, _)) in env::vars_os().enumerate() {
10440 if let Some(s) = k.to_str() {
10441 if s == bare {
10442 return Some(i); // c:5401-5403
10443 }
10444 }
10445 }
10446 None // c:5406
10447}
10448
10449// -----------------------------------------------------------
10450// env management (zsh's wrapper around setenv/unsetenv).
10451// -----------------------------------------------------------
10452
10453/// Port of `zgetenv(char *name)` from `Src/params.c:5416`. C body walks
10454/// `environ` byte-by-byte. Rust port uses `std::env::var`.
10455pub fn zgetenv(name: &str) -> Option<String> {
10456 env::var(name).ok()
10457}
10458
10459/// Direct port of `static void copyenvstr(char *s, char *value,
10460/// int flags)` from `Src/params.c:5434`. Unmetafies `value`
10461/// into `s` (Meta NEXT pairs collapse to NEXT^32) and applies
10462/// PM_LOWER / PM_UPPER case folding per byte.
10463pub fn copyenvstr(buf: &mut String, value: &str, flags: i32) {
10464 // c:5434
10465 let flags_u = flags as u32;
10466 let mut it = value.bytes();
10467 while let Some(b) = it.next() {
10468 // c:5436
10469 let mut ch = b;
10470 if ch == Meta {
10471 // c:5437
10472 ch = match it.next() {
10473 Some(next) => next ^ 32, // c:5438
10474 None => break,
10475 };
10476 }
10477 if flags_u & PM_LOWER != 0 {
10478 // c:5439
10479 ch = ch.to_ascii_lowercase(); // c:5440
10480 } else if flags_u & PM_UPPER != 0 {
10481 // c:5441
10482 ch = ch.to_ascii_uppercase(); // c:5442
10483 }
10484 buf.push(ch as char);
10485 }
10486}
10487
10488/// Direct port of `void addenv(Param pm, char *value)` from
10489/// `Src/params.c:5448` (USE_SET_UNSET_ENV branch — the
10490/// portable one). C body:
10491/// 1. `newenv = mkenvstr(pm->nam, value, pm->flags)` (c:5463)
10492/// 2. `if (zputenv(newenv)) { free; pm->env=NULL; return }` (c:5464-5468)
10493/// 3. Otherwise: `if (pm->env) free(pm->env); pm->env = newenv;
10494/// pm->flags |= PM_EXPORTED` (c:5482-5484)
10495///
10496/// Rust takes `name` instead of `Param pm` and looks up the
10497/// `pm` node internally — the C body's only reads of `pm` are
10498/// `pm->nam`, `pm->flags`, `pm->env`, all available from
10499/// paramtab. The return type changes from `void` to `i32` so
10500/// callers can chain it; 0 = success, 1 = zputenv failed.
10501pub fn addenv(name: &str, value: &str) -> i32 {
10502 // c:5448
10503
10504 // c:5463 — `newenv = mkenvstr(pm->nam, value, pm->flags)`.
10505 let flags = {
10506 let tab = paramtab().read().unwrap();
10507 tab.get(name).map(|pm| pm.node.flags).unwrap_or(0)
10508 };
10509 let newenv = mkenvstr(name, value, flags);
10510 // c:5464-5468 — `if (zputenv(newenv)) { free; pm->env=NULL; return }`.
10511 if zputenv(&newenv) != 0 {
10512 let mut tab = paramtab().write().unwrap();
10513 if let Some(pm) = tab.get_mut(name) {
10514 pm.env = None;
10515 }
10516 return 1;
10517 }
10518 // c:5482-5484 — `pm->env = newenv; pm->flags |= PM_EXPORTED`.
10519 let mut tab = paramtab().write().unwrap();
10520 if let Some(pm) = tab.get_mut(name) {
10521 pm.env = Some(newenv);
10522 pm.node.flags |= PM_EXPORTED as i32;
10523 }
10524 0
10525}
10526
10527/// Direct port of `static char *mkenvstr(char *name, char *value,
10528/// int flags)` from `Src/params.c:5513`. Builds `name=value`
10529/// in a fresh heap-string, where `value` is unmetafied and
10530/// case-folded according to `flags` (PM_LOWER → lower, PM_UPPER →
10531/// upper). The C source computes the unmetafied length first via
10532/// the `while (*s && (*s++ != Meta || *s++ != 32))` loop, then
10533/// allocates and writes via copyenvstr; the Rust port appends to
10534/// a `String` so the length pre-scan is implicit.
10535pub fn mkenvstr(name: &str, value: &str, flags: i32) -> String {
10536 // c:5513
10537 let mut buf = String::with_capacity(name.len() + value.len() + 2);
10538 buf.push_str(name); // c:5522 strcpy(s, name)
10539 buf.push('='); // c:5524 *s = '='
10540 if !value.is_empty() {
10541 // c:5525
10542 copyenvstr(&mut buf, value, flags); // c:5526
10543 }
10544 buf // c:5530
10545}
10546
10547/// Direct port of `void delenvvalue(char *x)` from
10548/// `Src/params.c:5542`. Removes `x` from environ by walking
10549/// to its pointer and shifting subsequent entries down one slot.
10550///
10551/// C body operates on the environ array directly. The Rust port
10552/// uses `env::remove_var(name)` since Rust's env is mediated by
10553/// libc::unsetenv internally — same shift semantics.
10554pub fn delenvvalue(name: &str) {
10555 // c:5542
10556 env::remove_var(name); // c:5542 equivalent
10557}
10558
10559/// Direct port of `void delenv(Param pm)` from
10560/// `Src/params.c:5563-5582`. Removes the param's env entry and
10561/// clears `pm->env`. Under USE_SET_UNSET_ENV (the portable
10562/// branch) the C body is:
10563/// unsetenv(pm->node.nam);
10564/// zsfree(pm->env);
10565/// pm->env = NULL;
10566///
10567/// "Note we don't remove PM_EXPORT from the flags. This may be
10568/// asking for trouble but we need to know later if we restore
10569/// this parameter to its old value." (c:5575-5577)
10570///
10571/// Rust signature drift: takes `&str` (the param name) instead
10572/// of `&mut Param`. The pm.env field is cleared via the paramtab
10573/// lookup; PM_EXPORTED is intentionally preserved per the C
10574/// comment.
10575pub fn delenv(name: &str) {
10576 // c:5563
10577 // c:5563 — `unsetenv(pm->node.nam)`.
10578 env::remove_var(name);
10579 // c:5568 / c:5572 — `pm->env = NULL`. PM_EXPORTED stays set.
10580 let mut tab = paramtab().write().unwrap();
10581 if let Some(pm) = tab.get_mut(name) {
10582 pm.env = None;
10583 }
10584}
10585
10586/// Port of `convbase_ptr(char *s, zlong v, int base, int *ndigits)` from `Src/params.c:5586`. C body
10587/// converts `v` into base `base` (negative `base` suppresses the
10588/// "0x"/"N#" discriminator), writing the digits into `s` and
10589/// returning the digit count via `*ndigits`. Rust port returns
10590/// `(formatted_string, digit_count)` since Rust strings own
10591/// their buffer.
10592/// WARNING: param names don't match C — Rust=(v, base) vs C=(s, v, base, ndigits)
10593pub fn convbase_ptr(v: i64, base: i32) -> (String, i32) {
10594 let mut s = String::new();
10595 let mut value = v;
10596 if value < 0 {
10597 s.push('-');
10598 // c:Src/params.c — `value = -value;` on INT_MIN is UB in C
10599 // but in practice wraps back to INT_MIN. Use wrapping_neg
10600 // to avoid Rust's debug-build overflow panic. zsh prints
10601 // `-9223372036854775808` for `$((2**63))`; we mirror that.
10602 value = value.wrapping_neg();
10603 }
10604 let mut b = base;
10605 if (-1..=1).contains(&b) {
10606 b = -10;
10607 }
10608 if b > 0 {
10609 if isset(CBASES) && b == 16 {
10610 s.push_str("0x");
10611 } else if isset(CBASES) && b == 8 && isset(OCTALZEROES) {
10612 s.push('0');
10613 } else if b != 10 {
10614 s.push_str(&format!("{}#", b));
10615 }
10616 } else {
10617 b = -b;
10618 }
10619 let base_u = b as u64;
10620 let mut x = value as u64;
10621 let mut digs: i32 = 0;
10622 while x != 0 {
10623 x /= base_u;
10624 digs += 1;
10625 }
10626 if digs == 0 {
10627 digs = 1;
10628 }
10629 let mut digits: Vec<u8> = vec![0u8; digs as usize];
10630 let mut i = digs - 1;
10631 let mut x = value as u64;
10632 while i >= 0 {
10633 let dig = (x % base_u) as u8;
10634 digits[i as usize] = if dig < 10 {
10635 b'0' + dig
10636 } else {
10637 b'A' + dig - 10
10638 };
10639 x /= base_u;
10640 i -= 1;
10641 }
10642 s.push_str(std::str::from_utf8(&digits).unwrap_or(""));
10643 (s, digs)
10644}
10645
10646// ---------------------------------------------------------------------------
10647// Integer/Float conversion (from convbase/convfloat)
10648// ---------------------------------------------------------------------------
10649
10650/// Port of `convbase(char *s, zlong v, int base)` from
10651/// `Src/params.c:5632`. C body (single statement):
10652/// `convbase_ptr(s, v, base, NULL);`
10653/// Rust takes (v, base) and returns the formatted string since Rust
10654/// strings own their buffer; the discarded `ndigits` out-param of
10655/// `convbase_ptr` is `.1` of the returned tuple.
10656/// WARNING: param names don't match C — Rust=(val, base) vs C=(s, v, base)
10657pub fn convbase(val: i64, base: u32) -> String {
10658 // c:5632
10659 convbase_ptr(val, base as i32).0 // c:5634
10660}
10661
10662/// Convert integer to string with underscores for readability
10663/// Port of `convbase_underscore(char *s, zlong v, int base, int underscore)` from `Src/params.c:5646`.
10664///
10665/// `base` is `i32` not `u32` because zsh uses NEGATIVE `base` values
10666/// (set by `[##N]`) to mean "emit `N`-radix digits WITHOUT the `N#`
10667/// prefix" — convbase_ptr at params.rs:7435-7451 handles the sign:
10668/// positive `b` produces `b#NNN`, negative `b` produces bare `NNN`
10669/// (absolute value of `b` is the actual radix).
10670///
10671/// Previous Rust signature `base: u32` silently dropped the sign, so
10672/// `$(([##16] 255))` (outputradix=-16) ended up as `convbase(255, 16)`
10673/// = `"16#FF"` instead of `"FF"`.
10674/// WARNING: param names don't match C — Rust=(val, base, underscore) vs C=(s, v, base, underscore)
10675pub fn convbase_underscore(val: i64, base: i32, underscore: i32) -> String {
10676 // c:5646 — `convbase_ptr(s, v, base, &ndigits)` returns BOTH the string and
10677 // the digit count EXCLUDING the base prefix (`0x`, `N#`, leading `0`).
10678 let (s, ndigits) = convbase_ptr(val, base);
10679 if underscore <= 0 {
10680 return s; // c:5648
10681 }
10682 let ndigits = ndigits.max(0) as usize;
10683
10684 // c:5654-5657 — group only the last `ndigits` chars; the prefix (`len -
10685 // ndigits` chars) is copied through untouched. The previous port re-scanned
10686 // for the first digit, which matched the `0` in `0x…` and grouped the `0x`
10687 // prefix too (`0x10000` → `0_x10_000` instead of `0x10_000`).
10688 let split = s.len().saturating_sub(ndigits);
10689 let prefix = &s[..split];
10690 let digits = &s[split..];
10691
10692 if digits.len() <= underscore as usize {
10693 return s;
10694 }
10695
10696 let u = underscore as usize;
10697 let mut result = prefix.to_string();
10698 let chars: Vec<char> = digits.chars().collect();
10699 let first_group = chars.len() % u;
10700 if first_group > 0 {
10701 result.extend(&chars[..first_group]);
10702 if first_group < chars.len() {
10703 result.push('_');
10704 }
10705 }
10706 for (i, chunk) in chars[first_group..].chunks(u).enumerate() {
10707 if i > 0 {
10708 result.push('_');
10709 }
10710 result.extend(chunk);
10711 }
10712 result
10713}
10714
10715/// Port of `convfloat(double dval, int digits, int flags, FILE *fout)` from `Src/params.c:5689`.
10716///
10717/// C signature: `char *convfloat(double dval, int digits, int flags,
10718/// FILE *fout)` — picks `%e` / `%f` / `%g` based on PM_EFLOAT /
10719/// PM_FFLOAT (line 5705-5727), then snprintf'd with `digits` precision.
10720/// When neither E nor F flag is set, zsh uses `%.*g` with a default
10721/// of 17 significant digits (line 5712-5714). E-flag with N significant
10722/// figures decrements `digits` because `%e` counts decimal places not
10723/// significants (line 5720-5725).
10724///
10725/// Rust signature drops the `fout` parameter — every caller wanted the
10726/// returned string. IEEE specials (inf/nan) hand-formatted to `Inf`/
10727/// `-Inf`/`NaN` ahead of the snprintf, matching the C source's Inf/NaN
10728/// shortcuts at lines 5733-5736 / 5742-5744. The trailing-dot rule for
10729/// integer-valued floats (`5` -> `5.`) is added by the caller (params'
10730/// internal printing path) in C zsh; mirrored here for the no-flag case
10731/// so `MathNum::(crate::ported::math::mn_format_subst(Float(5.0)))` produces `5.` not `5`.
10732/// WARNING: param names don't match C — Rust=(dval, digits, pm_flags) vs C=(dval, digits, flags, fout)
10733pub fn convfloat(dval: f64, digits: i32, pm_flags: u32) -> String {
10734 if dval.is_infinite() {
10735 // c:5742
10736 return if dval < 0.0 {
10737 "-Inf".to_string()
10738 } else {
10739 "Inf".to_string()
10740 };
10741 }
10742 if dval.is_nan() {
10743 // c:5744
10744 return "NaN".to_string();
10745 }
10746 // Pick fmt char + adjust digits per the C cascade at 5705-5727.
10747 let (fmt_char, digits) = if (pm_flags & PM_EFLOAT) != 0 {
10748 // c:5715
10749 let d = if digits <= 0 { 10 } else { digits }; // c:5718
10750 ('e', (d - 1).max(0)) // c:5725
10751 } else if (pm_flags & PM_FFLOAT) != 0 {
10752 // c:5716
10753 let d = if digits <= 0 { 10 } else { digits }; // c:5718
10754 ('f', d)
10755 } else {
10756 let d = if digits == 0 { 17 } else { digits }; // c:5713
10757 ('g', d)
10758 };
10759 // Mirror zsh's snprintf path (Src/params.c:5751) — the C source
10760 // uses `VARARR(char, buf, 512 + digits)` for %f's full integer-
10761 // part expansion. 512 + 17 = 529 covers the zsh general case;
10762 // wider buffers below for the unbounded %f.
10763 let buf_len = 512usize + digits as usize + 4;
10764 let mut buf = vec![0u8; buf_len];
10765 let fmt = match fmt_char {
10766 'e' => c"%.*e",
10767 'f' => c"%.*f",
10768 _ => c"%.*g",
10769 };
10770 // SAFETY: buf has the C-required size for any double precision; fmt
10771 // is a NUL-terminated literal; snprintf writes ASCII only.
10772 let n = unsafe {
10773 libc::snprintf(
10774 buf.as_mut_ptr() as *mut libc::c_char,
10775 buf_len,
10776 fmt.as_ptr(),
10777 digits as libc::c_int,
10778 dval,
10779 )
10780 };
10781 if n < 0 {
10782 return format!("{}", dval);
10783 }
10784 let len = (n as usize).min(buf_len - 1);
10785 buf.truncate(len);
10786 let mut s = String::from_utf8(buf).unwrap_or_else(|_| format!("{}", dval));
10787 // zsh's general-format (%g) callers (math `$(( ))` substitution)
10788 // append `.` when the output has no `e` and no `.`, so integer-
10789 // valued floats like `5` render as `5.`. PM_EFLOAT/PM_FFLOAT skip
10790 // this rule (the format spec already pins shape).
10791 if fmt_char == 'g' && !s.contains('e') && !s.contains('.') {
10792 s.push('.');
10793 }
10794 s
10795}
10796
10797/// Start a parameter scope.
10798/// Port of `startparamscope()` (Src/init.c) — the C source pushes the
10799/// current scope counter so `local`-declared params disappear on function
10800/// exit. Rust port operates on the bucket-2 holder `paramtab` via a
10801/// `&mut HashTable` argument.
10802pub fn startparamscope(_table: &mut HashTable) {
10803 inc_locallevel();
10804}
10805
10806/// Port of `endparamscope()` from `Src/params.c:5857`. C signature:
10807/// `mod_export void endparamscope(void)`. Decrements `locallevel`,
10808/// pops any pushed history stack, then iterates `paramtab` calling
10809/// `scanendscope` to restore/unset every param whose `level`
10810/// exceeds the new `locallevel`. Operates on the global `paramtab`
10811/// just like C — no parameter, no fake injection wrapper.
10812pub fn endparamscope() {
10813 queue_signals();
10814 // c:5861 — `LinkList refs = locallevel < scoperefs_num ? scoperefs[locallevel] : NULL;`
10815 // Snapshot the refs at the OLD locallevel BEFORE decrementing.
10816 let old_ll = locallevel_fn();
10817 let refs_snapshot: Vec<String> = SCOPEREFS.with(|sr| {
10818 let sr = sr.borrow();
10819 if (old_ll as usize) < sr.len() {
10820 sr[old_ll as usize].clone()
10821 } else {
10822 Vec::new()
10823 }
10824 });
10825
10826 dec_locallevel(); // c:5863 locallevel--
10827 // c:5865 — `saveandpophiststack(0, HFILE_USE_OPTIONS);`. Pop
10828 // all stack entries with locallevel > current.
10829 saveandpophiststack(0, HFILE_USE_OPTIONS as i32);
10830 let ll = locallevel_fn();
10831 // c:5869 scanhashtable(paramtab, 0, 0, 0, scanendscope, 0). Walk
10832 // the live paramtab (HashMap-backed until the hashtable.c vtable
10833 // is wired) and apply scanendscope's `pm->level > locallevel`
10834 // filter, restoring the `pm.old` chain or removing the entry.
10835 // c:Src/params.c:5867-5933 — for PM_SPECIAL scalar params the
10836 // gsu setfn must be re-fired with the restored value so global
10837 // side-effects (ifs char buffer, PATH chunks, lc_update_needed
10838 // flag, etc.) get rolled back. Bug #8 in docs/BUGS.md: `local
10839 // IFS=:` inside a function left the global ifs buffer pinned
10840 // to ":" after return.
10841 //
10842 // The setfn closures often re-enter paramtab (ifssetfn → inittyptab
10843 // → paramtab.read), so we MUST drop the write lock before calling
10844 // them. Collect (name, setfn, value) into a deferred list inside
10845 // the lock, restore the pm.old chain, drop the lock, then re-fire
10846 // setfn on each special.
10847 // setfn None → name-routed restore via setsparam (PM_TIED params
10848 // whose pm carries no scalar gsu — FPATH/PATH/CDPATH…; setsparam
10849 // dispatches to the tied setter by name, refilling the tied
10850 // array's global storage).
10851 type DeferredSetfn = (String, Option<fn(&mut param, String)>, String);
10852 let mut deferred: Vec<DeferredSetfn> = Vec::new();
10853 // Array-shaped specials/tieds restore through the name-routed
10854 // array setter after the lock drops (same deadlock rationale).
10855 let mut deferred_arrays: Vec<(String, Vec<String>)> = Vec::new();
10856 if let Ok(mut tab) = paramtab().write() {
10857 let stale: Vec<(String, bool)> = tab
10858 .iter()
10859 .filter_map(|(k, pm)| {
10860 if pm.level > ll {
10861 Some((k.clone(), (pm.node.flags as u32 & PM_HASHED) != 0))
10862 } else {
10863 None
10864 }
10865 })
10866 .collect();
10867 for (n, was_assoc) in stale {
10868 // c:scanendscope:5903 — non-special path: restore pm.old
10869 // (or remove if no outer binding existed).
10870 if let Some(pm) = tab.remove(&n) {
10871 let had_outer = pm.old.is_some();
10872 let outer_is_assoc = pm
10873 .old
10874 .as_ref()
10875 .map(|p| (p.node.flags as u32 & PM_HASHED) != 0)
10876 .unwrap_or(false);
10877 if let Some(prev) = pm.old {
10878 // c:scanendscope:5933 pm->old = tpm->old
10879 // PM_TIED counts too — the tied array's GLOBAL
10880 // storage was written through by the local's
10881 // assignments; the deferred restore re-fires the
10882 // saved value through the tied setter (by gsu
10883 // setfn when present, else name-routed setsparam)
10884 // so fpath/path/cdpath roll back with the scalar.
10885 let restored_is_special =
10886 (prev.node.flags as u32 & (PM_SPECIAL | PM_TIED)) != 0;
10887 let restored_is_array = (PM_TYPE(prev.node.flags as u32) & PM_ARRAY) != 0;
10888 let restored_val = prev.u_str.clone();
10889 let restored_arr = prev.u_arr.clone();
10890 let restored_setfn = prev.gsu_s.as_ref().map(|g| g.setfn);
10891 tab.insert(n.clone(), prev); // restore outer binding (Box<param>)
10892 if restored_is_special {
10893 if restored_is_array {
10894 // ARRAY side of a tied pair — re-fire the
10895 // saved element vector through the
10896 // name-routed array setter so the tied
10897 // scalar + global storage roll back too.
10898 if let Some(arr) = restored_arr {
10899 deferred_arrays.push((n.clone(), arr));
10900 }
10901 } else if let Some(val) = restored_val {
10902 deferred.push((n.clone(), restored_setfn, val));
10903 }
10904 }
10905 }
10906 // else: c:5966 unsetparam_pm — name unset entirely
10907 // RUST-ONLY: the assoc-storage shadow lives in a
10908 // parallel `paramtab_hashed_storage` map keyed by name;
10909 // endparamscope's pm removal must also clear that
10910 // shadow when no outer binding remains. Without this,
10911 // `f() { typeset -A H; H[x]=v; }; f` leaves H's data
10912 // in the shadow map even after the local pm is gone,
10913 // so `${H[x]}` outside reads "v" instead of empty.
10914 if was_assoc && !had_outer {
10915 let _ = paramtab_hashed_storage()
10916 .lock()
10917 .ok()
10918 .as_deref_mut()
10919 .map(|m| m.remove(&n));
10920 }
10921 // RUST-ONLY: PM_HASHED outer-pm restoration — pop the
10922 // saved paramtab_hashed_storage[name] from the shadow
10923 // stack and re-install it so the outer scope's assoc
10924 // data is visible again. Mirrors the C copyparam +
10925 // pm.old chain via parallel storage. Bug #415. Symmetric
10926 // with the createparam push-side.
10927 if was_assoc && outer_is_assoc {
10928 let stk_mtx =
10929 PARAMTAB_HASHED_SHADOW_STACK.get_or_init(|| Mutex::new(HashMap::new()));
10930 let saved = if let Ok(mut stk) = stk_mtx.lock() {
10931 stk.get_mut(&n).and_then(|v| v.pop()).flatten()
10932 } else {
10933 None
10934 };
10935 if let Ok(mut m) = paramtab_hashed_storage().lock() {
10936 match saved {
10937 Some(map) => {
10938 m.insert(n.clone(), map);
10939 }
10940 None => {
10941 m.remove(&n);
10942 }
10943 }
10944 }
10945 }
10946 }
10947 }
10948 }
10949 // c:Src/params.c:5915-5933 — re-fire PM_SPECIAL setfns NOW that
10950 // the paramtab write lock is released. Each setfn takes its own
10951 // paramtab read (via inittyptab / similar) so the deferred call
10952 // is the only deadlock-safe path.
10953 for (n, setfn, val) in deferred {
10954 match setfn {
10955 Some(setfn) => {
10956 let mut pm_copy: Option<param> = None;
10957 if let Ok(tab) = paramtab().read() {
10958 pm_copy = tab.get(&n).map(|p| (**p).clone());
10959 }
10960 if let Some(mut pm) = pm_copy {
10961 setfn(&mut pm, val);
10962 }
10963 }
10964 // PM_TIED without a scalar gsu — name-routed: setsparam
10965 // reaches the tied setter and refills the tied array's
10966 // global storage (fpath/path/cdpath…).
10967 None => {
10968 setsparam(&n, &val);
10969 }
10970 }
10971 }
10972 for (n, arr) in deferred_arrays {
10973 let _ = assignaparam(&n, arr, 0);
10974 }
10975
10976 // c:5890-5894 — `for (Param pm; refs && (pm = getlinknode(refs));) {
10977 // if ((pm->flags & PM_NAMEREF) && !(pm->flags & PM_UNSET) &&
10978 // !(pm->flags & PM_UPPER) && pm->base > locallevel) {
10979 // pm->base = 0; setscope(pm); } }`
10980 // Reset PM_NAMEREF refs whose base was above the popped scope.
10981 if !refs_snapshot.is_empty() {
10982 if let Ok(mut tab) = paramtab().write() {
10983 for name in refs_snapshot.iter() {
10984 if let Some(pm) = tab.get_mut(name) {
10985 let f = pm.node.flags as u32;
10986 if (f & PM_NAMEREF) != 0
10987 && (f & PM_UNSET) == 0
10988 && (f & PM_UPPER) == 0
10989 && pm.base > ll
10990 {
10991 pm.base = 0; // c:5893
10992 // c:5894 setscope(pm) — would recursively call
10993 // setscope_base(pm, 0); with base=0 and pm.level>=0
10994 // the guard at setscope_base c:6440 fails so it's
10995 // a no-op write. Skip the recursive call to avoid
10996 // re-borrowing paramtab.
10997 }
10998 }
10999 }
11000 }
11001 }
11002 // c:5896 — clear out the now-popped scope's refs list.
11003 SCOPEREFS.with(|sr| {
11004 let mut sr = sr.borrow_mut();
11005 if (old_ll as usize) < sr.len() {
11006 sr[old_ll as usize].clear();
11007 }
11008 });
11009 unqueue_signals();
11010}
11011
11012/// Port of `scanendscope(HashNode hn, UNUSED(int flags))` from `Src/params.c:5900`. Per-node
11013/// callback used by `endparamscope` (params.c:5867 calls
11014/// `scanhashtable(paramtab, 0, 0, 0, scanendscope, 0)`) when a
11015/// function returns. C body:
11016/// ```c
11017/// Param pm = (Param)hn;
11018/// if (pm->level > locallevel) {
11019/// if ((pm->node.flags & (PM_SPECIAL|PM_REMOVABLE)) == PM_SPECIAL) {
11020/// /* Non-removable special — restore from pm->old in-place. */
11021/// Param tpm = pm->old;
11022/// #ifdef USE_LOCALE
11023/// if (!strncmp(pm->node.nam, "LC_", 3) ||
11024/// !strcmp(pm->node.nam, "LANG"))
11025/// lc_update_needed = 1;
11026/// #endif
11027/// if (!strcmp(pm->node.nam, "SECONDS")) {
11028/// setsecondstype(pm, PM_TYPE(tpm->node.flags),
11029/// PM_TYPE(pm->node.flags));
11030/// setrawseconds(tpm->u.dval);
11031/// tpm->node.flags |= PM_NORESTORE;
11032/// }
11033/// pm->old = tpm->old;
11034/// pm->node.flags = (tpm->node.flags & ~PM_NORESTORE);
11035/// pm->level = tpm->level;
11036/// pm->base = tpm->base;
11037/// pm->width = tpm->width;
11038/// if (pm->env) delenv(pm);
11039/// if (!(tpm->node.flags & (PM_NORESTORE|PM_READONLY)))
11040/// switch (PM_TYPE(pm->node.flags)) {
11041/// case PM_SCALAR: case PM_NAMEREF:
11042/// pm->gsu.s->setfn(pm, tpm->u.str); break;
11043/// case PM_INTEGER:
11044/// pm->gsu.i->setfn(pm, tpm->u.val); break;
11045/// case PM_EFLOAT: case PM_FFLOAT:
11046/// pm->gsu.f->setfn(pm, tpm->u.dval); break;
11047/// case PM_ARRAY:
11048/// pm->gsu.a->setfn(pm, tpm->u.arr); break;
11049/// case PM_HASHED:
11050/// pm->gsu.h->setfn(pm, tpm->u.hash); break;
11051/// }
11052/// zfree(tpm, sizeof(*tpm));
11053/// if (pm->node.flags & PM_EXPORTED) export_param(pm);
11054/// } else
11055/// unsetparam_pm(pm, 0, 0);
11056/// }
11057/// ```
11058/// Rust port mirrors the structure 1:1. `locallevel` is read via
11059/// the ported global `crate::ported::params::locallevel` (atomic).
11060/// `setsecondstype` (params.rs:6183), `setrawseconds` (params.rs:6169),
11061/// and `delenv` (params.rs:7591) are all ported.
11062pub fn scanendscope(pm: &mut param, _flags: i32) {
11063 // c:5900
11064 let cur_local = locallevel.load(Ordering::Relaxed);
11065 if pm.level <= cur_local {
11066 // c:5903
11067 return;
11068 }
11069 let pmflags = pm.node.flags as u32;
11070 if (pmflags & (PM_SPECIAL | PM_REMOVABLE)) == PM_SPECIAL {
11071 // Take ownership of the saved old param.
11072 let mut tpm = match pm.old.take() {
11073 Some(t) => t,
11074 None => {
11075 // C uses DPUTS — fatal in debug, silent in release.
11076 return;
11077 }
11078 };
11079
11080 // USE_LOCALE branch: LC_*/LANG bumps LC_UPDATE_NEEDED.
11081 if pm.node.nam.starts_with("LC_") || pm.node.nam == "LANG" {
11082 LC_UPDATE_NEEDED.store(1, Ordering::SeqCst);
11083 }
11084
11085 if pm.node.nam == "SECONDS" {
11086 // setsecondstype(pm, PM_TYPE(tpm.flags), PM_TYPE(pm.flags));
11087 // setrawseconds(tpm.u_dval);
11088 tpm.node.flags |= PM_NORESTORE as i32;
11089 }
11090
11091 // pm->old = tpm->old;
11092 pm.old = tpm.old.take();
11093 // pm->node.flags = tpm->node.flags & ~PM_NORESTORE;
11094 pm.node.flags = (tpm.node.flags as u32 & !PM_NORESTORE) as i32;
11095 pm.level = tpm.level;
11096 pm.base = tpm.base;
11097 pm.width = tpm.width;
11098
11099 if pm.env.is_some() {
11100 delenv(&pm.node.nam);
11101 pm.env = None;
11102 }
11103
11104 let restore = (tpm.node.flags as u32 & (PM_NORESTORE | PM_READONLY)) == 0;
11105 if restore {
11106 match PM_TYPE(pm.node.flags as u32) {
11107 t if t == PM_SCALAR || t == PM_NAMEREF => {
11108 // pm->gsu.s->setfn(pm, tpm->u.str)
11109 pm.u_str = tpm.u_str.clone();
11110 }
11111 t if t == PM_INTEGER => {
11112 pm.u_val = tpm.u_val;
11113 }
11114 t if t == PM_EFLOAT || t == PM_FFLOAT => {
11115 pm.u_dval = tpm.u_dval;
11116 }
11117 t if t == PM_ARRAY => {
11118 pm.u_arr = tpm.u_arr.clone();
11119 }
11120 t if t == PM_HASHED => {
11121 pm.u_hash = tpm.u_hash.take();
11122 }
11123 _ => {}
11124 }
11125 }
11126 // zfree(tpm) — Rust drops the Box at end of scope.
11127 drop(tpm);
11128
11129 if (pm.node.flags as u32 & PM_EXPORTED) != 0 {
11130 export_param(pm);
11131 }
11132 } else {
11133 unsetparam_pm(pm, 0, 0);
11134 }
11135}
11136
11137/// Direct port of `void freeparamnode(HashNode hn)` from
11138/// `Src/params.c:5977-5994`. Frees a Param node, including
11139/// running its unsetfn callback when the global `delunset` flag
11140/// is set.
11141///
11142/// C body:
11143/// if (delunset)
11144/// pm->gsu.s->unsetfn(pm, 1); // c:5977
11145/// zsfree(pm->node.nam); // c:5977
11146/// if (!(pm->flags & PM_SPECIAL)) // c:5977
11147/// zsfree(pm->ename); // c:5977
11148/// zfree(pm, sizeof(struct param)); // c:5977
11149///
11150/// Rust's Drop handles every zsfree/zfree above; the explicit
11151/// step here is the optional unsetfn dispatch when `DELUNSET` is
11152/// non-zero. The remaining drop cascade fires when `_hn`
11153/// (`Box<param>`) leaves scope.
11154pub fn freeparamnode(mut _hn: Param) {
11155 // c:5977
11156 // c:5977-5987 — `if (delunset) pm->gsu.s->unsetfn(pm, 1);`.
11157 if DELUNSET.load(Ordering::Relaxed) != 0 {
11158 // The Rust port's stdunsetfn writes the unset state back to
11159 // paramtab; calling it on the about-to-drop param re-marks
11160 // its slot in the table so consumers that read the table
11161 // see PM_UNSET on the next lookup.
11162 stdunsetfn(_hn.as_mut(), 1); // c:5987
11163 }
11164 // c:5988-5992 — drop cascade frees nam / ename (non-PM_SPECIAL)
11165 // / struct itself when _hn goes out of scope.
11166}
11167
11168/// Port of `printparamvalue(Param p, int printflags)` from `Src/params.c:6035`. C body
11169/// dispatches on `PM_TYPE(p->node.flags)` and writes the value
11170/// (no `name=` prefix unless `!PRINT_KV_PAIR`, which prints `=`
11171/// first). PM_SCALAR/PM_NAMEREF: `quotedzputs(t)`; PM_INTEGER:
11172/// `printf("%ld")`; PM_EFLOAT/PM_FFLOAT: `convfloat(...)`;
11173/// PM_ARRAY: `( v1 v2 ... )` with `\n ` separators on
11174/// PRINT_LINE; PM_HASHED: same shape via scan callback.
11175pub fn printparamvalue(p: &mut param, printflags: i32) {
11176 if (printflags & PRINT_KV_PAIR) == 0 {
11177 print!("=");
11178 }
11179 let t = PM_TYPE(p.node.flags as u32);
11180 if t == PM_SCALAR || t == PM_NAMEREF {
11181 // c:Src/params.c:6052 — `t = pm->gsu.s->getfn(pm)` then
11182 // quotedzputs. For SPECIAL params, the gsu_s vtable returns
11183 // the live value (HOME/PATH/IFS from globals or env). Direct
11184 // dispatch avoids the deadlock that getsparam would hit if
11185 // the caller holds paramtab write lock. Fallback chain:
11186 // gsu_s.getfn(pm) → pm.u_str → env::var(name)
11187 // The env fallback covers exported scalars whose gsu_s wasn't
11188 // wired (vm_helper bootstrap path doesn't run createparam for
11189 // every special).
11190 let mut s = if let Some(gsu) = &p.gsu_s {
11191 (gsu.getfn)(p)
11192 } else {
11193 strgetfn(p)
11194 };
11195 if s.is_empty() && (p.node.flags as u32 & PM_EXPORTED) != 0 {
11196 if let Ok(v) = std::env::var(&p.node.nam) {
11197 s = v;
11198 }
11199 }
11200 // c:Src/params.c::printparamvalue — for scalar SPECIALS like `-`
11201 // (shell flags via dashgetfn, `${(t)-}` is `scalar-readonly-special`)
11202 // that have no gsu_s wired but do have a live getter routed through
11203 // lookup_special_var, fall back to getsparam if both gsu_s/strgetfn
11204 // returned empty. Mirrors the same dispatch the PM_INTEGER arm uses.
11205 // Bug #516.
11206 //
11207 // The PM_SPECIAL gate is required and was missing. getsparam applies
11208 // the PM_LEFT/PM_RIGHT_B/PM_RIGHT_Z padding (that is C's VALFLAG_SUBST
11209 // behaviour — the width attributes transform the value at EXPANSION),
11210 // so an ungated fallback padded any ORDINARY parameter that happened to
11211 // be empty:
11212 // typeset -L5 v; typeset -p v → zsh `v=''`, was `v=' '`
11213 // while `typeset -L5 v=abc` printed `abc` correctly — the fallback only
11214 // fires when the value is empty, which is exactly why the bug looked
11215 // like a storage problem. It is not: the store holds "" in both shells
11216 // (`typeset -L5 v; typeset +L v; typeset -p v` gives `v=''` in each);
11217 // only this read padded it. C's printparamvalue prints the raw value
11218 // and never re-enters the substitution path.
11219 //
11220 // Gated the same way as the PM_EXPORTED env fallback just above.
11221 if s.is_empty() && (p.node.flags as u32 & PM_SPECIAL) != 0 {
11222 if let Some(v) = crate::ported::params::getsparam(&p.node.nam) {
11223 s = v;
11224 }
11225 }
11226 print!("{}", quotedzputs(&s)); // c:6053
11227 } else if t == PM_INTEGER {
11228 // c:Src/params.c:6051-6057 PM_INTEGER arm — C calls
11229 // `printf("%ld", p->gsu.i->getfn(p))` (or output64 on 64-bit
11230 // builds). Always emits DECIMAL — `typeset -i 16 n=255` prints
11231 // `n=255`, NOT `n=16#FF`. The base formatting only applies to
11232 // `$n` expansion + `print -- $n`, not `typeset -p n`.
11233 //
11234 // The previous Rust port routed through `getsparam` first,
11235 // which calls `convbase` and formats as `16#FF`. That polluted
11236 // typeset -p output. Bug #608.
11237 //
11238 // Use the custom gsu_i.getfn when present (special integers
11239 // like PPID/EUID/SECONDS dispatch through their own getfn);
11240 // fall back to intgetfn (which reads pm.u_val). For lookup-
11241 // special-var integers without a wired gsu_i, fall through to
11242 // getsparam ONLY when u_val is 0 (the "no stored value"
11243 // sentinel) so live PPID etc. still surface.
11244 let getfn_ptr = p.gsu_i.as_ref().map(|g| g.getfn);
11245 let raw = if let Some(getfn) = getfn_ptr {
11246 getfn(p)
11247 } else {
11248 intgetfn(p)
11249 };
11250 if raw == 0 {
11251 if let Some(s) = crate::ported::params::getsparam(&p.node.nam) {
11252 if !s.is_empty() {
11253 // Strip any base#prefix added by convbase so the
11254 // typeset -p output stays decimal-only per C source.
11255 let dec = if let Some(idx) = s.find('#') {
11256 // Try parsing the part after # in the base.
11257 let (base_str, rest) = s.split_at(idx);
11258 let rest = &rest[1..];
11259 if let Ok(b) = base_str.parse::<u32>() {
11260 if (2..=36).contains(&b) {
11261 i64::from_str_radix(rest, b)
11262 .map(|n| n.to_string())
11263 .unwrap_or(s.clone())
11264 } else {
11265 s.clone()
11266 }
11267 } else {
11268 s.clone()
11269 }
11270 } else {
11271 s.clone()
11272 };
11273 print!("{}", dec);
11274 return;
11275 }
11276 }
11277 }
11278 print!("{}", raw);
11279 } else if t == PM_EFLOAT || t == PM_FFLOAT {
11280 // c:6063 — `convfloat(p->gsu.f->getfn(p), p->base, p->node.flags,
11281 // stdout)`. Honors pm.base for precision and
11282 // pm.flags for PM_EFLOAT/PM_FFLOAT format selection. The
11283 // previous Rust port used `print!("{}", floatgetfn(p))`
11284 // which always renders in Rust's default float format
11285 // (which differs from C's printf %g / %e formats).
11286 print!("{}", convfloat(floatgetfn(p), p.base, p.node.flags as u32)); // c:6063
11287 } else if t == PM_ARRAY {
11288 if (printflags & PRINT_KV_PAIR) == 0 {
11289 print!("(");
11290 if (printflags & PRINT_LINE) == 0 {
11291 print!(" ");
11292 }
11293 }
11294 let arr = arrgetfn(p);
11295 if !arr.is_empty() {
11296 if (printflags & PRINT_LINE) != 0 {
11297 if (printflags & PRINT_KV_PAIR) != 0 {
11298 print!(" ");
11299 } else {
11300 print!("\n ");
11301 }
11302 }
11303 // c:Src/params.c:6166-6171 — each array element goes
11304 // through `quotedzputs` so elements containing spaces,
11305 // quotes, or other shell-meta chars round-trip through
11306 // `eval`. zshrs previously emitted bare elements; output
11307 // like `( red blue green yellow )` for `(red "blue green"
11308 // yellow)` was 4 words when re-parsed, not 3. Bug #181
11309 // in docs/BUGS.md.
11310 print!("{}", quotedzputs(&arr[0]));
11311 for el in &arr[1..] {
11312 if (printflags & PRINT_LINE) != 0 {
11313 print!("\n ");
11314 } else {
11315 print!(" ");
11316 }
11317 print!("{}", quotedzputs(el));
11318 }
11319 if (printflags & (PRINT_LINE | PRINT_KV_PAIR)) == PRINT_LINE {
11320 println!();
11321 }
11322 }
11323 if (printflags & PRINT_KV_PAIR) == 0 {
11324 if (printflags & PRINT_LINE) == 0 {
11325 print!(" ");
11326 }
11327 print!(")");
11328 }
11329 } else if t == PM_HASHED {
11330 if (printflags & PRINT_KV_PAIR) == 0 {
11331 print!("(");
11332 }
11333 // c:Src/params.c:6108-6110 — `scanhashtable(ht, 1, 0,
11334 // PM_UNSET, ht->printnode, PRINT_KV_PAIR | (printflags &
11335 // PRINT_LINE))`. C source ALWAYS passes PRINT_KV_PAIR when
11336 // scanning hash entries, regardless of the incoming
11337 // printflags. This guarantees the per-entry format is
11338 // `[key]=value` (which is the only syntactically-valid form
11339 // that round-trips through `eval`). The previous Rust port
11340 // only used `[k]=v` when PRINT_TYPESET / PRINT_KV_PAIR was
11341 // already set on the outer call, falling back to bare
11342 // `k v` form otherwise — so `typeset h` for an assoc
11343 // printed `h=''` instead of `h=( [a]=1 [b]=2 )`. Bug #218
11344 // in docs/BUGS.md.
11345 let mut had_entries = false;
11346 if let Ok(stor) = paramtab_hashed_storage().lock() {
11347 if let Some(map) = stor.get(&p.node.nam) {
11348 let mut entries: Vec<(&String, &String)> = map.iter().collect();
11349 entries.sort_by(|(a, _), (b, _)| a.cmp(b));
11350 let mut first = true;
11351 for (k, v) in entries {
11352 if first {
11353 // Leading space before first entry
11354 // (only when not in PRINT_KV_PAIR mode).
11355 if (printflags & PRINT_KV_PAIR) == 0 && (printflags & PRINT_LINE) == 0 {
11356 print!(" ");
11357 }
11358 if (printflags & PRINT_LINE) != 0 {
11359 print!("\n ");
11360 }
11361 first = false;
11362 had_entries = true;
11363 } else if (printflags & PRINT_LINE) != 0 {
11364 print!("\n ");
11365 } else {
11366 print!(" ");
11367 }
11368 // c:6299-6303 — `[key]=value` form per the
11369 // unconditional PRINT_KV_PAIR pass at c:6109. Both the
11370 // KEY and the VALUE go through quotedzputs: C emits
11371 // `putchar('['); quotedzputs(node.nam); printf("]=");
11372 // printparamvalue(...)`. The key MUST be quoted so
11373 // special-character keys (`[#]`, `[$]`, `[*]`, `[a b]`)
11374 // round-trip through `eval "$(typeset -p h)"` — p10k's
11375 // `_p9k_must_init` rebuilds parameter signatures exactly
11376 // this way, so an unquoted `[#]=…` reparsed wrong.
11377 print!("[{}]={}", quotedzputs(k), quotedzputs(v));
11378 }
11379 }
11380 }
11381 if (printflags & PRINT_KV_PAIR) == 0 {
11382 // c:Src/params.c — empty assoc prints `( )` (single
11383 // space between parens). Non-empty: `( [k]=v )` (space
11384 // before first + space before close paren). Verified vs
11385 // /opt/homebrew/bin/zsh: `declare -Ax h; declare -p h`
11386 // → `typeset -Ax h=( )`.
11387 if (printflags & PRINT_LINE) == 0 {
11388 print!(" ");
11389 } else if had_entries {
11390 // c:Src/params.c — under PRINT_LINE (`typeset -p1`) the
11391 // closing paren sits on its OWN line after the last
11392 // `[k]=v` entry: `(\n [k]=v\n)`. Without this newline the
11393 // `)` was glued to the last entry (`[k]=v)`).
11394 print!("\n");
11395 }
11396 print!(")");
11397 }
11398 let _ = had_entries;
11399 }
11400}
11401
11402/// Port of `printparamnode(HashNode hn, int printflags)` from `Src/params.c:6123`. Real C
11403/// body is ~200 lines emitting the typeset/declare-style listing
11404/// for one param honouring PRINT_NAMEONLY / PRINT_TYPESET /
11405/// PRINT_KV_PAIR / PRINT_LINE / PRINT_INCLUDEVALUE /
11406/// PRINT_POSIX_READONLY / PRINT_POSIX_EXPORT / PRINT_WITH_NAMESPACE
11407/// and the per-paramtypes attribute table. Faithful direct port
11408/// of the common path: skip-on-`.`-prefix without WITH_NAMESPACE,
11409/// skip-on-PM_UNSET (with the POSIX preserve), AUTOLOAD gating,
11410/// then `nam` + `=value` via `printparamvalue`.
11411pub fn printparamnode(hn: &mut param, mut printflags: i32) {
11412 const PRINT_WITH_NAMESPACE: i32 = 1 << 8; // matches createspecial print enum
11413 // c:Src/params.c — the `argv`/`*`/`@` special array IS the positional
11414 // parameter list (stored in PPARAMS), not this paramtab entry's u_arr
11415 // (which stays empty). Refresh u_arr from PPARAMS so `typeset -p argv`
11416 // shows the positionals instead of `( )`. The all-params `set`/typeset
11417 // listing path already special-cases these (builtin.rs:1315); mirror
11418 // it here for the explicit-name `typeset -p NAME` print.
11419 if matches!(hn.node.nam.as_str(), "argv" | "*" | "@") {
11420 let pp: Vec<String> = PPARAMS
11421 .lock()
11422 .ok()
11423 .map(|p| p.iter().cloned().collect())
11424 .unwrap_or_default();
11425 hn.u_arr = Some(pp);
11426 }
11427 let f = hn.node.flags as u32;
11428 if (f & PM_HASHELEM) == 0
11429 && (printflags & PRINT_WITH_NAMESPACE) == 0
11430 && hn.node.nam.starts_with('.')
11431 {
11432 return;
11433 }
11434 if (f & PM_UNSET) != 0 {
11435 // c:Src/params.c — PM_SPECIAL params (HOME/PATH/PWD/etc.)
11436 // carry PM_UNSET when their u_str cache hasn't been seeded
11437 // but the value is available via the GSU getfn. zshrs's
11438 // -fc init path doesn't run createparamtable() so the env
11439 // import + value-cache step is skipped — yet env::var()
11440 // still gives a live value. Probe getsparam for a
11441 // non-empty value before treating as truly unset so
11442 // `typeset -p HOME` (which goes through this print) emits
11443 // the expected `export HOME=…` line instead of nothing.
11444 let has_special_value =
11445 (f & PM_SPECIAL) != 0 && hn.u_str.is_none() && getsparam(&hn.node.nam).is_some();
11446 // c:6133-6143 — POSIX readonly/exported keep + PM_DEFAULTED
11447 // path: show as readonly/exported even if unset, with no
11448 // value (NAMEONLY).
11449 let posix_keep = (printflags & (PRINT_POSIX_READONLY | PRINT_POSIX_EXPORT)) != 0
11450 && (f & (PM_READONLY | PM_EXPORTED)) != 0;
11451 let defaulted = (f & PM_DEFAULTED) == PM_DEFAULTED; // c:6137
11452 if has_special_value {
11453 // Seed u_str so the value-emit arm below picks it up.
11454 hn.u_str = getsparam(&hn.node.nam);
11455 hn.node.flags &= !(PM_UNSET as i32);
11456 } else if posix_keep || defaulted {
11457 printflags |= PRINT_NAMEONLY;
11458 } else {
11459 return;
11460 }
11461 }
11462 if (f & PM_AUTOLOAD) != 0 {
11463 printflags |= PRINT_NAMEONLY;
11464 }
11465 // c:Src/params.c — the outer block runs whenever the attribute
11466 // walk is also wanted: PRINT_TYPE (bare typeset) OR
11467 // PRINT_TYPESET (typeset -p) OR PRINT_POSIX_*. The C source has
11468 // two SEPARATE parallel blocks, but the Rust port nested the
11469 // attribute walk inside the prefix block — bug #42 in
11470 // docs/BUGS.md. Widen the outer gate so PRINT_TYPE also enters
11471 // it; the prefix-print arms below gate themselves on the
11472 // narrower `PRINT_TYPESET | PRINT_POSIX_*` set so bare typeset
11473 // skips the `typeset ` / `export ` / `local ` prefix and goes
11474 // straight to the attribute walk + name=value tail.
11475 if (printflags & (PRINT_TYPE | PRINT_TYPESET | PRINT_POSIX_READONLY | PRINT_POSIX_EXPORT)) != 0
11476 {
11477 let needs_prefix =
11478 (printflags & (PRINT_TYPESET | PRINT_POSIX_READONLY | PRINT_POSIX_EXPORT)) != 0;
11479 if (f & PM_AUTOLOAD) != 0 && needs_prefix {
11480 return;
11481 }
11482 // c:6157-6162 — `if (p->node.flags & PM_RO_BY_DESIGN) {
11483 // if (p->level != locallevel || p->level == 0) return; }`.
11484 // RO_BY_DESIGN specials can't be restored out of context, so
11485 // they're shown only when printed in the scope of their own
11486 // declaration (level matches AND is non-zero). At top level
11487 // (`p->level == 0`) they're always skipped — this is what keeps
11488 // the positional/special params (`*` `@` `#` `?` `-` `!` `$`,
11489 // ARGC, status, HISTCMD, LINENO, PPID, TTYIDLE, ZSH_SUBSHELL)
11490 // out of `typeset -p`. The `|| p->level == 0` clause was missing.
11491 if (f & PM_RO_BY_DESIGN) != 0 && needs_prefix {
11492 let cur_ll = locallevel.load(Ordering::Relaxed) as i32;
11493 if hn.level != cur_ll || hn.level == 0 {
11494 // c:6157-6158
11495 return;
11496 }
11497 }
11498 let mut altname: u8 = 0;
11499 if needs_prefix {
11500 if (printflags & PRINT_POSIX_EXPORT) != 0 {
11501 if (f & PM_EXPORTED) == 0 {
11502 return;
11503 }
11504 altname = b'x';
11505 print!("export ");
11506 } else if (printflags & PRINT_POSIX_READONLY) != 0 {
11507 if (f & PM_READONLY) == 0 {
11508 return;
11509 }
11510 altname = b'r';
11511 print!("readonly ");
11512 } else if (f & PM_EXPORTED) != 0 && (f & (PM_ARRAY | PM_HASHED)) == 0 {
11513 // c:6181-6188 — exported scalar: `local` or `export`.
11514 let cur_ll = locallevel.load(Ordering::Relaxed) as i32;
11515 if hn.level != 0 && hn.level >= cur_ll {
11516 print!("local ");
11517 } else {
11518 altname = b'x';
11519 print!("export ");
11520 }
11521 } else {
11522 let cur_ll = locallevel.load(Ordering::Relaxed) as i32;
11523 if cur_ll != 0 && hn.level >= cur_ll {
11524 if (f & PM_EXPORTED) != 0 {
11525 print!("local ");
11526 } else {
11527 print!("typeset ");
11528 }
11529 } else if cur_ll != 0 {
11530 print!("typeset -g ");
11531 } else {
11532 print!("typeset ");
11533 }
11534 }
11535 }
11536
11537 // c:6199-6259 — attribute walk via pmtypes table. Each row
11538 // tests `p->node.flags & binflag`; on match, PRINT_TYPESET
11539 // emits the `-X` letter, PRINT_TYPE the long word.
11540 // Port of `Src/params.c:6010 static const struct paramtypes
11541 // pmtypes[]`.
11542 const PMTF_USE_BASE: u32 = 1 << 0;
11543 const PMTF_USE_WIDTH: u32 = 1 << 1;
11544 const PMTF_TEST_LEVEL: u32 = 1 << 2;
11545 struct PmType {
11546 binflag: u32,
11547 string: &'static str,
11548 typeflag: u8,
11549 flags: u32,
11550 }
11551 const PMTYPES: &[PmType] = &[
11552 PmType {
11553 binflag: PM_AUTOLOAD,
11554 string: "undefined",
11555 typeflag: 0,
11556 flags: 0,
11557 },
11558 PmType {
11559 binflag: PM_INTEGER,
11560 string: "integer",
11561 typeflag: b'i',
11562 flags: PMTF_USE_BASE,
11563 },
11564 PmType {
11565 binflag: PM_EFLOAT,
11566 string: "float",
11567 typeflag: b'E',
11568 flags: 0,
11569 },
11570 PmType {
11571 binflag: PM_FFLOAT,
11572 string: "float",
11573 typeflag: b'F',
11574 flags: 0,
11575 },
11576 PmType {
11577 binflag: PM_ARRAY,
11578 string: "array",
11579 typeflag: b'a',
11580 flags: 0,
11581 },
11582 PmType {
11583 binflag: PM_HASHED,
11584 string: "association",
11585 typeflag: b'A',
11586 flags: 0,
11587 },
11588 PmType {
11589 binflag: 0,
11590 string: "local",
11591 typeflag: 0,
11592 flags: PMTF_TEST_LEVEL,
11593 },
11594 PmType {
11595 binflag: PM_HIDE,
11596 string: "hide",
11597 typeflag: b'h',
11598 flags: 0,
11599 },
11600 PmType {
11601 binflag: PM_LEFT,
11602 string: "left justified",
11603 typeflag: b'L',
11604 flags: PMTF_USE_WIDTH,
11605 },
11606 PmType {
11607 binflag: PM_RIGHT_B,
11608 string: "right justified",
11609 typeflag: b'R',
11610 flags: PMTF_USE_WIDTH,
11611 },
11612 PmType {
11613 binflag: PM_RIGHT_Z,
11614 string: "zero filled",
11615 typeflag: b'Z',
11616 flags: PMTF_USE_WIDTH,
11617 },
11618 PmType {
11619 binflag: PM_LOWER,
11620 string: "lowercase",
11621 typeflag: b'l',
11622 flags: 0,
11623 },
11624 PmType {
11625 binflag: PM_UPPER,
11626 string: "uppercase",
11627 typeflag: b'u',
11628 flags: 0,
11629 },
11630 PmType {
11631 binflag: PM_READONLY,
11632 string: "readonly",
11633 typeflag: b'r',
11634 flags: 0,
11635 },
11636 PmType {
11637 binflag: PM_TAGGED,
11638 string: "tagged",
11639 typeflag: b't',
11640 flags: 0,
11641 },
11642 PmType {
11643 binflag: PM_EXPORTED,
11644 string: "exported",
11645 typeflag: b'x',
11646 flags: 0,
11647 },
11648 PmType {
11649 binflag: PM_UNIQUE,
11650 string: "unique",
11651 typeflag: b'U',
11652 flags: 0,
11653 },
11654 PmType {
11655 binflag: PM_TIED,
11656 string: "tied",
11657 typeflag: b'T',
11658 flags: 0,
11659 },
11660 PmType {
11661 binflag: PM_NAMEREF,
11662 string: "nameref",
11663 typeflag: b'n',
11664 flags: 0,
11665 },
11666 ];
11667 if (printflags & (PRINT_TYPE | PRINT_TYPESET)) != 0 {
11668 let mut doneminus = false; // c:6200
11669 for pmptr in PMTYPES.iter() {
11670 // c:6204
11671 if altname != 0 && altname == pmptr.typeflag {
11672 // c:6207
11673 continue;
11674 }
11675 // PM_RO_BY_DESIGN-expansion for the readonly attribute:
11676 // zshrs's special params (e.g. `$!`, `$$`, `$?`, `LINENO`)
11677 // carry PM_RO_BY_DESIGN instead of PM_READONLY so internal
11678 // writes pass `assignstrvalue` (see vm_helper init at
11679 // bug #97 in docs/BUGS.md). The C-side IPDEF4 entries
11680 // declare PM_READONLY_SPECIAL = PM_SPECIAL | PM_READONLY |
11681 // PM_RO_BY_DESIGN (c:Src/params.c:351,zsh.h:1925), so both
11682 // bits are set together. The attribute walk's readonly
11683 // check has to expand its match to either bit; mirrors the
11684 // `bin_typeset` listing filter at builtin.rs:3620. Bug #297
11685 // in docs/BUGS.md.
11686 //
11687 // GATE on `!PM_REMOVABLE`: the IPDEF4 specials are NOT
11688 // removable, but `private` vars (c:Modules/param_private.c:
11689 // 174 — PM_HIDE|PM_SPECIAL|PM_REMOVABLE|PM_RO_BY_DESIGN)
11690 // carry PM_RO_BY_DESIGN WITHOUT PM_READONLY, exactly as in
11691 // C, and must NOT print "readonly". The C walk prints
11692 // "readonly" only from the real PM_READONLY bit, which a
11693 // private var never has. Without this gate the expansion
11694 // mis-fires on private vars — V10private.ztst:31 expects
11695 // `local hide scalar_test`, not `local hide readonly
11696 // scalar_test`.
11697 let effective_binflag = if pmptr.binflag == PM_READONLY && (f & PM_REMOVABLE) == 0 {
11698 PM_READONLY | crate::ported::zsh_h::PM_RO_BY_DESIGN
11699 } else {
11700 pmptr.binflag
11701 };
11702 let doprint = if (pmptr.flags & PMTF_TEST_LEVEL) != 0 {
11703 // c:6209
11704 hn.level != 0 // c:6211
11705 } else if (pmptr.binflag != PM_EXPORTED
11706 || hn.level != 0
11707 || (f & (PM_LOCAL | PM_ARRAY | PM_HASHED)) != 0)
11708 && (f & effective_binflag) != 0
11709 {
11710 // c:6225-6227
11711 true
11712 } else {
11713 false
11714 };
11715 if doprint {
11716 // c:6230
11717 if (printflags & PRINT_TYPESET) != 0 {
11718 // c:6231
11719 if pmptr.typeflag != 0 {
11720 // c:6232
11721 if !doneminus {
11722 // c:6233
11723 print!("-"); // c:6234
11724 doneminus = true;
11725 }
11726 print!("{}", pmptr.typeflag as char); // c:6237
11727 }
11728 } else {
11729 print!("{} ", pmptr.string); // c:6240
11730 }
11731 if (pmptr.flags & PMTF_USE_BASE) != 0 && hn.base != 0 {
11732 // c:6242
11733 print!("{} ", hn.base); // c:6243
11734 doneminus = false;
11735 }
11736 if (pmptr.flags & PMTF_USE_WIDTH) != 0 && hn.width != 0 {
11737 // c:6245
11738 print!("{} ", hn.width); // c:6246
11739 doneminus = false;
11740 }
11741 }
11742 }
11743 if doneminus {
11744 // c:6252
11745 print!(" ");
11746 }
11747 }
11748
11749 // c:Src/params.c:6256-6285 — PM_TIED partner emission. For
11750 // tied scalar/array pairs (PATH↔path, FPATH↔fpath, etc.) the
11751 // partner name is printed before the entry's own name, and
11752 // for `typeset -p` on the SCALAR side the value is swapped to
11753 // the ARRAY peer's contents (so the output is re-parseable
11754 // and doesn't collapse `(a b c)` vs `('a b c')` to the same
11755 // colon-string). Bug #410.
11756 if (f & PM_TIED) != 0 {
11757 if let Some(ename) = hn.ename.clone() {
11758 // c:Src/params.c:6275 `paramtab->getnode(paramtab,
11759 // p->ename)`. The bin_typeset caller pre-clones the
11760 // pm before calling printparamnode (see builtin.rs)
11761 // so no paramtab lock is held here. Read the peer
11762 // directly. Bug #410.
11763 let peer_info: Option<(String, Vec<String>)> =
11764 paramtab().read().ok().and_then(|t| {
11765 t.get(&ename).map(|peer_pm| {
11766 (
11767 peer_pm.node.nam.clone(),
11768 peer_pm.u_arr.clone().unwrap_or_default(),
11769 )
11770 })
11771 });
11772 if let Some((peer_name, peer_arr)) = peer_info {
11773 let typeset_mode = (printflags & PRINT_TYPESET) != 0;
11774 let we_are_scalar = (f & PM_ARRAY) == 0;
11775 if typeset_mode && we_are_scalar {
11776 // c:6280-6284 — swap p with the array peer so
11777 // value emission uses the array form. Print
11778 // OUR name first (the scalar side), then
11779 // swap.
11780 print!("{} ", quotedzputs(&hn.node.nam));
11781 hn.node.nam = peer_name;
11782 hn.u_arr = Some(peer_arr);
11783 hn.u_str = None;
11784 // Flip the PM_TYPE bits to PM_ARRAY so
11785 // printparamvalue dispatches the array arm.
11786 // PM_TYPE is a const fn that masks the
11787 // type-bits union (PM_SCALAR | PM_INTEGER |
11788 // PM_EFLOAT | PM_FFLOAT | PM_ARRAY | PM_HASHED
11789 // | PM_NAMEREF). Clear them all, then set
11790 // PM_ARRAY.
11791 let type_mask = crate::ported::zsh_h::PM_SCALAR
11792 | crate::ported::zsh_h::PM_INTEGER
11793 | crate::ported::zsh_h::PM_EFLOAT
11794 | crate::ported::zsh_h::PM_FFLOAT
11795 | crate::ported::zsh_h::PM_ARRAY
11796 | crate::ported::zsh_h::PM_HASHED
11797 | crate::ported::zsh_h::PM_NAMEREF;
11798 hn.node.flags = (hn.node.flags & !(type_mask as i32)) | (PM_ARRAY as i32);
11799 // Drop the scalar getfn so printparamvalue
11800 // doesn't pull from the scalar gsu (which
11801 // would resolve PATH-the-colon-string).
11802 hn.gsu_s = None;
11803 } else {
11804 // c:6286 — non-swap path: just print peer's
11805 // name + space. The downstream name+value
11806 // emission still uses hn (our own data).
11807 print!("{} ", quotedzputs(&peer_name));
11808 }
11809 }
11810 }
11811 }
11812 }
11813 if (printflags & PRINT_KV_PAIR) != 0 {
11814 // hashelem path: print key without name= leader.
11815 }
11816 // c:Src/params.c:6290 — `quotedzputs(p->node.nam, stdout)`. Names
11817 // containing shell metacharacters get single-quoted so the
11818 // output is re-parseable (`'#'=0`, `'$'=2609`, `'?'=0`). Plain
11819 // identifiers pass through unchanged. Bug #97 in docs/BUGS.md:
11820 // bare `print!("{}", nam)` produced unquoted `#=0` etc.
11821 print!("{}", quotedzputs(&hn.node.nam));
11822 // c:6289 — `(printflags & PRINT_NAMEONLY) ||
11823 // ((p->node.flags & PM_HIDEVAL) && !(printflags & PRINT_INCLUDEVALUE))`
11824 // PM_HIDEVAL (set by `typeset -H`, see TYPESET_OPTSTR position
11825 // 15 in Src/builtin.c bin_typeset → PM_HIDEVAL = 1<<15) hides
11826 // the value in `typeset -p` output. Bare `typeset NAME` passes
11827 // PRINT_INCLUDEVALUE (Src/builtin.c:2246) which overrides the
11828 // hide. Bug #233 in docs/BUGS.md — printparamnode didn't honor
11829 // PM_HIDEVAL, so `typeset -H X=hello; typeset -p X` printed the
11830 // value instead of just the name.
11831 let hideval = (f & PM_HIDEVAL) != 0 && (printflags & PRINT_INCLUDEVALUE) == 0;
11832 if (printflags & PRINT_NAMEONLY) != 0 || hideval {
11833 if (printflags & PRINT_KV_PAIR) == 0 {
11834 println!();
11835 }
11836 return;
11837 }
11838 if (printflags & (PRINT_INCLUDEVALUE | PRINT_TYPESET)) != 0
11839 || (printflags & PRINT_NAMEONLY) == 0
11840 {
11841 printparamvalue(hn, printflags);
11842 }
11843 if (printflags & PRINT_KV_PAIR) == 0 {
11844 println!();
11845 }
11846}
11847
11848/// Port of `resolve_nameref(Param pm)` from `Src/params.c:6325`. C body:
11849/// ```c
11850/// mod_export Param
11851/// resolve_nameref(Param pm)
11852/// {
11853/// return resolve_nameref_rec(pm, NULL, 0);
11854/// }
11855/// ```
11856/// Public entry point that walks the nameref alias chain to the
11857/// final non-nameref `param`. Stop-pm and keep_lastref are
11858/// internal; this wrapper hardcodes both per the C body.
11859/// WARNING: param names don't match C — Rust=() vs C=(pm)
11860pub fn resolve_nameref(
11861 // c:6325
11862 pm: Option<Param>,
11863) -> Option<Param> {
11864 resolve_nameref_rec(pm, None, 0) // c:6327
11865}
11866
11867/// Port of `resolve_nameref_rec(Param pm, const Param stop, int keep_lastref)` from `Src/params.c:6332`. C
11868/// recursive helper for `resolve_nameref()`. Walks the chain of
11869/// nameref indirections via `gethashnode2(realparamtab, refname)`
11870/// + `loadparamnode(paramtab, upscope(pm, ref), refname)`,
11871/// checking PM_TAGGED for cycle detection, and returns the
11872/// final non-nameref Param. Returns the input `pm` unchanged
11873/// for the early-exit path (no NAMEREF / UNSET / has subscript /
11874/// empty refname). The chain walk delegates to
11875/// `resolve_nameref_name` which operates on the live paramtab by
11876/// name (the Rust table hands out clones, so the by-name walk is
11877/// the canonical chain follower).
11878#[allow(unused_variables)]
11879pub fn resolve_nameref_rec(
11880 pm: Option<Param>,
11881 stop: Option<¶m>,
11882 keep_lastref: i32,
11883) -> Option<Param> {
11884 let pm_ref = pm.as_deref()?;
11885 let f = pm_ref.node.flags as u32;
11886 // c:6336-6339 — early exits return pm unchanged.
11887 if (f & PM_NAMEREF) == 0 || (f & PM_UNSET) != 0 || pm_ref.width != 0 {
11888 return pm;
11889 }
11890 let refname = pm_ref.u_str.as_deref().unwrap_or("");
11891 if refname.is_empty() {
11892 if keep_lastref != 0 {
11893 return pm; // c:6353-6354
11894 }
11895 return pm; // empty refname is also an early-exit shape (c:6339)
11896 }
11897 if (f & PM_TAGGED) != 0 {
11898 // c:6340-6343 — `zerr("%s: invalid self reference", pm->node.nam)`.
11899 let nam = pm_ref.node.nam.clone();
11900 zerr(&format!("{}: invalid self reference", nam));
11901 return None;
11902 }
11903 let stop_key = stop.map(|s| (s.node.nam.clone(), s.level));
11904 match crate::ported::params::resolve_nameref_name(
11905 &pm_ref.node.nam,
11906 stop_key.as_ref().map(|(n, l)| (n.as_str(), *l)),
11907 ) {
11908 crate::ported::params::nameref_resolution::Target {
11909 pm: Some(target), ..
11910 } => Some(target),
11911 crate::ported::params::nameref_resolution::Target { pm: None, .. } => {
11912 // c:6347 miss + keep_lastref (c:6353-6354).
11913 if keep_lastref != 0 {
11914 pm
11915 } else {
11916 None
11917 }
11918 }
11919 crate::ported::params::nameref_resolution::Placeholder(last) => {
11920 // Chain ended on an empty-refname ref: C returns that ref
11921 // (the early-exit at c:6336-6339 of the recursive call).
11922 let tab = paramtab().read().ok()?;
11923 tab.get(&last).cloned().or(pm)
11924 }
11925 crate::ported::params::nameref_resolution::SelfRef => None, // c:6343
11926 crate::ported::params::nameref_resolution::OutOfScope => None, // c:6347-6349
11927 crate::ported::params::nameref_resolution::NotRef => pm,
11928 }
11929}
11930
11931/// ```c
11932/// Param pm = (Param) gethashnode2(realparamtab, name);
11933/// if (pm && (pm->node.flags & PM_NAMEREF)) {
11934/// if (pm->node.flags & PM_READONLY) {
11935/// zerr("read-only reference: %s", pm->node.nam); return;
11936/// }
11937/// pm->base = pm->width = 0;
11938/// SETREFNAME(pm, ztrdup(value));
11939/// pm->node.flags &= ~PM_UNSET;
11940/// setscope(pm);
11941/// } else
11942/// setsparam(name, ztrdup(value));
11943/// ```
11944/// `gethashnode2` is the no-autoload paramtab lookup. The
11945/// nameref branch updates the alias target in-place; the normal
11946/// branch falls through to `setsparam`.
11947/// Port of `setloopvar(char *name, char *value)` from `Src/params.c:6362`.
11948pub fn setloopvar(name: &str, value: &str) {
11949 // c:6367 — `Param pm = (Param) gethashnode2(realparamtab, name);`
11950 // realparamtab and paramtab are the same backing store in zshrs
11951 // (the alias-flip during assoc iteration isn't modelled); the
11952 // operative table is `paramtab()`.
11953 // Scope the write lock so we drop it before calling setsparam below.
11954 let nameref_branch = {
11955 let mut tab = paramtab().write().unwrap();
11956 if let Some(pm) = tab.get_mut(name) {
11957 // c:6369 — `if (pm && (pm->node.flags & PM_NAMEREF))`
11958 if (pm.node.flags as u32 & PM_NAMEREF) != 0 {
11959 // c:6370 — `if (pm->node.flags & PM_READONLY)`
11960 if (pm.node.flags as u32 & PM_READONLY) != 0 {
11961 // c:6372 — `zerr("read-only reference: %s", pm->node.nam);`
11962 zerr(&format!("read-only reference: {}", pm.node.nam));
11963 // c:6373 — `return;`
11964 return;
11965 }
11966 // c:6376 — `pm->base = pm->width = 0;`
11967 pm.base = 0;
11968 pm.width = 0;
11969 // c:6377 — `SETREFNAME(pm, ztrdup(value));`
11970 // SETREFNAME (params.c:482) macro: for PM_SPECIAL,
11971 // call gsu_s.setfn(pm, S); else free pm->u.str and
11972 // assign new. The PM_SPECIAL gsu vtable isn't fully
11973 // wired in zshrs; both branches collapse to the
11974 // direct `u_str` assignment which matches the
11975 // non-special path verbatim.
11976 pm.u_str = Some(value.to_string());
11977 // c:6378 — `pm->node.flags &= ~PM_UNSET;`
11978 pm.node.flags &= !(PM_UNSET as i32);
11979 true
11980 } else {
11981 false
11982 }
11983 } else {
11984 false
11985 }
11986 };
11987 if nameref_branch {
11988 // c:6379 — `setscope(pm);` — run the full-table variant with
11989 // no lock held (it manages its own short-lived locks).
11990 crate::ported::params::setscope_by_name(name);
11991 } else {
11992 // c:6381 — `setsparam(name, ztrdup(value));`
11993 setsparam(name, value);
11994 }
11995}
11996
11997/// PM_NAMEREF: extract `refname = GETREFNAME(pm)`, locate first
11998/// `[` to split name vs subscript (sets pm->width), look up the
11999/// base param via `gethashnode2(realparamtab, refname)` →
12000/// `loadparamnode` (skipping self) → `setscope_base(pm,
12001/// basepm->level)`; if pm->base > pm->level emits the KSH global
12002/// reference error or WARNNESTEDVAR diagnostic; finally walks the
12003/// `resolve_nameref_rec` chain to detect self-references.
12004/// In-place variant: operates only on the passed `&mut param`
12005/// (width computation + literal self-name check). Callers that
12006/// hold no paramtab lock should use `setscope_by_name` which runs
12007/// the full base-scope + chain self-ref detection against the
12008/// live table.
12009/// Port of `setscope(Param pm)` from `Src/params.c:6382`.
12010pub fn setscope(pm: &mut param) {
12011 queue_signals();
12012 if (pm.node.flags as u32 & PM_NAMEREF) != 0 {
12013 // Refname is stored in pm.u_str for nameref-typed params.
12014 let refname = pm.u_str.clone();
12015 if let Some(rn) = refname {
12016 // c:6391-6400 — compute pm->width by finding the first `[`.
12017 if let Some(i) = rn.find('[') {
12018 pm.width = i as i32;
12019 }
12020 }
12021 }
12022 unqueue_signals();
12023}
12024
12025/// ```c
12026/// if ((pm->base = base) > pm->level) {
12027/// LinkList refs;
12028/// /* grow scoperefs[] to base+1 entries */
12029/// refs = scoperefs[base];
12030/// if (!refs) refs = scoperefs[base] = znewlinklist();
12031/// zpushnode(refs, pm);
12032/// }
12033/// ```
12034/// Records `pm` on the per-scope reference list so a future
12035/// scope-pop can resolve nameref/upper bindings. Rust port
12036/// records the param's name on `SCOPEREFS[base]` so a future
12037/// scope-pop can resolve upper/nameref references.
12038/// Port of `setscope_base(Param pm, int base)` from `Src/params.c:6438`.
12039pub fn setscope_base(pm: &mut param, base: i32) {
12040 // c:6438
12041 // c:6440 — `if ((pm->base = base) > pm->level) {`
12042 pm.base = base;
12043 if base > pm.level {
12044 SCOPEREFS.with(|sr| {
12045 let mut sr = sr.borrow_mut();
12046 // c:6442-6447 — `if (base >= scoperefs_num) { ... grow ... }`
12047 // Rust Vec grows on demand via resize; mirrors
12048 // the C double-and-zero-init pattern via the
12049 // max(8, 2*base) growth heuristic.
12050 if (base as usize) >= sr.len() {
12051 let new_num = (2 * base as usize).max(8);
12052 sr.resize(new_num, Vec::new());
12053 }
12054 // c:6448-6451 — `refs = scoperefs[base]; if (!refs)
12055 // refs = scoperefs[base] = znewlinklist();
12056 // zpushnode(refs, pm);`
12057 // Rust pushes the param NAME (Vec<String>),
12058 // not a raw pointer — borrow-safe and the
12059 // name is the canonical key for upscope walks.
12060 sr[base as usize].insert(0, pm.node.nam.clone()); // c:6451
12061 });
12062 }
12063}
12064
12065/// `scoperefs` — port of `static LinkList *scoperefs` from `Src/params.c:503`.
12066/// One Vec<String> (param names) per scope index. Per-evaluator (bucket 1)
12067/// because each worker thread has its own nameref-resolution context.
12068thread_local! {
12069 /// `SCOPEREFS` static.
12070 pub static SCOPEREFS: std::cell::RefCell<Vec<Vec<String>>>
12071 = const { std::cell::RefCell::new(Vec::new()) };
12072}
12073
12074/// Port of `upscope(Param pm, const Param ref)` from `Src/params.c:6455`. C body:
12075/// ```c
12076/// if (ref->node.flags & PM_UPPER)
12077/// while (pm->level > ref->level - 1 && (pm = pm->old));
12078/// else
12079/// for (; pm->old && pm->old->level >= ref->base; pm = pm->old);
12080/// return pm;
12081/// ```
12082/// Walks `pm->old` chain to the param at the right scope depth
12083/// for a nameref. Rust signature mirrors C `Param upscope(Param,
12084/// const Param ref)`.
12085/// WARNING: param names don't match C — Rust=(pm, reference) vs C=(pm, ref)
12086pub fn upscope(mut pm: Param, reference: ¶m) -> Param {
12087 if (reference.node.flags as u32 & PM_UPPER) != 0 {
12088 while pm.level > reference.level - 1 {
12089 match pm.old.take() {
12090 Some(o) => pm = o,
12091 None => break,
12092 }
12093 }
12094 } else {
12095 loop {
12096 let next_level = pm.old.as_ref().map(|o| o.level);
12097 match next_level {
12098 Some(l) if l >= reference.base => {
12099 pm = pm.old.take().unwrap();
12100 }
12101 _ => break,
12102 }
12103 }
12104 }
12105 pm
12106}
12107
12108/// Port of `valid_refname(char *val, int flags)` from `Src/params.c:6466`. C body
12109/// validates a nameref target name. Two paths:
12110/// - PM_UPPER (`typeset -nu`): reject digit-leader (positional
12111/// refs would loop) and the literal `argv`/`ARGC` names.
12112/// - non-PM_UPPER: positional digit-leader is permitted (must be
12113/// all-digits before any `[`); otherwise scan via
12114/// `itype_end(INAMESPC)`.
12115/// Either path then accepts the trailing one-char specials
12116/// `! ? $ - _` and an optional `[subscript]` tail. Returns 1 on
12117/// valid, 0 otherwise. The Rust port follows the same control
12118/// flow with `is_ascii_digit`/`is_alphabetic` standing in for
12119/// `idigit`/`itype_end`.
12120pub fn valid_refname(val: &str, flags: i32) -> bool {
12121 // c:6466
12122 if val.is_empty() {
12123 return false;
12124 }
12125 let first = val.chars().next().unwrap();
12126 let pm_upper = (flags as u32 & PM_UPPER) != 0;
12127 let mut t: usize;
12128 if pm_upper {
12129 // c:6470
12130 if first.is_ascii_digit() {
12131 // c:6472
12132 return false; // c:6473
12133 }
12134 // c:6474 — `t = itype_end(val, INAMESPC, 0)`; INAMESPC stops
12135 // at `.` and other non-namespace chars. Approximate with
12136 // alphanumeric/_ scan.
12137 t = val
12138 .char_indices()
12139 .find(|(_, c)| !(c.is_alphanumeric() || *c == '_'))
12140 .map(|(i, _)| i)
12141 .unwrap_or(val.len());
12142 if t - 0 == 4 // c:6475
12143 && (val.starts_with("argv") || val.starts_with("ARGC"))
12144 // c:6476-6477
12145 {
12146 return false; // c:6478
12147 }
12148 } else if first.is_ascii_digit() {
12149 // c:6479
12150 // c:6480-6485 — all-digit run; first non-digit must be `[`.
12151 t = 1;
12152 for (i, c) in val.char_indices().skip(1) {
12153 if !c.is_ascii_digit() {
12154 t = i;
12155 break;
12156 }
12157 t = i + c.len_utf8();
12158 }
12159 if t < val.len() && val.as_bytes()[t] != b'[' {
12160 // c:6484
12161 return false; // c:6485
12162 }
12163 } else {
12164 // c:6487 — `t = itype_end(val, INAMESPC, 0)`.
12165 t = val
12166 .char_indices()
12167 .find(|(_, c)| !(c.is_alphanumeric() || *c == '_' || *c == '.'))
12168 .map(|(i, _)| i)
12169 .unwrap_or(val.len());
12170 }
12171
12172 if t == 0 {
12173 // c:6489
12174 let c = val.as_bytes()[0];
12175 if !(c == b'!' || c == b'?' || c == b'$' || c == b'-' || c == b'_') {
12176 // c:6490
12177 return false; // c:6493
12178 }
12179 t = 1; // c:6494
12180 }
12181 if t < val.len() && val.as_bytes()[t] == b'[' {
12182 // c:6496
12183 // c:6498-6504 — parse_subscript/Inbrack/Outbrack walk. The
12184 // tokenize+parse_subscript pair isn't ported; accept any
12185 // balanced `[…]` tail (single-level) to remain conservative.
12186 let tail = &val[t + 1..];
12187 if let Some(close) = tail.find(']') {
12188 // c:6505-6508 — anything past `]` is rejected.
12189 if close + 1 < tail.len() {
12190 return false;
12191 }
12192 } else {
12193 return false;
12194 }
12195 }
12196 true // c:6510
12197}
12198
12199/// Read `foundparam`. Returns the last param name observed by
12200/// `scanparamvals`; cleared by callers after consumption.
12201pub fn foundparam() -> Option<String> {
12202 foundparam_lock().lock().unwrap().clone()
12203}
12204
12205/// Set `foundparam`. Called from `scanparamvals`.
12206pub fn set_foundparam(nam: Option<String>) {
12207 *foundparam_lock().lock().unwrap() = nam;
12208}
12209
12210/// Port of `fetchvalue(Value v, char **pptr, int bracks, int scanflags)` from `Src/params.c:2180` — see real
12211/// implementation below; this slot kept for the C-source linenum
12212/// citation and is now an alias.
12213// (real fetchvalue is defined later)
12214
12215/// Port of `static int delunset;` from `Src/params.c:610`. Flag
12216/// `deleteparamtable` flips to 1 around the inner `deletehashtable`
12217/// call so each freed node runs its `unsetfn`. `freeparamnode`
12218/// consults this before invoking the unset hook (c:5986).
12219pub static DELUNSET: std::sync::atomic::AtomicI32 = // c:610
12220 std::sync::atomic::AtomicI32::new(0);
12221
12222pub(crate) fn paramtab_hashed_storage() -> &'static Mutex<HashMap<String, IndexMap<String, String>>>
12223{
12224 PARAMTAB_HASHED_STORAGE_INNER.get_or_init(|| Mutex::new(HashMap::new()))
12225}
12226
12227/// Shadow stack for `paramtab_hashed_storage` entries displaced by
12228/// `local -A NAME` / `typeset -A NAME` shadows inside a function. The
12229/// canonical assoc data lives in `paramtab_hashed_storage` (a flat
12230/// HashMap keyed by name with NO scope dimension — Rust-only parallel
12231/// store; the C side keeps assoc data in pm.u_hash so it rides the
12232/// pm.old chain automatically). createparam pushes the displaced
12233/// value when a PM_LOCAL|PM_HASHED shadow installs; endparamscope
12234/// pops on PM_HASHED restoration so the outer scope's bag comes
12235/// back. Mirrors C's `copyparam` (Src/builtin.c:2382-2424) via
12236/// parallel storage. Bug #415.
12237pub(crate) static PARAMTAB_HASHED_SHADOW_STACK: OnceLock<
12238 Mutex<HashMap<String, Vec<Option<IndexMap<String, String>>>>>,
12239> = OnceLock::new();
12240
12241/// Mirror the global `paramtab` (and the parallel hashed-storage
12242/// table) into the three HashMaps that `SubstState` uses as its
12243/// transient backing during `prefork()` (Src/subst.c:100). This
12244/// is a port-transition shim: once `subst.rs` reads parameters
12245/// directly through `paramtab().read()` / `.write()` instead of carrying
12246/// `state.variables`/`state.arrays`/`state.assoc_arrays`, this
12247/// helper goes away.
12248pub fn sync_state_from_paramtab(
12249 variables: &mut HashMap<String, String>,
12250 arrays: &mut HashMap<String, Vec<String>>,
12251 assoc_arrays: &mut HashMap<String, IndexMap<String, String>>,
12252) {
12253 let tab = paramtab().read().unwrap();
12254 for (name, pm) in tab.iter() {
12255 let f = pm.node.flags as u32;
12256 if (f & PM_ARRAY) != 0 {
12257 if let Some(arr) = pm.u_arr.as_ref() {
12258 arrays.insert(name.clone(), arr.clone());
12259 }
12260 variables.remove(name);
12261 assoc_arrays.remove(name);
12262 } else if (f & PM_HASHED) != 0 {
12263 if let Some(map) = paramtab_hashed_storage().lock().unwrap().get(name) {
12264 assoc_arrays.insert(name.clone(), map.clone());
12265 }
12266 variables.remove(name);
12267 arrays.remove(name);
12268 } else if let Some(s) = pm.u_str.as_ref() {
12269 // PM_SCALAR / PM_NAMEREF / numeric — fold to the string view.
12270 variables.insert(name.clone(), s.clone());
12271 arrays.remove(name);
12272 assoc_arrays.remove(name);
12273 }
12274 }
12275}
12276
12277/// Format float with underscores
12278pub fn convfloat_underscore(dval: f64, underscore: i32) -> String {
12279 let s = convfloat(dval, 0, 0);
12280 if underscore <= 0 {
12281 return s;
12282 }
12283
12284 let u = underscore as usize;
12285 let (sign, rest) = if let Some(after) = s.strip_prefix('-') {
12286 ("-", after)
12287 } else {
12288 ("", s.as_str())
12289 };
12290
12291 let (int_part, frac_exp) = if let Some(dot_pos) = rest.find('.') {
12292 (&rest[..dot_pos], &rest[dot_pos..])
12293 } else {
12294 (rest, "")
12295 };
12296
12297 // Add underscores to integer part
12298 let int_chars: Vec<char> = int_part.chars().collect();
12299 let mut result = sign.to_string();
12300 let first_group = int_chars.len() % u;
12301 if first_group > 0 {
12302 result.extend(&int_chars[..first_group]);
12303 if first_group < int_chars.len() {
12304 result.push('_');
12305 }
12306 }
12307 for (i, chunk) in int_chars[first_group..].chunks(u).enumerate() {
12308 if i > 0 {
12309 result.push('_');
12310 }
12311 result.extend(chunk);
12312 }
12313
12314 // Add underscores to fractional part
12315 if let Some(frac) = frac_exp.strip_prefix('.') {
12316 result.push('.');
12317 let (frac_digits, exp) = if let Some(e_pos) = frac.find('e') {
12318 (&frac[..e_pos], &frac[e_pos..])
12319 } else {
12320 (frac, "")
12321 };
12322
12323 let frac_chars: Vec<char> = frac_digits.chars().collect();
12324 for (i, chunk) in frac_chars.chunks(u).enumerate() {
12325 if i > 0 {
12326 result.push('_');
12327 }
12328 result.extend(chunk);
12329 }
12330 result.push_str(exp);
12331 } else {
12332 result.push_str(frac_exp);
12333 }
12334
12335 result
12336}
12337
12338pub(crate) fn ifs_lock() -> &'static Mutex<String> {
12339 static IFS_VAR: OnceLock<Mutex<String>> = OnceLock::new();
12340 IFS_VAR.get_or_init(|| Mutex::new(" \t\n\0".to_string()))
12341}
12342
12343fn home_lock() -> &'static Mutex<String> {
12344 static HOME_VAR: OnceLock<Mutex<String>> = OnceLock::new();
12345 HOME_VAR.get_or_init(|| Mutex::new(env::var("HOME").unwrap_or_default()))
12346}
12347
12348fn term_lock() -> &'static Mutex<String> {
12349 static TERM_VAR: OnceLock<Mutex<String>> = OnceLock::new();
12350 TERM_VAR.get_or_init(|| Mutex::new(env::var("TERM").unwrap_or_default()))
12351}
12352
12353pub(crate) fn wordchars_lock() -> &'static Mutex<String> {
12354 static WORDCHARS_VAR: OnceLock<Mutex<String>> = OnceLock::new();
12355 WORDCHARS_VAR.get_or_init(|| Mutex::new("*?_-.[]~=/&;!#$%^(){}<>".to_string()))
12356}
12357
12358fn histchars_lock() -> &'static Mutex<[u8; 3]> {
12359 static HISTCHARS_VAR: OnceLock<Mutex<[u8; 3]>> = OnceLock::new();
12360 HISTCHARS_VAR.get_or_init(|| Mutex::new([b'!', b'^', b'#']))
12361}
12362
12363fn keyboardhack_lock() -> &'static Mutex<u8> {
12364 static KEYBOARDHACK_VAR: OnceLock<Mutex<u8>> = OnceLock::new();
12365 KEYBOARDHACK_VAR.get_or_init(|| Mutex::new(0))
12366}
12367
12368fn histsiz_lock() -> &'static Mutex<i64> {
12369 static HISTSIZ_VAR: OnceLock<Mutex<i64>> = OnceLock::new();
12370 // Match observed `zsh -fc 'echo $HISTSIZE'` output on zsh 5.9+
12371 // (Homebrew). Upstream's `configure.ac` defines DEFAULT_HISTSIZE
12372 // as 30 but distributed binaries seed the cap at 999999999 — the
12373 // parity goal here is "match the binary the user actually runs",
12374 // not "match the source-code default".
12375 HISTSIZ_VAR.get_or_init(|| Mutex::new(999_999_999))
12376}
12377
12378fn savehistsiz_lock() -> &'static Mutex<i64> {
12379 static SAVEHISTSIZ_VAR: OnceLock<Mutex<i64>> = OnceLock::new();
12380 // Same rationale as `histsiz_lock` — observed `zsh -fc
12381 // 'echo $SAVEHIST'` returns 99999999 on zsh 5.9+. Source has
12382 // savehistsiz default to 0 but distributed binaries cap at 99M.
12383 SAVEHISTSIZ_VAR.get_or_init(|| Mutex::new(99_999_999))
12384}
12385
12386fn zsh_terminfo_lock() -> &'static Mutex<String> {
12387 static TERMINFO_VAR: OnceLock<Mutex<String>> = OnceLock::new();
12388 TERMINFO_VAR.get_or_init(|| Mutex::new(env::var("TERMINFO").unwrap_or_default()))
12389}
12390
12391fn zsh_terminfodirs_lock() -> &'static Mutex<String> {
12392 static TERMINFODIRS_VAR: OnceLock<Mutex<String>> = OnceLock::new();
12393 TERMINFODIRS_VAR.get_or_init(|| Mutex::new(env::var("TERMINFO_DIRS").unwrap_or_default()))
12394}
12395
12396fn cached_username_lock() -> &'static Mutex<String> {
12397 static USERNAME_VAR: OnceLock<Mutex<String>> = OnceLock::new();
12398 USERNAME_VAR.get_or_init(|| Mutex::new(initial_username()))
12399}
12400
12401// Port of `static unsigned numparamvals;` (params.c:626) and the
12402// related per-scan statics at params.c:637-640. Per PORT.md Rule D
12403// these are file-scope statics, NOT aggregated into a state struct.
12404//
12405// c:626 static unsigned numparamvals;
12406// c:637 static Patprog scanprog;
12407// c:638 static char *scanstr;
12408// c:639 static char **paramvals;
12409// c:640 static Param foundparam; <-- exposed earlier as FOUNDPARAM
12410/// `NUMPARAMVALS` static.
12411pub static NUMPARAMVALS: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0); // c:626
12412/// `SCANPROG` static.
12413pub static SCANPROG: OnceLock<Mutex<Option<String>>> = OnceLock::new(); // c:637
12414/// `SCANSTR` static.
12415pub static SCANSTR: OnceLock<Mutex<Option<String>>> = OnceLock::new(); // c:638
12416/// `PARAMVALS` static.
12417pub static PARAMVALS: OnceLock<Mutex<Vec<String>>> = OnceLock::new(); // c:639
12418
12419/// Resolve the current user's name. Mirrors C's `get_username()`
12420/// init at Src/init.c which reads `getpwuid(getuid())->pw_name`
12421/// rather than `$USER`. Falls back to env vars only if the
12422/// passwd lookup fails (rare on real systems).
12423fn initial_username() -> String {
12424 #[cfg(unix)]
12425 {
12426 let uid = unsafe { libc::getuid() };
12427 let mut pwd: libc::passwd = unsafe { std::mem::zeroed() };
12428 // libc::c_char is i8 on x86_64/aarch64-darwin and x86_64-linux but u8 on
12429 // aarch64-linux. Use c_char so getpwuid_r's pointer type matches per-target.
12430 let mut buf: Vec<libc::c_char> = vec![0; 1024];
12431 let mut result: *mut libc::passwd = std::ptr::null_mut();
12432 let rc =
12433 unsafe { libc::getpwuid_r(uid, &mut pwd, buf.as_mut_ptr(), buf.len(), &mut result) };
12434 if rc == 0 && !result.is_null() && !pwd.pw_name.is_null() {
12435 let cstr = unsafe { std::ffi::CStr::from_ptr(pwd.pw_name) };
12436 return cstr.to_string_lossy().into_owned();
12437 }
12438 }
12439 env::var("USER")
12440 .or_else(|_| env::var("LOGNAME"))
12441 .unwrap_or_default()
12442}
12443
12444fn pipestats_lock() -> &'static Mutex<Vec<i32>> {
12445 static PIPESTATS_VAR: OnceLock<Mutex<Vec<i32>>> = OnceLock::new();
12446 PIPESTATS_VAR.get_or_init(|| Mutex::new(Vec::new()))
12447}
12448/// `shtimer_lock` — see implementation.
12449pub fn shtimer_lock() -> &'static Mutex<Duration> {
12450 static SHTIMER_VAR: OnceLock<Mutex<Duration>> = OnceLock::new();
12451 SHTIMER_VAR.get_or_init(|| {
12452 Mutex::new(
12453 SystemTime::now()
12454 .duration_since(UNIX_EPOCH)
12455 .unwrap_or_default(),
12456 )
12457 })
12458}
12459
12460fn pparams_lock() -> &'static Mutex<Vec<String>> {
12461 // Mirror of zsh's `pparams` (positional params $1, $2, ...).
12462 // Used by `poundgetfn` for `$#`. The canonical store is
12463 // `builtin::PPARAMS` (Src/init.c `pparams`); set/shift builtins
12464 // write there. Point at that single store so `$#` reads the
12465 // live value instead of an isolated empty mirror.
12466 &PPARAMS
12467}
12468
12469fn zunderscore_lock() -> &'static Mutex<String> {
12470 static ZUNDERSCORE_VAR: OnceLock<Mutex<String>> = OnceLock::new();
12471 ZUNDERSCORE_VAR.get_or_init(|| Mutex::new(String::new()))
12472}
12473
12474/// Update `$_` with the last argument of the just-completed
12475/// command. Mirrors C zsh's writeback in `execcmd_exec` (Src/exec.c)
12476/// where `zunderscore` is set to the last argv slot before
12477/// returning. Callers: every command-dispatch hook in
12478/// fusevm_bridge / vm_helper.
12479pub fn set_zunderscore(argv: &[String]) {
12480 let new = if let Some(last) = argv.last() {
12481 last.clone()
12482 } else {
12483 String::new()
12484 };
12485 *zunderscore_lock().lock().expect("zunderscore poisoned") = new;
12486}
12487
12488/// Direct port of `static int dontimport(int flags)` from
12489/// `Src/params.c:796-810`.
12490/// ```c
12491/// /* If explicitly marked as don't import */
12492/// if (flags & PM_DONTIMPORT)
12493/// return 1;
12494/// /* If value already exported */
12495/// if (flags & PM_EXPORTED)
12496/// return 1;
12497/// /* If security issue when importing and running with some privilege */
12498/// if ((flags & PM_DONTIMPORT_SUID) && isset(PRIVILEGED))
12499/// return 1;
12500/// /* OK to import */
12501/// return 0;
12502/// ```
12503/// Port of `dontimport(int flags)` from `Src/params.c:796`.
12504fn dontimport(flags: i32) -> i32 {
12505 // c:796
12506 let flags = flags as u32;
12507 // c:799-800 — `if (flags & PM_DONTIMPORT) return 1`.
12508 if flags & PM_DONTIMPORT != 0 {
12509 // c:799
12510 return 1; // c:800
12511 }
12512 // c:802-803 — `if (flags & PM_EXPORTED) return 1`.
12513 if flags & PM_EXPORTED != 0 {
12514 // c:802
12515 return 1; // c:803
12516 }
12517 // c:805-806 — `if ((flags & PM_DONTIMPORT_SUID) && isset(PRIVILEGED)) return 1`.
12518 if flags & PM_DONTIMPORT_SUID != 0 // c:805
12519 && isset(PRIVILEGED)
12520 {
12521 return 1; // c:806
12522 }
12523 0 // c:809
12524}
12525
12526// ===========================================================
12527// GSU dispatch table — maps special-parameter NAMES to their
12528// getfn callback. C zsh dispatches reads of `$RANDOM` /
12529// `$USERNAME` / `$UID` / etc. through `Param.gsu->getfn`, where
12530// each special parameter has a `Param` entry in `paramtab`
12531// pointing at its specific getfn (Src/params.c:225 SPECIAL_PARAM
12532// table seeds these mappings).
12533//
12534// zshrs has the GSU callbacks ported (uidgetfn, randomgetfn,
12535// usernamegetfn, etc. above) but the shell's parameter-read path
12536// (fusevm_bridge::expand_param) reads from ShellExecutor.variables
12537// directly — never dispatching through the callbacks. Result:
12538// `echo $RANDOM` returned the cached HashMap value (or empty),
12539// not a fresh `rand() & 0x7fff` from `randomgetfn`.
12540//
12541// `lookup_special_var(name)` is the bridge: given a variable
12542// name, returns the GSU getfn's output if `name` is a recognized
12543// special, else None. Callers (expand_param, subst.rs reads)
12544// check this before falling back to `variables.get(name)`.
12545// ===========================================================
12546
12547/// Registry of special-parameter names that have been `unset` and
12548/// should bypass the getfn regenerator.
12549///
12550/// c:Src/params.c:3853 — `unsetparam_pm` sets `PM_UNSET` on the pm
12551/// node which getfn callbacks check. zshrs's `lookup_special_var`
12552/// dispatches to libc/getfn directly without a paramtab pm node, so
12553/// PM_UNSET tracking happens in this side-set instead. Bug #417/#418.
12554fn unset_specials() -> &'static std::sync::Mutex<std::collections::HashSet<String>> {
12555 static SET: std::sync::OnceLock<std::sync::Mutex<std::collections::HashSet<String>>> =
12556 std::sync::OnceLock::new();
12557 SET.get_or_init(|| std::sync::Mutex::new(std::collections::HashSet::new()))
12558}
12559
12560/// Mark a special-parameter NAME as unset. Future
12561/// `lookup_special_var(name)` calls return `None` instead of dispatching
12562/// to the getfn regenerator. Re-assigning the name (e.g. `RANDOM=42`)
12563/// should clear this flag via `clear_unset_special`.
12564pub fn mark_unset_special(name: &str) {
12565 if let Ok(mut s) = unset_specials().lock() {
12566 s.insert(name.to_string());
12567 }
12568}
12569
12570/// Clear the unset flag for a special-parameter NAME — called when the
12571/// name is re-assigned so the getfn regenerator becomes active again.
12572pub fn clear_unset_special(name: &str) {
12573 if let Ok(mut s) = unset_specials().lock() {
12574 s.remove(name);
12575 }
12576}
12577
12578fn is_unset_special(name: &str) -> bool {
12579 unset_specials()
12580 .lock()
12581 .map(|s| s.contains(name))
12582 .unwrap_or(false)
12583}
12584
12585/// Look up a special-parameter NAME and dispatch to its GSU getfn.
12586///
12587/// Returns `Some(value_string)` if `name` is one of zshrs's
12588/// recognized specials with a real GSU getfn; `None` otherwise
12589/// (caller should fall back to `variables.get`).
12590///
12591/// This is the bridge between the named getfn callbacks above
12592/// (uidgetfn / randomgetfn / etc.) and the shell's parameter-read
12593/// path. Mirrors the `Param.gsu->getfn` dispatch C zsh does
12594/// inside `getsparam` / `getstrvalue` (Src/params.c:3076 / 2335).
12595pub fn lookup_special_var(name: &str) -> Option<String> {
12596 // c:Src/params.c:3853 — PM_UNSET-flagged specials skip getfn.
12597 // Only applies to regenerator-style specials (RANDOM, SECONDS,
12598 // EPOCHSECONDS, TTYIDLE, ERRNO) — identity specials like UID, GID,
12599 // PPID stay live since they're not user-clearable in zsh either.
12600 //
12601 // Two sources of "is unset" — the side-set populated by
12602 // `mark_unset_special` from explicit `unset NAME`, and the
12603 // initial PM_UNSET flag set by `Src/params.c:298 IPDEF1(...,
12604 // PM_UNSET)`. ERRNO is the only IPDEF1-special with the initial
12605 // PM_UNSET flag (params.c:298) — zsh -fc reports `$ERRNO` as
12606 // empty because nothing has set errno since startup. Mirror by
12607 // also consulting the paramtab pm flags. Without this, `$ERRNO`
12608 // returned the live errno value instead of empty.
12609 if matches!(
12610 name,
12611 "RANDOM" | "SECONDS" | "EPOCHSECONDS" | "EPOCHREALTIME" | "TTYIDLE" | "ERRNO"
12612 ) && (is_unset_special(name) || {
12613 // c:Src/params.c — paramtab PM_UNSET check. ERRNO carries
12614 // this flag from IPDEF1 initialization (params.c:298); reads
12615 // route through getsparam → paramtab pm.flags check at
12616 // line 4356-4358 normally, but ERRNO/RANDOM/etc. short-
12617 // circuit through lookup_special_var BEFORE that check.
12618 paramtab()
12619 .read()
12620 .ok()
12621 .and_then(|t| t.get(name).map(|pm| (pm.node.flags as u32 & PM_UNSET) != 0))
12622 .unwrap_or(false)
12623 }) {
12624 return None;
12625 }
12626 // All-digit positional: $1..$N from canonical PPARAMS.
12627 // C zsh dispatches positional params through pparams (Src/init.c).
12628 if !name.is_empty() && name.chars().all(|c| c.is_ascii_digit()) {
12629 let n: usize = name.parse().ok()?;
12630 if n == 0 {
12631 return argzero();
12632 }
12633 let pp = pparams_lock().lock().ok()?;
12634 return pp.get(n - 1).cloned();
12635 }
12636 match name {
12637 // libc identity callbacks.
12638 "UID" => Some(uidgetfn().to_string()),
12639 "GID" => Some(gidgetfn().to_string()),
12640 "EUID" => Some(euidgetfn().to_string()),
12641 "EGID" => Some(egidgetfn().to_string()),
12642 // c:Src/params.c:350 `IPDEF4("PPID", &ppid)` — ppid is the
12643 // file-static set at shell startup from getppid(). zshrs's
12644 // ported special_paramdef list registers PPID but nothing
12645 // populates the paramtab slot from getppid(2), so $PPID
12646 // always read 0. Route through the libc syscall directly.
12647 "PPID" => Some((unsafe { libc::getppid() } as i64).to_string()),
12648 // libc syscall callbacks.
12649 "RANDOM" => Some(randomgetfn().to_string()),
12650 "TTYIDLE" => Some(ttyidlegetfn().to_string()),
12651 "ERRNO" => Some(errnogetfn().to_string()),
12652 // Time callbacks.
12653 "SECONDS" => {
12654 // c:Src/params.c:4561/4591 — PM_TYPE dispatches between
12655 // intsecondsgetfn (PM_INTEGER, default) and floatsecondsgetfn
12656 // (PM_EFLOAT/PM_FFLOAT after `typeset -F`/`-E SECONDS`). The
12657 // pm's gsu_i vs gsu_f vtable swap happens in setfn during
12658 // typeset; we read pm.flags from paramtab here.
12659 let pm_type = paramtab()
12660 .read()
12661 .ok()
12662 .and_then(|t| t.get("SECONDS").map(|pm| PM_TYPE(pm.node.flags as u32)))
12663 .unwrap_or(PM_INTEGER);
12664 if pm_type == PM_EFLOAT || pm_type == PM_FFLOAT {
12665 let v = floatsecondsgetfn();
12666 let base = paramtab()
12667 .read()
12668 .ok()
12669 .and_then(|t| t.get("SECONDS").map(|pm| pm.base))
12670 .unwrap_or(0);
12671 Some(convfloat(v, base, pm_type))
12672 } else {
12673 Some(intsecondsgetfn().to_string())
12674 }
12675 }
12676 // zsh/datetime module params. In recent zsh these are
12677 // auto-loaded via `zmodload zsh/datetime` and the param
12678 // appears in paramtab via the module's `p:NAME` feature
12679 // descriptor (Src/Modules/datetime.c:25). In zshrs the
12680 // datetime module's per-param wireup hasn't been ported
12681 // through paramtab yet — the getters exist
12682 // (modules::datetime::getcurrentsecs / getcurrentrealtime)
12683 // but no Param entry is created on `zmodload`. Mirror the C
12684 // behavior by routing the names through their canonical
12685 // getters here so `$EPOCHSECONDS` / `$EPOCHREALTIME` read
12686 // live time values regardless of explicit zmodload. p10k
12687 // and many other prompts rely on this without an explicit
12688 // zmodload (zsh ships datetime preloaded in most configs).
12689 "EPOCHSECONDS" => {
12690 // c:Src/Modules/datetime.c:206 `getcurrentsecs`. zsh requires
12691 // explicit `zmodload zsh/datetime` before EPOCHSECONDS is
12692 // bound (Src/Modules/datetime.c:25 `p:EPOCHSECONDS` feature
12693 // descriptor). Without the load, the name is unset and
12694 // ${EPOCHSECONDS:-x} falls through to "x". Match by gating
12695 // the getter on the module's loaded state. Bug #31 in
12696 // docs/BUGS.md.
12697 if !crate::ported::module::MODULESTAB
12698 .lock()
12699 .unwrap()
12700 .is_loaded("zsh/datetime")
12701 {
12702 return None;
12703 }
12704 Some(crate::ported::modules::datetime::getcurrentsecs().to_string())
12705 }
12706 "EPOCHREALTIME" => {
12707 // c:Src/Modules/datetime.c:212 `getcurrentrealtime`. Same
12708 // zsh/datetime gate as EPOCHSECONDS above. Bug #31.
12709 if !crate::ported::module::MODULESTAB
12710 .lock()
12711 .unwrap()
12712 .is_loaded("zsh/datetime")
12713 {
12714 return None;
12715 }
12716 let v = crate::ported::modules::datetime::getcurrentrealtime();
12717 Some(format!("{:.10}", v))
12718 }
12719 "epochtime" => {
12720 // c:Src/Modules/datetime.c:220 `getcurrenttime` returns
12721 // [tv_sec, tv_nsec]. Bare `$epochtime` joins the two with
12722 // ` ` (the default IFS separator), matching the C path
12723 // `getstrvalue` → sepjoin on PM_ARRAY. Bug #317 in
12724 // docs/BUGS.md. Gate on zsh/datetime load per #31.
12725 if !crate::ported::module::MODULESTAB
12726 .lock()
12727 .unwrap()
12728 .is_loaded("zsh/datetime")
12729 {
12730 return None;
12731 }
12732 let arr = crate::ported::modules::datetime::getcurrenttime();
12733 Some(arr.join(" "))
12734 }
12735 // Cached-state callbacks. C dispatches `pm->gsu.s->getfn(pm)`
12736 // where pm is `paramtab->getnode(name)`. Mirror: look up pm,
12737 // pass it through. Each getfn here ignores pm (matches C's
12738 // UNUSED(Param pm)), so a fallback default-constructed param
12739 // is acceptable when the table isn't populated yet.
12740 //
12741 // c:Src/params.c paramsubst c:3193 — the `vunset = (!v || ...)`
12742 // check inspects `pm->node.flags & PM_UNSET`, not the value
12743 // returned by getfn. For specials whose getfn reads global
12744 // cached state (`ifs`, `wordchars`, `keyboardhackchar`), the
12745 // global keeps its last value across `unset NAME` (because
12746 // stdunsetfn only flips PM_UNSET; it doesn't clear the global).
12747 // Without consulting PM_UNSET here, `${IFS+set}` after
12748 // `unset IFS` still returned the default-IFS getter result,
12749 // diverging from zsh.
12750 "USERNAME" | "HOME" | "TERM" | "WORDCHARS" | "IFS" | "TERMINFO" | "TERMINFO_DIRS"
12751 | "KEYBOARD_HACK" | "histchars" | "HISTCHARS" => {
12752 let tab = paramtab().read().ok()?;
12753 let pm = tab.get(name)?;
12754 if (pm.node.flags as u32 & crate::ported::zsh_h::PM_UNSET) != 0 {
12755 return None;
12756 }
12757 Some(match name {
12758 "USERNAME" => usernamegetfn(pm),
12759 "HOME" => homegetfn(pm),
12760 "TERM" => termgetfn(pm),
12761 "WORDCHARS" => wordcharsgetfn(pm),
12762 "IFS" => ifsgetfn(pm),
12763 "TERMINFO" => terminfogetfn(pm),
12764 "TERMINFO_DIRS" => terminfodirsgetfn(pm),
12765 "KEYBOARD_HACK" => keyboardhackgetfn(pm),
12766 "histchars" | "HISTCHARS" => histcharsgetfn(pm),
12767 _ => unreachable!(),
12768 })
12769 }
12770 "_" => Some(underscoregetfn()),
12771 // Counters with int return.
12772 "HISTSIZE" => Some(histsizegetfn().to_string()),
12773 "SAVEHIST" => Some(savehistsizegetfn().to_string()),
12774 "#" | "ARGC" => Some(poundgetfn().to_string()),
12775 // c:Src/params.c:871 — `setsparam("TIMEFMT", DEFAULT_TIMEFMT)`.
12776 // createparamtable seeds this; the zshrs main binary skips
12777 // that init path so paramtab is empty for TIMEFMT. Route
12778 // here from the canonical default. paramtab check first so
12779 // an explicit user-set value sticks.
12780 "TIMEFMT" => {
12781 let tab_val = paramtab()
12782 .read()
12783 .ok()
12784 .and_then(|t| t.get("TIMEFMT").and_then(|pm| pm.u_str.clone()));
12785 if let Some(v) = tab_val {
12786 if !v.is_empty() {
12787 return Some(v);
12788 }
12789 }
12790 Some(crate::ported::zsh_system_h::DEFAULT_TIMEFMT.to_string())
12791 }
12792 // c:Src/init.c:1214-1215 — `nullcmd = ztrdup("cat");
12793 // readnullcmd = ztrdup(DEFAULT_READNULLCMD);`. C seeds these
12794 // into the REAL paramtab at startup; `unset NULLCMD` then
12795 // removes the entry and getsparam returns NULL — which is
12796 // load-bearing: A04redirect "null redir with NULLCMD unset"
12797 // requires `unset NULLCMD; >file` to error "redirection with
12798 // no command". The previous read-time fallback here faked the
12799 // seed on EVERY lookup, making unset impossible. The seed now
12800 // lives in ShellExecutor::new (vm_helper.rs, next to TIMEFMT)
12801 // and absent means absent.
12802 // $0 routes through utils::argzero.
12803 "0" => argzero(),
12804 // POSIX shell-special scalars. C dispatches these through
12805 // dedicated gsu getfn callbacks (Src/params.c special_assigns).
12806 // c:Src/params.c lastvalgetfn — `?` and `status` are aliases
12807 // for the last-command exit code (lastval). C wires them via
12808 // separate IPDEF entries that share the same getfn.
12809 "?" | "status" => Some(LASTVAL.load(Ordering::Relaxed).to_string()),
12810 // c:Src/loop.c:719 — `try_errflag = -1` reset before
12811 // each `{ try } always { catch }` block; reads `-1` when
12812 // outside a try block. zsh exposes TRY_BLOCK_ERROR as an
12813 // integer special-param: inside an always-arm after a
12814 // normal-exit try, reads 0; -1 only when no try has yet
12815 // fired in this scope. BUILTIN_SET_TRY_BLOCK_ERROR writes
12816 // via set_scalar (u_str), so accept either storage form
12817 // and treat a present-but-empty u_str as 0 too.
12818 "TRY_BLOCK_ERROR" => {
12819 // c:Src/loop.c:719 — `zlong try_errflag = -1;` global,
12820 // exported via IPDEF6 (c:Src/params.c:364) so `$TRY_BLOCK_ERROR`
12821 // reads it directly. Initialized to -1 (sentinel: "no try
12822 // block has fired yet"); the `exectry` always-arm sets it
12823 // to `errflag & ERRFLAG_ERROR` (c:765) before running the
12824 // body, then restores at c:787. Read straight from the
12825 // canonical atomic — paramtab's u_str / u_val are NOT the
12826 // source of truth (matches C's IPDEF6 getfn signature).
12827 Some(
12828 crate::ported::r#loop::try_errflag
12829 .load(std::sync::atomic::Ordering::Relaxed)
12830 .to_string(),
12831 )
12832 }
12833 "TRY_BLOCK_INTERRUPT" => {
12834 // c:Src/loop.c:727 — `zlong try_interrupt = -1;` global.
12835 // Same shape as TRY_BLOCK_ERROR.
12836 Some(
12837 crate::ported::r#loop::try_interrupt
12838 .load(std::sync::atomic::Ordering::Relaxed)
12839 .to_string(),
12840 )
12841 }
12842 "$" => Some(std::process::id().to_string()),
12843 "!" => {
12844 // c:Src/params.c:345 IPDEF4("!", &lastpid) — `$!` reads
12845 // directly from the `lastpid` atomic (Src/jobs.c:73).
12846 // The previous Rust port read from paramtab["!"].u_str,
12847 // which only had a value if something wrote to it via
12848 // setsparam/assignsparam — and `"!"` is not a valid
12849 // identifier (isident("!") == 0 per params.c:1288), so
12850 // those calls failed loudly. Read directly from the
12851 // canonical store like the C getter does.
12852 let pid =
12853 crate::ported::modules::clone::lastpid.load(std::sync::atomic::Ordering::Relaxed);
12854 Some(pid.to_string())
12855 }
12856 // $* / $@ join positional params via sepjoin's IFS default —
12857 // c:Src/utils.c:3936-3945: set-but-empty IFS joins with ""
12858 // (`IFS=""; echo "$*"` concatenates); unset IFS joins with
12859 // " ". The previous `.unwrap_or(' ')` collapsed empty-IFS to
12860 // a space.
12861 "*" | "@" => pparams_lock()
12862 .lock()
12863 .ok()
12864 .map(|p| crate::ported::utils::sepjoin(&p, None)),
12865 // $- : current option-letter set.
12866 // c:Src/params.c:3262 (IPDEF) → dashparamgetfn in options.c:890.
12867 // Canonical C body walks `zshletters[FIRST_OPT..=LAST_OPT]`
12868 // (c:292-368) emitting each letter whose mapped option is
12869 // active (XOR-ing with the c:295 negation prefix `-OPT`).
12870 // The previous Rust port hand-rolled a hardcoded subset
12871 // ("569X" + 8 ad-hoc letters) that diverged from C's table:
12872 // letter 'h' wrongly mapped to `hashall` (c:271 ALIAS for
12873 // HASHCMDS) instead of HISTIGNOREDUPS (c:349). Parity bug
12874 // #32 — `$-` last char differed (zsh `f`, zshrs `fh`).
12875 // Route through `dashgetfn` (options.rs:835) which is the
12876 // direct port of `Src/options.c:890`.
12877 "-" => Some(crate::ported::options::dashgetfn()),
12878 // Arrays — joined with space for scalar context.
12879 "pipestatus" => {
12880 let arr = pipestatgetfn();
12881 if arr.is_empty() {
12882 None
12883 } else {
12884 Some(arr.join(" "))
12885 }
12886 }
12887 _ => None,
12888 }
12889}
12890
12891/// Shared test mutex for histsiz mutations (gsu_tests +
12892/// tests submodules both write the same global; this lock
12893/// serialises them under parallel test execution).
12894#[cfg(test)]
12895pub(crate) static HISTSIZ_TEST_LOCK: Mutex<()> = Mutex::new(());
12896
12897/// Shared test mutex for histchars mutations (gsu_tests +
12898/// tests submodules both write bangchar/hatchar/hashchar atomics;
12899/// this lock serialises them under parallel test execution).
12900#[cfg(test)]
12901pub(crate) static HISTCHARS_TEST_LOCK_SHARED: Mutex<()> = Mutex::new(());
12902
12903#[cfg(test)]
12904mod gsu_tests {
12905 use super::*;
12906
12907 #[test]
12908 fn test_libc_id_callbacks_match_libc() {
12909 let _g = crate::test_util::global_state_lock();
12910 assert_eq!(uidgetfn(), unsafe { libc::getuid() } as i64);
12911 assert_eq!(gidgetfn(), unsafe { libc::getgid() } as i64);
12912 assert_eq!(euidgetfn(), unsafe { libc::geteuid() } as i64);
12913 assert_eq!(egidgetfn(), unsafe { libc::getegid() } as i64);
12914 }
12915
12916 /// Pin: `usernamegetfn` routes through `get_username()` per
12917 /// `Src/params.c:4658` (which refreshes cache on uid change
12918 /// per `Src/utils.c:1082`). The previous Rust port read a
12919 /// stale cached value directly. Verify the getter returns
12920 /// the same name as a direct libc `getpwuid(getuid())` —
12921 /// confirming the path WENT through the refresh helper, not
12922 /// the stale paramtab Mutex.
12923 #[test]
12924 fn usernamegetfn_matches_libc_getpwuid_for_current_uid() {
12925 let _g = crate::test_util::global_state_lock();
12926 let __pm = crate::ported::zsh_h::param::default();
12927 let uname = usernamegetfn(&__pm);
12928 // The current process is running as some uid; the getter
12929 // must return either a populated name OR an empty string
12930 // (when getpwuid fails, e.g. sandboxed builds). It must
12931 // NOT panic and must NOT return a stale cached value
12932 // from a different uid.
12933 let direct = unsafe {
12934 let pw = libc::getpwuid(libc::getuid());
12935 if pw.is_null() {
12936 String::new()
12937 } else {
12938 std::ffi::CStr::from_ptr((*pw).pw_name)
12939 .to_string_lossy()
12940 .into_owned()
12941 }
12942 };
12943 assert_eq!(
12944 uname, direct,
12945 "c:4658 — usernamegetfn must match getpwuid(getuid())->pw_name"
12946 );
12947 }
12948
12949 #[test]
12950 fn test_random_returns_15_bit_value() {
12951 let _g = crate::test_util::global_state_lock();
12952 for _ in 0..100 {
12953 let v = randomgetfn();
12954 assert!(v >= 0 && v < 0x8000);
12955 }
12956 }
12957
12958 #[test]
12959 fn test_random_set_seeds_deterministically() {
12960 let _g = crate::test_util::global_state_lock();
12961 randomsetfn(42);
12962 let a = randomgetfn();
12963 randomsetfn(42);
12964 let b = randomgetfn();
12965 assert_eq!(a, b);
12966 }
12967
12968 #[test]
12969 fn test_ifs_round_trip() {
12970 let _g = crate::test_util::global_state_lock();
12971 let mut __pm = crate::ported::zsh_h::param::default();
12972 let original = ifsgetfn(&__pm);
12973 ifssetfn(&mut __pm, ":,;".to_string());
12974 assert_eq!(ifsgetfn(&__pm), ":,;");
12975 ifssetfn(&mut __pm, original);
12976 }
12977
12978 #[test]
12979 fn test_histsiz_clamps_to_1() {
12980 let _g = crate::test_util::global_state_lock();
12981 let _g = HISTSIZ_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
12982 let original = histsizegetfn();
12983 histsizesetfn(0);
12984 assert_eq!(histsizegetfn(), 1);
12985 histsizesetfn(-5);
12986 assert_eq!(histsizegetfn(), 1);
12987 histsizesetfn(500);
12988 assert_eq!(histsizegetfn(), 500);
12989 histsizesetfn(original);
12990 }
12991
12992 #[test]
12993 fn test_savehistsiz_clamps_to_0() {
12994 let _g = crate::test_util::global_state_lock();
12995 let original = savehistsizegetfn();
12996 savehistsizesetfn(-5);
12997 assert_eq!(savehistsizegetfn(), 0);
12998 savehistsizesetfn(100);
12999 assert_eq!(savehistsizegetfn(), 100);
13000 savehistsizesetfn(original);
13001 }
13002
13003 /// Pin: `savehistsizesetfn` syncs BOTH storage mirrors so the
13004 /// twin-storage Rust adaptation behaves like the single global
13005 /// in C. The params.rs Mutex<i64> drives `$SAVEHIST` reads;
13006 /// the hist.rs AtomicI64 drives the history-file writer cap.
13007 /// Previously only the params.rs side was written, so
13008 /// `SAVEHIST=10000` left hist.rs at 0 and the writer would
13009 /// cap at zero lines.
13010 #[test]
13011 fn savehistsizesetfn_syncs_to_hist_module() {
13012 let _g = crate::test_util::global_state_lock();
13013 let original_params = savehistsizegetfn();
13014 let original_hist = savehistsiz.load(Ordering::SeqCst);
13015 // Set via the setfn — both storages must reflect the value.
13016 savehistsizesetfn(12345);
13017 assert_eq!(
13018 savehistsizegetfn(),
13019 12345,
13020 "c:4994 — params.rs Mutex<i64> reflects new value"
13021 );
13022 assert_eq!(
13023 savehistsiz.load(Ordering::SeqCst),
13024 12345,
13025 "c:4994 — hist.rs AtomicI64 synced (was the previous gap)"
13026 );
13027 // Negative clamps to 0 in BOTH stores.
13028 savehistsizesetfn(-99);
13029 assert_eq!(savehistsizegetfn(), 0, "c:4998 — params.rs clamps to 0");
13030 assert_eq!(
13031 savehistsiz.load(Ordering::SeqCst),
13032 0,
13033 "c:4998 — hist.rs clamps to 0 too"
13034 );
13035 // Restore.
13036 savehistsizesetfn(original_params);
13037 savehistsiz.store(original_hist, Ordering::SeqCst);
13038 }
13039
13040 #[test]
13041 fn test_pipestat_round_trip() {
13042 let _g = crate::test_util::global_state_lock();
13043 pipestatsetfn(Some(vec![
13044 "1".to_string(),
13045 "0".to_string(),
13046 "127".to_string(),
13047 ]));
13048 let v = pipestatgetfn();
13049 assert_eq!(v, vec!["1", "0", "127"]);
13050 pipestatsetfn(None);
13051 assert_eq!(pipestatgetfn(), Vec::<String>::new());
13052 }
13053
13054 /// Pin: `setnumvalue` actually STORES the scalar string per
13055 /// `Src/params.c:2862-2872`. The previous Rust port computed
13056 /// the string then dropped it via `let _ = s;` — meaning a
13057 /// numeric assignment to a SCALAR param stored NOTHING.
13058 ///
13059 /// C body for PM_SCALAR: `setstrvalue(v, convbase_underscore(
13060 /// val.u.l, pm->base, pm->width));`. We pin the round-trip
13061 /// for an integer assigned to a scalar param.
13062 #[test]
13063 fn setnumvalue_stores_int_value_into_scalar_pm() {
13064 let _g = crate::test_util::global_state_lock();
13065 // c:2860 — setnumvalue bails when unset(EXECOPT). The unit-test
13066 // env doesn't run through createoptiontable so we set "exec"
13067 // explicitly to simulate normal runtime.
13068 let saved_exec = opt_state_get("exec").unwrap_or(false);
13069 opt_state_set("exec", true);
13070 // Build a scalar Param with no special base/width.
13071 let mut pm = Box::new(param {
13072 node: hashnode {
13073 next: None,
13074 nam: "x".to_string(),
13075 flags: PM_SCALAR as i32,
13076 },
13077 u_data: 0,
13078 u_tied: None,
13079 u_arr: None,
13080 u_str: Some(String::new()),
13081 u_val: 0,
13082 u_dval: 0.0,
13083 u_hash: None,
13084 gsu_s: None,
13085 gsu_i: None,
13086 gsu_f: None,
13087 gsu_a: None,
13088 gsu_h: None,
13089 base: 0,
13090 width: 0,
13091 env: None,
13092 ename: None,
13093 old: None,
13094 level: 0,
13095 });
13096 let mut v = value {
13097 pm: Some(pm.clone()),
13098 arr: Vec::new(),
13099 scanflags: 0,
13100 valflags: 0,
13101 start: 0,
13102 end: -1,
13103 };
13104 let val = mnumber {
13105 l: 42,
13106 d: 0.0,
13107 type_: MN_INTEGER,
13108 };
13109 setnumvalue(Some(&mut v), val);
13110 // c:2871 — the scalar storage now holds "42".
13111 let stored = v.pm.as_ref().unwrap().u_str.clone().unwrap_or_default();
13112 assert_eq!(
13113 stored, "42",
13114 "c:2871 — setnumvalue must store the rendered integer; \
13115 was previously dropped via `let _ = s;`"
13116 );
13117 let _ = pm;
13118 opt_state_set("exec", saved_exec);
13119 }
13120
13121 #[test]
13122 fn test_simple_arrayuniq_first_wins() {
13123 let _g = crate::test_util::global_state_lock();
13124 let v = vec![
13125 "a".to_string(),
13126 "b".to_string(),
13127 "a".to_string(),
13128 "c".to_string(),
13129 ];
13130 assert_eq!(simple_arrayuniq(v), vec!["a", "b", "c"]);
13131 }
13132
13133 #[test]
13134 fn test_split_env_string() {
13135 let _g = crate::test_util::global_state_lock();
13136 assert_eq!(
13137 split_env_string("PATH=/usr/bin:/bin"),
13138 Some(("PATH".to_string(), "/usr/bin:/bin".to_string()))
13139 );
13140 assert_eq!(
13141 split_env_string("EMPTY="),
13142 Some(("EMPTY".to_string(), "".to_string()))
13143 );
13144 assert_eq!(split_env_string("NOEQUALS"), None);
13145 }
13146
13147 #[test]
13148 fn test_mkenvstr() {
13149 let _g = crate::test_util::global_state_lock();
13150 assert_eq!(mkenvstr("PATH", "/usr/bin", 0), "PATH=/usr/bin");
13151 assert_eq!(mkenvstr("EMPTY", "", 0), "EMPTY=");
13152 }
13153
13154 #[test]
13155 fn test_seconds_round_trip() {
13156 let _g = crate::test_util::global_state_lock();
13157 intsecondssetfn(0);
13158 let s1 = intsecondsgetfn();
13159 std::thread::sleep(Duration::from_millis(5));
13160 let s2 = intsecondsgetfn();
13161 assert!(s2 >= s1);
13162 // Reset to a known offset and read back.
13163 setrawseconds(100.0);
13164 assert_eq!(getrawseconds(), 100.0);
13165 }
13166
13167 #[test]
13168 fn test_argzero_round_trip() {
13169 let _g = crate::test_util::global_state_lock();
13170 // pm is UNUSED in argzerosetfn / argzerogetfn (C signature
13171 // matches Rust). Use Param::default() as the dummy carrier.
13172 let mut pm = param::default();
13173 argzerosetfn(&mut pm, "/bin/zsh".to_string());
13174 assert_eq!(argzerogetfn(&pm), "/bin/zsh");
13175 argzerosetfn(&mut pm, String::new());
13176 }
13177
13178 #[test]
13179 fn test_env_get_set() {
13180 let _g = crate::test_util::global_state_lock();
13181 let result = zputenv("ZSHRS_TEST_VAR=hello");
13182 assert_eq!(result, 0);
13183 assert_eq!(zgetenv("ZSHRS_TEST_VAR"), Some("hello".to_string()));
13184 delenv("ZSHRS_TEST_VAR");
13185 assert_eq!(zgetenv("ZSHRS_TEST_VAR"), None);
13186 }
13187
13188 #[test]
13189 fn test_keyboardhack_one_char() {
13190 let _g = crate::test_util::global_state_lock();
13191 let mut __pm = crate::ported::zsh_h::param::default();
13192 keyboardhacksetfn(&mut __pm, "\\".to_string());
13193 assert_eq!(keyboardhackgetfn(&__pm), "\\");
13194 keyboardhacksetfn(&mut __pm, String::new());
13195 assert_eq!(keyboardhackgetfn(&__pm), "");
13196 }
13197
13198 /// Pin: `keyboardhacksetfn` accepts ASCII chars cleanly per
13199 /// `Src/params.c:5040-5060`. Tests the canonical happy path
13200 /// — single ASCII char, empty input, and the ASCII guard.
13201 ///
13202 /// The previous Rust port skipped `unmetafy(x, &len)` (c:5044)
13203 /// before the length and ASCII checks. This test exercises
13204 /// the surface API; the unmetafy fix is doc-pinned in the
13205 /// fn body since constructing Meta-encoded String values for
13206 /// the test fixture would require unsafe (Rust strings must
13207 /// be valid UTF-8 and the Meta byte 0x83 is not a valid
13208 /// UTF-8 lead).
13209 #[test]
13210 fn keyboardhacksetfn_handles_ascii_and_empty() {
13211 let _g = crate::test_util::global_state_lock();
13212 let mut __pm = crate::ported::zsh_h::param::default();
13213 // c:5056 — single ASCII char stored.
13214 keyboardhacksetfn(&mut __pm, ";".to_string());
13215 assert_eq!(
13216 keyboardhackgetfn(&__pm),
13217 ";",
13218 "c:5056 — single ASCII char stored verbatim"
13219 );
13220 // c:5056 — different ASCII char stored.
13221 keyboardhacksetfn(&mut __pm, ",".to_string());
13222 assert_eq!(keyboardhackgetfn(&__pm), ",");
13223 // c:5058 — empty input clears to '\0'.
13224 keyboardhacksetfn(&mut __pm, String::new());
13225 assert_eq!(keyboardhackgetfn(&__pm), "");
13226 }
13227
13228 #[test]
13229 fn test_histchars_default() {
13230 let _g = crate::test_util::global_state_lock();
13231 let _g = HISTCHARS_TEST_LOCK_SHARED
13232 .lock()
13233 .unwrap_or_else(|e| e.into_inner());
13234 histcharssetfn(&mut param::default(), String::new());
13235 assert_eq!(histcharsgetfn(¶m::default()), "!^#");
13236 histcharssetfn(&mut param::default(), "@$&".to_string());
13237 assert_eq!(histcharsgetfn(¶m::default()), "@$&");
13238 histcharssetfn(&mut param::default(), String::new());
13239 }
13240
13241 /// Pin: `histcharssetfn` runs `unmetafy` per Src/params.c:5086
13242 /// BEFORE the length truncation and ASCII guard. Previously
13243 /// the Rust port skipped unmetafy, so a Meta-pair would
13244 /// inflate the byte count past 3 and the truncation would
13245 /// drop valid characters.
13246 ///
13247 /// Test the happy path: 1-char, 2-char, 3-char ASCII inputs
13248 /// all parse correctly and each char-position fills the
13249 /// matching atomic.
13250 #[test]
13251 fn histcharssetfn_handles_1_2_3_char_inputs() {
13252 let _g = crate::test_util::global_state_lock();
13253 let _g = HISTCHARS_TEST_LOCK_SHARED
13254 .lock()
13255 .unwrap_or_else(|e| e.into_inner());
13256 // 1-char: bangchar=='Q', hatchar=='\0', hashchar=='\0'.
13257 histcharssetfn(&mut param::default(), "Q".to_string());
13258 assert_eq!(bangchar.load(Ordering::SeqCst), b'Q' as i32);
13259 assert_eq!(hatchar.load(Ordering::SeqCst), 0);
13260 assert_eq!(hashchar.load(Ordering::SeqCst), 0);
13261 // 2-char: bangchar=='X', hatchar=='Y', hashchar=='\0'.
13262 histcharssetfn(&mut param::default(), "XY".to_string());
13263 assert_eq!(bangchar.load(Ordering::SeqCst), b'X' as i32);
13264 assert_eq!(hatchar.load(Ordering::SeqCst), b'Y' as i32);
13265 assert_eq!(hashchar.load(Ordering::SeqCst), 0);
13266 // 3-char: bangchar=='A', hatchar=='B', hashchar=='C'.
13267 histcharssetfn(&mut param::default(), "ABC".to_string());
13268 assert_eq!(bangchar.load(Ordering::SeqCst), b'A' as i32);
13269 assert_eq!(hatchar.load(Ordering::SeqCst), b'B' as i32);
13270 assert_eq!(hashchar.load(Ordering::SeqCst), b'C' as i32);
13271 // 4+ char: c:5087-5088 truncates to 3.
13272 histcharssetfn(&mut param::default(), "WXYZ".to_string());
13273 assert_eq!(bangchar.load(Ordering::SeqCst), b'W' as i32);
13274 assert_eq!(hatchar.load(Ordering::SeqCst), b'X' as i32);
13275 assert_eq!(hashchar.load(Ordering::SeqCst), b'Y' as i32);
13276 // Reset to default.
13277 histcharssetfn(&mut param::default(), String::new());
13278 assert_eq!(bangchar.load(Ordering::SeqCst), b'!' as i32);
13279 assert_eq!(hatchar.load(Ordering::SeqCst), b'^' as i32);
13280 assert_eq!(hashchar.load(Ordering::SeqCst), b'#' as i32);
13281 }
13282}
13283
13284// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
13285// ─── RUST-ONLY ACCESSORS ───
13286//
13287// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
13288// RwLock<T>>` globals declared above. C zsh uses direct global
13289// access; Rust needs these wrappers because `OnceLock::get_or_init`
13290// is the only way to lazily construct shared state. These ported sit
13291// here so the body of this file reads in C source order without
13292// the accessor wrappers interleaved between real port ported.
13293// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
13294
13295// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
13296// ─── RUST-ONLY ACCESSORS ───
13297//
13298// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
13299// RwLock<T>>` globals declared above. C zsh uses direct global
13300// access; Rust needs these wrappers because `OnceLock::get_or_init`
13301// is the only way to lazily construct shared state. These ported sit
13302// here so the body of this file reads in C source order without
13303// the accessor wrappers interleaved between real port ported.
13304// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
13305
13306// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
13307// ─── RUST-ONLY ACCESSORS ───
13308//
13309// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
13310// RwLock<T>>` globals declared above. C zsh uses direct global
13311// access; Rust needs these wrappers because `OnceLock::get_or_init`
13312// is the only way to lazily construct shared state. These ported sit
13313// here so the body of this file reads in C source order without
13314// the accessor wrappers interleaved between real port ported.
13315// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
13316
13317fn foundparam_lock() -> &'static Mutex<Option<String>> {
13318 FOUNDPARAM.get_or_init(|| Mutex::new(None))
13319}
13320
13321/// Accessor for the global `paramtab` (Src/params.c:515).
13322/// Mirrors C's `paramtab->...` dereference by handing back the
13323/// inner RwLock; callers `.read()` for lookups and `.write()` for
13324/// mutation, operating on the `HashMap<String, Param>` directly.
13325pub fn paramtab() -> &'static RwLock<crate::fast_hash::FastMap<String, Param>> {
13326 PARAMTAB_INNER.get_or_init(|| RwLock::new(crate::fast_hash::FastMap::default()))
13327}
13328
13329/// Accessor for the global `realparamtab` (Src/params.c:515).
13330/// Same role as `paramtab` for the not-currently-redirected case;
13331/// the alias-flip during assoc-array iteration isn't modelled yet.
13332pub fn realparamtab() -> &'static RwLock<crate::fast_hash::FastMap<String, Param>> {
13333 REALPARAMTAB_INNER.get_or_init(|| RwLock::new(crate::fast_hash::FastMap::default()))
13334}
13335
13336fn scanprog_lock() -> &'static Mutex<Option<String>> {
13337 SCANPROG.get_or_init(|| Mutex::new(None))
13338}
13339
13340fn scanstr_lock() -> &'static Mutex<Option<String>> {
13341 SCANSTR.get_or_init(|| Mutex::new(None))
13342}
13343
13344fn paramvals_lock() -> &'static Mutex<Vec<String>> {
13345 PARAMVALS.get_or_init(|| Mutex::new(Vec::new()))
13346}
13347
13348#[cfg(test)]
13349mod tests {
13350 use super::*;
13351 use crate::ported::zsh_h::Pound;
13352 use crate::zsh_h::hashnode;
13353
13354 /// `setscope_base` pushes the param name onto `SCOPEREFS[base]`
13355 /// when `base > pm.level` (c:6440). Grows the SCOPEREFS Vec as
13356 /// needed (c:6442-6447).
13357 #[test]
13358 fn setscope_base_pushes_name_when_base_above_level() {
13359 let _g = crate::test_util::global_state_lock();
13360 SCOPEREFS.with(|s| s.borrow_mut().clear());
13361 let mut pm = param {
13362 node: hashnode {
13363 next: None,
13364 nam: "foo".to_string(),
13365 flags: 0,
13366 },
13367 u_data: 0,
13368 u_tied: None,
13369 u_arr: None,
13370 u_str: None,
13371 u_val: 0,
13372 u_dval: 0.0,
13373 u_hash: None,
13374 gsu_s: None,
13375 gsu_i: None,
13376 gsu_f: None,
13377 gsu_a: None,
13378 gsu_h: None,
13379 base: 0,
13380 width: 0,
13381 env: None,
13382 ename: None,
13383 old: None,
13384 level: 2,
13385 };
13386 setscope_base(&mut pm, 5);
13387 assert_eq!(pm.base, 5);
13388 SCOPEREFS.with(|s| {
13389 let s = s.borrow();
13390 assert!(s.len() >= 6, "SCOPEREFS grew to fit index 5");
13391 assert_eq!(s[5], vec!["foo".to_string()]);
13392 });
13393 }
13394
13395 /// `assignaparam` rejects slice into PM_HASHED with the canonical
13396 /// "attempt to set slice of associative array" zerr (c:3386-3390).
13397 #[test]
13398 fn assignaparam_rejects_slice_into_hashed() {
13399 let _g = crate::test_util::global_state_lock();
13400 opt_state_set("exec", true);
13401 unsetparam("aa_h");
13402 // Create a hashed param.
13403 sethparam("aa_h", vec!["k".to_string(), "v".to_string()]);
13404 let before = paramtab().read().unwrap().contains_key("aa_h");
13405 assert!(before);
13406 // Slice write should be rejected.
13407 let result = assignaparam("aa_h[idx]", vec!["x".to_string()], 0);
13408 assert!(result.is_none(), "slice into hashed must return None");
13409 unsetparam("aa_h");
13410 opt_state_set("exec", false);
13411 }
13412
13413 /// `assignaparam` on a PM_NAMEREF param resolves the chain first
13414 /// (fetchvalue at c:3392): a ref bound to a not-yet-defined name
13415 /// REDIRECTS the array assignment, creating the TARGET as an
13416 /// array (K01nameref.ztst "assign new array via nameref":
13417 /// `typeset -n ptr=var; ptr=(val1 val2)` → `typeset -g -a var`).
13418 /// The "can't change type of a named reference" rejection
13419 /// (c:3395-3398) only fires for PLACEHOLDER refs (empty refname),
13420 /// covered by the K01 test-setting-ref matrix.
13421 #[test]
13422 fn assignaparam_rejects_nameref_type_change() {
13423 let _g = crate::test_util::global_state_lock();
13424 opt_state_set("exec", true);
13425 unsetparam("aa_nr");
13426 // Insert a PM_NAMEREF param directly.
13427 let pm = param {
13428 node: hashnode {
13429 next: None,
13430 nam: "aa_nr".to_string(),
13431 flags: (PM_NAMEREF | PM_SCALAR) as i32,
13432 },
13433 u_data: 0,
13434 u_tied: None,
13435 u_arr: None,
13436 u_str: Some("target".to_string()),
13437 u_val: 0,
13438 u_dval: 0.0,
13439 u_hash: None,
13440 gsu_s: None,
13441 gsu_i: None,
13442 gsu_f: None,
13443 gsu_a: None,
13444 gsu_h: None,
13445 base: 0,
13446 width: 0,
13447 env: None,
13448 ename: None,
13449 old: None,
13450 level: 0,
13451 };
13452 paramtab()
13453 .write()
13454 .unwrap()
13455 .insert("aa_nr".to_string(), Box::new(pm));
13456
13457 unsetparam("target");
13458 let result = assignaparam("aa_nr", vec!["a".to_string(), "b".to_string()], 0);
13459 assert!(
13460 result.is_some(),
13461 "assignment redirects to the ref's target (c:3392 fetchvalue resolve)"
13462 );
13463
13464 // PM_NAMEREF flag still present on the REF (not stripped).
13465 let pm = paramtab().read().unwrap().get("aa_nr").cloned().unwrap();
13466 assert_ne!(pm.node.flags as u32 & PM_NAMEREF, 0, "PM_NAMEREF preserved");
13467 assert_eq!(
13468 pm.u_str.as_deref(),
13469 Some("target"),
13470 "refname unchanged by assignment-through"
13471 );
13472 // The TARGET was created as an array with the value.
13473 let t = paramtab().read().unwrap().get("target").cloned().unwrap();
13474 assert_eq!(
13475 t.u_arr.as_deref(),
13476 Some(&["a".to_string(), "b".to_string()][..]),
13477 "target holds the assigned array"
13478 );
13479
13480 unsetparam("target");
13481 unsetparam("aa_nr");
13482 opt_state_set("exec", false);
13483 }
13484
13485 /// `assignaparam` with ASSPM_AUGMENT against a scalar param must
13486 /// prepend the previous scalar value as `val[0]` of the new array
13487 /// (c:3404-3412). Implements `a=x; a+=(y z)` → `a=(x y z)`. A
13488 /// regression that drops the prepend would yield `a=(y z)` and
13489 /// silently lose the original scalar — invisible at write time,
13490 /// surfaces only when the caller reads `${a[1]}`.
13491 #[test]
13492 fn assignaparam_augment_prepends_old_scalar() {
13493 let _g = crate::test_util::global_state_lock();
13494 opt_state_set("exec", true);
13495 unsetparam("aa_aug");
13496 // Seed a scalar.
13497 setsparam("aa_aug", "old");
13498 // Augment with two new values.
13499 let pm = assignaparam(
13500 "aa_aug",
13501 vec!["new1".to_string(), "new2".to_string()],
13502 ASSPM_AUGMENT,
13503 )
13504 .expect("augment should succeed");
13505 let arr = pm.u_arr.expect("ASSPM_AUGMENT must produce u_arr");
13506 assert_eq!(
13507 arr,
13508 vec!["old".to_string(), "new1".to_string(), "new2".to_string()],
13509 "c:3408-3411 — scalar prepended at index 0, then new values follow"
13510 );
13511 unsetparam("aa_aug");
13512 opt_state_set("exec", false);
13513 }
13514
13515 /// `assignaparam` against a PM_UNIQUE-flagged target dedupes
13516 /// the value array (c:3401 + arrsetfn's uniqarray). Implements
13517 /// `typeset -U arr; arr=(a b a c b)` → `arr=(a b c)`. A
13518 /// regression that drops the uniqarray call would let duplicates
13519 /// linger — invisible until a downstream `[[ -n ${arr[(r)b]} ]]`
13520 /// check counts more matches than expected, or `$path` grows
13521 /// unbounded with repeated directory entries.
13522 #[test]
13523 fn assignaparam_unique_flag_dedupes_values() {
13524 let _g = crate::test_util::global_state_lock();
13525 opt_state_set("exec", true);
13526 unsetparam("aa_uniq");
13527 // Seed an empty PM_ARRAY|PM_UNIQUE.
13528 setaparam("aa_uniq", vec![]);
13529 {
13530 let mut tab = paramtab().write().unwrap();
13531 let pm = tab.get_mut("aa_uniq").expect("aa_uniq must exist");
13532 pm.node.flags |= PM_UNIQUE as i32;
13533 }
13534 // Now write duplicates; PM_UNIQUE must collapse them.
13535 let pm = assignaparam(
13536 "aa_uniq",
13537 vec!["a".into(), "b".into(), "a".into(), "c".into(), "b".into()],
13538 0,
13539 )
13540 .expect("assignment succeeds");
13541 let arr = pm.u_arr.expect("u_arr populated");
13542 assert_eq!(
13543 arr,
13544 vec!["a".to_string(), "b".to_string(), "c".to_string()],
13545 "c:3401 — PM_UNIQUE collapses duplicates, keeping first occurrence"
13546 );
13547 // The PM_UNIQUE flag must persist through the assignment.
13548 let pm_check = paramtab().read().unwrap().get("aa_uniq").cloned().unwrap();
13549 assert_ne!(
13550 pm_check.node.flags as u32 & PM_UNIQUE,
13551 0,
13552 "PM_UNIQUE flag preserved across assignment"
13553 );
13554 unsetparam("aa_uniq");
13555 opt_state_set("exec", false);
13556 }
13557
13558 /// `getsparam` reads PM_INTEGER params via convbase, not via
13559 /// `u_str` (which is None for typed integers). Pins the latent
13560 /// bug fix that made `read REPLY` after `(( REPLY=42 ))` return
13561 /// nothing — every numeric param read used to fall through to
13562 /// the OS env-var fallback.
13563 #[test]
13564 fn getsparam_returns_integer_via_convbase() {
13565 let _g = crate::test_util::global_state_lock();
13566 opt_state_set("exec", true);
13567 unsetparam("gs_int");
13568 setiparam("gs_int", 999);
13569 assert_eq!(getsparam("gs_int").as_deref(), Some("999"));
13570 unsetparam("gs_int");
13571 opt_state_set("exec", false);
13572 }
13573
13574 /// `getsparam` reads PM_FFLOAT params via convfloat. Same fix
13575 /// shape as the PM_INTEGER path.
13576 #[test]
13577 fn getsparam_returns_float_via_convfloat() {
13578 let _g = crate::test_util::global_state_lock();
13579 opt_state_set("exec", true);
13580 unsetparam("gs_f");
13581 // Stash a float via the setnumvalue / setnparam path.
13582 let v = mnumber {
13583 l: 0,
13584 d: 2.5,
13585 type_: MN_FLOAT,
13586 };
13587 setnparam("gs_f", v);
13588 let s = getsparam("gs_f").expect("PM_FFLOAT should serialize");
13589 // convfloat formats with default precision; just check it's
13590 // not empty and parses back.
13591 assert!(
13592 s.parse::<f64>()
13593 .map(|f| (f - 2.5).abs() < 1e-6)
13594 .unwrap_or(false),
13595 "expected ~2.5 round-trip, got {:?}",
13596 s
13597 );
13598 unsetparam("gs_f");
13599 opt_state_set("exec", false);
13600 }
13601
13602 /// `getsparam` against a non-default `pm.base` integer param must
13603 /// render using that base via `convbase`. The c:2364 dispatch
13604 /// passes `pm.base` (or 10) to convbase; a regression that
13605 /// hardcodes base=10 would silently break `typeset -i 16 hex=255`
13606 /// readers (would see "255" instead of the C-faithful "16#FF").
13607 #[test]
13608 fn getsparam_integer_honors_pm_base() {
13609 let _g = crate::test_util::global_state_lock();
13610 opt_state_set("exec", true);
13611 unsetparam("hex_param");
13612 // Manually insert a PM_INTEGER param with base=16.
13613 let pm = param {
13614 node: hashnode {
13615 next: None,
13616 nam: "hex_param".to_string(),
13617 flags: PM_INTEGER as i32,
13618 },
13619 u_data: 0,
13620 u_tied: None,
13621 u_arr: None,
13622 u_str: None,
13623 u_val: 255,
13624 u_dval: 0.0,
13625 u_hash: None,
13626 gsu_s: None,
13627 gsu_i: None,
13628 gsu_f: None,
13629 gsu_a: None,
13630 gsu_h: None,
13631 base: 16,
13632 width: 0,
13633 env: None,
13634 ename: None,
13635 old: None,
13636 level: 0,
13637 };
13638 paramtab()
13639 .write()
13640 .unwrap()
13641 .insert("hex_param".to_string(), Box::new(pm));
13642
13643 let s = getsparam("hex_param").expect("PM_INTEGER must serialize");
13644 // convbase emits "16#FF" form for base 16.
13645 assert!(
13646 s.contains("FF") || s.contains("ff"),
13647 "c:2364 — base-16 must render hex digits; got {:?}",
13648 s
13649 );
13650
13651 unsetparam("hex_param");
13652 opt_state_set("exec", false);
13653 }
13654
13655 /// `endparamscope` clears `SCOPEREFS[old_locallevel]` and resets
13656 /// nameref params' base when their base exceeds the new locallevel.
13657 /// End-to-end of the setscope_base writer + endparamscope reader.
13658 #[test]
13659 fn endparamscope_resets_scoperefs_and_nameref_base() {
13660 let _g = crate::test_util::global_state_lock();
13661 SCOPEREFS.with(|s| s.borrow_mut().clear());
13662
13663 // Set up: locallevel = 3, push a PM_NAMEREF param with base=5 onto SCOPEREFS[3].
13664 set_locallevel(3);
13665 let pm = param {
13666 node: hashnode {
13667 next: None,
13668 nam: "ref1".to_string(),
13669 flags: (PM_NAMEREF | PM_SCALAR) as i32,
13670 },
13671 u_data: 0,
13672 u_tied: None,
13673 u_arr: None,
13674 u_str: Some(String::new()),
13675 u_val: 0,
13676 u_dval: 0.0,
13677 u_hash: None,
13678 gsu_s: None,
13679 gsu_i: None,
13680 gsu_f: None,
13681 gsu_a: None,
13682 gsu_h: None,
13683 base: 5,
13684 width: 0,
13685 env: None,
13686 ename: None,
13687 old: None,
13688 level: 0,
13689 };
13690 paramtab()
13691 .write()
13692 .unwrap()
13693 .insert("ref1".to_string(), Box::new(pm));
13694 // Populate SCOPEREFS[3] manually with "ref1".
13695 SCOPEREFS.with(|sr| {
13696 let mut sr = sr.borrow_mut();
13697 sr.resize(8, Vec::new());
13698 sr[3].push("ref1".to_string());
13699 });
13700
13701 endparamscope();
13702
13703 // After endparamscope: locallevel decremented to 2; "ref1"'s
13704 // base reset to 0 (was 5 > new ll=2). SCOPEREFS[3] cleared.
13705 let pm_after = paramtab().read().unwrap().get("ref1").cloned();
13706 assert!(pm_after.is_some(), "ref1 should still exist (level=0)");
13707 assert_eq!(pm_after.unwrap().base, 0, "PM_NAMEREF.base reset to 0");
13708 SCOPEREFS.with(|sr| {
13709 assert!(sr.borrow()[3].is_empty(), "SCOPEREFS[3] cleared");
13710 });
13711
13712 // Cleanup
13713 paramtab().write().unwrap().remove("ref1");
13714 set_locallevel(0);
13715 }
13716
13717 /// `endparamscope` MUST NOT reset `base` on PM_UPPER namerefs
13718 /// (c:5891 — the `!(pm->node.flags & PM_UPPER)` clause guards
13719 /// the reset). PM_UPPER namerefs point UPWARD in the scope chain
13720 /// (e.g. `typeset -n -u up=outer` from inside a function) and
13721 /// their base must persist across scope pops so the upward
13722 /// resolution keeps working. A regression that drops the PM_UPPER
13723 /// guard would silently degrade upward namerefs into local ones
13724 /// after the first function return.
13725 #[test]
13726 fn endparamscope_preserves_pm_upper_nameref_base() {
13727 let _g = crate::test_util::global_state_lock();
13728 SCOPEREFS.with(|s| s.borrow_mut().clear());
13729
13730 set_locallevel(3);
13731 let pm = param {
13732 node: hashnode {
13733 next: None,
13734 nam: "up_ref".to_string(),
13735 flags: (PM_NAMEREF | PM_SCALAR | PM_UPPER) as i32,
13736 },
13737 u_data: 0,
13738 u_tied: None,
13739 u_arr: None,
13740 u_str: Some(String::new()),
13741 u_val: 0,
13742 u_dval: 0.0,
13743 u_hash: None,
13744 gsu_s: None,
13745 gsu_i: None,
13746 gsu_f: None,
13747 gsu_a: None,
13748 gsu_h: None,
13749 base: 5,
13750 width: 0,
13751 env: None,
13752 ename: None,
13753 old: None,
13754 level: 0,
13755 };
13756 paramtab()
13757 .write()
13758 .unwrap()
13759 .insert("up_ref".to_string(), Box::new(pm));
13760 SCOPEREFS.with(|sr| {
13761 let mut sr = sr.borrow_mut();
13762 sr.resize(8, Vec::new());
13763 sr[3].push("up_ref".to_string());
13764 });
13765
13766 endparamscope();
13767
13768 let pm_after = paramtab()
13769 .read()
13770 .unwrap()
13771 .get("up_ref")
13772 .cloned()
13773 .expect("up_ref must survive scope pop");
13774 assert_eq!(
13775 pm_after.base, 5,
13776 "c:5891 — PM_UPPER nameref base MUST be preserved (was 5, must stay 5)"
13777 );
13778 assert_ne!(
13779 pm_after.node.flags as u32 & PM_UPPER,
13780 0,
13781 "PM_UPPER flag itself must persist"
13782 );
13783
13784 paramtab().write().unwrap().remove("up_ref");
13785 set_locallevel(0);
13786 }
13787
13788 /// `setscope_base` does NOT push when `base <= pm.level` (the C
13789 /// `if ((pm->base = base) > pm->level)` guard at c:6440 fails).
13790 #[test]
13791 fn setscope_base_no_push_when_base_below_level() {
13792 let _g = crate::test_util::global_state_lock();
13793 SCOPEREFS.with(|s| s.borrow_mut().clear());
13794 let mut pm = param {
13795 node: hashnode {
13796 next: None,
13797 nam: "bar".to_string(),
13798 flags: 0,
13799 },
13800 u_data: 0,
13801 u_tied: None,
13802 u_arr: None,
13803 u_str: None,
13804 u_val: 0,
13805 u_dval: 0.0,
13806 u_hash: None,
13807 gsu_s: None,
13808 gsu_i: None,
13809 gsu_f: None,
13810 gsu_a: None,
13811 gsu_h: None,
13812 base: 0,
13813 width: 0,
13814 env: None,
13815 ename: None,
13816 old: None,
13817 level: 10,
13818 };
13819 setscope_base(&mut pm, 3); // 3 <= 10 → no push
13820 assert_eq!(pm.base, 3);
13821 SCOPEREFS.with(|s| {
13822 // No push happened; SCOPEREFS stays empty.
13823 assert!(s.borrow().is_empty() || s.borrow().iter().all(|v| v.is_empty()));
13824 });
13825 }
13826
13827 /// `setscope_base` boundary: `base == pm.level` must NOT push.
13828 /// The C guard c:6440 is strictly `>` (`> pm->level`, not `>=`).
13829 /// A regression that uses `>=` would push every assignment at
13830 /// the current scope into SCOPEREFS, causing endparamscope to
13831 /// re-process every same-scope param on every function return —
13832 /// O(n) extra work per call PLUS spurious base resets.
13833 #[test]
13834 fn setscope_base_equal_level_does_not_push() {
13835 let _g = crate::test_util::global_state_lock();
13836 SCOPEREFS.with(|s| s.borrow_mut().clear());
13837 let mut pm = param {
13838 node: hashnode {
13839 next: None,
13840 nam: "edge".to_string(),
13841 flags: 0,
13842 },
13843 u_data: 0,
13844 u_tied: None,
13845 u_arr: None,
13846 u_str: None,
13847 u_val: 0,
13848 u_dval: 0.0,
13849 u_hash: None,
13850 gsu_s: None,
13851 gsu_i: None,
13852 gsu_f: None,
13853 gsu_a: None,
13854 gsu_h: None,
13855 base: 0,
13856 width: 0,
13857 env: None,
13858 ename: None,
13859 old: None,
13860 level: 5,
13861 };
13862 setscope_base(&mut pm, 5); // base == level → strict `>` fails
13863 assert_eq!(pm.base, 5, "base assignment always happens");
13864 SCOPEREFS.with(|sr| {
13865 let any_push = sr.borrow().iter().any(|v| !v.is_empty());
13866 assert!(
13867 !any_push,
13868 "c:6440 — `base > pm->level` is STRICT; equal must not push"
13869 );
13870 });
13871 }
13872
13873 #[test]
13874 fn test_colonarr_conversion() {
13875 let _g = crate::test_util::global_state_lock();
13876 let arr = colonsplit("/bin:/usr/bin:/usr/local/bin", false);
13877 assert_eq!(arr, vec!["/bin", "/usr/bin", "/usr/local/bin"]);
13878 let path = colonarrgetfn(&arr);
13879 assert_eq!(path, "/bin:/usr/bin:/usr/local/bin");
13880 }
13881 #[test]
13882 fn test_isident() {
13883 let _g = crate::test_util::global_state_lock();
13884 assert!(isident("foo"));
13885 assert!(isident("_bar"));
13886 assert!(isident("FOO_BAR"));
13887 assert!(isident("x123"));
13888 assert!(isident("123")); // positional params
13889 assert!(!isident(""));
13890 assert!(!isident("foo bar"));
13891 }
13892
13893 /// Pin: `isident` requires balanced `[...]` per `Src/params.c:1329-1330`:
13894 /// if (*ss != '[') return 0;
13895 /// if (!(ss = parse_subscript(++ss, 1, ']'))) return 0;
13896 ///
13897 /// The previous Rust port accepted ANY `[` as a valid
13898 /// terminator (`if c == '[' { return true; }`) without
13899 /// checking for a matching `]`. So `foo[` (no close) was
13900 /// accepted as a valid identifier — diverging from C which
13901 /// rejects.
13902 #[test]
13903 fn isident_requires_balanced_subscript_brackets() {
13904 let _g = crate::test_util::global_state_lock();
13905 // Balanced `[...]` is valid.
13906 assert!(
13907 isident("foo[0]"),
13908 "c:1330 — balanced [0] passes parse_subscript"
13909 );
13910 assert!(
13911 isident("foo[bar]"),
13912 "c:1330 — balanced [bar] passes parse_subscript"
13913 );
13914 // UNBALANCED — open without close — must be rejected.
13915 assert!(
13916 !isident("foo["),
13917 "c:1330 — `foo[` missing `]` MUST be rejected"
13918 );
13919 // Trailing chars after `]` — C parse_subscript returns
13920 // a position INSIDE the string, the surrounding isident
13921 // body checks that nothing follows; our port currently
13922 // returns true at the first `[` either way, but pin the
13923 // balanced case as a working invariant.
13924 assert!(isident("a[1]"), "c:1330 — short balanced subscript valid");
13925 }
13926
13927 #[test]
13928 fn test_unique_array() {
13929 let _g = crate::test_util::global_state_lock();
13930 let arr = vec!["a".into(), "b".into(), "a".into(), "c".into(), "b".into()];
13931 let result = uniqarray(arr);
13932 assert_eq!(result, vec!["a", "b", "c"]);
13933 }
13934
13935 #[test]
13936 fn test_convbase() {
13937 let _g = crate::test_util::global_state_lock();
13938 // CBASES off (default): `16#FF` / `8#7` form. The `0x.../
13939 // 0...` short-prefix output is gated on `setopt CBASES` —
13940 // see Src/params.c:5599-5605.
13941 assert_eq!(convbase(255, 16), "16#FF");
13942 assert_eq!(convbase(10, 10), "10");
13943 assert_eq!(convbase(-5, 10), "-5");
13944 assert_eq!(convbase(7, 8), "8#7");
13945 assert_eq!(convbase(5, 2), "2#101");
13946 }
13947
13948 #[test]
13949 fn test_convfloat() {
13950 let _g = crate::test_util::global_state_lock();
13951 // Use 2.5 instead of 3.14 — clippy errors on the latter as
13952 // an approx PI constant. The test checks 2-decimal formatting
13953 // round-trips, which the exact value doesn't influence.
13954 let s = convfloat(2.5, 2, PM_FFLOAT);
13955 assert!(s.starts_with("2.50"));
13956
13957 assert_eq!(convfloat(f64::INFINITY, 0, 0), "Inf");
13958 assert_eq!(convfloat(f64::NEG_INFINITY, 0, 0), "-Inf");
13959 assert_eq!(convfloat(f64::NAN, 0, 0), "NaN");
13960 }
13961
13962 #[test]
13963 fn test_getarrvalue() {
13964 let _g = crate::test_util::global_state_lock();
13965 let arr = vec!["a".into(), "b".into(), "c".into(), "d".into()];
13966 assert_eq!(getarrvalue(&arr, 2, 3), vec!["b", "c"]);
13967 assert_eq!(getarrvalue(&arr, -2, -1), vec!["c", "d"]);
13968 assert_eq!(getarrvalue(&arr, 1, 4), vec!["a", "b", "c", "d"]);
13969 }
13970
13971 #[test]
13972 fn test_setarrvalue() {
13973 let _g = crate::test_util::global_state_lock();
13974 // c:2897 — setarrvalue bails when unset(EXECOPT). Set "exec"
13975 // for the unit-test env (real zsh defaults exec=true).
13976 let saved_exec = opt_state_get("exec").unwrap_or(false);
13977 opt_state_set("exec", true);
13978 // C-faithful: setarrvalue takes a Value pointing at a Param
13979 // with u_arr set. Construct one inline.
13980 let pm = Box::new(param {
13981 node: hashnode {
13982 next: None,
13983 nam: "test".to_string(),
13984 flags: PM_ARRAY as i32,
13985 },
13986 u_data: 0,
13987 u_tied: None,
13988 u_arr: Some(vec!["a".into(), "b".into(), "c".into(), "d".into()]),
13989 u_str: None,
13990 u_val: 0,
13991 u_dval: 0.0,
13992 u_hash: None,
13993 gsu_s: None,
13994 gsu_i: None,
13995 gsu_f: None,
13996 gsu_a: None,
13997 gsu_h: None,
13998 base: 0,
13999 width: 0,
14000 env: None,
14001 ename: None,
14002 old: None,
14003 level: 0,
14004 });
14005 let mut v = value {
14006 pm: Some(pm),
14007 arr: Vec::new(),
14008 scanflags: 0,
14009 valflags: 0,
14010 start: 2,
14011 end: 3,
14012 };
14013 setarrvalue(&mut v, vec!["X".into(), "Y".into()]);
14014 let arr = v.pm.unwrap().u_arr.unwrap();
14015 assert_eq!(arr, vec!["a", "X", "Y", "d"]);
14016 opt_state_set("exec", saved_exec);
14017 }
14018
14019 #[test]
14020 fn test_valid_refname() {
14021 let _g = crate::test_util::global_state_lock();
14022 assert!(valid_refname("foo", 0));
14023 assert!(valid_refname("_bar", 0));
14024 assert!(valid_refname("1", 0));
14025 assert!(valid_refname("!", 0));
14026 assert!(valid_refname("arr[1]", 0));
14027 assert!(!valid_refname("", 0));
14028 // C semantics: empty leader without one of `! ? $ - _` is rejected.
14029 assert!(!valid_refname(" ", 0));
14030 // PM_UPPER rejects digit-leader and argv/ARGC.
14031 assert!(!valid_refname("1", PM_UPPER as i32));
14032 assert!(!valid_refname("argv", PM_UPPER as i32));
14033 assert!(!valid_refname("ARGC", PM_UPPER as i32));
14034 }
14035
14036 #[test]
14037 fn test_uniq_array_empty() {
14038 let _g = crate::test_util::global_state_lock();
14039 let empty: Vec<String> = Vec::new();
14040 assert!(uniqarray(empty).is_empty());
14041 }
14042
14043 #[test]
14044 fn test_convbase_underscore() {
14045 let _g = crate::test_util::global_state_lock();
14046 let s = convbase_underscore(1234567, 10, 3);
14047 assert_eq!(s, "1_234_567");
14048 }
14049
14050 fn val_str(v: getarg_out<'_>) -> String {
14051 match v {
14052 getarg_out::Value(v) => v.to_str(),
14053 getarg_out::Flags { .. } => panic!("expected Value, got Flags"),
14054 }
14055 }
14056
14057 #[test]
14058 fn getarg_n_flag_picks_second_exact_match() {
14059 let _g = crate::test_util::global_state_lock();
14060 // C params.c:1431-1442 + 1758 — `(en.2.)pat` picks 2nd exact match.
14061 let arr: Vec<String> = vec!["foo".into(), "bar".into(), "foo".into(), "baz".into()];
14062 let out = getarg("(en.2.r)foo", Some(&arr), None, None).expect("Some");
14063 assert_eq!(val_str(out), "foo");
14064 }
14065
14066 #[test]
14067 fn getarg_n_flag_third_exact_match() {
14068 let _g = crate::test_util::global_state_lock();
14069 let arr: Vec<String> = vec!["a".into(), "a".into(), "a".into(), "b".into()];
14070 let out = getarg("(en.3.r)a", Some(&arr), None, None).expect("Some");
14071 assert_eq!(val_str(out), "a");
14072 }
14073
14074 #[test]
14075 fn getarg_n_flag_returns_index_with_i() {
14076 let _g = crate::test_util::global_state_lock();
14077 // (en.2.i) — return INDEX of 2nd exact match.
14078 let arr: Vec<String> = vec!["x".into(), "y".into(), "x".into(), "y".into()];
14079 let out = getarg("(en.2.i)x", Some(&arr), None, None).expect("Some");
14080 assert_eq!(val_str(out), "3");
14081 }
14082
14083 #[test]
14084 fn getarg_negative_n_flips_search_direction() {
14085 let _g = crate::test_util::global_state_lock();
14086 // C params.c:1488-1491 — negative `num` flips down (reverse).
14087 // (en.-1.) on forward-default search matches from the end.
14088 let arr: Vec<String> = vec!["a".into(), "a".into(), "a".into()];
14089 let out = getarg("(en.-1.i)a", Some(&arr), None, None).expect("Some");
14090 assert_eq!(val_str(out), "3");
14091 }
14092
14093 #[test]
14094 fn getarg_n_flag_zero_treated_as_one() {
14095 let _g = crate::test_util::global_state_lock();
14096 // C params.c:1438-1439 — `if (!num) num = 1`.
14097 let arr: Vec<String> = vec!["x".into(), "y".into()];
14098 let out = getarg("(en.0.r)x", Some(&arr), None, None).expect("Some");
14099 assert_eq!(val_str(out), "x");
14100 }
14101
14102 #[test]
14103 fn getarg_unknown_flag_char_returns_none() {
14104 let _g = crate::test_util::global_state_lock();
14105 // C params.c:1477-1483 flagerr — invalid flag char reports error.
14106 let arr: Vec<String> = vec!["x".into()];
14107 assert!(getarg("(z)x", Some(&arr), None, None).is_none());
14108 }
14109
14110 #[test]
14111 fn getarg_n_flag_unterminated_arg_returns_none() {
14112 let _g = crate::test_util::global_state_lock();
14113 // (n.5 missing closing delimiter — flagerr.
14114 let arr: Vec<String> = vec!["x".into()];
14115 assert!(getarg("(n.5", Some(&arr), None, None).is_none());
14116 }
14117
14118 #[test]
14119 fn getarg_b_flag_starts_search_at_index() {
14120 let _g = crate::test_util::global_state_lock();
14121 // C params.c:1748-1760 — `(b.N.e)pat` skips first N-1 elements
14122 // forward (parsed value `N`, normalized to `beg = N-1`).
14123 let arr: Vec<String> = vec!["x".into(), "y".into(), "x".into(), "y".into()];
14124 // Forward, beg=2 (skip first 2) → starts at idx 2 → 'x' at 3.
14125 let out = getarg("(b.3.ei)x", Some(&arr), None, None).expect("Some");
14126 assert_eq!(val_str(out), "3");
14127 }
14128
14129 #[test]
14130 fn getarg_b_flag_with_R_reverse_from_offset() {
14131 let _g = crate::test_util::global_state_lock();
14132 // C params.c:1750-1755 — reverse search starting at parsed-1 idx.
14133 // arr=(x y x y), beg=2 (parsed 3-1), reverse → walks 2,1,0; first
14134 // exact 'x' is at idx 2 → 1-based "3".
14135 let arr: Vec<String> = vec!["x".into(), "y".into(), "x".into(), "y".into()];
14136 let out = getarg("(b.3.eIR)x", Some(&arr), None, None).expect("Some");
14137 assert_eq!(val_str(out), "3");
14138 }
14139
14140 #[test]
14141 fn getarg_b_flag_out_of_bounds_forward_returns_empty() {
14142 let _g = crate::test_util::global_state_lock();
14143 // c:1746 — beg >= len returns len+1 (empty for value-mode).
14144 let arr: Vec<String> = vec!["x".into()];
14145 let out = getarg("(b.5.er)x", Some(&arr), None, None).expect("Some");
14146 assert_eq!(val_str(out), "");
14147 }
14148
14149 #[test]
14150 fn getarg_b_flag_out_of_bounds_index_mode_returns_len_plus_one() {
14151 let _g = crate::test_util::global_state_lock();
14152 let arr: Vec<String> = vec!["x".into(), "y".into()];
14153 let out = getarg("(b.5.ei)x", Some(&arr), None, None).expect("Some");
14154 assert_eq!(val_str(out), "3");
14155 }
14156
14157 #[test]
14158 fn getarg_hash_neg_num_on_lowercase_r_returns_all() {
14159 let _g = crate::test_util::global_state_lock();
14160 // C params.c:1488-1491 — neg `num` flips down on `r`,
14161 // converting hash search to return-all-matches semantics.
14162 let mut h: IndexMap<String, String> = IndexMap::new();
14163 h.insert("a".into(), "1".into());
14164 h.insert("b".into(), "1".into());
14165 h.insert("c".into(), "2".into());
14166 let out = getarg("(en.-1.r)1", None, Some(&h), None).expect("Some");
14167 // r + neg = R semantics → all values where pat matches value.
14168 assert_eq!(val_str(out), "1 1");
14169 }
14170
14171 #[test]
14172 fn getarg_hash_neg_num_on_uppercase_R_returns_single() {
14173 let _g = crate::test_util::global_state_lock();
14174 // R + neg `num` un-flips back to single-match (r semantics).
14175 let mut h: IndexMap<String, String> = IndexMap::new();
14176 h.insert("a".into(), "1".into());
14177 h.insert("b".into(), "1".into());
14178 h.insert("c".into(), "2".into());
14179 let out = getarg("(en.-1.R)1", None, Some(&h), None).expect("Some");
14180 // R + neg → r → single first match.
14181 assert_eq!(val_str(out), "1");
14182 }
14183
14184 #[test]
14185 fn getarg_hash_i_flag_matches_keys_not_values() {
14186 let _g = crate::test_util::global_state_lock();
14187 // On an associative array the `i` flag matches the pattern against
14188 // KEYS (not values) and returns the matching key. A value-search
14189 // therefore finds nothing, and the `b<NUM>` begin-offset does not
14190 // skip entries for an associative key search. Verified against
14191 // zsh 5.9: `h=(a 1 b 1 c 1); ${h[(i)b]}` → "b"; `${h[(b:3:ei)1]}` → "".
14192 // (The prior expectation "c" — a b-offset skip returning a value —
14193 // does not match real zsh; the code already matches zsh.)
14194 let mut h: IndexMap<String, String> = IndexMap::new();
14195 h.insert("a".into(), "1".into());
14196 h.insert("b".into(), "1".into());
14197 h.insert("c".into(), "1".into());
14198 // `i` globs the KEY "b" and returns that key.
14199 let by_key = getarg("(i)b", None, Some(&h), None).expect("Some");
14200 assert_eq!(val_str(by_key), "b");
14201 // Searching a VALUE ("1") matches no key named "1" → empty.
14202 let by_val = getarg("(b.3.ei)1", None, Some(&h), None).expect("Some");
14203 assert_eq!(val_str(by_val), "");
14204 }
14205
14206 #[test]
14207 fn getarg_hash_I_flag_matches_all_keys() {
14208 let _g = crate::test_util::global_state_lock();
14209 // The `I` flag returns ALL matching keys. `(I)*` globs every key;
14210 // a value-search matches no key. Verified against zsh 5.9:
14211 // `h=(a 1 b 1 c 1); ${h[(I)*]}` → "a b c"; `${h[(b:2:eI)1]}` → "".
14212 // (The prior expectation "b c" for a value-search does not match
14213 // real zsh; the code already matches zsh.)
14214 let mut h: IndexMap<String, String> = IndexMap::new();
14215 h.insert("a".into(), "1".into());
14216 h.insert("b".into(), "1".into());
14217 h.insert("c".into(), "1".into());
14218 // `(I)*` matches all keys via glob, in insertion order.
14219 let all_keys = getarg("(I)*", None, Some(&h), None).expect("Some");
14220 assert_eq!(val_str(all_keys), "a b c");
14221 // Value-search "1" matches no key → empty.
14222 let out = getarg("(b.2.eI)1", None, Some(&h), None).expect("Some");
14223 assert_eq!(val_str(out), "");
14224 }
14225
14226 #[test]
14227 fn getarg_hash_b_flag_out_of_bounds_returns_empty() {
14228 let _g = crate::test_util::global_state_lock();
14229 // c:1746 — beg >= len with single-match → empty.
14230 let mut h: IndexMap<String, String> = IndexMap::new();
14231 h.insert("a".into(), "1".into());
14232 let out = getarg("(b.5.e)1", None, Some(&h), None).expect("Some");
14233 assert_eq!(val_str(out), "");
14234 }
14235
14236 #[test]
14237 fn getarg_w_flag_splits_multi_word_array_elements() {
14238 let _g = crate::test_util::global_state_lock();
14239 // C params.c:1761-1797 — `(w)N` joins array then re-splits by
14240 // IFS-default whitespace. arr=("a b" "c d"); (w)2 → "b" not "c d".
14241 let arr: Vec<String> = vec!["a b".into(), "c d".into()];
14242 let out = getarg("(w)2", Some(&arr), None, None).expect("Some");
14243 assert_eq!(val_str(out), "b");
14244 }
14245
14246 #[test]
14247 fn getarg_w_flag_simple_array_indexing_still_works() {
14248 let _g = crate::test_util::global_state_lock();
14249 let arr: Vec<String> = vec!["one".into(), "two".into(), "three".into()];
14250 let out = getarg("(w)2", Some(&arr), None, None).expect("Some");
14251 assert_eq!(val_str(out), "two");
14252 }
14253
14254 #[test]
14255 fn getarg_f_flag_splits_by_newline() {
14256 let _g = crate::test_util::global_state_lock();
14257 // C params.c:1424-1427 — `f` flag aliases `w` with sep="\n".
14258 // arr=("a b\nc d"); (f)2 → "c d" (split by \n only, not space).
14259 let arr: Vec<String> = vec!["a b\nc d".into()];
14260 let out = getarg("(f)2", Some(&arr), None, None).expect("Some");
14261 assert_eq!(val_str(out), "c d");
14262 }
14263
14264 #[test]
14265 fn getarg_scalar_w_flag_picks_nth_word() {
14266 let _g = crate::test_util::global_state_lock();
14267 // C params.c:1761-1797 — scalar word-mode arm. `(w)2` on
14268 // scalar "hello world foo" returns the 2nd whitespace word.
14269 let out = getarg("(w)2", None, None, Some("hello world foo")).expect("Some");
14270 assert_eq!(val_str(out), "world");
14271 }
14272
14273 #[test]
14274 fn getarg_scalar_w_flag_negative_index_counts_from_end() {
14275 let _g = crate::test_util::global_state_lock();
14276 let out = getarg("(w)-1", None, None, Some("alpha beta gamma")).expect("Some");
14277 assert_eq!(val_str(out), "gamma");
14278 }
14279
14280 #[test]
14281 fn getarg_scalar_re_returns_char_at_match_position() {
14282 let _g = crate::test_util::global_state_lock();
14283 // C params.c:1798-1980 — char-search returns CHAR at match
14284 // position, not full substring. Verified empirically:
14285 // /bin/zsh -c 's="barfooxyz"; print "${s[(r)foo]}"' → "f"
14286 let out = getarg("(re)bc", None, None, Some("abcdef")).expect("Some");
14287 assert_eq!(val_str(out), "b");
14288 }
14289
14290 #[test]
14291 fn getarg_scalar_ie_returns_position_of_first_match() {
14292 let _g = crate::test_util::global_state_lock();
14293 let out = getarg("(ie)cd", None, None, Some("abcdef")).expect("Some");
14294 // 'cd' starts at 1-based position 3.
14295 assert_eq!(val_str(out), "3");
14296 }
14297
14298 #[test]
14299 fn getarg_scalar_Ie_returns_position_of_last_match() {
14300 let _g = crate::test_util::global_state_lock();
14301 let out = getarg("(Ie)b", None, None, Some("abcabc")).expect("Some");
14302 // Last 'b' is at 1-based position 5.
14303 assert_eq!(val_str(out), "5");
14304 }
14305
14306 #[test]
14307 fn getarg_scalar_ie_no_match_returns_len_plus_one() {
14308 let _g = crate::test_util::global_state_lock();
14309 let out = getarg("(ie)z", None, None, Some("abc")).expect("Some");
14310 assert_eq!(val_str(out), "4");
14311 }
14312
14313 #[test]
14314 fn getarg_scalar_Ie_no_match_returns_zero() {
14315 let _g = crate::test_util::global_state_lock();
14316 let out = getarg("(Ie)z", None, None, Some("abc")).expect("Some");
14317 assert_eq!(val_str(out), "0");
14318 }
14319
14320 #[test]
14321 fn getarg_scalar_n_flag_picks_second_match() {
14322 let _g = crate::test_util::global_state_lock();
14323 // C params.c:1929/1964 — `!--num` Nth-match counter on
14324 // scalar char-search. abcabc: 'a' at idx 0 and 3 → 2nd match
14325 // at byte position 4 (1-based).
14326 let out = getarg("(en.2.i)a", None, None, Some("abcabc")).expect("Some");
14327 assert_eq!(val_str(out), "4");
14328 }
14329
14330 #[test]
14331 fn getarg_scalar_b_flag_starts_from_offset() {
14332 let _g = crate::test_util::global_state_lock();
14333 // C params.c:1740-1742 — `(b.N.)` starts search from idx N-1.
14334 // abc bc abc: with b=4, skip first 3 chars; first 'b' at byte 5.
14335 let out = getarg("(b.4.ei)b", None, None, Some("abcbc")).expect("Some");
14336 assert_eq!(val_str(out), "4");
14337 }
14338
14339 #[test]
14340 fn getarg_scalar_re_n2_picks_second_substring() {
14341 let _g = crate::test_util::global_state_lock();
14342 let out = getarg("(en.2.r)b", None, None, Some("abab")).expect("Some");
14343 assert_eq!(val_str(out), "b");
14344 }
14345
14346 /// c:3076/3193 — assignsparam writes into paramtab; getsparam
14347 /// reads it back. The round-trip is the spine of every
14348 /// `foo=bar; print $foo` flow. Regression here would silently
14349 /// drop assignments.
14350 #[test]
14351 fn assignsparam_then_getsparam_round_trips() {
14352 let _g = crate::test_util::global_state_lock(); // c:3193
14353 // c:2697 — assignsparam → assignstrvalue bails when
14354 // unset(EXECOPT). The unit-test env doesn't run through
14355 // createoptiontable so we set "exec" explicitly to simulate
14356 // normal runtime. Mirrors the same setup used by
14357 // `setnumvalue_stores_int_value_into_scalar_pm` above.
14358 let saved_exec = opt_state_get("exec") // c:2697
14359 .unwrap_or(false); // c:2697
14360 opt_state_set("exec", true); // c:2697
14361 let name = "ZSHRS_TEST_ASSIGN_GET"; // c:3193
14362 assignsparam(name, "test_value_42", 0); // c:3193
14363 assert_eq!(
14364 // c:3076
14365 getsparam(name).as_deref(), // c:3076
14366 Some("test_value_42") // c:3076
14367 ); // c:3076
14368 // Cleanup so other tests don't see leaked param.
14369 let _ = paramtab().write().unwrap().remove(name); // c:3819
14370 opt_state_set("exec", saved_exec); // c:2697
14371 }
14372
14373 /// Regression: a subscript WRITE to a hash-storage-backed special assoc
14374 /// (`$compstate`, populated during completion via set_compstate_str) with a
14375 /// STRING key must succeed, not error "assignment to invalid subscript
14376 /// range". After the undeclared-subscript auto-vivify was removed, the
14377 /// completion system's `compstate[insert]=menu` writes started arithmetic-
14378 /// evaluating the `insert` key to 0 and failing — flooding real shells with
14379 /// errors on every completion. The fix recognises any name present in
14380 /// paramtab_hashed_storage() as associative. A name NOT in that store still
14381 /// errors (matches zsh: `compstate` only exists during completion).
14382 #[test]
14383 fn subscript_write_to_hash_backed_special_assoc_succeeds() {
14384 let _g = crate::test_util::global_state_lock();
14385 let saved_exec = opt_state_get("exec").unwrap_or(false);
14386 opt_state_set("exec", true);
14387 // Simulate the completion setup that populates $compstate before the
14388 // user completer widget runs (compcore::callcompfunc → set_compstate_str).
14389 paramtab_hashed_storage()
14390 .lock()
14391 .unwrap()
14392 .entry("compstate".to_string())
14393 .or_default();
14394 // User completer write: `compstate[insert]=menu`.
14395 let ok = assignsparam("compstate[insert]", "menu", 0);
14396 assert!(ok.is_some(), "compstate[insert]=menu must succeed, got error");
14397 assert_eq!(
14398 paramtab_hashed_storage()
14399 .lock()
14400 .unwrap()
14401 .get("compstate")
14402 .and_then(|m| m.get("insert"))
14403 .map(String::as_str),
14404 Some("menu"),
14405 );
14406 // Negative: a name with NO hash-storage backing and a non-numeric key
14407 // still errors (returns None) — the undeclared-subscript guard holds.
14408 let bad = assignsparam("zshrs_no_such_assoc[k1]", "bb", 0);
14409 assert!(bad.is_none(), "undeclared non-numeric subscript must still error");
14410 // Cleanup.
14411 let _ = paramtab_hashed_storage().lock().unwrap().remove("compstate");
14412 let _ = paramtab().write().unwrap().remove("compstate");
14413 let _ = paramtab().write().unwrap().remove("zshrs_no_such_assoc");
14414 opt_state_set("exec", saved_exec);
14415 }
14416
14417 /// c:3076 — getsparam on a non-existent param returns None.
14418 /// A regression returning Some("") would mask unset-param errors.
14419 #[test]
14420 fn getsparam_unknown_param_returns_none() {
14421 let _g = crate::test_util::global_state_lock();
14422 assert!(getsparam("ZSHRS_TEST_DEFINITELY_UNSET").is_none());
14423 }
14424
14425 /// c:3819 — direct paramtab.remove drops the entry; subsequent
14426 /// getsparam returns None. The set→remove→lookup gap verifies
14427 /// the canonical paramtab is actually backing both reads + writes.
14428 #[test]
14429 fn paramtab_remove_makes_getsparam_return_none() {
14430 let _g = crate::test_util::global_state_lock();
14431 let name = "ZSHRS_TEST_UNSET_FLOW";
14432 assignsparam(name, "to_be_removed", 0);
14433 assert!(
14434 getsparam(name).is_some(),
14435 "param must be set before remove path"
14436 );
14437 let _ = paramtab().write().unwrap().remove(name);
14438 assert!(
14439 getsparam(name).is_none(),
14440 "after remove, getsparam must return None"
14441 );
14442 }
14443
14444 /// c:3357 — assignaparam stores an array. getsparam on an array
14445 /// param returns the first element OR a join (depends on IFS).
14446 /// Verify the slot was populated AT ALL by querying paramtab.
14447 #[test]
14448 fn assignaparam_populates_paramtab_with_array() {
14449 let _g = crate::test_util::global_state_lock();
14450 let name = "ZSHRS_TEST_ARR_X";
14451 assignaparam(name, vec!["a".into(), "b".into(), "c".into()], 0);
14452 let tab = paramtab().read().expect("paramtab poisoned");
14453 let pm = tab.get(name).expect("param installed");
14454 assert_eq!(
14455 pm.u_arr.as_deref(),
14456 Some(&["a".to_string(), "b".to_string(), "c".to_string()][..]),
14457 "assignaparam stores all three elements"
14458 );
14459 drop(tab);
14460 let _ = paramtab().write().unwrap().remove(name);
14461 }
14462
14463 // Use the module-scope HISTCHARS_TEST_LOCK_SHARED (declared
14464 // outside the test modules) so gsu_tests + tests serialise
14465 // against the same Mutex rather than two independent ones.
14466
14467 /// `Src/params.c:5095-5097` — `histcharssetfn` stores bangchar /
14468 /// hatchar / hashchar in the per-char globals. Pin the round-trip
14469 /// for ALL THREE: change HISTCHARS to a custom 3-char string,
14470 /// verify each atomic global reflects the new value, and verify
14471 /// the canonical default `"!^#"` restores on NULL.
14472 #[test]
14473 fn histcharssetfn_syncs_all_three_histchar_globals() {
14474 let _g = crate::test_util::global_state_lock();
14475 let _g = HISTCHARS_TEST_LOCK_SHARED
14476 .lock()
14477 .unwrap_or_else(|e| e.into_inner());
14478 // Default state.
14479 histcharssetfn(&mut param::default(), String::new());
14480 assert_eq!(bangchar.load(Ordering::SeqCst), b'!' as i32);
14481 assert_eq!(hatchar.load(Ordering::SeqCst), b'^' as i32);
14482 assert_eq!(hashchar.load(Ordering::SeqCst), b'#' as i32);
14483 // Set HISTCHARS to "@:%".
14484 histcharssetfn(&mut param::default(), "@:%".to_string());
14485 assert_eq!(
14486 bangchar.load(Ordering::SeqCst),
14487 b'@' as i32,
14488 "c:5095 — bangchar = first byte of HISTCHARS"
14489 );
14490 assert_eq!(
14491 hatchar.load(Ordering::SeqCst),
14492 b':' as i32,
14493 "c:5096 — hatchar = second byte of HISTCHARS"
14494 );
14495 assert_eq!(
14496 hashchar.load(Ordering::SeqCst),
14497 b'%' as i32,
14498 "c:5097 — hashchar = third byte of HISTCHARS"
14499 );
14500 // Restore.
14501 histcharssetfn(&mut param::default(), String::new());
14502 assert_eq!(bangchar.load(Ordering::SeqCst), b'!' as i32);
14503 assert_eq!(hashchar.load(Ordering::SeqCst), b'#' as i32);
14504 }
14505
14506 /// `Src/params.c:5064-5074` — `histcharsgetfn` reads from the
14507 /// three atomic globals and returns a string of non-NUL bytes.
14508 /// Pin set→get symmetry: after `histcharssetfn(Some("@&%"))`,
14509 /// `histcharsgetfn(¶m::default())` returns `"@&%"`.
14510 #[test]
14511 fn histcharsgetfn_round_trips_with_histcharssetfn() {
14512 let _g = crate::test_util::global_state_lock();
14513 let _g = HISTCHARS_TEST_LOCK_SHARED
14514 .lock()
14515 .unwrap_or_else(|e| e.into_inner());
14516 histcharssetfn(&mut param::default(), "@&%".to_string());
14517 assert_eq!(
14518 histcharsgetfn(¶m::default()),
14519 "@&%",
14520 "c:5068-5073 — getfn reads atomic globals setfn wrote"
14521 );
14522 // Restore default and verify round-trip.
14523 histcharssetfn(&mut param::default(), String::new());
14524 assert_eq!(
14525 histcharsgetfn(¶m::default()),
14526 "!^#",
14527 "default `!^#` round-trips through atomics"
14528 );
14529 }
14530
14531 /// `Src/params.c:5118-5128` — `homesetfn(x)` round-trip:
14532 /// `homesetfn(s); homegetfn() == s` for non-symlink paths and
14533 /// CHASELINKS-off. Pins the basic store-then-read contract.
14534 #[test]
14535 fn homesetfn_stores_value_for_getfn() {
14536 let _g = crate::test_util::global_state_lock();
14537 let mut __pm = crate::ported::zsh_h::param::default();
14538 let saved = homegetfn(&__pm);
14539 homesetfn(&mut __pm, "/tmp/zshrs_test_home".to_string());
14540 assert_eq!(
14541 homegetfn(&__pm),
14542 "/tmp/zshrs_test_home",
14543 "c:5121-5126 — homesetfn → homegetfn round-trip"
14544 );
14545 // Restore.
14546 homesetfn(&mut __pm, saved);
14547 }
14548
14549 /// `Src/params.c:5125-5126` — empty input becomes `ztrdup("")`.
14550 /// Pin empty-string handling.
14551 #[test]
14552 fn homesetfn_empty_input_stores_empty() {
14553 let _g = crate::test_util::global_state_lock();
14554 let mut __pm = crate::ported::zsh_h::param::default();
14555 let saved = homegetfn(&__pm);
14556 homesetfn(&mut __pm, String::new());
14557 assert_eq!(
14558 homegetfn(&__pm),
14559 "",
14560 "c:5126 — empty x stores empty (no panic)"
14561 );
14562 homesetfn(&mut __pm, saved);
14563 }
14564
14565 /// `Src/params.c:5004-5011` — `errnosetfn(x)` writes errno
14566 /// unconditionally, then warns (NOT errors) on truncation. The
14567 /// store happens regardless of warning. Pin set→get round-trip.
14568 #[test]
14569 #[cfg(any(target_os = "macos", target_os = "linux"))]
14570 fn errnosetfn_writes_through_to_libc_errno_getfn() {
14571 let _g = crate::test_util::global_state_lock();
14572 // Set errno to a small int.
14573 errnosetfn(42);
14574 assert_eq!(
14575 errnogetfn(),
14576 42,
14577 "c:5006 — errno = (int)x; subsequent getfn must read it back"
14578 );
14579 errnosetfn(0);
14580 assert_eq!(errnogetfn(), 0);
14581 }
14582
14583 /// `Src/params.c:5008-5010` — truncation check fires when
14584 /// `(zlong)errno != x`. C also resets errno indirectly inside
14585 /// `zwarn` (libc calls touch errno) — so after the warning,
14586 /// the user's observed `$ERRNO` is the post-warning value, NOT
14587 /// the truncated cast. Faithful Rust port has the same behavior.
14588 /// Pin only that the function returns normally and doesn't crash;
14589 /// any specific post-call errno value is implementation-defined.
14590 #[test]
14591 #[cfg(any(target_os = "macos", target_os = "linux"))]
14592 fn errnosetfn_does_not_panic_on_truncation() {
14593 let _g = crate::test_util::global_state_lock();
14594 // i64::MAX → truncates to i32 = -1 → warning fires inside.
14595 // The store at c:5008 happens; whether the warning's libc
14596 // calls then overwrite errno is implementation-defined.
14597 errnosetfn(i64::MAX);
14598 // Just verify the call returned (no panic) and getfn works.
14599 let _ = errnogetfn();
14600 // Reset.
14601 errnosetfn(0);
14602 }
14603
14604 /// `Src/params.c:5090-5093` — non-ASCII chars in HISTCHARS
14605 /// produce a warning and the function returns WITHOUT updating
14606 /// any globals. Pin the rejection: state before == state after
14607 /// when a non-ASCII byte is in position 0/1/2.
14608 #[test]
14609 fn histcharssetfn_rejects_non_ascii_chars() {
14610 let _g = crate::test_util::global_state_lock();
14611 let _g = HISTCHARS_TEST_LOCK_SHARED
14612 .lock()
14613 .unwrap_or_else(|e| e.into_inner());
14614 // Reset to defaults.
14615 histcharssetfn(&mut param::default(), String::new());
14616 let bang_before = bangchar.load(Ordering::SeqCst);
14617 let hat_before = hatchar.load(Ordering::SeqCst);
14618 // Try to set HISTCHARS with non-ASCII char.
14619 histcharssetfn(&mut param::default(), "é".to_string());
14620 // c:5092 — rejection returns BEFORE any state changes.
14621 assert_eq!(
14622 bangchar.load(Ordering::SeqCst),
14623 bang_before,
14624 "c:5092 — bangchar unchanged after non-ASCII rejection"
14625 );
14626 assert_eq!(hatchar.load(Ordering::SeqCst), hat_before);
14627 }
14628
14629 /// Shared mutex for tests that mutate argzero/posixzero — both
14630 /// share global state and race when run in parallel.
14631 static ARGZERO_TEST_LOCK: Mutex<()> = Mutex::new(());
14632
14633 // HISTSIZ_TEST_LOCK is defined at module scope to share between
14634 // gsu_tests and tests submodules — both mutate histsiz.
14635
14636 /// `Src/params.c:4974-4977` — `histsizesetfn` floors at 1 then
14637 /// calls `resizehistents()` to prune the in-memory ring to the
14638 /// new cap. The previous Rust port skipped the resize call (and
14639 /// also failed to mirror the value into `hist::histsiz`), so
14640 /// HISTSIZE shrinks didn't take effect until the next implicit
14641 /// prune. Pin: setting HISTSIZE to N caps both the param store
14642 /// AND the hist::histsiz atomic used by resizehistents.
14643 #[test]
14644 fn histsizesetfn_floors_at_one_and_mirrors_to_hist_module() {
14645 let _g = crate::test_util::global_state_lock();
14646 let _g = HISTSIZ_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
14647 let saved_param = histsizegetfn();
14648 let saved_hist = histsiz.load(Ordering::SeqCst);
14649
14650 // c:4976 — value < 1 floors at 1.
14651 histsizesetfn(0);
14652 assert_eq!(histsizegetfn(), 1, "c:4976 — HISTSIZE 0 must floor at 1");
14653 assert_eq!(
14654 histsiz.load(Ordering::SeqCst),
14655 1,
14656 "c:4977 — mirror into hist::histsiz so resizehistents sees it"
14657 );
14658
14659 // Negative floors too.
14660 histsizesetfn(-5);
14661 assert_eq!(histsizegetfn(), 1, "c:4976 — negative floors at 1");
14662
14663 // Positive passes through.
14664 histsizesetfn(500);
14665 assert_eq!(histsizegetfn(), 500);
14666 assert_eq!(histsiz.load(Ordering::SeqCst), 500);
14667
14668 // Restore.
14669 *histsiz_lock().lock().unwrap() = saved_param;
14670 histsiz.store(saved_hist, Ordering::SeqCst);
14671 }
14672
14673 /// `Src/params.c:5152-5158` — `underscoregetfn` returns
14674 /// `dupstring(zunderscore)` then runs `untokenize(u)` on it.
14675 /// The Rust port previously skipped untokenize, exposing raw
14676 /// lexer-injected token bytes (Stringg, Equals, ...) in `$_`
14677 /// reads.
14678 #[test]
14679 fn underscoregetfn_runs_untokenize_on_zunderscore() {
14680 let _g = crate::test_util::global_state_lock();
14681 // Inject zunderscore containing a Pound token byte (\u{84})
14682 // and verify it gets stripped by untokenize in the return.
14683 let saved = zunderscore_lock().lock().unwrap().clone();
14684
14685 // Set zunderscore to a string containing a Pound token byte
14686 // surrounded by literals.
14687 let pound = Pound;
14688 let mut s = String::new();
14689 s.push('a');
14690 s.push(pound);
14691 s.push('b');
14692 *zunderscore_lock().lock().unwrap() = s;
14693
14694 let result = underscoregetfn();
14695 // c:5156 — untokenize replaces Pound (ITOK) with '#'
14696 // (its ztokens entry). The raw \u{84} byte must NOT survive.
14697 assert!(
14698 !result.contains(pound),
14699 "c:5156 — untokenize must strip Pound token byte from $_"
14700 );
14701 assert!(
14702 result.contains('#') || result.contains("a"),
14703 "c:5156 — Pound (ITOK) maps to '#' via ztokens[0]"
14704 );
14705
14706 // Restore.
14707 *zunderscore_lock().lock().unwrap() = saved;
14708 }
14709
14710 /// `Src/params.c:4954-4961` — `argzerogetfn` returns `posixzero`
14711 /// when `isset(POSIXARGZERO)`, else `argzero`. After mutating
14712 /// argzero (e.g. `exec -a foo`), `$0` under POSIXARGZERO must
14713 /// report the ORIGINAL startup argv[0], not the rewritten name.
14714 #[test]
14715 fn argzerogetfn_respects_posixargzero_option() {
14716 let _g = crate::test_util::global_state_lock();
14717 let _g = ARGZERO_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
14718
14719 // Save state.
14720 let saved_argzero = argzero();
14721 let saved_posixzero = posixzero();
14722 let saved_pos_option = opt_state_get("posixargzero").unwrap_or(false);
14723
14724 // Set up: posixzero (original) ≠ argzero (rewritten).
14725 set_posixzero(Some("/bin/zsh".to_string()));
14726 set_argzero(Some("rewritten-name".to_string()));
14727 // The set_argzero call mirrors to posixzero only if unset,
14728 // and we set posixzero first → mirror skipped. Confirm separation.
14729
14730 // pm is UNUSED in argzerogetfn (C signature matches Rust).
14731 // Use param::default() as the dummy carrier.
14732 let pm = param::default();
14733
14734 // POSIXARGZERO off → returns argzero.
14735 opt_state_set("posixargzero", false);
14736 assert_eq!(
14737 argzerogetfn(&pm),
14738 "rewritten-name",
14739 "c:4960 — !POSIXARGZERO returns argzero (current display name)"
14740 );
14741
14742 // POSIXARGZERO on → returns posixzero (the preserved startup argv[0]).
14743 opt_state_set("posixargzero", true);
14744 assert_eq!(
14745 argzerogetfn(&pm),
14746 "/bin/zsh",
14747 "c:4959 — POSIXARGZERO on returns posixzero (original startup argv[0])"
14748 );
14749
14750 // Restore.
14751 set_argzero(saved_argzero);
14752 set_posixzero(saved_posixzero);
14753 opt_state_set("posixargzero", saved_pos_option);
14754 }
14755
14756 /// `Src/init.c:271` — `argv0 = argzero = posixzero = *argv++`.
14757 /// At shell init both share the same source. The Rust port
14758 /// preserves this contract by having `set_argzero` mirror to
14759 /// `posixzero` ONLY on first call (when posixzero is None).
14760 /// Subsequent argzero changes (function frames, exec -a) must
14761 /// NOT clobber posixzero.
14762 #[test]
14763 fn set_argzero_mirrors_to_posixzero_only_on_first_call() {
14764 let _g = crate::test_util::global_state_lock();
14765 let _g = ARGZERO_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
14766
14767 let saved_argzero = argzero();
14768 let saved_posixzero = posixzero();
14769
14770 // Reset both to None.
14771 set_argzero(None);
14772 set_posixzero(None);
14773 // First call: posixzero is None, so it should mirror.
14774 set_argzero(Some("/usr/local/bin/zsh".to_string()));
14775 assert_eq!(
14776 posixzero().as_deref(),
14777 Some("/usr/local/bin/zsh"),
14778 "c:271 — first set_argzero mirrors to posixzero (was None)"
14779 );
14780 // Second call: posixzero now Some, so mirror is skipped.
14781 set_argzero(Some("function-name".to_string()));
14782 assert_eq!(
14783 posixzero().as_deref(),
14784 Some("/usr/local/bin/zsh"),
14785 "c:271 — second set_argzero does NOT clobber posixzero"
14786 );
14787 assert_eq!(
14788 argzero().as_deref(),
14789 Some("function-name"),
14790 "argzero updated as normal"
14791 );
14792
14793 // Restore.
14794 set_posixzero(saved_posixzero);
14795 set_argzero(saved_argzero);
14796 }
14797
14798 /// Locale-touching tests share process-wide env + libc state.
14799 /// Cargo runs tests in parallel by default, so without
14800 /// serialization a concurrent `env::set_var("LC_ALL")` can race
14801 /// a `env::remove_var("LC_ALL")` and corrupt assertions. Pin
14802 /// every locale test through this Mutex.
14803 fn locale_test_lock() -> &'static Mutex<()> {
14804 static L: OnceLock<Mutex<()>> = OnceLock::new();
14805 L.get_or_init(|| Mutex::new(()))
14806 }
14807
14808 /// Pin `LC_NAMES` to the canonical zsh `lc_names[]` table at
14809 /// `Src/params.c:4805-4825`. The five categories in entry order
14810 /// (LC_COLLATE, LC_CTYPE, LC_MESSAGES, LC_NUMERIC, LC_TIME) MUST
14811 /// match — `lcsetfn` walks this table by `strcmp(ln->name, pm->node.nam)`
14812 /// per c:4926 and dispatches to `setlocale(ln->category, ...)`.
14813 #[test]
14814 fn lc_names_match_zsh_canonical_table() {
14815 let _g = crate::test_util::global_state_lock();
14816 let names: Vec<&str> = LC_NAMES.iter().map(|(n, _)| *n).collect();
14817 assert_eq!(
14818 names,
14819 vec![
14820 "LC_COLLATE",
14821 "LC_CTYPE",
14822 "LC_MESSAGES",
14823 "LC_NUMERIC",
14824 "LC_TIME"
14825 ],
14826 "Src/params.c:4805-4825 — lc_names entry order must be preserved"
14827 );
14828 // Verify each name maps to a distinct libc category — proves
14829 // we aren't aliasing LC_NUMERIC to LC_TIME etc.
14830 let cats: Vec<libc::c_int> = LC_NAMES.iter().map(|(_, c)| *c).collect();
14831 let mut sorted = cats.clone();
14832 sorted.sort();
14833 sorted.dedup();
14834 assert_eq!(sorted.len(), 5, "all five LC_* categories must be distinct");
14835 assert!(cats.contains(&libc::LC_COLLATE));
14836 assert!(cats.contains(&libc::LC_CTYPE));
14837 assert!(cats.contains(&libc::LC_MESSAGES));
14838 assert!(cats.contains(&libc::LC_NUMERIC));
14839 assert!(cats.contains(&libc::LC_TIME));
14840 }
14841
14842 /// Pin `lcsetfn` to the canonical `setlocale` invocation at
14843 /// `Src/params.c:4925-4927`. When LC_ALL is empty and pm matches
14844 /// an entry in `lc_names`, libc setlocale MUST be called with
14845 /// the corresponding category. Verified by reading libc state
14846 /// back via `setlocale(cat, NULL)` after the assignment.
14847 #[test]
14848 fn lcsetfn_invokes_libc_setlocale_for_matching_category() {
14849 let _g = crate::test_util::global_state_lock();
14850 let _g = locale_test_lock().lock().unwrap_or_else(|e| e.into_inner());
14851 // Save LC_ALL/LC_CTYPE state.
14852 let saved_lc_all = env::var("LC_ALL").ok();
14853 let saved_lc_ctype = env::var("LC_CTYPE").ok();
14854 env::remove_var("LC_ALL"); // c:4912 LC_ALL must be empty for body to run
14855 // Drop LC_ALL from paramtab too — the sibling
14856 // `lcsetfn_short_circuits_when_lc_all_set` test sets it via
14857 // setsparam (params.rs:12986) and getsparam reads paramtab
14858 // BEFORE env::var (params.rs:4346). Without this clear, lcsetfn
14859 // sees the leftover paramtab value and short-circuits.
14860 unsetparam("LC_ALL");
14861
14862 // Read libc's current LC_CTYPE setting.
14863 let before = unsafe {
14864 let p = libc::setlocale(libc::LC_CTYPE, std::ptr::null());
14865 if p.is_null() {
14866 String::new()
14867 } else {
14868 std::ffi::CStr::from_ptr(p).to_string_lossy().into_owned()
14869 }
14870 };
14871
14872 // Call lcsetfn with LC_CTYPE → "C" (universally available POSIX locale).
14873 lcsetfn("LC_CTYPE", Some("C".to_string()));
14874
14875 // Read it back — must report "C" since C invokes setlocale(LC_CTYPE, "C").
14876 let after = unsafe {
14877 let p = libc::setlocale(libc::LC_CTYPE, std::ptr::null());
14878 if p.is_null() {
14879 String::new()
14880 } else {
14881 std::ffi::CStr::from_ptr(p).to_string_lossy().into_owned()
14882 }
14883 };
14884 assert_eq!(
14885 after, "C",
14886 "Src/params.c:4927 — lcsetfn must call setlocale(LC_CTYPE, \"C\")"
14887 );
14888
14889 // Env mirror also set.
14890 assert_eq!(env::var("LC_CTYPE").unwrap_or_default(), "C");
14891
14892 // Restore libc + env state.
14893 let _ = unsafe {
14894 let c = std::ffi::CString::new(before.as_bytes()).unwrap_or_default();
14895 libc::setlocale(libc::LC_CTYPE, c.as_ptr())
14896 };
14897 match saved_lc_all {
14898 Some(v) => env::set_var("LC_ALL", v),
14899 None => env::remove_var("LC_ALL"),
14900 }
14901 match saved_lc_ctype {
14902 Some(v) => env::set_var("LC_CTYPE", v),
14903 None => env::remove_var("LC_CTYPE"),
14904 }
14905 }
14906
14907 /// Pin `lcsetfn`'s LC_ALL early-return per c:4912-4913: when
14908 /// LC_ALL is non-empty, lcsetfn must short-circuit BEFORE
14909 /// touching libc setlocale for the per-category override.
14910 #[test]
14911 fn lcsetfn_short_circuits_when_lc_all_set() {
14912 let _g = crate::test_util::global_state_lock();
14913 let _g = locale_test_lock().lock().unwrap_or_else(|e| e.into_inner());
14914 let saved_lc_all = env::var("LC_ALL").ok();
14915 let saved_lc_ctype = env::var("LC_CTYPE").ok();
14916 env::set_var("LC_ALL", "C"); // c:4912 non-empty LC_ALL
14917 // Also stamp paramtab — `getsparam` reads paramtab FIRST
14918 // (params.rs:4346) and only falls through to `env::var` when
14919 // the key is absent. A prior test that left LC_ALL in paramtab
14920 // with an empty value (PM_UNSET tombstone or similar) makes
14921 // getsparam return Some("") and the env::set_var above never
14922 // reaches lcsetfn. Setting paramtab here pins the contract.
14923 setsparam("LC_ALL", "C");
14924
14925 // Capture libc state before.
14926 let before = unsafe {
14927 let p = libc::setlocale(libc::LC_CTYPE, std::ptr::null());
14928 if p.is_null() {
14929 String::new()
14930 } else {
14931 std::ffi::CStr::from_ptr(p).to_string_lossy().into_owned()
14932 }
14933 };
14934
14935 // Try to set LC_CTYPE; should NOT touch libc state.
14936 lcsetfn("LC_CTYPE", Some("POSIX".to_string()));
14937
14938 // libc state must be unchanged.
14939 let after = unsafe {
14940 let p = libc::setlocale(libc::LC_CTYPE, std::ptr::null());
14941 if p.is_null() {
14942 String::new()
14943 } else {
14944 std::ffi::CStr::from_ptr(p).to_string_lossy().into_owned()
14945 }
14946 };
14947 assert_eq!(
14948 before, after,
14949 "c:4912-4913 — lcsetfn must early-return when LC_ALL is non-empty"
14950 );
14951
14952 // Restore.
14953 match saved_lc_all {
14954 Some(v) => env::set_var("LC_ALL", v),
14955 None => env::remove_var("LC_ALL"),
14956 }
14957 match saved_lc_ctype {
14958 Some(v) => env::set_var("LC_CTYPE", v),
14959 None => env::remove_var("LC_CTYPE"),
14960 }
14961 }
14962
14963 /// Pin `getsparam_u` to its canonical C body at
14964 /// `Src/params.c:3089-3094`: returns `unmeta(getsparam(s))`,
14965 /// NOT a PM_SCALAR-checked `getstrvalue` wrapper.
14966 ///
14967 /// Before this fix, the Rust port took `Option<&mut value>`
14968 /// and gated on `PM_TYPE == PM_SCALAR` — a complete fabrication
14969 /// with no caller because no caller's type fit the bogus sig.
14970 #[test]
14971 fn getsparam_u_unmetas_getsparam_result() {
14972 let _g = crate::test_util::global_state_lock();
14973 let _g = locale_test_lock().lock().unwrap_or_else(|e| e.into_inner());
14974
14975 // Plain ASCII: getsparam_u returns the same content as
14976 // getsparam (no Meta bytes to strip).
14977 let saved = env::var("ZSHRS_TEST_LOCALE_GSU").ok();
14978 env::set_var("ZSHRS_TEST_LOCALE_GSU", "en_US.UTF-8");
14979 assert_eq!(
14980 getsparam_u("ZSHRS_TEST_LOCALE_GSU"),
14981 Some("en_US.UTF-8".to_string()),
14982 "Src/params.c:3092 — getsparam_u returns unmeta(getsparam(s)) for ASCII"
14983 );
14984
14985 // Missing param: returns None (matches C `if ((s = getsparam(s)))` false branch).
14986 env::remove_var("ZSHRS_TEST_LOCALE_GSU_MISSING");
14987 assert_eq!(
14988 getsparam_u("ZSHRS_TEST_LOCALE_GSU_MISSING"),
14989 None,
14990 "Src/params.c:3094 — getsparam_u returns NULL when getsparam returns NULL"
14991 );
14992
14993 // Restore.
14994 match saved {
14995 Some(v) => env::set_var("ZSHRS_TEST_LOCALE_GSU", v),
14996 None => env::remove_var("ZSHRS_TEST_LOCALE_GSU"),
14997 }
14998 }
14999
15000 /// Pin `setarrvalue` EXECOPT bail per `Src/params.c:2897-2898`.
15001 /// Same NO_EXEC semantic as setnumvalue: dry-run shell evaluation
15002 /// must not mutate array params.
15003 #[test]
15004 fn setarrvalue_bails_under_no_exec() {
15005 let _g = crate::test_util::global_state_lock();
15006
15007 let saved_exec = opt_state_get("exec").unwrap_or(false);
15008
15009 opt_state_set("exec", false);
15010 let pm = Box::new(param {
15011 node: hashnode {
15012 next: None,
15013 nam: "noexec_arr".to_string(),
15014 flags: PM_ARRAY as i32,
15015 },
15016 u_data: 0,
15017 u_tied: None,
15018 u_arr: Some(vec!["initial".to_string()]),
15019 u_str: None,
15020 u_val: 0,
15021 u_dval: 0.0,
15022 u_hash: None,
15023 gsu_s: None,
15024 gsu_i: None,
15025 gsu_f: None,
15026 gsu_a: None,
15027 gsu_h: None,
15028 base: 0,
15029 width: 0,
15030 env: None,
15031 ename: None,
15032 old: None,
15033 level: 0,
15034 });
15035 let mut v = value {
15036 pm: Some(pm),
15037 arr: Vec::new(),
15038 scanflags: 0,
15039 valflags: 0,
15040 start: 0,
15041 end: -1,
15042 };
15043 // Under NO_EXEC, the assign must be skipped.
15044 setarrvalue(&mut v, vec!["new1".to_string(), "new2".to_string()]);
15045 let arr = v.pm.as_ref().unwrap().u_arr.clone().unwrap_or_default();
15046 assert_eq!(
15047 arr,
15048 vec!["initial".to_string()],
15049 "c:2897 — NO_EXEC: setarrvalue must NOT replace u_arr"
15050 );
15051
15052 // With exec=true, the same call replaces.
15053 opt_state_set("exec", true);
15054 setarrvalue(&mut v, vec!["new1".to_string(), "new2".to_string()]);
15055 let arr = v.pm.as_ref().unwrap().u_arr.clone().unwrap_or_default();
15056 assert_eq!(
15057 arr,
15058 vec!["new1".to_string(), "new2".to_string()],
15059 "with EXEC set, setarrvalue replaces u_arr"
15060 );
15061
15062 opt_state_set("exec", saved_exec);
15063 }
15064
15065 /// Pin `setnumvalue` EXECOPT bail per `Src/params.c:2860`.
15066 /// When unset(EXECOPT) (i.e. NO_EXEC mode via `zsh -n` or
15067 /// `set -n`), param mutations MUST be skipped so dry-run shell
15068 /// evaluation doesn't leak state into the param table.
15069 #[test]
15070 fn setnumvalue_bails_under_no_exec() {
15071 let _g = crate::test_util::global_state_lock();
15072
15073 let saved_exec = opt_state_get("exec").unwrap_or(false);
15074
15075 // c:2860 — NO_EXEC: setnumvalue must not mutate the param.
15076 opt_state_set("exec", false);
15077 let mut pm = Box::new(param {
15078 node: hashnode {
15079 next: None,
15080 nam: "ne".to_string(),
15081 flags: PM_INTEGER as i32,
15082 },
15083 u_data: 0,
15084 u_tied: None,
15085 u_arr: None,
15086 u_str: None,
15087 u_val: 999,
15088 u_dval: 0.0,
15089 u_hash: None,
15090 gsu_s: None,
15091 gsu_i: None,
15092 gsu_f: None,
15093 gsu_a: None,
15094 gsu_h: None,
15095 base: 0,
15096 width: 0,
15097 env: None,
15098 ename: None,
15099 old: None,
15100 level: 0,
15101 });
15102 let mut v = value {
15103 pm: Some(pm.clone()),
15104 arr: Vec::new(),
15105 scanflags: 0,
15106 valflags: 0,
15107 start: 0,
15108 end: -1,
15109 };
15110 let val = mnumber {
15111 l: 42,
15112 d: 0.0,
15113 type_: MN_INTEGER,
15114 };
15115 setnumvalue(Some(&mut v), val);
15116 // pm.u_val MUST still be 999 (the initial), not 42.
15117 let stored = v.pm.as_ref().unwrap().u_val;
15118 assert_eq!(
15119 stored, 999,
15120 "c:2860 — NO_EXEC: setnumvalue must NOT mutate pm.u_val \
15121 (was {} but should stay 999)",
15122 stored
15123 );
15124
15125 // With exec=true, the same call mutates.
15126 opt_state_set("exec", true);
15127 setnumvalue(Some(&mut v), val);
15128 let stored = v.pm.as_ref().unwrap().u_val;
15129 assert_eq!(stored, 42, "with EXEC set, setnumvalue stores u_val = 42");
15130
15131 let _ = pm;
15132 opt_state_set("exec", saved_exec);
15133 }
15134
15135 /// Pin `$-` rendering to honor `set -n` (noexec). The previous
15136 /// Rust port called `opt("noexec")` which isn't a real option
15137 /// name in zsh — the lookup always returned false so `$-` never
15138 /// included 'n' even when `set -n` was active.
15139 #[test]
15140 fn dash_param_rendering_honors_noexec_via_exec_negation() {
15141 let _g = crate::test_util::global_state_lock();
15142 let saved = opt_state_get("exec").unwrap_or(false);
15143
15144 // With exec=true (default), $- should NOT include 'n'.
15145 opt_state_set("exec", true);
15146 let s = lookup_special_var("-").unwrap_or_default();
15147 assert!(
15148 !s.contains('n'),
15149 "exec=true → $-=`{}` must NOT include 'n'",
15150 s
15151 );
15152
15153 // With exec=false (`set -n`), $- SHOULD include 'n'.
15154 opt_state_set("exec", false);
15155 let s = lookup_special_var("-").unwrap_or_default();
15156 assert!(
15157 s.contains('n'),
15158 "exec=false → $-=`{}` MUST include 'n' (was silently dropped \
15159 when reading non-existent option name `noexec`)",
15160 s
15161 );
15162
15163 opt_state_set("exec", saved);
15164 }
15165
15166 /// Pin `TERM_UNKNOWN` bit value to the canonical C value at
15167 /// `Src/zsh.h:1986`. The previous params.rs duplicate had
15168 /// `1 << 0 = 0x01` which is actually C's TERM_BAD (Src/zsh.h:1985);
15169 /// the correct TERM_UNKNOWN value is 0x02. This single-byte
15170 /// drift caused the params.rs term-init code to silently set the
15171 /// TERM_BAD bit instead of TERM_UNKNOWN, while prompt.rs guards
15172 /// imported the correct 0x02 value from zsh_h.rs — the two
15173 /// paths disagreed about which bit means \"unknown terminal\".
15174 #[test]
15175 fn term_unknown_bit_value_matches_c() {
15176 let _g = crate::test_util::global_state_lock();
15177 assert_eq!(
15178 TERM_UNKNOWN, 0x02,
15179 "Src/zsh.h:1986 — TERM_UNKNOWN must be 0x02, got {:#x}",
15180 TERM_UNKNOWN
15181 );
15182 assert_eq!(
15183 TERM_BAD, 0x01,
15184 "Src/zsh.h:1985 — TERM_BAD must be 0x01 (and != TERM_UNKNOWN)"
15185 );
15186 // Crucially: TERM_BAD and TERM_UNKNOWN must be DISTINCT bits.
15187 assert_ne!(
15188 TERM_BAD, TERM_UNKNOWN,
15189 "TERM_BAD and TERM_UNKNOWN must be distinct (caught the 1<<0 drift bug)"
15190 );
15191 }
15192
15193 /// Pin `getstrvalue` PM_INTEGER branch to canonical C convbase
15194 /// dispatch at `Src/params.c:2373`. The previous Rust port used
15195 /// naked `.to_string()` (base-10) regardless of `pm.base`; C
15196 /// honors the param's stored base so `typeset -i 16 x=255` renders
15197 /// as `0xff` not `255`.
15198 #[test]
15199 fn getstrvalue_pm_integer_honors_pm_base() {
15200 let _g = crate::test_util::global_state_lock();
15201
15202 let saved_cbases_top = opt_state_get("cbases").unwrap_or(false);
15203 opt_state_set("cbases", true);
15204
15205 // Build a PM_INTEGER param with u_val=255 and base=16.
15206 let mut pm = Box::new(param {
15207 node: hashnode {
15208 next: None,
15209 nam: "test_hex_var".to_string(),
15210 flags: PM_INTEGER as i32,
15211 },
15212 u_data: 0,
15213 u_tied: None,
15214 u_arr: None,
15215 u_str: None,
15216 u_val: 255,
15217 u_dval: 0.0,
15218 u_hash: None,
15219 gsu_s: None,
15220 gsu_i: None,
15221 gsu_f: None,
15222 gsu_a: None,
15223 gsu_h: None,
15224 base: 16,
15225 width: 0,
15226 env: None,
15227 ename: None,
15228 old: None,
15229 level: 0,
15230 });
15231 let mut v = value {
15232 pm: Some(pm.clone()),
15233 arr: Vec::new(),
15234 scanflags: 0,
15235 valflags: 0,
15236 start: 0,
15237 end: -1,
15238 };
15239 let rendered = getstrvalue(Some(&mut v));
15240 assert_eq!(
15241 rendered, "0xFF",
15242 "c:2373 / c:5621 — PM_INTEGER base=16 + u_val=255 with CBASES \
15243 renders as `0xFF` (uppercase per C `dig - 10 + 'A'`), got {:?}",
15244 rendered
15245 );
15246
15247 // Base-8 (octal) with OCTALZEROES.
15248 let saved_oct = opt_state_get("octalzeroes").unwrap_or(false);
15249 let saved_cbases = opt_state_get("cbases").unwrap_or(false);
15250 opt_state_set("cbases", true);
15251 opt_state_set("octalzeroes", true);
15252 pm.base = 8;
15253 pm.u_val = 8;
15254 v.pm = Some(pm.clone());
15255 let rendered = getstrvalue(Some(&mut v));
15256 assert_eq!(
15257 rendered, "010",
15258 "c:2373 — PM_INTEGER base=8 with OCTALZEROES renders as `010`, got {:?}",
15259 rendered
15260 );
15261 opt_state_set("cbases", saved_cbases);
15262 opt_state_set("octalzeroes", saved_oct);
15263
15264 // Base=0 (default) → base-10.
15265 pm.base = 0;
15266 pm.u_val = 42;
15267 v.pm = Some(pm.clone());
15268 let rendered = getstrvalue(Some(&mut v));
15269 assert_eq!(
15270 rendered, "42",
15271 "c:2373 — PM_INTEGER base=0 defaults to base-10"
15272 );
15273
15274 opt_state_set("cbases", saved_cbases_top);
15275 }
15276
15277 /// Pin `unsetparam` to its canonical C body at `Src/params.c:3819-3833`.
15278 /// Two guards the previous Rust port skipped:
15279 /// 1. PM_NAMEREF params are NOT removed by unsetparam (c:3830).
15280 /// 2. PM_READONLY rejection per unsetparam_pm c:3850 — readonly
15281 /// params survive the unset call.
15282 #[test]
15283 fn unsetparam_skips_nameref_and_readonly() {
15284 let _g = crate::test_util::global_state_lock();
15285
15286 let saved_exec = opt_state_get("exec").unwrap_or(false);
15287 opt_state_set("exec", true);
15288
15289 // Helper: install a scalar param with the given flag-set.
15290 fn install(name: &str, value: &str, flags: u32) {
15291 let mut tab = paramtab().write().unwrap();
15292 tab.insert(
15293 name.to_string(),
15294 Box::new(param {
15295 node: hashnode {
15296 next: None,
15297 nam: name.to_string(),
15298 flags: (PM_SCALAR | flags) as i32,
15299 },
15300 u_data: 0,
15301 u_tied: None,
15302 u_arr: None,
15303 u_str: Some(value.to_string()),
15304 u_val: 0,
15305 u_dval: 0.0,
15306 u_hash: None,
15307 gsu_s: None,
15308 gsu_i: None,
15309 gsu_f: None,
15310 gsu_a: None,
15311 gsu_h: None,
15312 base: 0,
15313 width: 0,
15314 env: None,
15315 ename: None,
15316 old: None,
15317 level: 0,
15318 }),
15319 );
15320 }
15321
15322 // c:3830 — nameref params skip the unset.
15323 let nameref_name = "zshrs_test_unsetparam_nameref";
15324 install(nameref_name, "target_var_name", PM_NAMEREF);
15325 unsetparam(nameref_name);
15326 {
15327 let tab = paramtab().read().unwrap();
15328 assert!(
15329 tab.contains_key(nameref_name),
15330 "c:3830 — PM_NAMEREF param survives unsetparam"
15331 );
15332 }
15333
15334 // c:3850 (via unsetparam_pm) — readonly rejection.
15335 let ro_name = "zshrs_test_unsetparam_readonly";
15336 install(ro_name, "locked", PM_READONLY);
15337 unsetparam(ro_name);
15338 {
15339 let tab = paramtab().read().unwrap();
15340 assert!(
15341 tab.contains_key(ro_name),
15342 "c:3850 — PM_READONLY param survives unsetparam"
15343 );
15344 }
15345
15346 // Plain scalar removed normally.
15347 let plain_name = "zshrs_test_unsetparam_plain";
15348 install(plain_name, "removable", 0);
15349 unsetparam(plain_name);
15350 {
15351 let tab = paramtab().read().unwrap();
15352 assert!(
15353 !tab.contains_key(plain_name),
15354 "plain scalar successfully removed"
15355 );
15356 }
15357
15358 // Clean up.
15359 {
15360 let mut tab = paramtab().write().unwrap();
15361 tab.remove(nameref_name);
15362 tab.remove(ro_name);
15363 tab.remove(plain_name);
15364 }
15365 opt_state_set("exec", saved_exec);
15366 }
15367
15368 /// Pin `assigniparam` to its canonical C body at `Src/params.c:3754-3761`.
15369 /// Three-arg signature: `(s, val, flags)`. Previous Rust port
15370 /// dropped the flags arg AND returned void; this restores both.
15371 #[test]
15372 fn assigniparam_takes_flags_arg_and_returns_param() {
15373 let _g = crate::test_util::global_state_lock();
15374
15375 let saved_exec = opt_state_get("exec").unwrap_or(false);
15376 opt_state_set("exec", true);
15377
15378 let name = "zshrs_test_assigniparam_x";
15379 {
15380 let mut tab = paramtab().write().unwrap();
15381 tab.remove(name);
15382 }
15383
15384 // c:3755-3760 — assigniparam returns Param and threads flags through.
15385 let r = assigniparam(name, 77, ASSPM_WARN as i32);
15386 assert!(
15387 r.is_some(),
15388 "c:3760 — returns Some(Param) for new int param"
15389 );
15390 {
15391 let tab = paramtab().read().unwrap();
15392 let pm = tab.get(name).expect("integer param created");
15393 assert_ne!(
15394 (pm.node.flags as u32) & PM_INTEGER,
15395 0,
15396 "c:3757-3760 — PM_INTEGER flag set"
15397 );
15398 assert_eq!(pm.u_val, 77, "c:3759 — value stored in u_val");
15399 }
15400
15401 // Reassign with a different flag value (0 — no warnings).
15402 let r = assigniparam(name, 88, 0);
15403 assert!(r.is_some(), "reassign returns Some");
15404 {
15405 let tab = paramtab().read().unwrap();
15406 let pm = tab.get(name).expect("param still present");
15407 assert_eq!(pm.u_val, 88, "reassign updates u_val");
15408 }
15409
15410 // Clean up.
15411 {
15412 let mut tab = paramtab().write().unwrap();
15413 tab.remove(name);
15414 }
15415 opt_state_set("exec", saved_exec);
15416 }
15417
15418 /// Pin `setnparam` to its canonical C body at `Src/params.c:3745-3749`.
15419 /// MUST accept `mnumber` (integer or float) and return Param.
15420 /// Previous Rust port took `f64` only and returned void — losing
15421 /// the integer side and the Param return entirely.
15422 #[test]
15423 fn setnparam_accepts_both_integer_and_float() {
15424 let _g = crate::test_util::global_state_lock();
15425
15426 let saved_exec = opt_state_get("exec").unwrap_or(false);
15427 opt_state_set("exec", true);
15428
15429 // Clean up any leftover.
15430 let int_name = "zshrs_test_setnparam_i";
15431 let flt_name = "zshrs_test_setnparam_f";
15432 {
15433 let mut tab = paramtab().write().unwrap();
15434 tab.remove(int_name);
15435 tab.remove(flt_name);
15436 }
15437
15438 // c:3748 — integer branch: setnparam returns Some(Param) with
15439 // PM_INTEGER flag and u_val set.
15440 let r = setnparam(
15441 int_name,
15442 mnumber {
15443 l: 999,
15444 d: 0.0,
15445 type_: MN_INTEGER,
15446 },
15447 );
15448 assert!(r.is_some(), "setnparam returns Some for new param");
15449 {
15450 let tab = paramtab().read().unwrap();
15451 let pm = tab.get(int_name).expect("integer param created");
15452 assert_ne!(
15453 (pm.node.flags as u32) & PM_INTEGER,
15454 0,
15455 "c:3748 — PM_INTEGER flag set for integer mnumber"
15456 );
15457 assert_eq!(pm.u_val, 999, "c:3748 — integer value stored in u_val");
15458 }
15459
15460 // c:3748 — float branch: setnparam with MN_FLOAT creates PM_FFLOAT.
15461 let r = setnparam(
15462 flt_name,
15463 mnumber {
15464 l: 0,
15465 d: 3.14,
15466 type_: MN_FLOAT,
15467 },
15468 );
15469 assert!(r.is_some(), "setnparam returns Some for new float param");
15470 {
15471 let tab = paramtab().read().unwrap();
15472 let pm = tab.get(flt_name).expect("float param created");
15473 assert_ne!(
15474 (pm.node.flags as u32) & PM_FFLOAT,
15475 0,
15476 "c:3748 — PM_FFLOAT flag set for float mnumber"
15477 );
15478 assert!(
15479 (pm.u_dval - 3.14).abs() < 1e-10,
15480 "c:3748 — float value stored in u_dval"
15481 );
15482 }
15483
15484 // Clean up.
15485 {
15486 let mut tab = paramtab().write().unwrap();
15487 tab.remove(int_name);
15488 tab.remove(flt_name);
15489 }
15490 opt_state_set("exec", saved_exec);
15491 }
15492
15493 /// Pin `setiparam` to its canonical C body at `Src/params.c:3767-3773`.
15494 /// MUST create the param as PM_INTEGER via `assignnparam`, not as
15495 /// PM_SCALAR via `assignsparam` with a stringified value.
15496 #[test]
15497 fn setiparam_creates_pm_integer_param() {
15498 let _g = crate::test_util::global_state_lock();
15499 let name = "zshrs_test_setiparam_x";
15500
15501 // C: `assignnparam` bails when `unset(EXECOPT)` (Src/params.c:3679).
15502 // Real zsh startup sets exec=true; the unit-test env doesn't run
15503 // through `createoptiontable` so we set "exec" explicitly to
15504 // simulate normal runtime.
15505 let saved_exec = opt_state_get("exec").unwrap_or(false);
15506 opt_state_set("exec", true);
15507
15508 // Clean up any leftover.
15509 {
15510 let mut tab = paramtab().write().unwrap();
15511 tab.remove(name);
15512 }
15513
15514 // Set integer value.
15515 setiparam(name, 42);
15516
15517 // Param should exist with PM_INTEGER flag set + u_val == 42.
15518 {
15519 let tab = paramtab().read().unwrap();
15520 let pm = tab.get(name).expect("setiparam must create the param");
15521 assert_ne!(
15522 (pm.node.flags as u32) & PM_INTEGER,
15523 0,
15524 "c:3770-3772 — created param must have PM_INTEGER flag set, \
15525 got flags = {:#x}",
15526 pm.node.flags
15527 );
15528 assert_eq!(pm.u_val, 42, "c:3771 — integer value stored in pm.u_val");
15529 }
15530
15531 // Reassign to verify update path also keeps PM_INTEGER.
15532 setiparam(name, 100);
15533 {
15534 let tab = paramtab().read().unwrap();
15535 let pm = tab.get(name).expect("setiparam reassign must keep param");
15536 assert_eq!(pm.u_val, 100, "reassign updates the integer value");
15537 assert_ne!(
15538 (pm.node.flags as u32) & PM_INTEGER,
15539 0,
15540 "reassign keeps PM_INTEGER flag"
15541 );
15542 }
15543
15544 // Clean up.
15545 {
15546 let mut tab = paramtab().write().unwrap();
15547 tab.remove(name);
15548 }
15549 // Restore EXECOPT.
15550 opt_state_set("exec", saved_exec);
15551 }
15552
15553 /// Pin `gethparam` / `gethkparam` to their canonical C bodies at
15554 /// `Src/params.c:3117-3140`. Same signature-fix family as `getaparam`:
15555 /// the `name: &str` path with digit-first reject + PM_HASHED check.
15556 #[test]
15557 fn gethparam_and_gethkparam_signature_matches_c() {
15558 let _g = crate::test_util::global_state_lock();
15559 // c:3122 / c:3136 — digit-first name reject.
15560 assert_eq!(
15561 gethparam("123abc"),
15562 None,
15563 "c:3122 — digit-first name rejected"
15564 );
15565 assert_eq!(
15566 gethkparam("123abc"),
15567 None,
15568 "c:3136 — digit-first name rejected"
15569 );
15570
15571 // Missing param → None.
15572 assert_eq!(
15573 gethparam("zshrs_test_hashparam_xyz"),
15574 None,
15575 "missing param returns None"
15576 );
15577 assert_eq!(
15578 gethkparam("zshrs_test_hashparam_xyz"),
15579 None,
15580 "missing param returns None"
15581 );
15582
15583 // PM_SCALAR param (not hashed) → None.
15584 {
15585 let mut tab = paramtab().write().unwrap();
15586 tab.insert(
15587 "zshrs_test_gethp_scalar".to_string(),
15588 Box::new(param {
15589 node: hashnode {
15590 next: None,
15591 nam: "zshrs_test_gethp_scalar".to_string(),
15592 flags: PM_SCALAR as i32,
15593 },
15594 u_data: 0,
15595 u_tied: None,
15596 u_arr: None,
15597 u_str: Some("scalar value".to_string()),
15598 u_val: 0,
15599 u_dval: 0.0,
15600 u_hash: None,
15601 gsu_s: None,
15602 gsu_i: None,
15603 gsu_f: None,
15604 gsu_a: None,
15605 gsu_h: None,
15606 base: 0,
15607 width: 0,
15608 env: None,
15609 ename: None,
15610 old: None,
15611 level: 0,
15612 }),
15613 );
15614 }
15615 assert_eq!(
15616 gethparam("zshrs_test_gethp_scalar"),
15617 None,
15618 "c:3123 — non-PM_HASHED returns None"
15619 );
15620 assert_eq!(
15621 gethkparam("zshrs_test_gethp_scalar"),
15622 None,
15623 "c:3137 — non-PM_HASHED returns None"
15624 );
15625
15626 // PM_HASHED param → Some(Vec::new()) (backend not yet wired,
15627 // but signature should at least classify the type correctly).
15628 {
15629 let mut tab = paramtab().write().unwrap();
15630 tab.insert(
15631 "zshrs_test_gethp_hash".to_string(),
15632 Box::new(param {
15633 node: hashnode {
15634 next: None,
15635 nam: "zshrs_test_gethp_hash".to_string(),
15636 flags: PM_HASHED as i32,
15637 },
15638 u_data: 0,
15639 u_tied: None,
15640 u_arr: None,
15641 u_str: None,
15642 u_val: 0,
15643 u_dval: 0.0,
15644 u_hash: None,
15645 gsu_s: None,
15646 gsu_i: None,
15647 gsu_f: None,
15648 gsu_a: None,
15649 gsu_h: None,
15650 base: 0,
15651 width: 0,
15652 env: None,
15653 ename: None,
15654 old: None,
15655 level: 0,
15656 }),
15657 );
15658 }
15659 assert_eq!(
15660 gethparam("zshrs_test_gethp_hash"),
15661 Some(Vec::new()),
15662 "c:3123-3124 — PM_HASHED empty-storage returns Some(empty vec)"
15663 );
15664 assert_eq!(
15665 gethkparam("zshrs_test_gethp_hash"),
15666 Some(Vec::new()),
15667 "c:3137-3138 — PM_HASHED empty-storage returns Some(empty vec)"
15668 );
15669
15670 // Populate paramtab_hashed_storage and verify gethparam/gethkparam
15671 // return the actual values/keys per c:3124 (SCANPM_WANTVALS) and
15672 // c:3138 (SCANPM_WANTKEYS). IndexMap preserves insertion order.
15673 {
15674 let mut store = paramtab_hashed_storage().lock().unwrap();
15675 let mut map: IndexMap<String, String> = IndexMap::new();
15676 map.insert("k1".to_string(), "v1".to_string());
15677 map.insert("k2".to_string(), "v2".to_string());
15678 store.insert("zshrs_test_gethp_hash".to_string(), map);
15679 }
15680 assert_eq!(
15681 gethparam("zshrs_test_gethp_hash"),
15682 Some(vec!["v1".to_string(), "v2".to_string()]),
15683 "c:3124 — paramvalarr(SCANPM_WANTVALS) returns values from hashed-storage"
15684 );
15685 assert_eq!(
15686 gethkparam("zshrs_test_gethp_hash"),
15687 Some(vec!["k1".to_string(), "k2".to_string()]),
15688 "c:3138 — paramvalarr(SCANPM_WANTKEYS) returns keys from hashed-storage"
15689 );
15690
15691 // Clean up.
15692 {
15693 let mut tab = paramtab().write().unwrap();
15694 tab.remove("zshrs_test_gethp_scalar");
15695 tab.remove("zshrs_test_gethp_hash");
15696 }
15697 paramtab_hashed_storage()
15698 .lock()
15699 .unwrap()
15700 .remove("zshrs_test_gethp_hash");
15701 }
15702
15703 /// Pin `getaparam` to its canonical C body at `Src/params.c:3101-3110`.
15704 /// Three branches: digit-first reject (c:3107), PM_ARRAY return
15705 /// (c:3108-3109), non-array / missing-param return None (c:3110).
15706 #[test]
15707 fn getaparam_returns_array_for_pm_array_only() {
15708 let _g = crate::test_util::global_state_lock();
15709 // c:3107 — digit-first name → None (positional params reject).
15710 assert_eq!(
15711 getaparam("123abc"),
15712 None,
15713 "c:3107 — digit-first name rejected"
15714 );
15715
15716 // c:3110 — missing param → None.
15717 assert_eq!(
15718 getaparam("zshrs_test_arr_nonexistent_xyz"),
15719 None,
15720 "c:3110 — missing param returns None"
15721 );
15722
15723 // Helper that builds a Param via the canonical createparam
15724 // path so we don't reach into struct internals (param has
15725 // many fields and no Default).
15726 fn build_arr(name: &str, arr: Vec<String>) {
15727 let mut tab = paramtab().write().unwrap();
15728 tab.insert(
15729 name.to_string(),
15730 Box::new(param {
15731 node: hashnode {
15732 next: None,
15733 nam: name.to_string(),
15734 flags: PM_ARRAY as i32,
15735 },
15736 u_data: 0,
15737 u_tied: None,
15738 u_arr: Some(arr),
15739 u_str: None,
15740 u_val: 0,
15741 u_dval: 0.0,
15742 u_hash: None,
15743 gsu_s: None,
15744 gsu_i: None,
15745 gsu_f: None,
15746 gsu_a: None,
15747 gsu_h: None,
15748 base: 0,
15749 width: 0,
15750 env: None,
15751 ename: None,
15752 old: None,
15753 level: 0,
15754 }),
15755 );
15756 }
15757 fn build_scalar(name: &str, s: &str) {
15758 let mut tab = paramtab().write().unwrap();
15759 tab.insert(
15760 name.to_string(),
15761 Box::new(param {
15762 node: hashnode {
15763 next: None,
15764 nam: name.to_string(),
15765 flags: PM_SCALAR as i32,
15766 },
15767 u_data: 0,
15768 u_tied: None,
15769 u_arr: None,
15770 u_str: Some(s.to_string()),
15771 u_val: 0,
15772 u_dval: 0.0,
15773 u_hash: None,
15774 gsu_s: None,
15775 gsu_i: None,
15776 gsu_f: None,
15777 gsu_a: None,
15778 gsu_h: None,
15779 base: 0,
15780 width: 0,
15781 env: None,
15782 ename: None,
15783 old: None,
15784 level: 0,
15785 }),
15786 );
15787 }
15788
15789 // c:3108 — PM_ARRAY param returns the array contents.
15790 build_arr(
15791 "zshrs_test_getaparam_arr",
15792 vec!["one".to_string(), "two".to_string(), "three".to_string()],
15793 );
15794 assert_eq!(
15795 getaparam("zshrs_test_getaparam_arr"),
15796 Some(vec![
15797 "one".to_string(),
15798 "two".to_string(),
15799 "three".to_string()
15800 ]),
15801 "c:3108-3109 — PM_ARRAY param returns its array"
15802 );
15803
15804 // c:3108 — PM_SCALAR (non-array) param → None.
15805 build_scalar("zshrs_test_getaparam_scalar", "not an array");
15806 assert_eq!(
15807 getaparam("zshrs_test_getaparam_scalar"),
15808 None,
15809 "c:3108 — non-PM_ARRAY param returns None"
15810 );
15811
15812 // Clean up.
15813 {
15814 let mut tab = paramtab().write().unwrap();
15815 tab.remove("zshrs_test_getaparam_arr");
15816 tab.remove("zshrs_test_getaparam_scalar");
15817 }
15818 }
15819
15820 // ═══════════════════════════════════════════════════════════════════
15821 // setX/getX round-trips and cross-type coercion.
15822 // Anchored to zsh behavior:
15823 // - `typeset -i x=42; print -- $x` → "42"
15824 // - `x=42; print -- ${(t)x}` → "scalar" (set without typeset stays scalar)
15825 // - integer ↔ scalar coercion when reading back.
15826 // Each test sets up via setX, reads back via getX, and asserts the
15827 // observable shell-level value.
15828 // ═══════════════════════════════════════════════════════════════════
15829
15830 fn with_exec<F: FnOnce()>(body: F) {
15831 let _g = crate::test_util::global_state_lock();
15832 let saved = opt_state_get("exec").unwrap_or(false);
15833 opt_state_set("exec", true);
15834 body();
15835 opt_state_set("exec", saved);
15836 }
15837
15838 // ── Scalar round-trip ──────────────────────────────────────────
15839 /// `setsparam("X", "hello"); getsparam("X")` → Some("hello").
15840 /// Anchor: `X=hello; print -r -- "$X"` in zsh → `hello`.
15841 #[test]
15842 fn setsparam_then_getsparam_roundtrip_basic() {
15843 with_exec(|| {
15844 unsetparam("zshrs_rt_s1");
15845 setsparam("zshrs_rt_s1", "hello");
15846 assert_eq!(getsparam("zshrs_rt_s1").as_deref(), Some("hello"));
15847 unsetparam("zshrs_rt_s1");
15848 });
15849 }
15850
15851 /// Empty string round-trip — scalar can hold "".
15852 #[test]
15853 fn setsparam_empty_string_roundtrips_as_empty() {
15854 with_exec(|| {
15855 unsetparam("zshrs_rt_s2");
15856 setsparam("zshrs_rt_s2", "");
15857 assert_eq!(getsparam("zshrs_rt_s2").as_deref(), Some(""));
15858 unsetparam("zshrs_rt_s2");
15859 });
15860 }
15861
15862 /// Multi-byte UTF-8 scalar round-trip.
15863 /// Anchor: `X="日本語"; print -r -- "$X"` → "日本語"
15864 #[test]
15865 fn setsparam_multibyte_utf8_roundtrips() {
15866 with_exec(|| {
15867 unsetparam("zshrs_rt_s3");
15868 setsparam("zshrs_rt_s3", "日本語");
15869 assert_eq!(getsparam("zshrs_rt_s3").as_deref(), Some("日本語"));
15870 unsetparam("zshrs_rt_s3");
15871 });
15872 }
15873
15874 /// Embedded newline survives round-trip.
15875 #[test]
15876 fn setsparam_embedded_newline_roundtrips() {
15877 with_exec(|| {
15878 unsetparam("zshrs_rt_s4");
15879 setsparam("zshrs_rt_s4", "a\nb\nc");
15880 assert_eq!(getsparam("zshrs_rt_s4").as_deref(), Some("a\nb\nc"));
15881 unsetparam("zshrs_rt_s4");
15882 });
15883 }
15884
15885 /// Overwrite: setsparam twice returns the latter value.
15886 #[test]
15887 fn setsparam_overwrite_replaces_previous_value() {
15888 with_exec(|| {
15889 unsetparam("zshrs_rt_s5");
15890 setsparam("zshrs_rt_s5", "first");
15891 setsparam("zshrs_rt_s5", "second");
15892 assert_eq!(getsparam("zshrs_rt_s5").as_deref(), Some("second"));
15893 unsetparam("zshrs_rt_s5");
15894 });
15895 }
15896
15897 // ── Integer round-trip ─────────────────────────────────────────
15898 /// `setiparam("X", 42); getiparam("X")` → 42.
15899 /// Anchor: `typeset -i X=42; print -- $X` → "42".
15900 #[test]
15901 fn setiparam_then_getiparam_roundtrip_basic() {
15902 with_exec(|| {
15903 unsetparam("zshrs_rt_i1");
15904 setiparam("zshrs_rt_i1", 42);
15905 assert_eq!(getiparam("zshrs_rt_i1"), 42);
15906 unsetparam("zshrs_rt_i1");
15907 });
15908 }
15909
15910 /// Negative integer round-trip.
15911 #[test]
15912 fn setiparam_negative_value_roundtrips() {
15913 with_exec(|| {
15914 unsetparam("zshrs_rt_i2");
15915 setiparam("zshrs_rt_i2", -12345);
15916 assert_eq!(getiparam("zshrs_rt_i2"), -12345);
15917 unsetparam("zshrs_rt_i2");
15918 });
15919 }
15920
15921 /// Zero round-trip — distinct from "unset".
15922 #[test]
15923 fn setiparam_zero_roundtrips() {
15924 with_exec(|| {
15925 unsetparam("zshrs_rt_i3");
15926 setiparam("zshrs_rt_i3", 0);
15927 assert_eq!(getiparam("zshrs_rt_i3"), 0);
15928 unsetparam("zshrs_rt_i3");
15929 });
15930 }
15931
15932 /// `i64::MAX` and `i64::MIN` survive — zsh uses zlong (= long long
15933 /// on 64-bit hosts, which is i64 in Rust).
15934 #[test]
15935 fn setiparam_i64_extremes_roundtrip() {
15936 with_exec(|| {
15937 unsetparam("zshrs_rt_i4");
15938 setiparam("zshrs_rt_i4", i64::MAX);
15939 assert_eq!(getiparam("zshrs_rt_i4"), i64::MAX);
15940 setiparam("zshrs_rt_i4", i64::MIN);
15941 assert_eq!(getiparam("zshrs_rt_i4"), i64::MIN);
15942 unsetparam("zshrs_rt_i4");
15943 });
15944 }
15945
15946 // ── Cross-type coercion: setiparam → getsparam (int → string) ──
15947 /// `setiparam("X", 42); getsparam("X")` → Some("42").
15948 /// Anchor: `typeset -i X=42; print -r -- "$X"` → "42".
15949 #[test]
15950 fn setiparam_then_getsparam_coerces_to_decimal_string() {
15951 with_exec(|| {
15952 unsetparam("zshrs_rt_x1");
15953 setiparam("zshrs_rt_x1", 42);
15954 assert_eq!(getsparam("zshrs_rt_x1").as_deref(), Some("42"));
15955 unsetparam("zshrs_rt_x1");
15956 });
15957 }
15958
15959 /// Negative int → "-N" string form via getsparam.
15960 #[test]
15961 fn setiparam_negative_int_to_string_carries_minus_sign() {
15962 with_exec(|| {
15963 unsetparam("zshrs_rt_x2");
15964 setiparam("zshrs_rt_x2", -7);
15965 assert_eq!(getsparam("zshrs_rt_x2").as_deref(), Some("-7"));
15966 unsetparam("zshrs_rt_x2");
15967 });
15968 }
15969
15970 // ── Cross-type: setsparam("42") → getiparam → 42 ───────────────
15971 /// `setsparam("X", "42"); getiparam("X")` → 42 (numeric parse).
15972 /// Anchor: `X=42; print -- $((X+0))` → "42" (arith coerces).
15973 #[test]
15974 fn setsparam_numeric_string_then_getiparam_coerces_to_int() {
15975 with_exec(|| {
15976 unsetparam("zshrs_rt_x3");
15977 setsparam("zshrs_rt_x3", "42");
15978 assert_eq!(getiparam("zshrs_rt_x3"), 42);
15979 unsetparam("zshrs_rt_x3");
15980 });
15981 }
15982
15983 /// Non-numeric scalar → getiparam returns 0 (C zsh behavior).
15984 /// Anchor: `X=hello; print -- $((X+0))` → "0" (no numeric parse).
15985 #[test]
15986 fn setsparam_non_numeric_then_getiparam_returns_zero() {
15987 with_exec(|| {
15988 unsetparam("zshrs_rt_x4");
15989 setsparam("zshrs_rt_x4", "not a number");
15990 assert_eq!(getiparam("zshrs_rt_x4"), 0);
15991 unsetparam("zshrs_rt_x4");
15992 });
15993 }
15994
15995 // ── Array round-trip ───────────────────────────────────────────
15996 /// `setaparam("X", vec![...]); getaparam("X")` round-trips elements.
15997 /// Anchor: `X=(a b c); print -r -- "${X[@]}"` → "a b c".
15998 #[test]
15999 fn setaparam_then_getaparam_roundtrip_basic() {
16000 with_exec(|| {
16001 unsetparam("zshrs_rt_a1");
16002 setaparam("zshrs_rt_a1", vec!["a".into(), "b".into(), "c".into()]);
16003 assert_eq!(
16004 getaparam("zshrs_rt_a1"),
16005 Some(vec!["a".into(), "b".into(), "c".into()])
16006 );
16007 unsetparam("zshrs_rt_a1");
16008 });
16009 }
16010
16011 /// Empty array round-trip — distinct from unset.
16012 #[test]
16013 fn setaparam_empty_array_roundtrips_as_empty_vec() {
16014 with_exec(|| {
16015 unsetparam("zshrs_rt_a2");
16016 setaparam("zshrs_rt_a2", vec![]);
16017 assert_eq!(getaparam("zshrs_rt_a2"), Some(vec![]));
16018 unsetparam("zshrs_rt_a2");
16019 });
16020 }
16021
16022 /// Array element with embedded space stays one element (not split).
16023 /// Anchor: `X=("hi there" world); print ${#X}` → 2.
16024 #[test]
16025 fn setaparam_element_with_space_stays_one_element() {
16026 with_exec(|| {
16027 unsetparam("zshrs_rt_a3");
16028 setaparam("zshrs_rt_a3", vec!["hi there".into(), "world".into()]);
16029 assert_eq!(
16030 getaparam("zshrs_rt_a3"),
16031 Some(vec!["hi there".into(), "world".into()])
16032 );
16033 unsetparam("zshrs_rt_a3");
16034 });
16035 }
16036
16037 // ── unsetparam: idempotent + clears value ──────────────────────
16038 /// Unsetting a never-set param does nothing (no panic, no error).
16039 #[test]
16040 fn unsetparam_on_never_set_param_is_noop() {
16041 let _g = crate::test_util::global_state_lock();
16042 unsetparam("zshrs_rt_never_existed");
16043 // No assertion — just must not panic.
16044 }
16045
16046 /// Unset then get → None.
16047 #[test]
16048 fn unsetparam_then_getsparam_returns_none() {
16049 with_exec(|| {
16050 setsparam("zshrs_rt_u1", "value");
16051 unsetparam("zshrs_rt_u1");
16052 assert_eq!(getsparam("zshrs_rt_u1"), None);
16053 });
16054 }
16055
16056 /// Unsetting twice is idempotent.
16057 #[test]
16058 fn unsetparam_twice_is_idempotent() {
16059 with_exec(|| {
16060 setsparam("zshrs_rt_u2", "value");
16061 unsetparam("zshrs_rt_u2");
16062 unsetparam("zshrs_rt_u2"); // second call must not panic
16063 assert_eq!(getsparam("zshrs_rt_u2"), None);
16064 });
16065 }
16066
16067 // ── Cross-type: setiparam writes through scalar table, getaparam None ─
16068 /// PM_INTEGER is NOT PM_ARRAY — getaparam on integer returns None.
16069 /// Anchor: `typeset -i X=42; print -- ${X[1]}` errors (not an array).
16070 #[test]
16071 fn setiparam_then_getaparam_returns_none_not_an_array() {
16072 with_exec(|| {
16073 unsetparam("zshrs_rt_x5");
16074 setiparam("zshrs_rt_x5", 42);
16075 assert_eq!(
16076 getaparam("zshrs_rt_x5"),
16077 None,
16078 "PM_INTEGER is not PM_ARRAY — getaparam must return None"
16079 );
16080 unsetparam("zshrs_rt_x5");
16081 });
16082 }
16083
16084 // ── Read missing param: returns None / 0 ───────────────────────
16085 /// `getsparam("doesnt_exist")` → None.
16086 #[test]
16087 fn getsparam_on_unset_returns_none() {
16088 let _g = crate::test_util::global_state_lock();
16089 unsetparam("zshrs_rt_missing");
16090 assert_eq!(getsparam("zshrs_rt_missing"), None);
16091 }
16092
16093 /// `getiparam("doesnt_exist")` → 0 (C zsh semantics).
16094 /// Anchor: `unset X; print -- $((X+1))` → "1" (X read as 0).
16095 #[test]
16096 fn getiparam_on_unset_returns_zero() {
16097 let _g = crate::test_util::global_state_lock();
16098 unsetparam("zshrs_rt_missing2");
16099 assert_eq!(getiparam("zshrs_rt_missing2"), 0);
16100 }
16101
16102 /// `getaparam("doesnt_exist")` → None.
16103 #[test]
16104 fn getaparam_on_unset_returns_none() {
16105 let _g = crate::test_util::global_state_lock();
16106 unsetparam("zshrs_rt_missing3");
16107 assert_eq!(getaparam("zshrs_rt_missing3"), None);
16108 }
16109
16110 // ─── zsh corpus pins: params set/get round-trips ─────────────────
16111
16112 /// `setsparam("foo", "bar")` followed by `getsparam("foo")` returns
16113 /// `"bar"`. Round-trip scalar.
16114 #[test]
16115 fn params_corpus_scalar_round_trip_simple() {
16116 let _g = crate::test_util::global_state_lock();
16117 unsetparam("ZP_RT1");
16118 setsparam("ZP_RT1", "bar");
16119 assert_eq!(getsparam("ZP_RT1").as_deref(), Some("bar"));
16120 unsetparam("ZP_RT1");
16121 }
16122
16123 /// `setsparam("x", "")` then `getsparam("x")` returns `Some("")`,
16124 /// NOT `None` — empty-set is distinct from unset (per zsh semantics).
16125 #[test]
16126 fn params_corpus_empty_scalar_is_set_not_unset() {
16127 let _g = crate::test_util::global_state_lock();
16128 unsetparam("ZP_EMP");
16129 setsparam("ZP_EMP", "");
16130 assert_eq!(getsparam("ZP_EMP").as_deref(), Some(""), "empty-set is set");
16131 unsetparam("ZP_EMP");
16132 }
16133
16134 /// `unsetparam` after `setsparam` removes the param entirely:
16135 /// `getsparam` returns `None`.
16136 #[test]
16137 fn params_corpus_unset_after_set_returns_none() {
16138 let _g = crate::test_util::global_state_lock();
16139 setsparam("ZP_UR", "v");
16140 unsetparam("ZP_UR");
16141 assert_eq!(getsparam("ZP_UR"), None, "unsetparam removes param");
16142 }
16143
16144 /// `setiparam("i", 42)` and `getiparam` round-trip integer.
16145 #[test]
16146 fn params_corpus_integer_round_trip() {
16147 let _g = crate::test_util::global_state_lock();
16148 unsetparam("ZP_I");
16149 setiparam("ZP_I", 42);
16150 assert_eq!(getiparam("ZP_I"), 42);
16151 unsetparam("ZP_I");
16152 }
16153
16154 /// `setiparam` then `getsparam` reads integer as string.
16155 #[test]
16156 fn params_corpus_integer_read_as_string() {
16157 let _g = crate::test_util::global_state_lock();
16158 unsetparam("ZP_IS");
16159 setiparam("ZP_IS", 42);
16160 assert_eq!(
16161 getsparam("ZP_IS").as_deref(),
16162 Some("42"),
16163 "integer param reads as string repr"
16164 );
16165 unsetparam("ZP_IS");
16166 }
16167
16168 /// `setaparam("a", vec!["one","two","three"])` round-trips via
16169 /// `getaparam`.
16170 #[test]
16171 fn params_corpus_array_round_trip() {
16172 let _g = crate::test_util::global_state_lock();
16173 unsetparam("ZP_A");
16174 setaparam("ZP_A", vec!["one".into(), "two".into(), "three".into()]);
16175 assert_eq!(
16176 getaparam("ZP_A").as_deref(),
16177 Some(&["one".into(), "two".into(), "three".into()][..]),
16178 "array round-trip",
16179 );
16180 unsetparam("ZP_A");
16181 }
16182
16183 /// Empty array round-trip — `setaparam("a", vec![])` then
16184 /// `getaparam` returns `Some(empty vec)`, not `None`.
16185 #[test]
16186 fn params_corpus_empty_array_is_set_not_unset() {
16187 let _g = crate::test_util::global_state_lock();
16188 unsetparam("ZP_EA");
16189 setaparam("ZP_EA", vec![]);
16190 let got = getaparam("ZP_EA");
16191 assert!(got.is_some(), "empty array is set, not None");
16192 assert_eq!(got.unwrap().len(), 0, "len = 0");
16193 unsetparam("ZP_EA");
16194 }
16195
16196 /// `setsparam` overwrites previous value (scalar reassignment).
16197 #[test]
16198 fn params_corpus_scalar_overwrite() {
16199 let _g = crate::test_util::global_state_lock();
16200 unsetparam("ZP_O");
16201 setsparam("ZP_O", "first");
16202 setsparam("ZP_O", "second");
16203 assert_eq!(
16204 getsparam("ZP_O").as_deref(),
16205 Some("second"),
16206 "scalar overwrites cleanly"
16207 );
16208 unsetparam("ZP_O");
16209 }
16210
16211 /// Re-assigning a scalar to an array (changing type via setaparam).
16212 /// Default zsh allows this: array replaces scalar.
16213 #[test]
16214 fn params_corpus_scalar_to_array_replace_type() {
16215 let _g = crate::test_util::global_state_lock();
16216 unsetparam("ZP_ST");
16217 setsparam("ZP_ST", "scalar_val");
16218 setaparam("ZP_ST", vec!["a".into(), "b".into()]);
16219 assert_eq!(
16220 getaparam("ZP_ST").as_deref(),
16221 Some(&["a".into(), "b".into()][..]),
16222 "setaparam replaces scalar type",
16223 );
16224 unsetparam("ZP_ST");
16225 }
16226
16227 /// `unsetparam` on a name that doesn't exist is a no-op
16228 /// (zsh's idempotent unset).
16229 #[test]
16230 fn params_corpus_unset_on_missing_is_noop() {
16231 let _g = crate::test_util::global_state_lock();
16232 unsetparam("ZP_NEVER_EXISTED_42_xyz");
16233 // No panic: just returns None on subsequent get.
16234 assert_eq!(getsparam("ZP_NEVER_EXISTED_42_xyz"), None);
16235 }
16236
16237 /// Empty string passed to integer slot: `setiparam("z", 0)`
16238 /// returns `Some(0)` via getiparam (zero is a legitimate int).
16239 #[test]
16240 fn params_corpus_integer_zero_is_set() {
16241 let _g = crate::test_util::global_state_lock();
16242 unsetparam("ZP_IZ");
16243 setiparam("ZP_IZ", 0);
16244 assert_eq!(getiparam("ZP_IZ"), 0);
16245 assert_eq!(getsparam("ZP_IZ").as_deref(), Some("0"));
16246 unsetparam("ZP_IZ");
16247 }
16248
16249 /// Negative integers round-trip with sign preserved.
16250 #[test]
16251 fn params_corpus_integer_negative_preserves_sign() {
16252 let _g = crate::test_util::global_state_lock();
16253 unsetparam("ZP_IN");
16254 setiparam("ZP_IN", -42);
16255 assert_eq!(getiparam("ZP_IN"), -42);
16256 assert_eq!(getsparam("ZP_IN").as_deref(), Some("-42"));
16257 unsetparam("ZP_IN");
16258 }
16259
16260 // ─── associative array (hash) pins ───────────────────────────────
16261
16262 /// `sethparam` + `gethparam` round-trip. C `gethparam` returns
16263 /// `paramvalarr(..., SCANPM_WANTVALS)` (Src/params.c:3122) — i.e.
16264 /// the VALUES side of the hash, not flat k/v pairs. For
16265 /// `(key1 val1 key2 val2)` that's `[val1, val2]`. Keys come from
16266 /// `gethkparam` (covered by `..._hash_keys_only_returns_keys`).
16267 #[test]
16268 fn params_corpus_hash_round_trip_basic() {
16269 let _g = crate::test_util::global_state_lock();
16270 unsetparam("ZP_H");
16271 sethparam(
16272 "ZP_H",
16273 vec!["key1".into(), "val1".into(), "key2".into(), "val2".into()],
16274 );
16275 let got = gethparam("ZP_H");
16276 assert!(got.is_some(), "hash param set");
16277 let g = got.unwrap();
16278 assert_eq!(g.len(), 2, "2 values (SCANPM_WANTVALS) preserved");
16279 let mut sorted = g.clone();
16280 sorted.sort();
16281 assert_eq!(
16282 sorted,
16283 vec!["val1".to_string(), "val2".to_string()],
16284 "values come back regardless of hash-iter order"
16285 );
16286 unsetparam("ZP_H");
16287 }
16288
16289 /// `gethparam` on missing returns None.
16290 #[test]
16291 fn params_corpus_hash_missing_returns_none() {
16292 let _g = crate::test_util::global_state_lock();
16293 unsetparam("ZP_HM_xyz");
16294 assert!(gethparam("ZP_HM_xyz").is_none());
16295 }
16296
16297 /// `gethkparam` returns the keys-only view of a hash. With
16298 /// `{a:1,b:2}`, keys are `["a","b"]` (order may not be stable).
16299 #[test]
16300 fn params_corpus_hash_keys_only_returns_keys() {
16301 let _g = crate::test_util::global_state_lock();
16302 unsetparam("ZP_HK");
16303 sethparam(
16304 "ZP_HK",
16305 vec!["a".into(), "1".into(), "b".into(), "2".into()],
16306 );
16307 let keys = gethkparam("ZP_HK");
16308 assert!(keys.is_some(), "keys-only view exists");
16309 let k = keys.unwrap();
16310 assert_eq!(k.len(), 2, "2 keys");
16311 let mut sorted = k.clone();
16312 sorted.sort();
16313 assert_eq!(sorted, vec!["a".to_string(), "b".to_string()]);
16314 unsetparam("ZP_HK");
16315 }
16316
16317 /// Setting a hash with empty `Vec` removes the previous key/value
16318 /// pairs. After empty `sethparam`, lookups find no entries. Note
16319 /// `gethparam` returns SCANPM_WANTVALS (values only); `(a 1)` has
16320 /// 1 value, not 2 — see `..._hash_round_trip_basic`.
16321 #[test]
16322 fn params_corpus_hash_set_empty_clears_entries() {
16323 let _g = crate::test_util::global_state_lock();
16324 unsetparam("ZP_HC");
16325 sethparam("ZP_HC", vec!["a".into(), "1".into()]);
16326 assert_eq!(gethparam("ZP_HC").map(|v| v.len()), Some(1));
16327 sethparam("ZP_HC", vec![]);
16328 // empty hash still exists (some implementations distinguish
16329 // empty hash from unset)
16330 let after = gethparam("ZP_HC");
16331 if let Some(v) = after {
16332 assert!(v.is_empty(), "empty hash has 0 elements");
16333 }
16334 unsetparam("ZP_HC");
16335 }
16336
16337 /// Large integer at i64 boundary: i64::MAX round-trips.
16338 #[test]
16339 fn params_corpus_integer_max_round_trips() {
16340 let _g = crate::test_util::global_state_lock();
16341 unsetparam("ZP_IMX");
16342 setiparam("ZP_IMX", i64::MAX);
16343 assert_eq!(getiparam("ZP_IMX"), i64::MAX);
16344 unsetparam("ZP_IMX");
16345 }
16346
16347 /// i64::MIN round-trips with sign.
16348 #[test]
16349 fn params_corpus_integer_min_round_trips() {
16350 let _g = crate::test_util::global_state_lock();
16351 unsetparam("ZP_IMN");
16352 setiparam("ZP_IMN", i64::MIN);
16353 assert_eq!(getiparam("ZP_IMN"), i64::MIN);
16354 unsetparam("ZP_IMN");
16355 }
16356
16357 // ═══════════════════════════════════════════════════════════════════
16358 // C-parity tests pinning Src/params.c:1288 (isident). Foundational
16359 // identifier validator used by every param-name validation site.
16360 // ═══════════════════════════════════════════════════════════════════
16361
16362 /// `isident("")` returns false. C c:1292:
16363 /// `if (!*s) return 0;`
16364 #[test]
16365 fn isident_empty_string_returns_false() {
16366 let _g = crate::test_util::global_state_lock();
16367 assert!(!isident(""));
16368 }
16369
16370 /// `isident("foo")` returns true — basic identifier.
16371 #[test]
16372 fn isident_alpha_identifier_returns_true() {
16373 let _g = crate::test_util::global_state_lock();
16374 assert!(isident("foo"));
16375 }
16376
16377 /// `isident("_foo")` — underscore prefix allowed.
16378 #[test]
16379 fn isident_underscore_prefix_returns_true() {
16380 let _g = crate::test_util::global_state_lock();
16381 assert!(isident("_foo"));
16382 }
16383
16384 /// `isident("foo_bar123")` — alnum + underscore mid.
16385 #[test]
16386 fn isident_alnum_with_underscore_returns_true() {
16387 let _g = crate::test_util::global_state_lock();
16388 assert!(isident("foo_bar123"));
16389 }
16390
16391 /// `isident("123")` — all-digit positional param. C c:1300+:
16392 /// "All-digit names are valid (positional params)"
16393 #[test]
16394 fn isident_all_digit_positional_returns_true() {
16395 let _g = crate::test_util::global_state_lock();
16396 assert!(isident("123"), "all-digit names are positional params");
16397 }
16398
16399 /// `isident("123abc")` — digit prefix with letters → false.
16400 /// Mixed digit-first not a valid positional param OR identifier.
16401 #[test]
16402 fn isident_digit_prefix_alpha_returns_false() {
16403 let _g = crate::test_util::global_state_lock();
16404 assert!(!isident("123abc"));
16405 }
16406
16407 /// `isident(".ns.foo")` — ksh93 namespace dotted name allowed.
16408 /// C c:1296-1311 — leading `.` accepted if not followed by digit.
16409 #[test]
16410 fn isident_ksh93_namespace_dot_prefix_returns_true() {
16411 let _g = crate::test_util::global_state_lock();
16412 assert!(isident(".ns.foo"));
16413 }
16414
16415 /// `isident(".0bad")` — namespace must NOT start with digit.
16416 /// C c:1300: `if (idigit(s[1])) return 0;`
16417 #[test]
16418 fn isident_namespace_starting_with_digit_returns_false() {
16419 let _g = crate::test_util::global_state_lock();
16420 assert!(!isident(".0bad"), "namespace can't start with digit");
16421 }
16422
16423 /// `isident("foo-bar")` — dash not allowed in identifier.
16424 #[test]
16425 fn isident_dash_in_name_returns_false() {
16426 let _g = crate::test_util::global_state_lock();
16427 assert!(!isident("foo-bar"));
16428 }
16429
16430 /// `isident("foo bar")` — space not allowed.
16431 #[test]
16432 fn isident_space_in_name_returns_false() {
16433 let _g = crate::test_util::global_state_lock();
16434 assert!(!isident("foo bar"));
16435 }
16436
16437 /// `isident("foo[0]")` — subscript at end is allowed (C handles
16438 /// it in `isident_requires_balanced_subscript_brackets`).
16439 /// Pin: a simple `[0]` subscript validates.
16440 #[test]
16441 fn isident_with_simple_subscript_returns_true() {
16442 let _g = crate::test_util::global_state_lock();
16443 assert!(isident("foo[0]"));
16444 }
16445
16446 // ═══════════════════════════════════════════════════════════════════
16447 // Additional C-parity tests for Src/params.c isident.
16448 // ═══════════════════════════════════════════════════════════════════
16449
16450 /// c:1288 — `isident("")` returns false (empty not an identifier).
16451 #[test]
16452 fn isident_empty_returns_false() {
16453 let _g = crate::test_util::global_state_lock();
16454 assert!(!isident(""));
16455 }
16456
16457 /// c:1288 — single underscore IS valid ident.
16458 #[test]
16459 fn isident_single_underscore_is_valid() {
16460 let _g = crate::test_util::global_state_lock();
16461 assert!(isident("_"));
16462 }
16463
16464 /// c:1288 — single letter IS valid ident.
16465 #[test]
16466 fn isident_single_letter_is_valid() {
16467 let _g = crate::test_util::global_state_lock();
16468 assert!(isident("a"));
16469 assert!(isident("Z"));
16470 }
16471
16472 /// c:1288 — all-digit name (positional param like '$1') IS valid.
16473 #[test]
16474 fn isident_all_digits_is_valid_positional() {
16475 let _g = crate::test_util::global_state_lock();
16476 assert!(isident("1"));
16477 assert!(isident("123"));
16478 assert!(isident("0"));
16479 }
16480
16481 /// c:1288 — digit-first mixed (e.g. '1abc') is INVALID.
16482 #[test]
16483 fn isident_digit_first_mixed_is_invalid() {
16484 let _g = crate::test_util::global_state_lock();
16485 assert!(!isident("1abc"));
16486 assert!(!isident("0x"));
16487 }
16488
16489 /// c:1288 — underscore + alnum is valid.
16490 #[test]
16491 fn isident_underscore_prefix_alnum_is_valid() {
16492 let _g = crate::test_util::global_state_lock();
16493 assert!(isident("_foo"));
16494 assert!(isident("_123"));
16495 assert!(isident("foo_bar"));
16496 }
16497
16498 /// c:1288 — alphabetic + digits is valid.
16499 #[test]
16500 fn isident_alpha_digits_mix_is_valid() {
16501 let _g = crate::test_util::global_state_lock();
16502 assert!(isident("foo123"));
16503 assert!(isident("var2"));
16504 }
16505
16506 /// c:1326 — unbalanced subscript `foo[` is invalid.
16507 #[test]
16508 fn isident_unbalanced_open_bracket_returns_false() {
16509 let _g = crate::test_util::global_state_lock();
16510 assert!(!isident("foo["));
16511 assert!(!isident("foo[0"));
16512 }
16513
16514 /// c:1326 — special chars like `-`, `.`, `@` not allowed in identifier.
16515 #[test]
16516 fn isident_special_chars_rejected() {
16517 let _g = crate::test_util::global_state_lock();
16518 assert!(!isident("foo-bar")); // hyphen
16519 assert!(!isident("foo@bar"));
16520 assert!(!isident("foo!"));
16521 }
16522
16523 /// c:1288 — namespace prefix `.ns.var` requires a non-digit after dot.
16524 #[test]
16525 fn isident_namespace_prefix_dot_rejects_digit_after() {
16526 let _g = crate::test_util::global_state_lock();
16527 assert!(!isident(".0"), "dot + digit invalid");
16528 assert!(!isident("."), "lone dot invalid");
16529 }
16530
16531 /// c:1288 — namespace prefix `.foo` is valid.
16532 #[test]
16533 fn isident_namespace_prefix_alpha_after_dot_valid() {
16534 let _g = crate::test_util::global_state_lock();
16535 assert!(isident(".foo"));
16536 assert!(isident(".ns.var"));
16537 }
16538
16539 /// c:1288 — `isident` is deterministic.
16540 #[test]
16541 fn isident_is_deterministic() {
16542 let _g = crate::test_util::global_state_lock();
16543 for s in ["foo", "_bar", "123", "1abc", "", ".foo", "foo[", "foo-bar"] {
16544 let first = isident(s);
16545 for _ in 0..5 {
16546 assert_eq!(isident(s), first, "{:?} must be pure", s);
16547 }
16548 }
16549 }
16550
16551 // ═══════════════════════════════════════════════════════════════════
16552 // Additional C-parity tests for Src/params.c
16553 // c:1403 issetvar / c:2152 setsparam / c:2317 isident / c:4099 getiparam
16554 // c:4191 getsparam / c:4326 getsparam_u / c:4353 getaparam /
16555 // c:5363 setaparam / c:5696 setiparam / c:5775 unsetparam
16556 // ═══════════════════════════════════════════════════════════════════
16557
16558 /// c:1403 — `issetvar` returns i32 (compile-time type pin).
16559 #[test]
16560 fn issetvar_returns_i32_type() {
16561 let _g = crate::test_util::global_state_lock();
16562 let _: i32 = issetvar("anything");
16563 }
16564
16565 /// c:1403 — `issetvar("__nonexistent")` returns 0 for unset.
16566 #[test]
16567 fn issetvar_unknown_returns_zero() {
16568 let _g = crate::test_util::global_state_lock();
16569 let r = issetvar("__never_a_real_var_xyz_zshrs__");
16570 assert_eq!(r, 0, "unset var → 0");
16571 }
16572
16573 /// c:4191 — `getsparam` returns Option<String>.
16574 #[test]
16575 fn getsparam_returns_option_string_type() {
16576 let _g = crate::test_util::global_state_lock();
16577 let _: Option<String> = getsparam("anything");
16578 }
16579
16580 /// c:4191 — `getsparam("__nonexistent")` returns None.
16581 #[test]
16582 fn getsparam_unknown_returns_none() {
16583 let _g = crate::test_util::global_state_lock();
16584 assert!(
16585 getsparam("__never_a_real_var_xyz_zshrs__").is_none(),
16586 "unknown var → None"
16587 );
16588 }
16589
16590 /// c:4099 — `getiparam` returns i64.
16591 #[test]
16592 fn getiparam_returns_i64_type() {
16593 let _g = crate::test_util::global_state_lock();
16594 let _: i64 = getiparam("anything");
16595 }
16596
16597 /// c:4353 — `getaparam` returns Option<Vec<String>>.
16598 #[test]
16599 fn getaparam_returns_option_vec_string_type() {
16600 let _g = crate::test_util::global_state_lock();
16601 let _: Option<Vec<String>> = getaparam("anything");
16602 }
16603
16604 /// c:4326 — `getsparam_u("")` empty returns None.
16605 #[test]
16606 fn getsparam_u_empty_returns_none() {
16607 let _g = crate::test_util::global_state_lock();
16608 assert!(getsparam_u("").is_none(), "empty → None");
16609 }
16610
16611 /// c:2152 — `setsparam`/`getsparam` round-trip.
16612 #[test]
16613 fn setsparam_getsparam_round_trip() {
16614 let _g = crate::test_util::global_state_lock();
16615 let name = "ZSHRS_TEST_SETSPARAM_ROUND_TRIP";
16616 let _ = unsetparam(name);
16617 setsparam(name, "hello");
16618 assert_eq!(
16619 getsparam(name).as_deref(),
16620 Some("hello"),
16621 "setsparam/getsparam round-trip"
16622 );
16623 unsetparam(name);
16624 }
16625
16626 /// c:5696 — `setiparam`/`getiparam` round-trip.
16627 #[test]
16628 fn setiparam_getiparam_round_trip() {
16629 let _g = crate::test_util::global_state_lock();
16630 let name = "ZSHRS_TEST_SETIPARAM_ROUND_TRIP";
16631 unsetparam(name);
16632 setiparam(name, 42);
16633 assert_eq!(getiparam(name), 42, "setiparam/getiparam round-trip");
16634 unsetparam(name);
16635 }
16636
16637 /// c:5363 — `setaparam`/`getaparam` round-trip preserves array.
16638 #[test]
16639 fn setaparam_getaparam_round_trip() {
16640 let _g = crate::test_util::global_state_lock();
16641 let name = "ZSHRS_TEST_SETAPARAM_ROUND_TRIP";
16642 unsetparam(name);
16643 let v = vec!["a".to_string(), "b".to_string(), "c".to_string()];
16644 setaparam(name, v.clone());
16645 assert_eq!(getaparam(name), Some(v), "setaparam/getaparam round-trip");
16646 unsetparam(name);
16647 }
16648
16649 /// c:5775 — `unsetparam` is idempotent on unset names.
16650 #[test]
16651 fn unsetparam_unknown_name_no_panic() {
16652 let _g = crate::test_util::global_state_lock();
16653 unsetparam("__never_real_var_xyz__");
16654 unsetparam("__never_real_var_xyz__");
16655 }
16656
16657 // ═══════════════════════════════════════════════════════════════════
16658 // Additional C-parity tests for Src/params.c GSU vtable callbacks.
16659 // c:3993 intgetfn / c:4002 intsetfn / c:4011 floatgetfn
16660 // c:4020 floatsetfn / c:4029 strgetfn / c:4057 arrgetfn
16661 // c:4084 hashgetfn / c:4093 hashsetfn / c:4180 nullstrsetfn
16662 // c:4192 nullunsetfn
16663 // ═══════════════════════════════════════════════════════════════════
16664
16665 /// c:3993-3997 — `intgetfn` body is `return pm->u.val;`. Pin that
16666 /// the Rust port reads `pm.u_val` verbatim with no transform.
16667 #[test]
16668 fn intgetfn_returns_u_val_verbatim() {
16669 let _g = crate::test_util::global_state_lock();
16670 let mut pm = param::default();
16671 pm.u_val = 0;
16672 assert_eq!(intgetfn(&pm), 0, "u_val=0 → 0");
16673 pm.u_val = 42;
16674 assert_eq!(intgetfn(&pm), 42, "u_val=42 → 42");
16675 pm.u_val = i64::MIN;
16676 assert_eq!(intgetfn(&pm), i64::MIN, "u_val=i64::MIN preserved");
16677 pm.u_val = i64::MAX;
16678 assert_eq!(intgetfn(&pm), i64::MAX, "u_val=i64::MAX preserved");
16679 }
16680
16681 /// c:4002-4006 — `intsetfn` body is `pm->u.val = x;`. For a
16682 /// non-special name (not SECONDS/RANDOM), the Rust port must
16683 /// write `pm.u_val = x` and intgetfn must round-trip.
16684 #[test]
16685 fn intsetfn_intgetfn_round_trip_for_non_special_name() {
16686 let _g = crate::test_util::global_state_lock();
16687 let mut pm = param::default();
16688 pm.node.nam = "X".to_string(); // non-SECONDS, non-RANDOM
16689 intsetfn(&mut pm, 7);
16690 assert_eq!(intgetfn(&pm), 7, "non-special intsetfn writes u_val");
16691 intsetfn(&mut pm, -123);
16692 assert_eq!(intgetfn(&pm), -123, "negative i64 round-trips");
16693 }
16694
16695 /// c:4011-4015 — `floatgetfn` body is `return pm->u.dval;`.
16696 /// Read `pm.u_dval` verbatim with no transform.
16697 #[test]
16698 fn floatgetfn_returns_u_dval_verbatim() {
16699 let _g = crate::test_util::global_state_lock();
16700 let mut pm = param::default();
16701 pm.u_dval = 0.0;
16702 assert_eq!(floatgetfn(&pm), 0.0, "u_dval=0.0 → 0.0");
16703 pm.u_dval = 3.14;
16704 assert_eq!(floatgetfn(&pm), 3.14, "u_dval=3.14 preserved");
16705 pm.u_dval = -2.71;
16706 assert_eq!(floatgetfn(&pm), -2.71, "negative dval preserved");
16707 }
16708
16709 /// c:4020-4024 — `floatsetfn` body for non-SECONDS is
16710 /// `pm->u.dval = x;`. Round-trip via floatgetfn.
16711 #[test]
16712 fn floatsetfn_floatgetfn_round_trip_for_non_seconds() {
16713 let _g = crate::test_util::global_state_lock();
16714 let mut pm = param::default();
16715 pm.node.nam = "Y".to_string(); // non-SECONDS
16716 floatsetfn(&mut pm, 1.5);
16717 assert_eq!(floatgetfn(&pm), 1.5, "non-SECONDS floatsetfn writes u_dval");
16718 floatsetfn(&mut pm, -0.0);
16719 assert_eq!(floatgetfn(&pm), -0.0, "neg zero round-trips");
16720 }
16721
16722 /// c:4029-4033 — `strgetfn` C body returns `pm->u.str ? pm->u.str
16723 /// : (char *) hcalloc(1);` — when u_str is None the C path
16724 /// returns a freshly allocated empty C string. Rust port must
16725 /// return `String::new()` (empty owned String) for the None case.
16726 #[test]
16727 fn strgetfn_returns_empty_string_when_u_str_none() {
16728 let _g = crate::test_util::global_state_lock();
16729 let mut pm = param::default();
16730 pm.u_str = None;
16731 let s = strgetfn(&pm);
16732 assert!(
16733 s.is_empty(),
16734 "None u_str → empty String (port of hcalloc(1))"
16735 );
16736 }
16737
16738 /// c:4029-4033 — `strgetfn` returns u_str clone when present.
16739 #[test]
16740 fn strgetfn_returns_clone_when_u_str_some() {
16741 let _g = crate::test_util::global_state_lock();
16742 let mut pm = param::default();
16743 pm.u_str = Some("hello".to_string());
16744 assert_eq!(strgetfn(&pm), "hello", "Some(s) → s");
16745 }
16746
16747 /// c:4057-4061 — `arrgetfn` C body returns `pm->u.arr ? pm->u.arr
16748 /// : &nullarray;` — when u_arr is None, the C path returns a
16749 /// pointer to the static empty `nullarray`. Rust port returns
16750 /// `Vec::new()` for the None case (empty owned Vec).
16751 #[test]
16752 fn arrgetfn_returns_empty_vec_when_u_arr_none() {
16753 let _g = crate::test_util::global_state_lock();
16754 let mut pm = param::default();
16755 pm.u_arr = None;
16756 let v = arrgetfn(&pm);
16757 assert!(v.is_empty(), "None u_arr → empty Vec (port of &nullarray)");
16758 }
16759
16760 /// c:4057-4061 — `arrgetfn` returns u_arr clone when present.
16761 #[test]
16762 fn arrgetfn_returns_clone_when_u_arr_some() {
16763 let _g = crate::test_util::global_state_lock();
16764 let mut pm = param::default();
16765 let src = vec!["a".to_string(), "b".to_string(), "c".to_string()];
16766 pm.u_arr = Some(src.clone());
16767 assert_eq!(arrgetfn(&pm), src, "Some(v) → v clone");
16768 }
16769
16770 /// c:4084-4088 — `hashgetfn` C body is `return pm->u.hash;`. The
16771 /// Rust port returns `Option<&HashTable>` (borrowing form of the
16772 /// C nullable pointer). Pin both the None and Some cases.
16773 #[test]
16774 fn hashgetfn_returns_option_ref_hashtable() {
16775 let _g = crate::test_util::global_state_lock();
16776 let mut pm = param::default();
16777 pm.u_hash = None;
16778 assert!(hashgetfn(&pm).is_none(), "None u_hash → None");
16779 pm.u_hash = newparamtable(8, "h");
16780 assert!(hashgetfn(&pm).is_some(), "Some u_hash → Some");
16781 }
16782
16783 /// c:4093-4097 — `hashsetfn` C body installs `x` into `pm->u.hash`.
16784 /// Rust port writes `pm.u_hash = Some(x)`; round-trip via
16785 /// hashgetfn must observe the same table.
16786 #[test]
16787 fn hashsetfn_hashgetfn_round_trip() {
16788 let _g = crate::test_util::global_state_lock();
16789 let mut pm = param::default();
16790 let ht = newparamtable(8, "h").expect("newparamtable returns Some");
16791 let want_size = ht.hsize;
16792 hashsetfn(&mut pm, ht);
16793 let got = hashgetfn(&pm).expect("hashsetfn must store table");
16794 assert_eq!(got.hsize, want_size, "stored table size preserved");
16795 }
16796
16797 /// c:4180-4183 — `nullstrsetfn(UNUSED pm, char *x)` is the
16798 /// PM_SPECIAL "cannot be set" hook: `zsfree(x);` discards x and
16799 /// makes no change to pm. Pin that the Rust port leaves every
16800 /// field of pm untouched.
16801 #[test]
16802 fn nullstrsetfn_leaves_pm_unchanged() {
16803 let _g = crate::test_util::global_state_lock();
16804 let mut pm = param::default();
16805 pm.node.nam = "PINNED".to_string();
16806 pm.u_str = Some("orig".to_string());
16807 pm.u_val = 99;
16808 nullstrsetfn(&mut pm, "discarded".to_string());
16809 assert_eq!(pm.node.nam, "PINNED", "nam untouched");
16810 assert_eq!(pm.u_str.as_deref(), Some("orig"), "u_str untouched");
16811 assert_eq!(pm.u_val, 99, "u_val untouched");
16812 }
16813
16814 /// c:4192-4195 — `nullunsetfn(UNUSED pm, UNUSED exp)` is the
16815 /// PM_SPECIAL "cannot be unset" hook: empty body. Pin that the
16816 /// Rust port leaves every field of pm untouched.
16817 #[test]
16818 fn nullunsetfn_leaves_pm_unchanged() {
16819 let _g = crate::test_util::global_state_lock();
16820 let mut pm = param::default();
16821 pm.node.nam = "PINNED".to_string();
16822 pm.node.flags = PM_SCALAR as i32;
16823 pm.u_str = Some("keep".to_string());
16824 nullunsetfn(&mut pm, 0);
16825 nullunsetfn(&mut pm, 1);
16826 assert_eq!(pm.node.nam, "PINNED", "nam untouched");
16827 assert_eq!(pm.node.flags, PM_SCALAR as i32, "flags untouched");
16828 assert_eq!(pm.u_str.as_deref(), Some("keep"), "u_str untouched");
16829 }
16830}
16831
16832// ===========================================================
16833// !!! WARNING: RUST-ONLY NAMEREF ADAPTERS — NO DIRECT C
16834// COUNTERPARTS AS FREE FUNCTIONS !!!
16835//
16836// C's nameref machinery (Src/params.c:6325-6500 resolve_nameref /
16837// resolve_nameref_rec / upscope / setscope / valid_refname, plus the
16838// PM_NAMEREF arms of fetchvalue c:2247-2270 and assignstrvalue
16839// c:2690-2720) walks live Param POINTERS chained through
16840// realparamtab. zshrs's paramtab hands out CLONES behind an RwLock,
16841// so the chain walk must re-look-up each hop BY NAME — these
16842// adapters are that by-name walk plus its carriers. Every entry
16843// here exists to adapt a cited C pattern to the lock/clone storage,
16844// in the same class as fdtable_get / sess_get. The canonical
16845// C-named ports (resolve_nameref at params.rs, resolve_nameref_rec,
16846// upscope, setscope, valid_refname) delegate INTO this walk.
16847//
16848// Relocated from vm_helper.rs (2026-06-12): ported code was calling
16849// bridge-resident helpers, inverting the layering rule. Bridge
16850// callers reach these via the vm_helper re-export.
16851// ===========================================================
16852
16853/// Result of walking a nameref chain by name. Rust-side carrier for
16854/// what C's `resolve_nameref_rec` (Src/params.c:6332) communicates
16855/// via the returned `Param` pointer: the chain may end at the final
16856/// non-ref target, at a placeholder ref (empty refname), at a
16857/// subscripted ref (`pm->width` ends the chain per c:6339), at a
16858/// dangling name (target never defined), or in a loop.
16859#[allow(non_camel_case_types)]
16860pub enum nameref_resolution {
16861 /// `name` is not a nameref (or doesn't exist / is an unset ref).
16862 NotRef,
16863 /// Chain ends at a nameref whose refname is empty/None —
16864 /// the C `keep_lastref` shape (c:6354). Field = that ref's name.
16865 Placeholder(String),
16866 /// Final non-ref target. `pm` is a clone of the resolved binding
16867 /// (None when the target name was never defined — C returns NULL
16868 /// at c:6347 when gethashnode2 misses). `level` is the resolved
16869 /// binding's scope level (for old-chain writes).
16870 Target {
16871 /// final target name
16872 name: String,
16873 /// subscript from a `name[sub]` refname (ends the chain, c:6339)
16874 subscript: Option<String>,
16875 /// clone of the resolved binding (possibly a hidden old-chain node)
16876 pm: Option<Param>,
16877 /// scope level of the resolved binding (only when pm is Some)
16878 level: i32,
16879 },
16880 /// Loop detected — `zerr("...: invalid self reference")` already
16881 /// emitted (c:6341-6343).
16882 SelfRef,
16883 /// The refname EXISTS in the table but the upscope walk excluded
16884 /// every binding (c:6347-6349: `loadparamnode(upscope(...))`
16885 /// returned NULL with gethashnode2 non-NULL, so the `keep_lastref`
16886 /// else-arm is SKIPPED). Reads see unset; assignment FAILS
16887 /// (createparam c:1108-1118 refuses the existing ref).
16888 OutOfScope,
16889}
16890
16891/// Walk the nameref chain starting at `name` against the live
16892/// paramtab. Faithful adaptation of `resolve_nameref_rec`
16893/// (Src/params.c:6332-6357): per-hop `upscope` via the old-chain,
16894/// `pm->width` subscript chain-end (c:6338-6339), PM_TAGGED-style
16895/// loop detection (visited set of (name, level) stands in for the
16896/// C pointer tag — same revisit condition, borrow-safe), and the
16897/// `keep_lastref` placeholder shape. `stop_at` mirrors the C `stop`
16898/// param (compared by (name, level) identity).
16899pub fn resolve_nameref_name(name: &str, stop_at: Option<(&str, i32)>) -> nameref_resolution {
16900 // `${(!)…}` SCANPM_NONAMEREF scope (c:2247 `!(scanflags &
16901 // SCANPM_NONAMEREF)`) — treat nothing as a ref. Centralized here so
16902 // every caller inherits the gate (previously each caller guarded
16903 // via is_nameref first).
16904 if NAMEREF_SUPPRESS.with(|c| c.get()) > 0 {
16905 return nameref_resolution::NotRef;
16906 }
16907 // upscope (Src/params.c:6440-6463) over CLONED chain nodes —
16908 // nested in the engine (sole consumer) instead of a free fn.
16909 let upscope_clone =
16910 |visible: ¶m, ref_flags: u32, ref_level: i32, ref_base: i32| -> Option<Param> {
16911 let mut cur: ¶m = visible;
16912 if (ref_flags & PM_UPPER) != 0 {
16913 // c:6457-6458 — `while (pm->level > ref->level - 1 &&
16914 // (pm = pm->old));` — pm becomes NULL when the chain runs
16915 // out above the ref's scope (an upscope ref to a name that
16916 // only exists at/below its own level is DANGLING).
16917 while cur.level > ref_level - 1 {
16918 match cur.old.as_deref() {
16919 Some(o) => cur = o,
16920 None => return None,
16921 }
16922 }
16923 } else {
16924 // c:6460
16925 while let Some(o) = cur.old.as_deref() {
16926 if o.level >= ref_base {
16927 cur = o;
16928 } else {
16929 break;
16930 }
16931 }
16932 }
16933 Some(Box::new(cur.clone()))
16934 };
16935 let tab = match paramtab().read() {
16936 Ok(t) => t,
16937 Err(_) => return nameref_resolution::NotRef,
16938 };
16939 let first = match tab.get(name) {
16940 Some(p) => p,
16941 None => return nameref_resolution::NotRef,
16942 };
16943 let f = first.node.flags as u32;
16944 if (f & PM_NAMEREF) == 0 {
16945 return nameref_resolution::NotRef;
16946 }
16947 // c:6336-6339 — an UNSET nameref ends the chain at itself: reads
16948 // see "unset" (fetchvalue NULL), assignment writes its refname
16949 // (setstrvalue c:2712 clears PM_UNSET then the PM_NAMEREF arm
16950 // stores the new refname).
16951 if (f & PM_UNSET) != 0 {
16952 return nameref_resolution::Placeholder(first.node.nam.clone());
16953 }
16954 let mut cur: Param = first.clone();
16955 let mut visited: Vec<(String, i32)> = Vec::new();
16956 let mut hops = 0usize;
16957 loop {
16958 hops += 1;
16959 if hops > 256 {
16960 // Hard backstop (C relies on PM_TAGGED; this can't fire
16961 // unless the tag logic above missed a cycle).
16962 zerr(&format!("{}: invalid self reference", cur.node.nam));
16963 return nameref_resolution::SelfRef;
16964 }
16965 let cf = cur.node.flags as u32;
16966 if (cf & PM_NAMEREF) == 0 || (cf & PM_UNSET) != 0 {
16967 // Final non-ref (or unset-ref) target reached.
16968 let lvl = cur.level;
16969 let nm = cur.node.nam.clone();
16970 return nameref_resolution::Target {
16971 name: nm,
16972 subscript: None,
16973 pm: Some(cur),
16974 level: lvl,
16975 };
16976 }
16977 // c:6338-6339 — refname; subscripted refname ends the chain.
16978 let refname_full = match cur.u_str.as_deref() {
16979 Some(r) if !r.is_empty() => r.to_string(),
16980 _ => {
16981 // c:6354 keep_lastref — placeholder ref.
16982 return nameref_resolution::Placeholder(cur.node.nam.clone());
16983 }
16984 };
16985 // c:6339 — `pm->width` is the subscript offset; a subscripted
16986 // ref is the end of any chain (see fetchvalue c:2250-2256).
16987 if cur.width != 0 || refname_full.contains('[') {
16988 let (base_name, sub) = match refname_full.find('[') {
16989 Some(i) => {
16990 let tail = &refname_full[i + 1..];
16991 let key = tail.strip_suffix(']').unwrap_or(tail);
16992 (refname_full[..i].to_string(), Some(key.to_string()))
16993 }
16994 None => (refname_full.clone(), None),
16995 };
16996 let resolved = tab
16997 .get(&base_name)
16998 .and_then(|vis| upscope_clone(vis, cur.node.flags as u32, cur.level, cur.base));
16999 let lvl = resolved.as_ref().map(|p| p.level).unwrap_or(0);
17000 return nameref_resolution::Target {
17001 name: base_name,
17002 subscript: sub,
17003 pm: resolved,
17004 level: lvl,
17005 };
17006 }
17007 // c:6341-6343 — PM_TAGGED loop detection.
17008 let key = (cur.node.nam.clone(), cur.level);
17009 if visited.contains(&key) {
17010 zerr(&format!("{}: invalid self reference", cur.node.nam));
17011 return nameref_resolution::SelfRef;
17012 }
17013 visited.push(key);
17014 // c:6346-6347 — next hop via gethashnode2 + upscope.
17015 let next = match tab.get(&refname_full) {
17016 Some(vis) => {
17017 match upscope_clone(vis, cur.node.flags as u32, cur.level, cur.base) {
17018 Some(n) => n,
17019 None => {
17020 // c:6347-6349 — name exists but upscope ran
17021 // out: the keep_lastref else-arm is skipped,
17022 // resolve returns NULL.
17023 return nameref_resolution::OutOfScope;
17024 }
17025 }
17026 }
17027 None => {
17028 // c:6347 gethashnode2 miss — dangling target.
17029 return nameref_resolution::Target {
17030 name: refname_full,
17031 subscript: None,
17032 pm: None,
17033 level: 0,
17034 };
17035 }
17036 };
17037 // c:6348 — `pm != stop` guard: when the hop lands on the stop
17038 // param, do NOT recurse (used by setscope's self-ref check
17039 // via resolve_nameref_rec(pm, pm, 0) at c:6423).
17040 if let Some((snam, slvl)) = stop_at {
17041 if next.node.nam == snam && next.level == slvl {
17042 let lvl = next.level;
17043 let nm = next.node.nam.clone();
17044 return nameref_resolution::Target {
17045 name: nm,
17046 subscript: None,
17047 pm: Some(next),
17048 level: lvl,
17049 };
17050 }
17051 }
17052 // c:6348 — `!(pm->node.flags & PM_UNSET)` guard: an unset
17053 // target stops the recursion; the unset param is returned.
17054 if (next.node.flags as u32 & PM_UNSET) != 0 {
17055 // An unset NAMEREF hop behaves like a placeholder: the
17056 // assignment revives it by writing the refname (the
17057 // PM_NAMEREF assignstrvalue arm at c:2715).
17058 if (next.node.flags as u32 & PM_NAMEREF) != 0 {
17059 return nameref_resolution::Placeholder(next.node.nam.clone());
17060 }
17061 let lvl = next.level;
17062 let nm = next.node.nam.clone();
17063 return nameref_resolution::Target {
17064 name: nm,
17065 subscript: None,
17066 pm: Some(next),
17067 level: lvl,
17068 };
17069 }
17070 cur = next;
17071 }
17072}
17073
17074thread_local! {
17075/// `${(!)name}` — SCANPM_NONAMEREF depth (Src/params.c:2232-2235:
17076/// the `!` flag routes the lookup through `getnode2`, skipping
17077/// `resolve_nameref`). Non-zero ⇒ `is_nameref` reports false so
17078/// every deref point reads the ref itself.
17079pub static NAMEREF_SUPPRESS: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
17080}
17081
17082/// RAII guard for `${(!)...}` — suppresses nameref resolution on this
17083/// thread for its lifetime (SCANPM_NONAMEREF scope).
17084pub struct NamerefSuppressGuard;
17085
17086impl NamerefSuppressGuard {
17087 /// Enter a SCANPM_NONAMEREF scope.
17088 #[allow(clippy::new_without_default)]
17089 pub fn new() -> Self {
17090 NAMEREF_SUPPRESS.with(|c| c.set(c.get() + 1));
17091 NamerefSuppressGuard
17092 }
17093}
17094
17095impl Drop for NamerefSuppressGuard {
17096 fn drop(&mut self) {
17097 NAMEREF_SUPPRESS.with(|c| c.set(c.get().saturating_sub(1)));
17098 }
17099}
17100
17101/// Is the visible binding of `name` a PM_NAMEREF? (Cheap pre-check
17102/// so the resolver isn't invoked on every plain variable read.)
17103pub fn is_nameref(name: &str) -> bool {
17104 // `${(!)…}` SCANPM_NONAMEREF scope active → treat nothing as a ref.
17105 if NAMEREF_SUPPRESS.with(|c| c.get()) > 0 {
17106 return false;
17107 }
17108 paramtab()
17109 .read()
17110 .ok()
17111 .and_then(|t| t.get(name).map(|p| (p.node.flags as u32 & PM_NAMEREF) != 0))
17112 .unwrap_or(false)
17113}
17114
17115/// Raw scalar value of a (possibly hidden) param clone — the
17116/// type-dispatch heart of `getstrvalue` (Src/params.c:2350-2382)
17117/// without the VALFLAG_SUBST padding (refs to hidden bindings).
17118fn param_scalar_value(pm: ¶m) -> Option<String> {
17119 let t = PM_TYPE(pm.node.flags as u32);
17120 if t == PM_INTEGER {
17121 let base = if pm.base > 0 { pm.base } else { 10 };
17122 Some(convbase(pm.u_val, base as u32))
17123 } else if t == PM_EFLOAT || t == PM_FFLOAT {
17124 Some(convfloat(pm.u_dval, pm.base, pm.node.flags as u32))
17125 } else if t == PM_SCALAR && pm.gsu_s.is_some() {
17126 pm.gsu_s.as_ref().map(|gsu| (gsu.getfn)(pm))
17127 } else if let Some(s) = pm.u_str.as_ref() {
17128 Some(s.clone())
17129 } else if let Some(arr) = pm.u_arr.as_ref() {
17130 Some(arr.join(" "))
17131 } else if t == PM_HASHED {
17132 // assoc joined values (getstrvalue KSH `[0]` path is rare;
17133 // bare $assoc is empty in zsh, return empty)
17134 Some(String::new())
17135 } else {
17136 None
17137 }
17138}
17139
17140/// Element read for a nameref bound to `target[sub]` — the
17141/// fetchvalue REFSLICE / getindex hand-off (Src/params.c:2247-2270
17142/// + c:2289). Arrays take a math index (1-based, negative from
17143/// end), assocs a literal key, scalars a char index.
17144fn nameref_element_read(pm: ¶m, target: &str, key: &str) -> Option<String> {
17145 let t = PM_TYPE(pm.node.flags as u32);
17146 if t == PM_HASHED {
17147 // The assoc backing is name-keyed (paramtab_hashed_storage);
17148 // when the resolved binding is a HIDDEN old-chain node (a
17149 // `local` shadow covers it), the outer's data lives on the
17150 // shadow STACK (pushed by createparam's PM_HASHED save).
17151 let visible_level = paramtab()
17152 .read()
17153 .ok()
17154 .and_then(|tab| tab.get(target).map(|p| p.level));
17155 if visible_level != Some(pm.level) {
17156 if let Some(stk) = crate::ported::params::PARAMTAB_HASHED_SHADOW_STACK.get() {
17157 if let Ok(stk) = stk.lock() {
17158 if let Some(frames) = stk.get(target) {
17159 if let Some(Some(saved)) = frames.last() {
17160 return saved.get(key).cloned();
17161 }
17162 }
17163 }
17164 }
17165 }
17166 return paramtab_hashed_storage()
17167 .lock()
17168 .ok()
17169 .and_then(|m| m.get(target).and_then(|h| h.get(key).cloned()));
17170 }
17171 // numeric (math) index for arrays / scalars
17172 let idx: i64 = match key.trim().parse::<i64>() {
17173 Ok(i) => i,
17174 Err(_) => match crate::ported::math::mathevali(key) {
17175 Ok(i) => i,
17176 Err(_) => return None,
17177 },
17178 };
17179 if t == PM_ARRAY {
17180 let arr = pm.u_arr.as_ref()?;
17181 let len = arr.len() as i64;
17182 let pos = if idx < 0 { len + idx + 1 } else { idx };
17183 if pos >= 1 && pos <= len {
17184 return Some(arr[(pos - 1) as usize].clone());
17185 }
17186 return Some(String::new());
17187 }
17188 // scalar char index (1-based)
17189 let s = param_scalar_value(pm)?;
17190 let chars: Vec<char> = s.chars().collect();
17191 let len = chars.len() as i64;
17192 let pos = if idx < 0 { len + idx + 1 } else { idx };
17193 if pos >= 1 && pos <= len {
17194 return Some(chars[(pos - 1) as usize].to_string());
17195 }
17196 Some(String::new())
17197}
17198
17199/// Full-table variant of `setscope(Param pm)` (Src/params.c:6382).
17200/// Performs the parts the in-place `setscope` above cannot: the
17201/// base-scope computation against the live paramtab (c:6402-6410),
17202/// the WARNNESTEDVAR diagnostic (c:6411-6419), and the chain-walk
17203/// self-reference detection + unset (c:6421-6431). Returns 1 when
17204/// the self-reference error fired (the ref has been removed), else 0.
17205/// MUST be called without any paramtab lock held.
17206pub fn setscope_by_name(name: &str) -> i32 {
17207 queue_signals();
17208 // Snapshot the ref's state.
17209 let snap = {
17210 let tab = match paramtab().read() {
17211 Ok(t) => t,
17212 Err(_) => {
17213 unqueue_signals();
17214 return 0;
17215 }
17216 };
17217 match tab.get(name) {
17218 Some(pm) if (pm.node.flags as u32 & PM_NAMEREF) != 0 => {
17219 Some((pm.u_str.clone(), pm.level, pm.node.flags as u32))
17220 }
17221 _ => None,
17222 }
17223 };
17224 let (refname_opt, ref_level, ref_flags) = match snap {
17225 Some(s) => s,
17226 None => {
17227 unqueue_signals();
17228 return 0;
17229 }
17230 };
17231 let refname_full = refname_opt.unwrap_or_default();
17232 // c:6388-6400 — split refname at `[`, stamp pm->width.
17233 let (head, width) = match refname_full.find('[') {
17234 Some(i) => (refname_full[..i].to_string(), i as i32),
17235 None => (refname_full.clone(), 0),
17236 };
17237 if width != 0 {
17238 if let Ok(mut tab) = paramtab().write() {
17239 if let Some(pm) = tab.get_mut(name) {
17240 pm.width = width; // c:6398 pm->width = t - refname
17241 }
17242 }
17243 }
17244 // c:6402-6410 — compute pm->base from the target's visible level.
17245 // `basepm != pm || !basepm->old || (basepm = basepm->old)`: a ref
17246 // whose refname is its own name binds to the ENCLOSING binding.
17247 let mut basepm_is_self = false;
17248 if (ref_flags & PM_UPPER) == 0 && !head.is_empty() {
17249 let base_level: Option<i32> = {
17250 let tab = paramtab().read().unwrap();
17251 match tab.get(&head) {
17252 Some(vis) => {
17253 if head == name && vis.level == ref_level {
17254 // basepm == pm — prefer pm->old (c:6407).
17255 match vis.old.as_deref() {
17256 Some(o) => Some(o.level),
17257 None => {
17258 basepm_is_self = true;
17259 None
17260 }
17261 }
17262 } else {
17263 Some(vis.level)
17264 }
17265 }
17266 None => None,
17267 }
17268 };
17269 if let Some(bl) = base_level {
17270 if let Ok(mut tab) = paramtab().write() {
17271 if let Some(pm) = tab.get_mut(name) {
17272 setscope_base(pm, bl); // c:6409 setscope_base(pm, basepm->level)
17273 }
17274 }
17275 }
17276 // c:6411-6419 — base > level diagnostics.
17277 let base_now = paramtab()
17278 .read()
17279 .ok()
17280 .and_then(|t| t.get(name).map(|p| p.base))
17281 .unwrap_or(0);
17282 if base_now > ref_level && isset(crate::ported::zsh_h::WARNNESTEDVAR) {
17283 // c:6417-6418
17284 zwarn(&format!(
17285 "reference {} in enclosing scope set to local variable {}",
17286 name, head
17287 ));
17288 }
17289 }
17290 // c:6421-6431 — self-reference detection via the chain walk with
17291 // stop == pm.
17292 let mut selfref = basepm_is_self;
17293 if !head.is_empty() && width == 0 && !basepm_is_self {
17294 match resolve_nameref_name(name, Some((name, ref_level))) {
17295 nameref_resolution::Target {
17296 name: tname, level, ..
17297 } if tname == name && level == ref_level => {
17298 selfref = true; // chain looped back to pm
17299 }
17300 nameref_resolution::SelfRef => {
17301 // zerr already emitted inside the walk (with the
17302 // looping ref's name, matching the recursive C zerr).
17303 unqueue_signals();
17304 return 1;
17305 }
17306 _ => {}
17307 }
17308 }
17309 if selfref {
17310 // c:6426-6429 — `zerr("%s: invalid self reference", refname);
17311 // unsetparam_pm(pm, 0, 1);`
17312 zerr(&format!("{}: invalid self reference", head));
17313 if let Some(mut pm) = paramtab().write().ok().and_then(|mut t| t.remove(name)) {
17314 if let Some(prev) = pm.old.take() {
17315 // uncover any outer binding (unsetparam_pm restore shape)
17316 if let Ok(mut tab) = paramtab().write() {
17317 tab.insert(name.to_string(), prev);
17318 }
17319 }
17320 }
17321 unqueue_signals();
17322 return 1;
17323 }
17324 unqueue_signals();
17325 0
17326}