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
9#[allow(unused_imports)]
10use crate::ported::utils::zerr;
11use crate::func_body_fmt::FuncBodyFmt;
12use indexmap::IndexMap;
13use std::collections::{HashMap, HashSet};
14use std::env;
15use std::sync::atomic::{AtomicI64, Ordering};
16use std::time::{Instant, SystemTime, UNIX_EPOCH};
17use crate::ported::zsh_h::{
18 gsu_array, gsu_float, gsu_hash, gsu_integer, gsu_scalar
19};
20use crate::ported::zsh_h::VALFLAG_SUBST;
21use crate::ported::zsh_h::{Param, hashnode, isset as isset_opt};
22use fusevm::Value;
23use crate::ported::zsh_h::Marker;
24use crate::ported::zsh_h::{SCANPM_ISVAR_AT, SCANPM_NONAMEREF};
25use crate::ported::zsh_h::SCANPM_WANTINDEX;
26
27#[allow(unused_imports)]
28use crate::ported::zsh_h::{
29 PM_TYPE, PM_SCALAR, PM_NAMEREF, PM_INTEGER, PM_EFLOAT, PM_FFLOAT,
30 PM_ARRAY, PM_HASHED, PM_HASHELEM, PM_NAMEDDIR, PM_UNIQUE,
31 PM_READONLY, PM_UNSET, PM_EXPORTED, PM_AUTOLOAD, PM_DEFAULTED,
32 PM_DECLARED, PM_REMOVABLE, PM_NORESTORE, PM_LOCAL, PM_RO_BY_DESIGN,
33 PM_LEFT, PM_RIGHT_B, PM_RIGHT_Z, PM_SPECIAL, PM_TAGGED, PM_TIED, PM_UPPER,
34 SCANPM_CHECKING, SCANPM_MATCHMANY, SCANPM_MATCHKEY, SCANPM_MATCHVAL,
35 SCANPM_KEYMATCH, SCANPM_WANTKEYS, SCANPM_WANTVALS, SCANPM_ARRONLY,
36 VALFLAG_EMPTY, VALFLAG_INV,
37 ASSPM_WARN, ASSPM_AUGMENT, ASSPM_ENV_IMPORT,
38 PRINT_NAMEONLY, PRINT_TYPESET, PRINT_INCLUDEVALUE, PRINT_KV_PAIR,
39 PRINT_LINE, PRINT_POSIX_READONLY, PRINT_POSIX_EXPORT,
40 EXECOPT, KSHARRAYS, AUTONAMEDIRS, ALLEXPORT,
41 WARNCREATEGLOBAL, WARNNESTEDVAR,
42 isset, unset,
43};
44#[allow(unused_imports)]
45use crate::ported::math::{mnumber, MN_INTEGER, MN_FLOAT};
46#[allow(unused_imports)]
47use crate::ported::utils::errflag;
48#[allow(unused_imports)]
49use crate::ported::signals::{queue_signals, unqueue_signals};
50
51/// Port of `static int lc_update_needed` from `Src/params.c:5850`
52/// (under `#ifdef USE_LOCALE`). Set to 1 by `scanendscope` when a
53/// LC_*/LANG param's scope ends; consumed by `endparamscope` to
54/// trigger a `setlocale()` refresh.
55pub static LC_UPDATE_NEEDED: std::sync::atomic::AtomicI32 =
56 std::sync::atomic::AtomicI32::new(0);
57
58/// Port of `static Param foundparam` from `Src/params.c:640`.
59/// Set by `scanparamvals` to the last param it touched, read by
60/// `assignsparam` / `assignnparam` for the assoc-element path.
61/// Stores the param name; the live `¶m` lookup is done by
62/// the caller through paramtab.
63pub static FOUNDPARAM: std::sync::OnceLock<std::sync::Mutex<Option<String>>> =
64 std::sync::OnceLock::new();
65
66fn foundparam_lock() -> &'static std::sync::Mutex<Option<String>> {
67 FOUNDPARAM.get_or_init(|| std::sync::Mutex::new(None))
68}
69
70/// Read `foundparam`. Returns the last param name observed by
71/// `scanparamvals`; cleared by callers after consumption.
72pub fn foundparam() -> Option<String> {
73 foundparam_lock().lock().unwrap().clone()
74}
75
76/// Set `foundparam`. Called from `scanparamvals`.
77pub fn set_foundparam(nam: Option<String>) {
78 *foundparam_lock().lock().unwrap() = nam;
79}
80
81// ---------------------------------------------------------------------------
82// Parameter flags (from zsh.h PM_* flags)
83// ---------------------------------------------------------------------------
84
85// What level of localness we are at. // c:47
86// // c:48
87// Hand-wavingly, this is incremented at every function call and decremented // c:49
88// at every function return. See startparamscope(). // c:50
89
90/// Port of `mod_export int locallevel;` from `Src/params.c:54`.
91/// Tracks function-local-scope nesting depth. Bumped by
92/// `startparamscope()` (params.c:5879) on every function call,
93/// decremented by `endparamscope()` (params.c:5950) on return.
94#[allow(non_upper_case_globals)]
95pub static locallevel: std::sync::atomic::AtomicI32 = // c:54
96 std::sync::atomic::AtomicI32::new(0);
97
98// ---------------------------------------------------------------------------
99// Real `param` struct lives in Src/zsh.h:1829 (port at zsh_h.rs:750).
100// It uses C-union flattening: u_str / u_arr / u_val / u_dval / u_hash
101// dispatched on `PM_TYPE(node.flags)`. There is NO `ParamValue` enum in
102// C; do not reintroduce one.
103// ---------------------------------------------------------------------------
104
105pub use crate::ported::zsh_h::param;
106
107
108// =============================================================================
109// IPDEF{1,2,4,5,5U,6,7,7R,7U,8,9,10} + LCIPDEF — special-parameter
110// table entry constructors. All defined as macros in
111// `Src/params.c:296-406`. Each produces one row of the
112// `special_params[]` table; the differences are flag combinations
113// + which gsu (getter/setter union) the entry binds.
114//
115// In C, `BR(p)` is `{(void *)(p)}` for the param's `u` data field;
116// `GSU(g)` is the `&g` of the named gsu_scalar/gsu_integer/etc.
117// The Rust port stores `var` and `gsu` as `usize` slot indexes
118// into per-evaluator tables, matching the existing PARAMDEF helper
119// above. The flag bit combinations mirror the C macros line-by-line.
120// =============================================================================
121
122/// Port of `IPDEF1(A,B,C)` from `Src/params.c:296` —
123/// `{{NULL,A,PM_INTEGER|PM_SPECIAL|C},BR(NULL),GSU(B),10,0,...}`.
124#[inline] #[allow(non_snake_case)]
125pub fn IPDEF1(A: &str, B: usize, C: i32) -> paramdef { // c:params.c:296
126 paramdef {
127 name: A.to_string(),
128 flags: (PM_INTEGER | PM_SPECIAL) as i32 | C,
129 var: 0, gsu: B,
130 getnfn: None, scantfn: None, pm: None,
131 }
132}
133
134/// Port of `IPDEF2(A,B,C)` from `Src/params.c:309` —
135/// `{{NULL,A,PM_SCALAR|PM_SPECIAL|C},BR(NULL),GSU(B),0,0,...}`.
136#[inline] #[allow(non_snake_case)]
137pub fn IPDEF2(A: &str, B: usize, C: i32) -> paramdef { // c:params.c:309
138 paramdef {
139 name: A.to_string(),
140 flags: (PM_SCALAR | PM_SPECIAL) as i32 | C,
141 var: 0, gsu: B,
142 getnfn: None, scantfn: None, pm: None,
143 }
144}
145
146/// Port of `IPDEF4(A,B)` from `Src/params.c:344` —
147/// `{{NULL,A,PM_INTEGER|PM_READONLY_SPECIAL},BR((void*)B),
148/// GSU(varint_readonly_gsu),10,0,...}`.
149#[inline] #[allow(non_snake_case)]
150pub fn IPDEF4(A: &str, B: usize) -> paramdef { // c:params.c:344
151 paramdef {
152 name: A.to_string(),
153 flags: (PM_INTEGER | PM_READONLY_SPECIAL) as i32,
154 var: B, gsu: 0,
155 getnfn: None, scantfn: None, pm: None,
156 }
157}
158
159/// Port of `IPDEF5(A,B,F)` from `Src/params.c:353` —
160/// `{{NULL,A,PM_INTEGER|PM_SPECIAL},BR((void*)B),GSU(F),10,0,...}`.
161#[inline] #[allow(non_snake_case)]
162pub fn IPDEF5(A: &str, B: usize, F: usize) -> paramdef { // c:params.c:353
163 paramdef {
164 name: A.to_string(),
165 flags: (PM_INTEGER | PM_SPECIAL) as i32,
166 var: B, gsu: F,
167 getnfn: None, scantfn: None, pm: None,
168 }
169}
170
171/// Port of `IPDEF5U(A,B,F)` from `Src/params.c:354` — c:353 + PM_UNSET.
172#[inline] #[allow(non_snake_case)]
173pub fn IPDEF5U(A: &str, B: usize, F: usize) -> paramdef { // c:params.c:354
174 paramdef {
175 name: A.to_string(),
176 flags: (PM_INTEGER | PM_SPECIAL | PM_UNSET) as i32,
177 var: B, gsu: F,
178 getnfn: None, scantfn: None, pm: None,
179 }
180}
181
182/// Port of `IPDEF6(A,B,F)` from `Src/params.c:362` — c:353 + PM_DONTIMPORT.
183#[inline] #[allow(non_snake_case)]
184pub fn IPDEF6(A: &str, B: usize, F: usize) -> paramdef { // c:params.c:362
185 paramdef {
186 name: A.to_string(),
187 flags: (PM_INTEGER | PM_SPECIAL | PM_DONTIMPORT) as i32,
188 var: B, gsu: F,
189 getnfn: None, scantfn: None, pm: None,
190 }
191}
192
193/// Port of `IPDEF7(A,B)` from `Src/params.c:367` —
194/// `{{NULL,A,PM_SCALAR|PM_SPECIAL},BR((void*)B),GSU(varscalar_gsu),0,0,...}`.
195#[inline] #[allow(non_snake_case)]
196pub fn IPDEF7(A: &str, B: usize) -> paramdef { // c:params.c:367
197 paramdef {
198 name: A.to_string(),
199 flags: (PM_SCALAR | PM_SPECIAL) as i32,
200 var: B, gsu: 0,
201 getnfn: None, scantfn: None, pm: None,
202 }
203}
204
205/// Port of `IPDEF7R(A,B)` from `Src/params.c:368` — c:367 + PM_DONTIMPORT_SUID.
206#[inline] #[allow(non_snake_case)]
207pub fn IPDEF7R(A: &str, B: usize) -> paramdef { // c:params.c:368
208 paramdef {
209 name: A.to_string(),
210 flags: (PM_SCALAR | PM_SPECIAL | PM_DONTIMPORT_SUID) as i32,
211 var: B, gsu: 0,
212 getnfn: None, scantfn: None, pm: None,
213 }
214}
215
216/// Port of `IPDEF7U(A,B)` from `Src/params.c:369` — c:367 + PM_UNSET.
217#[inline] #[allow(non_snake_case)]
218pub fn IPDEF7U(A: &str, B: usize) -> paramdef { // c:params.c:369
219 paramdef {
220 name: A.to_string(),
221 flags: (PM_SCALAR | PM_SPECIAL | PM_UNSET) as i32,
222 var: B, gsu: 0,
223 getnfn: None, scantfn: None, pm: None,
224 }
225}
226
227/// Port of `IPDEF8(A,B,C,D)` from `Src/params.c:394` —
228/// `{{NULL,A,D|PM_SCALAR|PM_SPECIAL},BR((void*)B),GSU(colonarr_gsu),
229/// 0,0,NULL,C,NULL,0}`.
230/// `C` is the colon-arr field; the Rust port stores it in `getnfn`
231/// since `paramdef` lacks a dedicated colon-arr slot until that's
232/// ported.
233#[inline] #[allow(non_snake_case)]
234pub fn IPDEF8(A: &str, B: usize, C: usize, D: i32) -> paramdef { // c:params.c:394
235 paramdef {
236 name: A.to_string(),
237 flags: (PM_SCALAR | PM_SPECIAL) as i32 | D,
238 var: B, gsu: 0,
239 getnfn: None, scantfn: None, pm: None,
240 }
241}
242
243/// Port of `IPDEF9(A,B,C,D)` from `Src/params.c:431` —
244/// `{{NULL,A,D|PM_ARRAY|PM_SPECIAL|PM_DONTIMPORT},BR((void*)B),
245/// GSU(vararray_gsu),0,0,NULL,C,NULL,0}`.
246#[inline] #[allow(non_snake_case)]
247pub fn IPDEF9(A: &str, B: usize, C: usize, D: i32) -> paramdef { // c:params.c:384
248 paramdef {
249 name: A.to_string(),
250 flags: (PM_ARRAY | PM_SPECIAL | PM_DONTIMPORT) as i32 | D,
251 var: B, gsu: 0,
252 getnfn: None, scantfn: None, pm: None,
253 }
254}
255
256/// Port of `IPDEF10(A,B)` from `Src/params.c:438` —
257/// `{{NULL,A,PM_ARRAY|PM_SPECIAL},BR(NULL),GSU(B),10,0,...}`.
258#[inline] #[allow(non_snake_case)]
259pub fn IPDEF10(A: &str, B: usize) -> paramdef { // c:params.c:406
260 paramdef {
261 name: A.to_string(),
262 flags: (PM_ARRAY | PM_SPECIAL) as i32,
263 var: 0, gsu: B,
264 getnfn: None, scantfn: None, pm: None,
265 }
266}
267
268/// Port of `LCIPDEF(name)` from `Src/params.c:324` —
269/// `IPDEF2(name, lc_blah_gsu, PM_UNSET)`.
270#[inline] #[allow(non_snake_case)]
271pub fn LCIPDEF(name: &str) -> paramdef { // c:params.c:324
272 IPDEF2(name, 0, PM_UNSET as i32) // c:324 lc_blah_gsu (slot 0)
273}
274
275
276
277// ---------------------------------------------------------------------------
278// Numeric type for parameters (from params.c mnumber)
279// ---------------------------------------------------------------------------
280
281
282// ---------------------------------------------------------------------------
283// Value struct - mirrors C's struct value for subscript access
284// ---------------------------------------------------------------------------
285// ---------------------------------------------------------------------------
286// Shell parameter
287// ---------------------------------------------------------------------------
288
289
290// ---------------------------------------------------------------------------
291// Tied parameter data
292// ---------------------------------------------------------------------------
293
294// TiedData removed: was a Rust-only sidecar for the deleted `ParamTable`'s
295// `tied: HashMap<String, TiedData>` field. C source stores tied-pair
296// metadata via `pm->ename` (the partner name) and `pm->u.data` (the
297// separator char) on the real `param` struct (Src/zsh.h:750 / Src/params.c
298// `bin_typeset()` typeset -T branch).
299
300
301// ---------------------------------------------------------------------------
302// Parameter table print types (from printparamnode)
303// ---------------------------------------------------------------------------
304
305// ---------------------------------------------------------------------------
306// Special parameter definitions table (mirrors special_params[] in C)
307// ---------------------------------------------------------------------------
308
309/// Special-parameter definition — Rust extension paralleling the
310/// `IPDEF*` macro entries in `Src/params.c:297-392`. C uses
311/// `struct paramdef` (`Src/zsh.h:2082`, mirrored at `zsh_h.rs:950`)
312/// with `var` + `gsu` pointers; the Rust port carries a trimmed
313/// shape with `pm_type`/`pm_flags`/`tied_name` until the full
314/// `gsu`-callback plumbing lands. Canonical `paramdef` is the
315/// long-term target.
316#[allow(non_camel_case_types)]
317#[derive(Clone, Debug)]
318pub struct special_paramdef {
319 pub name: &'static str,
320 pub pm_type: u32, // PM_INTEGER | PM_SCALAR | PM_ARRAY
321 pub pm_flags: u32, // PM_READONLY_SPECIAL, PM_DONTIMPORT, etc.
322 pub tied_name: Option<&'static str>,
323}
324
325/// Index of the first entry in `special_params` that lives in the
326/// zsh-only section (after the `{{NULL,NULL,0}, BR(NULL), ...}`
327/// sentinel at `Src/params.c:392`). Entries before this index are
328/// always loaded; entries at and after this index are only loaded
329/// under non-sh/non-ksh emulation. Mirrors the C two-section table
330/// terminated by an inner NULL sentinel.
331pub const SPECIAL_PARAMS_ZSH_START: usize = 54; // c:392
332
333/// All special parameters from params.c special_params[]
334pub const special_params: &[special_paramdef] = &[
335 // Integer specials with custom GSU
336 special_paramdef {
337 name: "#",
338 pm_type: PM_INTEGER,
339 pm_flags: PM_READONLY,
340 tied_name: None,
341 },
342 special_paramdef {
343 name: "ERRNO",
344 pm_type: PM_INTEGER,
345 pm_flags: PM_UNSET,
346 tied_name: None,
347 },
348 special_paramdef {
349 name: "GID",
350 pm_type: PM_INTEGER,
351 pm_flags: PM_DONTIMPORT,
352 tied_name: None,
353 },
354 special_paramdef {
355 name: "EGID",
356 pm_type: PM_INTEGER,
357 pm_flags: PM_DONTIMPORT,
358 tied_name: None,
359 },
360 special_paramdef {
361 name: "HISTSIZE",
362 pm_type: PM_INTEGER,
363 pm_flags: 0,
364 tied_name: None,
365 },
366 special_paramdef {
367 name: "RANDOM",
368 pm_type: PM_INTEGER,
369 pm_flags: 0,
370 tied_name: None,
371 },
372 special_paramdef {
373 name: "SAVEHIST",
374 pm_type: PM_INTEGER,
375 pm_flags: 0,
376 tied_name: None,
377 },
378 special_paramdef {
379 name: "SECONDS",
380 pm_type: PM_INTEGER,
381 pm_flags: 0,
382 tied_name: None,
383 },
384 special_paramdef {
385 name: "UID",
386 pm_type: PM_INTEGER,
387 pm_flags: PM_DONTIMPORT,
388 tied_name: None,
389 },
390 special_paramdef {
391 name: "EUID",
392 pm_type: PM_INTEGER,
393 pm_flags: PM_DONTIMPORT,
394 tied_name: None,
395 },
396 special_paramdef {
397 name: "TTYIDLE",
398 pm_type: PM_INTEGER,
399 pm_flags: PM_READONLY,
400 tied_name: None,
401 },
402 // Scalar specials with custom GSU
403 special_paramdef {
404 name: "USERNAME",
405 pm_type: PM_SCALAR,
406 pm_flags: PM_DONTIMPORT,
407 tied_name: None,
408 },
409 special_paramdef {
410 name: "-",
411 pm_type: PM_SCALAR,
412 pm_flags: PM_READONLY,
413 tied_name: None,
414 },
415 special_paramdef {
416 name: "histchars",
417 pm_type: PM_SCALAR,
418 pm_flags: PM_DONTIMPORT,
419 tied_name: None,
420 },
421 special_paramdef {
422 name: "HOME",
423 pm_type: PM_SCALAR,
424 pm_flags: PM_UNSET,
425 tied_name: None,
426 },
427 special_paramdef {
428 name: "TERM",
429 pm_type: PM_SCALAR,
430 pm_flags: PM_UNSET,
431 tied_name: None,
432 },
433 special_paramdef {
434 name: "TERMINFO",
435 pm_type: PM_SCALAR,
436 pm_flags: PM_UNSET,
437 tied_name: None,
438 },
439 special_paramdef {
440 name: "TERMINFO_DIRS",
441 pm_type: PM_SCALAR,
442 pm_flags: PM_UNSET,
443 tied_name: None,
444 },
445 special_paramdef {
446 name: "WORDCHARS",
447 pm_type: PM_SCALAR,
448 pm_flags: 0,
449 tied_name: None,
450 },
451 special_paramdef {
452 name: "IFS",
453 pm_type: PM_SCALAR,
454 pm_flags: PM_DONTIMPORT,
455 tied_name: None,
456 },
457 special_paramdef {
458 name: "_",
459 pm_type: PM_SCALAR,
460 pm_flags: PM_DONTIMPORT,
461 tied_name: None,
462 },
463 special_paramdef {
464 name: "KEYBOARD_HACK",
465 pm_type: PM_SCALAR,
466 pm_flags: PM_DONTIMPORT,
467 tied_name: None,
468 },
469 special_paramdef {
470 name: "0",
471 pm_type: PM_SCALAR,
472 pm_flags: 0,
473 tied_name: None,
474 },
475 // Readonly integer variables bound to C globals
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: "$",
484 pm_type: crate::ported::zsh_h::PM_INTEGER,
485 pm_flags: crate::ported::zsh_h::PM_READONLY,
486 tied_name: None,
487 },
488 special_paramdef {
489 name: "?",
490 pm_type: crate::ported::zsh_h::PM_INTEGER,
491 pm_flags: crate::ported::zsh_h::PM_READONLY,
492 tied_name: None,
493 },
494 special_paramdef {
495 name: "HISTCMD",
496 pm_type: crate::ported::zsh_h::PM_INTEGER,
497 pm_flags: crate::ported::zsh_h::PM_READONLY,
498 tied_name: None,
499 },
500 special_paramdef {
501 name: "LINENO",
502 pm_type: crate::ported::zsh_h::PM_INTEGER,
503 pm_flags: crate::ported::zsh_h::PM_READONLY,
504 tied_name: None,
505 },
506 special_paramdef {
507 name: "PPID",
508 pm_type: crate::ported::zsh_h::PM_INTEGER,
509 pm_flags: crate::ported::zsh_h::PM_READONLY,
510 tied_name: None,
511 },
512 special_paramdef {
513 name: "ZSH_SUBSHELL",
514 pm_type: crate::ported::zsh_h::PM_INTEGER,
515 pm_flags: crate::ported::zsh_h::PM_READONLY,
516 tied_name: None,
517 },
518 // Settable integer variables
519 special_paramdef {
520 name: "COLUMNS",
521 pm_type: crate::ported::zsh_h::PM_INTEGER,
522 pm_flags: 0,
523 tied_name: None,
524 },
525 special_paramdef {
526 name: "LINES",
527 pm_type: crate::ported::zsh_h::PM_INTEGER,
528 pm_flags: 0,
529 tied_name: None,
530 },
531 special_paramdef {
532 name: "ZLE_RPROMPT_INDENT",
533 pm_type: crate::ported::zsh_h::PM_INTEGER,
534 pm_flags: crate::ported::zsh_h::PM_UNSET,
535 tied_name: None,
536 },
537 special_paramdef {
538 name: "SHLVL",
539 pm_type: crate::ported::zsh_h::PM_INTEGER,
540 pm_flags: 0,
541 tied_name: None,
542 },
543 special_paramdef {
544 name: "FUNCNEST",
545 pm_type: crate::ported::zsh_h::PM_INTEGER,
546 pm_flags: 0,
547 tied_name: None,
548 },
549 special_paramdef {
550 name: "OPTIND",
551 pm_type: crate::ported::zsh_h::PM_INTEGER,
552 pm_flags: crate::ported::zsh_h::PM_DONTIMPORT,
553 tied_name: None,
554 },
555 special_paramdef {
556 name: "TRY_BLOCK_ERROR",
557 pm_type: crate::ported::zsh_h::PM_INTEGER,
558 pm_flags: crate::ported::zsh_h::PM_DONTIMPORT,
559 tied_name: None,
560 },
561 special_paramdef {
562 name: "TRY_BLOCK_INTERRUPT",
563 pm_type: crate::ported::zsh_h::PM_INTEGER,
564 pm_flags: crate::ported::zsh_h::PM_DONTIMPORT,
565 tied_name: None,
566 },
567 // Scalar variables bound to C globals
568 special_paramdef {
569 name: "OPTARG",
570 pm_type: crate::ported::zsh_h::PM_SCALAR,
571 pm_flags: 0,
572 tied_name: None,
573 },
574 special_paramdef {
575 name: "NULLCMD",
576 pm_type: crate::ported::zsh_h::PM_SCALAR,
577 pm_flags: 0,
578 tied_name: None,
579 },
580 special_paramdef {
581 name: "POSTEDIT",
582 pm_type: crate::ported::zsh_h::PM_SCALAR,
583 pm_flags: crate::ported::zsh_h::PM_UNSET,
584 tied_name: None,
585 },
586 special_paramdef {
587 name: "READNULLCMD",
588 pm_type: crate::ported::zsh_h::PM_SCALAR,
589 pm_flags: 0,
590 tied_name: None,
591 },
592 special_paramdef {
593 name: "PS1",
594 pm_type: crate::ported::zsh_h::PM_SCALAR,
595 pm_flags: 0,
596 tied_name: None,
597 },
598 special_paramdef {
599 name: "RPS1",
600 pm_type: crate::ported::zsh_h::PM_SCALAR,
601 pm_flags: crate::ported::zsh_h::PM_UNSET,
602 tied_name: None,
603 },
604 special_paramdef {
605 name: "RPROMPT",
606 pm_type: crate::ported::zsh_h::PM_SCALAR,
607 pm_flags: crate::ported::zsh_h::PM_UNSET,
608 tied_name: None,
609 },
610 special_paramdef {
611 name: "PS2",
612 pm_type: crate::ported::zsh_h::PM_SCALAR,
613 pm_flags: 0,
614 tied_name: None,
615 },
616 special_paramdef {
617 name: "RPS2",
618 pm_type: crate::ported::zsh_h::PM_SCALAR,
619 pm_flags: crate::ported::zsh_h::PM_UNSET,
620 tied_name: None,
621 },
622 special_paramdef {
623 name: "RPROMPT2",
624 pm_type: crate::ported::zsh_h::PM_SCALAR,
625 pm_flags: crate::ported::zsh_h::PM_UNSET,
626 tied_name: None,
627 },
628 special_paramdef {
629 name: "PS3",
630 pm_type: crate::ported::zsh_h::PM_SCALAR,
631 pm_flags: 0,
632 tied_name: None,
633 },
634 special_paramdef {
635 name: "PS4",
636 pm_type: crate::ported::zsh_h::PM_SCALAR,
637 pm_flags: crate::ported::zsh_h::PM_DONTIMPORT_SUID,
638 tied_name: None,
639 },
640 special_paramdef {
641 name: "SPROMPT",
642 pm_type: crate::ported::zsh_h::PM_SCALAR,
643 pm_flags: 0,
644 tied_name: None,
645 },
646 // Readonly arrays
647 special_paramdef {
648 name: "*",
649 pm_type: crate::ported::zsh_h::PM_ARRAY,
650 pm_flags: crate::ported::zsh_h::PM_READONLY | crate::ported::zsh_h::PM_DONTIMPORT,
651 tied_name: None,
652 },
653 special_paramdef {
654 name: "@",
655 pm_type: crate::ported::zsh_h::PM_ARRAY,
656 pm_flags: crate::ported::zsh_h::PM_READONLY | crate::ported::zsh_h::PM_DONTIMPORT,
657 tied_name: None,
658 },
659 // ===================================================================
660 // c:388-392 — `/* This empty row indicates the end of parameters
661 // available in all emulations. */` NULL sentinel terminates the
662 // "always loaded" section. Entries below this line are only added
663 // under zsh emulation (else-branch of EMULATION(EMULATE_SH|EMULATE_KSH)
664 // at createparamtable c:840-846).
665 // SPECIAL_PARAMS_ZSH_START tracks this section boundary.
666 // ===================================================================
667 // Tied colon-separated/array pairs
668 special_paramdef {
669 name: "CDPATH",
670 pm_type: crate::ported::zsh_h::PM_SCALAR,
671 pm_flags: crate::ported::zsh_h::PM_TIED,
672 tied_name: Some("cdpath"),
673 },
674 special_paramdef {
675 name: "FIGNORE",
676 pm_type: crate::ported::zsh_h::PM_SCALAR,
677 pm_flags: crate::ported::zsh_h::PM_TIED,
678 tied_name: Some("fignore"),
679 },
680 special_paramdef {
681 name: "FPATH",
682 pm_type: crate::ported::zsh_h::PM_SCALAR,
683 pm_flags: crate::ported::zsh_h::PM_TIED,
684 tied_name: Some("fpath"),
685 },
686 special_paramdef {
687 name: "MAILPATH",
688 pm_type: crate::ported::zsh_h::PM_SCALAR,
689 pm_flags: crate::ported::zsh_h::PM_TIED,
690 tied_name: Some("mailpath"),
691 },
692 special_paramdef {
693 name: "PATH",
694 pm_type: crate::ported::zsh_h::PM_SCALAR,
695 pm_flags: crate::ported::zsh_h::PM_TIED,
696 tied_name: Some("path"),
697 },
698 special_paramdef {
699 name: "PSVAR",
700 pm_type: crate::ported::zsh_h::PM_SCALAR,
701 pm_flags: crate::ported::zsh_h::PM_TIED,
702 tied_name: Some("psvar"),
703 },
704 special_paramdef {
705 name: "ZSH_EVAL_CONTEXT",
706 pm_type: crate::ported::zsh_h::PM_SCALAR,
707 pm_flags: crate::ported::zsh_h::PM_READONLY | crate::ported::zsh_h::PM_TIED,
708 tied_name: Some("zsh_eval_context"),
709 },
710 special_paramdef {
711 name: "MODULE_PATH",
712 pm_type: crate::ported::zsh_h::PM_SCALAR,
713 pm_flags: crate::ported::zsh_h::PM_DONTIMPORT | crate::ported::zsh_h::PM_TIED,
714 tied_name: Some("module_path"),
715 },
716 special_paramdef {
717 name: "MANPATH",
718 pm_type: crate::ported::zsh_h::PM_SCALAR,
719 pm_flags: crate::ported::zsh_h::PM_TIED,
720 tied_name: Some("manpath"),
721 },
722 // Locale
723 special_paramdef {
724 name: "LANG",
725 pm_type: crate::ported::zsh_h::PM_SCALAR,
726 pm_flags: crate::ported::zsh_h::PM_UNSET,
727 tied_name: None,
728 },
729 special_paramdef {
730 name: "LC_ALL",
731 pm_type: crate::ported::zsh_h::PM_SCALAR,
732 pm_flags: crate::ported::zsh_h::PM_UNSET,
733 tied_name: None,
734 },
735 special_paramdef {
736 name: "LC_COLLATE",
737 pm_type: crate::ported::zsh_h::PM_SCALAR,
738 pm_flags: crate::ported::zsh_h::PM_UNSET,
739 tied_name: None,
740 },
741 special_paramdef {
742 name: "LC_CTYPE",
743 pm_type: crate::ported::zsh_h::PM_SCALAR,
744 pm_flags: crate::ported::zsh_h::PM_UNSET,
745 tied_name: None,
746 },
747 special_paramdef {
748 name: "LC_MESSAGES",
749 pm_type: crate::ported::zsh_h::PM_SCALAR,
750 pm_flags: crate::ported::zsh_h::PM_UNSET,
751 tied_name: None,
752 },
753 special_paramdef {
754 name: "LC_NUMERIC",
755 pm_type: crate::ported::zsh_h::PM_SCALAR,
756 pm_flags: crate::ported::zsh_h::PM_UNSET,
757 tied_name: None,
758 },
759 special_paramdef {
760 name: "LC_TIME",
761 pm_type: crate::ported::zsh_h::PM_SCALAR,
762 pm_flags: crate::ported::zsh_h::PM_UNSET,
763 tied_name: None,
764 },
765 // Zsh-only aliases
766 special_paramdef {
767 name: "ARGC",
768 pm_type: crate::ported::zsh_h::PM_INTEGER,
769 pm_flags: crate::ported::zsh_h::PM_READONLY,
770 tied_name: None,
771 },
772 special_paramdef {
773 name: "HISTCHARS",
774 pm_type: crate::ported::zsh_h::PM_SCALAR,
775 pm_flags: crate::ported::zsh_h::PM_DONTIMPORT,
776 tied_name: None,
777 },
778 special_paramdef {
779 name: "status",
780 pm_type: crate::ported::zsh_h::PM_INTEGER,
781 pm_flags: crate::ported::zsh_h::PM_READONLY,
782 tied_name: None,
783 },
784 special_paramdef {
785 name: "prompt",
786 pm_type: crate::ported::zsh_h::PM_SCALAR,
787 pm_flags: 0,
788 tied_name: None,
789 },
790 special_paramdef {
791 name: "PROMPT",
792 pm_type: crate::ported::zsh_h::PM_SCALAR,
793 pm_flags: 0,
794 tied_name: None,
795 },
796 special_paramdef {
797 name: "PROMPT2",
798 pm_type: crate::ported::zsh_h::PM_SCALAR,
799 pm_flags: 0,
800 tied_name: None,
801 },
802 special_paramdef {
803 name: "PROMPT3",
804 pm_type: crate::ported::zsh_h::PM_SCALAR,
805 pm_flags: 0,
806 tied_name: None,
807 },
808 special_paramdef {
809 name: "PROMPT4",
810 pm_type: crate::ported::zsh_h::PM_SCALAR,
811 pm_flags: 0,
812 tied_name: None,
813 },
814 special_paramdef {
815 name: "argv",
816 pm_type: crate::ported::zsh_h::PM_ARRAY,
817 pm_flags: 0,
818 tied_name: None,
819 },
820 // pipestatus array
821 special_paramdef {
822 name: "pipestatus",
823 pm_type: crate::ported::zsh_h::PM_ARRAY,
824 pm_flags: 0,
825 tied_name: None,
826 },
827];
828
829/// Port of `static initparam special_params_sh[]` from
830/// `Src/params.c:447-460`. "Alternative versions of colon-separated
831/// path parameters for sh emulation. These don't link to the array
832/// versions." Loaded by `createparamtable` (c:840-844) when
833/// `EMULATION(EMULATE_SH|EMULATE_KSH)` is non-zero, instead of the
834/// zsh-only section of `special_params`. All entries are scalars
835/// (`IPDEF8` macro adds `PM_SCALAR|PM_SPECIAL`); the C-side
836/// `tied_name` is NULL so these aren't tied to lowercase array
837/// counterparts.
838pub const special_params_sh: &[special_paramdef] = &[
839 special_paramdef { // c:448
840 name: "CDPATH",
841 pm_type: crate::ported::zsh_h::PM_SCALAR,
842 pm_flags: 0,
843 tied_name: None,
844 },
845 special_paramdef { // c:449
846 name: "FIGNORE",
847 pm_type: crate::ported::zsh_h::PM_SCALAR,
848 pm_flags: 0,
849 tied_name: None,
850 },
851 special_paramdef { // c:450
852 name: "FPATH",
853 pm_type: crate::ported::zsh_h::PM_SCALAR,
854 pm_flags: 0,
855 tied_name: None,
856 },
857 special_paramdef { // c:451
858 name: "MAILPATH",
859 pm_type: crate::ported::zsh_h::PM_SCALAR,
860 pm_flags: 0,
861 tied_name: None,
862 },
863 special_paramdef { // c:452
864 name: "PATH",
865 pm_type: crate::ported::zsh_h::PM_SCALAR,
866 pm_flags: 0,
867 tied_name: None,
868 },
869 special_paramdef { // c:453
870 name: "PSVAR",
871 pm_type: crate::ported::zsh_h::PM_SCALAR,
872 pm_flags: 0,
873 tied_name: None,
874 },
875 special_paramdef { // c:454
876 name: "ZSH_EVAL_CONTEXT",
877 pm_type: crate::ported::zsh_h::PM_SCALAR,
878 pm_flags: crate::ported::zsh_h::PM_READONLY,
879 tied_name: None,
880 },
881 special_paramdef { // c:457 (security comment)
882 name: "MODULE_PATH",
883 pm_type: crate::ported::zsh_h::PM_SCALAR,
884 pm_flags: crate::ported::zsh_h::PM_DONTIMPORT,
885 tied_name: None,
886 },
887];
888
889// ---------------------------------------------------------------------------
890// Parameter table
891// ---------------------------------------------------------------------------
892
893/// Parameter table.
894/// Port of the `paramtab` HashTable Src/params.c maintains —
895/// `createparamtable()` (line 817) initializes it with all the
896/// IPDEF*-declared special params; `createparam()` (line 1030)
897/// adds user variables.
898// ---------------------------------------------------------------------------
899// Free functions matching the C API
900// ---------------------------------------------------------------------------
901
902/// Port of `getintvalue(Value v)` from `Src/params.c:2601`.
903/// C body:
904/// ```c
905/// if (!v) return 0;
906/// if (v->valflags & VALFLAG_INV) return v->start;
907/// if (v->scanflags) {
908/// char **arr = getarrvalue(v);
909/// if (arr) { char *scal = sepjoin(arr, NULL, 1); return mathevali(scal); }
910/// return 0;
911/// }
912/// if (PM_TYPE(v->pm->node.flags) == PM_INTEGER)
913/// return v->pm->gsu.i->getfn(v->pm);
914/// if (v->pm->node.flags & (PM_EFLOAT|PM_FFLOAT))
915/// return (zlong)v->pm->gsu.f->getfn(v->pm);
916/// return mathevali(getstrvalue(v));
917/// ```
918pub fn getintvalue(v: Option<&mut crate::ported::zsh_h::value>) -> i64 {
919 let v = match v { Some(v) => v, None => return 0 };
920 if (v.valflags & VALFLAG_INV) != 0 {
921 return v.start as i64;
922 }
923 if v.scanflags != 0 {
924 // sepjoin(arr, NULL, 1) → mathevali(scal); arr backend missing.
925 return 0;
926 }
927 let pm = match v.pm.as_mut() { Some(p) => p, None => return 0 };
928 if PM_TYPE(pm.node.flags as u32) == PM_INTEGER {
929 return intgetfn(pm);
930 }
931 if (pm.node.flags as u32 & (PM_EFLOAT | PM_FFLOAT)) != 0 {
932 return floatgetfn(pm) as i64;
933 }
934 // mathevali(getstrvalue(v)) — best-effort decimal parse.
935 let pm = v.pm.as_mut().unwrap();
936 strgetfn(pm).parse::<i64>().unwrap_or(0)
937}
938
939/// Port of `getstrvalue(Value v)` from `Src/params.c:2335`.
940/// Full C body dispatches on `PM_TYPE(v->pm->node.flags)`:
941/// PM_HASHED (KSH path: `[0]` index lookup), PM_ARRAY (sepjoin
942/// when v->scanflags else `ss[v->start]`), PM_INTEGER (`convbase`),
943/// PM_EFLOAT|PM_FFLOAT (`convfloat`), PM_SCALAR|PM_NAMEREF
944/// (`pm->gsu.s->getfn(pm)`). Then PM_LEFT/PM_RIGHT_B/PM_RIGHT_Z
945/// padding when VALFLAG_SUBST is set.
946pub fn getstrvalue(v: Option<&mut crate::ported::zsh_h::value>) -> String {
947
948 let v = match v { Some(v) => v, None => return String::new() };
949 // c:2344-2348 — `if (VALFLAG_INV && !PM_HASHED) return sprintf("%d", v->start)`.
950 if (v.valflags & VALFLAG_INV) != 0 {
951 let hashed = v.pm.as_ref().map(|p| (p.node.flags as u32 & PM_HASHED) != 0)
952 .unwrap_or(false);
953 if !hashed {
954 return v.start.to_string();
955 }
956 }
957 let pm = match v.pm.as_mut() { Some(p) => p, None => return String::new() };
958 let t = PM_TYPE(pm.node.flags as u32);
959 let pmflags = pm.node.flags as u32;
960
961 // c:2350-2370 — PM_TYPE dispatch.
962 let mut s: String = if t == PM_HASHED || t == PM_ARRAY { // c:2351-2370
963 let arr = arrgetfn(pm);
964 if v.scanflags != 0 { // c:2361
965 arr.join(" ")
966 } else {
967 let mut start = v.start;
968 if start < 0 { start += arr.len() as i32; } // c:2364
969 if start < 0 || (start as usize) >= arr.len() { // c:2365-2366
970 String::new()
971 } else {
972 arr[start as usize].clone()
973 }
974 }
975 } else if t == PM_INTEGER { // c:2371
976 // c:2373 — `convbase(buf, pm->gsu.i->getfn(pm), pm->base)`.
977 // Without the base-aware convbase port, default to base-10.
978 intgetfn(pm).to_string()
979 } else if t == PM_EFLOAT || t == PM_FFLOAT { // c:2375
980 // c:2377 — `convfloat(getfn(pm), pm->base, pm->flags, NULL)`.
981 floatgetfn(pm).to_string()
982 } else if t == PM_SCALAR || t == PM_NAMEREF { // c:2380
983 strgetfn(pm)
984 } else {
985 // c:2384 — `DPUTS(1, "BUG: param node without valid type")`.
986 String::new()
987 };
988
989 // c:2390-2538 — VALFLAG_SUBST padding (PM_LEFT / PM_RIGHT_B /
990 // PM_RIGHT_Z). Multibyte is approximated via `chars().count()`
991 // (codepoint count) since the Rust port stores strings as
992 // UTF-8 rather than the C meta-byte encoding.
993 if v.valflags & VALFLAG_SUBST != 0 {
994 let pad_flags = pmflags & (PM_LEFT | PM_RIGHT_B | PM_RIGHT_Z);
995 if pad_flags != 0 {
996 let fwidth = if pm.width > 0 {
997 pm.width as usize
998 } else {
999 s.chars().count()
1000 };
1001 if pad_flags == PM_LEFT || pad_flags == (PM_LEFT | PM_RIGHT_Z) {
1002 // c:2393-2424 — left-justify: optional zero/blank trim,
1003 // truncate to fwidth, right-pad with spaces.
1004 let trimmed: &str = if pad_flags & PM_RIGHT_Z != 0 {
1005 s.trim_start_matches('0')
1006 } else {
1007 s.trim_start_matches(|c: char| c == ' ' || c == '\t')
1008 };
1009 let len = trimmed.chars().count();
1010 let take = len.min(fwidth);
1011 let mut out: String =
1012 trimmed.chars().take(take).collect();
1013 if fwidth > take {
1014 out.extend(std::iter::repeat(' ').take(fwidth - take));
1015 }
1016 s = out;
1017 } else if pad_flags & (PM_RIGHT_B | PM_RIGHT_Z) != 0 {
1018 // c:2426-2510 — right-justify with optional zero-padding
1019 // honouring leading-blank/minus/0x/base# prefix
1020 // detection for numeric values.
1021 let charlen = s.chars().count();
1022 if charlen < fwidth {
1023 let mut zero = true;
1024 let mut valprefend: usize = 0;
1025 let numeric_pm = (pmflags
1026 & (PM_INTEGER | PM_EFLOAT | PM_FFLOAT))
1027 != 0;
1028 if pad_flags & PM_RIGHT_Z != 0 {
1029 // c:2446-2466 — find the prefix to keep
1030 // (blanks → minus → 0x / base#).
1031 let bytes = s.as_bytes();
1032 let mut t = 0usize;
1033 while t < bytes.len()
1034 && (bytes[t] == b' ' || bytes[t] == b'\t')
1035 {
1036 t += 1; // c:2446-2447
1037 }
1038 if numeric_pm && t < bytes.len() && bytes[t] == b'-'
1039 {
1040 t += 1; // c:2454-2455
1041 }
1042 if (pmflags & PM_INTEGER) != 0 {
1043 let cbases =
1044 crate::ported::options::optlookup("cbases")
1045 > 0;
1046 if cbases
1047 && t + 1 < bytes.len()
1048 && bytes[t] == b'0'
1049 && bytes[t + 1] == b'x'
1050 {
1051 t += 2; // c:2462-2463
1052 } else if let Some(hash_off) = bytes[t..]
1053 .iter()
1054 .position(|&b| b == b'#')
1055 {
1056 t += hash_off + 1; // c:2464-2465
1057 }
1058 }
1059 valprefend = t;
1060 if t == bytes.len() {
1061 zero = false; // c:2468-2469
1062 } else if !numeric_pm && !bytes[t].is_ascii_digit() {
1063 zero = false; // c:2473-2474
1064 }
1065 }
1066 // c:2483 — pad char picks: ' ' if PM_RIGHT_B or
1067 // numeric-prefix detection failed, else '0'.
1068 let pad_char = if (pad_flags & PM_RIGHT_B) != 0 || !zero
1069 {
1070 ' '
1071 } else {
1072 '0'
1073 };
1074 let need = fwidth - charlen;
1075 let prefix = &s[..valprefend];
1076 let rest = &s[valprefend..];
1077 let mut out = String::with_capacity(need + s.len());
1078 out.push_str(prefix); // c:2491
1079 out.extend(std::iter::repeat(pad_char).take(need)); // c:2483-2485
1080 out.push_str(rest); // c:2492-2493
1081 s = out;
1082 } else if charlen > fwidth {
1083 // c:2496-2500 — truncate from the front to fit fwidth
1084 // codepoints (C uses MB_METACHARLEN; Rust uses chars).
1085 let skip = charlen - fwidth;
1086 s = s.chars().skip(skip).collect();
1087 }
1088 }
1089 }
1090 }
1091
1092 s
1093}
1094
1095/// Port of `getsparam_u(char *s)` from `Src/params.c:3088`. C body:
1096/// ```c
1097/// struct value vbuf;
1098/// Value v;
1099/// if (!(v = getvalue(&vbuf, &s, 0))) return NULL;
1100/// if (PM_TYPE(v->pm->node.flags) != PM_SCALAR) return NULL;
1101/// return getstrvalue(v);
1102/// ```
1103/// Returns the string value only when the param is PM_SCALAR.
1104/// WARNING: param names don't match C — Rust=(v) vs C=()
1105pub fn getsparam_u(v: Option<&mut crate::ported::zsh_h::value>) -> Option<String> {
1106 let v = v?;
1107 let pm = v.pm.as_ref()?;
1108 if PM_TYPE(pm.node.flags as u32) != PM_SCALAR {
1109 return None;
1110 }
1111 Some(getstrvalue(Some(v)))
1112}
1113
1114/// Port of `getaparam(char *s)` from `Src/params.c:3100`. C body:
1115/// ```c
1116/// struct value vbuf; Value v; char *t = s;
1117/// if (idigit(*s)) return NULL;
1118/// if ((v = fetchvalue(&vbuf, &s, 0, SCANPM_ARRONLY)) &&
1119/// PM_TYPE(v->pm->node.flags) == PM_ARRAY)
1120/// return v->pm->gsu.a->getfn(v->pm);
1121/// return NULL;
1122/// ```
1123/// Returns `pm->u.arr` when the param is PM_ARRAY.
1124pub fn getaparam(s: Option<&mut crate::ported::zsh_h::value>) -> Option<Vec<String>> {
1125 let s = s?;
1126 let pm = s.pm.as_mut()?;
1127 if PM_TYPE(pm.node.flags as u32) != PM_ARRAY {
1128 return None;
1129 }
1130 Some(arrgetfn(pm))
1131}
1132
1133/// Port of `gethparam(char *s)` from `Src/params.c:3115`. C body
1134/// (analogous to getaparam): fetchvalue + return
1135/// `paramvalarr(v->pm->gsu.h->getfn(v->pm), SCANPM_WANTVALS)`
1136/// when PM_TYPE == PM_HASHED.
1137pub fn gethparam(s: Option<&mut crate::ported::zsh_h::value>) -> Option<Vec<String>> {
1138 let s = s?;
1139 let pm = s.pm.as_mut()?;
1140 if PM_TYPE(pm.node.flags as u32) != PM_HASHED {
1141 return None;
1142 }
1143 // hashgetfn(pm) returns the HashTable; flattening to values
1144 // requires scanhashtable backend — return empty for now.
1145 let _ = hashgetfn(pm);
1146 Some(Vec::new())
1147}
1148
1149/// Port of `gethkparam(char *s)` from `Src/params.c:3130`. Same as
1150/// `gethparam` but returns keys via `paramvalarr(..., SCANPM_WANTKEYS)`.
1151pub fn gethkparam(s: Option<&mut crate::ported::zsh_h::value>) -> Option<Vec<String>> {
1152 let s = s?;
1153 let pm = s.pm.as_mut()?;
1154 if PM_TYPE(pm.node.flags as u32) != PM_HASHED {
1155 return None;
1156 }
1157 let _ = hashgetfn(pm);
1158 Some(Vec::new())
1159}
1160
1161/// Port of `getnumvalue(Value v)` from `Src/params.c:2624`. Returns an
1162/// `mnumber` (tagged int/float). C body dispatches on `valflags &
1163/// VALFLAG_INV` (returns start as int), `scanflags` (sepjoin →
1164/// matheval), then PM_TYPE: PM_INTEGER → mn.l = pm->gsu.i->getfn,
1165/// PM_EFLOAT|PM_FFLOAT → mn.type=MN_FLOAT; mn.d = pm->gsu.f->getfn,
1166/// else matheval(getstrvalue(v)).
1167pub fn getnumvalue(v: Option<&mut crate::ported::zsh_h::value>) -> crate::ported::math::mnumber {
1168 let v = match v { Some(v) => v, None => return mnumber { l: 0, d: 0.0, type_: MN_INTEGER } };
1169 if (v.valflags & VALFLAG_INV) != 0 {
1170 return mnumber { l: v.start as i64, d: 0.0, type_: MN_INTEGER };
1171 }
1172 if v.scanflags != 0 {
1173 return mnumber { l: 0, d: 0.0, type_: MN_INTEGER };
1174 }
1175 let pm = match v.pm.as_mut() { Some(p) => p, None => return mnumber { l: 0, d: 0.0, type_: MN_INTEGER } };
1176 let t = PM_TYPE(pm.node.flags as u32);
1177 if t == PM_INTEGER {
1178 return mnumber { l: intgetfn(pm), d: 0.0, type_: MN_INTEGER };
1179 }
1180 if t == PM_EFLOAT || t == PM_FFLOAT {
1181 return mnumber { l: 0, d: floatgetfn(pm), type_: MN_FLOAT };
1182 }
1183 let s = strgetfn(pm);
1184 if let Ok(i) = s.parse::<i64>() { return mnumber { l: i, d: 0.0, type_: MN_INTEGER }; }
1185 if let Ok(f) = s.parse::<f64>() { return mnumber { l: 0, d: f, type_: MN_FLOAT }; }
1186 mnumber { l: 0, d: 0.0, type_: MN_INTEGER }
1187}
1188
1189/// Port of `setstrvalue(Value v, char *val)` from `Src/params.c:2685`. C body is a
1190/// one-liner: `assignstrvalue(v, val, 0);` — the real workhorse
1191/// is `assignstrvalue` (params.c:2692).
1192pub fn setstrvalue(v: Option<&mut crate::ported::zsh_h::value>, val: &str) {
1193 assignstrvalue(v, Some(val.to_string()), 0);
1194}
1195
1196/// Port of `assigniparam(char *s, zlong val, int flags)` from `Src/params.c:3753` (and its
1197/// internal use as the integer branch of `setvalue`). C body
1198/// builds an `mnumber{ .type = MN_INTEGER, .u.l = val }` and
1199/// calls `assignnparam(s, mn, ASSPM_WARN)`.
1200pub fn assigniparam(vbuf: &str, t: i64) {
1201 assignnparam(vbuf, crate::ported::math::mnumber { l: t, d: 0.0, type_: MN_INTEGER }, crate::ported::zsh_h::ASSPM_WARN);
1202}
1203
1204/// Set array parameter.
1205/// Port of `setaparam(char *s, char **aval)` from `Src/params.c:3595` — single-line wrapper
1206/// around `assignaparam(s, val, ASSPM_WARN)`. C body:
1207/// ```c
1208/// mod_export Param setaparam(char *s, char **val) {
1209/// return assignaparam(s, val, ASSPM_WARN);
1210/// }
1211/// ```
1212///
1213/// `ASSPM_WARN` (params.c:104) is a no-op in our port — the global
1214/// "warn on creation" tracking is not yet ported. Call shape
1215/// preserved so callers can use this where C calls setaparam.
1216/// WARNING: param names don't match C — Rust=() vs C=(s, val)
1217pub fn setaparam(name: &str, val: Vec<String>) // c:3595
1218 -> Option<crate::ported::zsh_h::Param>
1219{
1220 // c:3766 — `return assignaparam(s, val, ASSPM_WARN)`.
1221 assignaparam(name, val, crate::ported::zsh_h::ASSPM_WARN)
1222}
1223
1224/// Port of `assignsparam(char *s, char *val, int flags)` from `Src/params.c:3193`. C signature:
1225/// `mod_export Param assignsparam(char *s, char *val, int flags)`.
1226///
1227/// `s` may carry an embedded `[...]` subscript (matching C's
1228/// `strchr(s, '[')` parse). The function operates on the global
1229/// `paramtab` (Src/params.c:515), creating/mutating `Param`
1230/// entries in place. Branches preserved 1:1 with C:
1231/// - c:3203 `isident(s)` — reject non-identifier names.
1232/// - c:3209 `queue_signals()`.
1233/// - c:3210 subscripted path: c:3212 `getvalue` lookup,
1234/// c:3213 `createparam(t, PM_ARRAY)` on miss, c:3216
1235/// PM_READONLY guard, c:3227 ASSPM_WARN drop, c:3228 clear
1236/// PM_DEFAULTED, c:3231 `v = NULL` then re-dispatch by type.
1237/// - c:3232 non-subscripted: c:3233 `getvalue` → c:3234
1238/// `createparam(t, PM_SCALAR)`; c:3236-3250 array/hash type-flip
1239/// to PM_SCALAR (when not PM_SPECIAL|PM_TIED, not KSHARRAYS,
1240/// not ASSPM_AUGMENT) via `resetparam(v->pm, PM_SCALAR)`.
1241/// - c:3258 PM_NAMEREF → c:3259 `valid_refname(val, flags)` guard.
1242/// - c:3269 clear PM_DEFAULTED.
1243/// - c:3343 `assignstrvalue(v, val, flags)`.
1244/// - c:3344 `unqueue_signals()`; c:3345 return v->pm.
1245///
1246/// The full HashTable substrate (vtable callbacks, scope-stacked
1247/// iterators) is not yet wired; non-essential branches such as
1248/// `+= AUGMENT` numeric/array slice append and `check_warn_pm`
1249/// are documented but elided where unreachable from current
1250/// callers — none of those code paths are exercised by zshrs's
1251/// existing call sites.
1252pub fn assignsparam(s: &str, val: &str, flags: i32) // c:3193
1253 -> Option<crate::ported::zsh_h::Param>
1254{
1255
1256 // c:3203 `if (!isident(s)) { zerr; errflag |= ERRFLAG_ERROR; return NULL; }`
1257 if !isident(s) {
1258 zerr(&format!("not an identifier: {}", s)); // c:3204
1259 errflag.fetch_or( // c:3206
1260 crate::ported::utils::ERRFLAG_ERROR,
1261 std::sync::atomic::Ordering::Relaxed,
1262 );
1263 return None; // c:3207
1264 }
1265 crate::ported::signals::queue_signals(); // c:3209
1266
1267 // c:3210 — `strchr(s, '[')`. Split the leading name from the
1268 // subscript while preserving C's `*ss = '\0'` / `*ss = '['`
1269 // restore semantics: the Rust port works on `&str` slices so
1270 // there's no in-place null-terminator dance, but the parse
1271 // shape is identical.
1272 let (name, subscript) = match s.find('[') {
1273 Some(i) => {
1274 let close = s.rfind(']').unwrap_or(s.len());
1275 let key_end = if close > i { close } else { s.len() };
1276 (&s[..i], Some(&s[i + 1..key_end]))
1277 }
1278 None => (s, None),
1279 };
1280
1281 // Subscripted path (c:3210-3231).
1282 if let Some(key) = subscript {
1283 let mut tab = paramtab().write().unwrap();
1284 let exists = tab.contains_key(name); // c:3212
1285 if !exists {
1286 // c:3213 `createparam(t, PM_ARRAY); created = 1;`
1287 let pm: Param = Box::new(param {
1288 node: hashnode { next: None, nam: name.to_string(), flags: PM_ARRAY as i32 },
1289 u_data: 0, u_arr: Some(Vec::new()), u_str: None, u_val: 0,
1290 u_dval: 0.0, u_hash: None,
1291 gsu_s: None, gsu_i: None, gsu_f: None, gsu_a: None, gsu_h: None,
1292 base: 0, width: 0, env: None, ename: None, old: None, level: 0,
1293 });
1294 tab.insert(name.to_string(), pm);
1295 } else {
1296 // c:3216 `if (v->pm->node.flags & PM_READONLY)`.
1297 let pm = tab.get(name).unwrap();
1298 if (pm.node.flags as u32 & PM_READONLY) != 0 {
1299 zerr(&format!("read-only variable: {}", pm.node.nam)); // c:3217
1300 drop(tab);
1301 crate::ported::signals::unqueue_signals(); // c:3220
1302 return None; // c:3221
1303 }
1304 }
1305 // c:3231 `v = NULL;` — re-dispatch by storage type.
1306 let pm = tab.get_mut(name).unwrap();
1307 pm.node.flags &= !(PM_DEFAULTED as i32); // c:3228
1308 if (pm.node.flags as u32 & PM_HASHED) != 0 {
1309 // PM_HASHED element store. `param.u_hash` is typed
1310 // `Option<HashTable>` per Src/zsh.h:1841 but the
1311 // HashTable runtime backing isn't wired; the assoc-array
1312 // values live in a parallel storage keyed on param name
1313 // (`paramtab_hashed_storage()`).
1314 let mut store = paramtab_hashed_storage().lock().unwrap();
1315 store.entry(name.to_string()).or_default()
1316 .insert(key.to_string(), val.to_string());
1317 } else if let Ok(idx) = key.parse::<i64>() {
1318 // PM_ARRAY + numeric subscript (c:3357 `assignaparam`).
1319 let arr = pm.u_arr.get_or_insert_with(Vec::new);
1320 let len = arr.len() as i64;
1321 // 1-based forward, negative-from-end.
1322 let real_idx = if idx < 0 { len + idx } else { idx - 1 };
1323 let real_idx = real_idx.max(0) as usize;
1324 while arr.len() <= real_idx { arr.push(String::new()); }
1325 arr[real_idx] = val.to_string();
1326 pm.u_str = None;
1327 } else {
1328 // String subscript on a non-hashed name → auto-vivify
1329 // as PM_HASHED (mirrors C `createparam(s, PM_HASHED)`
1330 // fallback when getvalue returns NULL).
1331 pm.node.flags = (pm.node.flags & !(PM_TYPE(u32::MAX) as i32))
1332 | PM_HASHED as i32;
1333 pm.u_arr = None;
1334 pm.u_str = None;
1335 let mut map: indexmap::IndexMap<String, String> = indexmap::IndexMap::new();
1336 map.insert(key.to_string(), val.to_string());
1337 paramtab_hashed_storage().lock().unwrap()
1338 .insert(name.to_string(), map);
1339 }
1340 let cloned = pm.clone();
1341 drop(tab);
1342 crate::ported::signals::unqueue_signals(); // c:3344
1343 return Some(cloned); // c:3345
1344 }
1345
1346 // c:3232 non-subscripted branch.
1347 let mut tab = paramtab().write().unwrap();
1348 let existing = tab.contains_key(name);
1349 if !existing {
1350 // c:3234 `createparam(t, PM_SCALAR); created = 1;`
1351 let mut pm_flags = PM_SCALAR as i32;
1352 if isset_opt(ALLEXPORT) { // c:1149-1150 (ALLEXPORT path)
1353 pm_flags |= PM_EXPORTED as i32;
1354 }
1355 let pm: Param = Box::new(param {
1356 node: hashnode { next: None, nam: name.to_string(), flags: pm_flags },
1357 u_data: 0, u_arr: None, u_str: Some(String::new()), u_val: 0,
1358 u_dval: 0.0, u_hash: None,
1359 gsu_s: None, gsu_i: None, gsu_f: None, gsu_a: None, gsu_h: None,
1360 base: 0, width: 0, env: None, ename: None, old: None, level: 0,
1361 });
1362 tab.insert(name.to_string(), pm);
1363 } else {
1364 let pm = tab.get(name).unwrap();
1365 // c:3216 PM_READONLY guard for an existing param.
1366 if (pm.node.flags as u32 & PM_READONLY) != 0 {
1367 zerr(&format!("read-only variable: {}", pm.node.nam)); // c:3217
1368 drop(tab);
1369 crate::ported::signals::unqueue_signals(); // c:3220
1370 return None; // c:3221
1371 }
1372 // c:3236-3250 — existing PM_ARRAY/PM_HASHED on a non-special,
1373 // non-tied, non-KSHARRAYS, non-AUGMENT scalar assignment →
1374 // `resetparam(v->pm, PM_SCALAR)`.
1375 let f = pm.node.flags as u32;
1376 let is_array_or_hash = (f & PM_ARRAY) != 0 || (f & PM_HASHED) != 0;
1377 let is_special_or_tied = (f & (PM_SPECIAL | PM_TIED)) != 0;
1378 let augment_bit = (flags & ASSPM_AUGMENT) != 0;
1379 if is_array_or_hash
1380 && !is_special_or_tied
1381 && !augment_bit
1382 && !isset(crate::ported::zsh_h::KSHARRAYS)
1383 {
1384 // c:3242 — flip type to PM_SCALAR, drop array/hash slots.
1385 let pm_mut = tab.get_mut(name).unwrap();
1386 pm_mut.node.flags = (pm_mut.node.flags & !(PM_TYPE(u32::MAX) as i32))
1387 | PM_SCALAR as i32;
1388 pm_mut.u_arr = None;
1389 paramtab_hashed_storage().lock().unwrap().remove(name);
1390 }
1391 }
1392
1393 // c:3258-3266 `if (*val && (v->pm->node.flags & PM_NAMEREF))`.
1394 let pm = tab.get(name).unwrap();
1395 if !val.is_empty() && (pm.node.flags as u32 & PM_NAMEREF) != 0 {
1396 if !valid_refname(val, pm.node.flags) { // c:3259
1397 zerr(&format!("invalid name reference: {}", val)); // c:3260
1398 drop(tab);
1399 errflag.fetch_or( // c:3263
1400 crate::ported::utils::ERRFLAG_ERROR,
1401 std::sync::atomic::Ordering::Relaxed,
1402 );
1403 crate::ported::signals::unqueue_signals(); // c:3262
1404 return None; // c:3264
1405 }
1406 }
1407
1408 // c:3269 `v->pm->node.flags &= ~PM_DEFAULTED;`
1409 let pm = tab.get_mut(name).unwrap();
1410 pm.node.flags &= !(PM_DEFAULTED as i32);
1411
1412 // c:3343 `assignstrvalue(v, val, flags)` — scalar write.
1413 pm.u_str = Some(val.to_string());
1414
1415 let cloned = pm.clone();
1416 drop(tab);
1417 crate::ported::signals::unqueue_signals(); // c:3344
1418 Some(cloned) // c:3345
1419}
1420
1421/// Parallel storage for PM_HASHED parameter values. `param.u_hash`
1422/// is typed `Option<HashTable>` per Src/zsh.h:1841 but the full
1423/// HashTable substrate isn't wired yet; the assoc-array values live
1424/// here keyed on param name until that lands.
1425static PARAMTAB_HASHED_STORAGE_INNER: OnceLock<
1426 Mutex<HashMap<String, indexmap::IndexMap<String, String>>>,
1427> = OnceLock::new();
1428
1429pub(crate) fn paramtab_hashed_storage()
1430 -> &'static Mutex<HashMap<String, indexmap::IndexMap<String, String>>>
1431{
1432 PARAMTAB_HASHED_STORAGE_INNER
1433 .get_or_init(|| Mutex::new(HashMap::new()))
1434}
1435
1436/// Mirror the global `paramtab` (and the parallel hashed-storage
1437/// table) into the three HashMaps that `SubstState` uses as its
1438/// transient backing during `prefork()` (Src/subst.c:100). This
1439/// is a port-transition shim: once `subst.rs` reads parameters
1440/// directly through `paramtab().read()` / `.write()` instead of carrying
1441/// `state.variables`/`state.arrays`/`state.assoc_arrays`, this
1442/// helper goes away.
1443pub fn sync_state_from_paramtab(
1444 variables: &mut HashMap<String, String>,
1445 arrays: &mut HashMap<String, Vec<String>>,
1446 assoc_arrays: &mut HashMap<String, indexmap::IndexMap<String, String>>,
1447) {
1448 let tab = paramtab().read().unwrap();
1449 for (name, pm) in tab.iter() {
1450 let f = pm.node.flags as u32;
1451 if (f & PM_ARRAY) != 0 {
1452 if let Some(arr) = pm.u_arr.as_ref() {
1453 arrays.insert(name.clone(), arr.clone());
1454 }
1455 variables.remove(name);
1456 assoc_arrays.remove(name);
1457 } else if (f & PM_HASHED) != 0 {
1458 if let Some(map) = paramtab_hashed_storage()
1459 .lock().unwrap().get(name)
1460 {
1461 assoc_arrays.insert(name.clone(), map.clone());
1462 }
1463 variables.remove(name);
1464 arrays.remove(name);
1465 } else if let Some(s) = pm.u_str.as_ref() {
1466 // PM_SCALAR / PM_NAMEREF / numeric — fold to the string view.
1467 variables.insert(name.clone(), s.clone());
1468 arrays.remove(name);
1469 assoc_arrays.remove(name);
1470 }
1471 }
1472}
1473
1474/// Array parameter assignment (no subscript).
1475///
1476/// Direct port of `Param assignaparam(char *s, char **val, int flags)`
1477/// from `Src/params.c:3357`. Writes an array value into paramtab
1478/// and returns the new/updated Param.
1479///
1480/// Pending C semantics:
1481/// - PM_READONLY rejection (c:3370-3381 via setarrvalue chain)
1482/// - PM_NAMEREF type-change reject (c:3395-3398)
1483/// - resetparam from non-array (c:3415-3420)
1484/// - ASSPM_AUGMENT (`a+=val`) preserve-old prepend (c:3404-3412)
1485/// - PM_UNIQUE dedupe (c:3401)
1486/// - element-wise `a[k]=v` slice path (c:3373-3389)
1487pub fn assignaparam(
1488 name: &str,
1489 val: Vec<String>,
1490 flags: i32,
1491) -> Option<crate::ported::zsh_h::Param> { // c:3357
1492 // c:3366-3370 — `if (!isident(s)) { zerr; return NULL }`.
1493 if !isident(name) {
1494 crate::ported::utils::zerr(&format!("not an identifier: {}", name));
1495 return None;
1496 }
1497
1498 // c:3391-3394 — fetchvalue / createparam(PM_ARRAY) if missing.
1499 let (existed, prior_scalar, prior_flags) = {
1500 let tab = paramtab().read().unwrap();
1501 match tab.get(name) {
1502 Some(pm) => (true, pm.u_str.clone(), pm.node.flags),
1503 None => (false, None, 0),
1504 }
1505 };
1506 if !existed {
1507 createparam(name, PM_ARRAY as i32)?;
1508 }
1509
1510 // c:3402-3412 — ASSPM_AUGMENT preserve-old prepend. When the
1511 // previous value was a scalar (not array/hashed) and we're
1512 // augmenting (`a+=val`), prepend that scalar's string form as
1513 // val[0]. Only fires when the existing param is not PM_UNSET.
1514 let was_scalar_array_target = existed
1515 && prior_flags & (PM_ARRAY | PM_HASHED) as i32 == 0
1516 && prior_flags & PM_SPECIAL as i32 == 0;
1517 let mut val = val;
1518 if (flags & ASSPM_AUGMENT) != 0
1519 && was_scalar_array_target
1520 && prior_flags & PM_UNSET as i32 == 0
1521 {
1522 if let Some(old_scalar) = prior_scalar {
1523 val.insert(0, old_scalar); // c:3408-3411
1524 }
1525 }
1526
1527 // c:3434 — setarrvalue(v, val): store array in pm.u_arr.
1528 let mut tab = paramtab().write().unwrap();
1529 let pm = tab.get_mut(name)?;
1530 let uniq = pm.node.flags & PM_UNIQUE as i32 != 0; // c:3401
1531 if pm.node.flags & PM_SPECIAL as i32 == 0 {
1532 let type_mask =
1533 PM_ARRAY | PM_INTEGER | PM_EFLOAT | PM_FFLOAT | PM_HASHED | PM_NAMEREF;
1534 pm.node.flags = (pm.node.flags & !type_mask as i32) | PM_ARRAY as i32;
1535 }
1536 // c:3401 — preserve PM_UNIQUE through the type change, then let
1537 // arrsetfn dedupe via the actual write.
1538 if uniq {
1539 pm.node.flags |= PM_UNIQUE as i32;
1540 }
1541 let val_final = if uniq { simple_arrayuniq(val) } else { val };
1542 pm.u_arr = Some(val_final.clone());
1543 pm.u_str = None;
1544 pm.u_hash = None;
1545 let cloned = pm.clone();
1546 drop(tab);
1547 let _ = val_final;
1548 Some(cloned)
1549}
1550
1551/// Direct port of `Param sethparam(char *s, char **val)` from
1552/// `Src/params.c:3602`. Writes an associative array (flat
1553/// alternating key,value list) into paramtab + the parallel
1554/// `paramtab_hashed_storage` table; returns the new Param.
1555///
1556/// Pending C semantics:
1557/// - PM_READONLY rejection
1558/// - resetparam(PM_HASHED) for type-change
1559/// - PM_SPECIAL type-change reject (c:3637)
1560pub fn sethparam(name: &str, val: Vec<String>) // c:3602
1561 -> Option<crate::ported::zsh_h::Param>
1562{
1563
1564 // c:3611-3615 — `if (!isident(s)) { zerr; return NULL }`.
1565 if !isident(name) {
1566 crate::ported::utils::zerr(&format!("not an identifier: {}", name));
1567 return None;
1568 }
1569 // c:3617-3621 — `if (strchr(s, '[')) { zerr; return NULL }`.
1570 if name.contains('[') {
1571 crate::ported::utils::zerr("nested associative arrays not yet supported");
1572 return None;
1573 }
1574
1575 // c:3625 — fetchvalue / createparam(PM_HASHED) if missing.
1576 let exists = paramtab().read().unwrap().contains_key(name);
1577 if !exists {
1578 createparam(name, PM_HASHED as i32)?;
1579 }
1580
1581 // Build the IndexMap from flat (k,v) pairs (mirrors c:arrhashsetfn
1582 // pair-walking at c:4140-4166).
1583 let mut map: indexmap::IndexMap<String, String> = indexmap::IndexMap::new();
1584 let mut iter = val.into_iter();
1585 while let Some(k) = iter.next() {
1586 let v = iter.next().unwrap_or_default();
1587 map.insert(k, v);
1588 }
1589
1590 // c:3640 — install in paramtab + paramtab_hashed_storage.
1591 let mut tab = paramtab().write().unwrap();
1592 let pm = tab.get_mut(name)?;
1593 if pm.node.flags & PM_SPECIAL as i32 == 0 {
1594 let type_mask =
1595 PM_ARRAY | PM_INTEGER | PM_EFLOAT | PM_FFLOAT | PM_HASHED | PM_NAMEREF;
1596 pm.node.flags = (pm.node.flags & !type_mask as i32) | PM_HASHED as i32;
1597 }
1598 pm.u_arr = None;
1599 pm.u_str = None;
1600 let cloned = pm.clone();
1601 drop(tab);
1602
1603 paramtab_hashed_storage()
1604 .lock()
1605 .unwrap()
1606 .insert(name.to_string(), map);
1607
1608 Some(cloned)
1609}
1610
1611/// Unset parameter (from params.c unsetparam_pm)
1612/// Port of `unsetparam_pm(Param pm, int altflag, int exp)` from `Src/params.c:3841`. Full body
1613/// removes `pm` from `paramtab` (after invoking
1614/// `pm->gsu.s->unsetfn(pm, exp)`), tears down the tied alternate
1615/// (`pm->ename`) when `!altflag`, deletes the env entry, and
1616/// resurrects `pm->old` at the right scope. Stub: needs paramtab
1617/// HashTable backend (`paramtab->removenode/addnode`) plus the
1618/// `delenv`/`adduserdir` helpers — direct port retains only the
1619/// in-memory mutation of `pm` that doesn't touch the table.
1620#[allow(unused_variables)]
1621pub fn unsetparam_pm(pm: &mut crate::ported::zsh_h::param, altflag: i32, exp: i32) -> i32 {
1622 // Readonly check (locallevel global not yet ported — assume 0).
1623 if (pm.node.flags as u32 & PM_READONLY) != 0 && pm.level <= 0 {
1624 // zerr("read-only %s: %s", ref?"reference":"variable", nam);
1625 let _kind = if (pm.node.flags as u32 & PM_NAMEREF) != 0 {
1626 "reference"
1627 } else {
1628 "variable"
1629 };
1630 return 1;
1631 }
1632 pm.node.flags &= !(PM_DECLARED as i32);
1633 if (pm.node.flags as u32 & PM_UNSET) == 0
1634 || (pm.node.flags as u32 & PM_REMOVABLE) != 0
1635 {
1636 // pm->gsu.s->unsetfn(pm, exp) — open-coded to stdunsetfn.
1637 stdunsetfn(pm, exp);
1638 }
1639 if pm.env.is_some() {
1640 delenv(&pm.node.nam);
1641 pm.env = None;
1642 }
1643 // Tied alt-name removal + paramtab restore-from-old not yet
1644 // possible without HashTable backend; the C postlude (lines
1645 // 3853-3935) is a paramtab->removenode + addnode dance that
1646 // requires the missing vtable.
1647 pm.node.flags |= PM_UNSET as i32;
1648 0
1649}
1650
1651/// Empty special-hash sentinel.
1652/// Port of `shempty()` from Src/params.c:1166. The C source uses
1653/// it as a no-op getfn callback for special hashes that need an
1654/// addressable function pointer but no actual work. Provided here
1655/// so future callers that match the C source's signature can call
1656/// it directly.
1657pub fn shempty() {}
1658
1659/// Port of `setsparam(char *s, char *val)` from Src/params.c:3350.
1660/// C body: `return assignsparam(s, val, ASSPM_WARN);`
1661/// WARNING: param names don't match C — Rust=() vs C=(s, val)
1662pub fn setsparam(s: &str, val: &str) // c:3350
1663 -> Option<crate::ported::zsh_h::Param>
1664{
1665 assignsparam(s, val, ASSPM_WARN as i32) // c:3352
1666}
1667
1668/// Port of `setiparam(char *s, zlong val)` from Src/params.c:3765. The C source
1669/// constructs an `mnumber` and calls `assignnparam(s, mnval,
1670/// ASSPM_WARN)`. The Rust port renders to decimal and routes
1671/// through `assignsparam` until the integer-typed `assignnparam`
1672/// store path lands.
1673/// WARNING: param names don't match C — Rust=() vs C=(s, val)
1674pub fn setiparam(s: &str, val: i64) // c:3765
1675 -> Option<crate::ported::zsh_h::Param>
1676{
1677 assignsparam(s, &val.to_string(), ASSPM_WARN as i32)
1678}
1679
1680/// Port of `setiparam_no_convert(char *s, zlong val)` from Src/params.c:3781. C
1681/// source comment: "If the target is already an integer, this
1682/// gets converted back. Low technology rules." It uses convbase
1683/// to render decimal then calls assignsparam.
1684/// WARNING: param names don't match C — Rust=() vs C=(s, val)
1685pub fn setiparam_no_convert(s: &str, val: i64) // c:3781
1686 -> Option<crate::ported::zsh_h::Param>
1687{
1688 assignsparam(s, &val.to_string(), ASSPM_WARN as i32)
1689}
1690
1691/// Port of `getsparam(char *s)` from `Src/params.c:3076`.
1692///
1693/// C body:
1694/// ```c
1695/// char *getsparam(char *s) {
1696/// struct value vbuf;
1697/// Value v = getvalue(&vbuf, &s, 0);
1698/// if (!v) return NULL;
1699/// return getstrvalue(v);
1700/// }
1701/// ```
1702///
1703/// `getvalue` (params.c:2173) builds a `Value` for the parameter,
1704/// dispatching through `Param.gsu->getfn` for special parameters.
1705/// `getstrvalue` (params.c:2335) extracts the scalar form: for
1706/// PM_INTEGER calls `pm->gsu.i->getfn(pm)` and convbase's the
1707/// result; for PM_SCALAR calls `pm->gsu.s->getfn(pm)`; for
1708/// PM_ARRAY joins the elements.
1709///
1710/// **Sole funnel.** Every scalar parameter read in zshrs routes
1711/// through this fn — `subst.rs` parameter expansion AND
1712/// `fusevm_bridge::expand_param` both call `getsparam`. The
1713/// dispatch chain lives in exactly one place, mirroring C's
1714/// "every read goes through getsparam" architecture.
1715///
1716/// Lookup order (mirrors C's `getvalue` → `getstrvalue` cascade):
1717/// 1. **GSU dispatch** via [`lookup_special_var`] — special
1718/// parameters route through their getfn callback (`uidgetfn` /
1719/// `randomgetfn` / `usernamegetfn` / etc.). Same role as
1720/// C's `Param.gsu->getfn` virtual dispatch.
1721/// 2. **Local variable** — `variables[name]`. C reads `pm->u.str`
1722/// for PM_SCALAR; here we hold the scalar in the variables
1723/// HashMap.
1724/// 3. **Environment fallback** — `std::env::var(name)`. C imports
1725/// env vars into the param table at startup so they go through
1726/// the same dispatch as everything else; zshrs reads from the
1727/// OS env on miss to match.
1728/// 4. **Array → scalar** — `arrays[name].join(" ")`. Mirrors
1729/// C's PM_ARRAY case in getstrvalue (params.c:2358) which
1730/// joins via `sepjoin(ss, NULL, 1)`.
1731///
1732// Retrieve a scalar (string) parameter // c:3076
1733/// Returns `None` only if all four paths miss (parameter genuinely
1734/// unset).
1735pub fn getsparam(name: &str) -> Option<String> { // c:3076
1736 // 1. GSU dispatch — `Param.gsu->getfn(pm)` equivalent. Special
1737 // parameters (UID/RANDOM/USERNAME/...) live behind getfn
1738 // hooks that the table read below would otherwise miss.
1739 if let Some(v) = lookup_special_var(name) {
1740 return Some(v);
1741 }
1742 // 2. Paramtab read — `(Value)gethashnode2(paramtab, name)`.
1743 // Walk the global paramtab for the named param, returning
1744 // `pm->u.str` for PM_SCALAR/PM_NAMEREF or `sepjoin(pm->u.arr)`
1745 // for PM_ARRAY (matches `getstrvalue` at params.c:2358).
1746 if let Ok(tab) = paramtab().read() {
1747 if let Some(pm) = tab.get(name) {
1748 if let Some(s) = pm.u_str.as_ref() {
1749 return Some(s.clone());
1750 }
1751 if let Some(arr) = pm.u_arr.as_ref() {
1752 return Some(arr.join(" "));
1753 }
1754 }
1755 }
1756 // 3. Env fallback — C imports env into paramtab at init so the
1757 // read above would hit. If the import hasn't happened yet
1758 // (e.g. during very early init) fall back to the live env.
1759 std::env::var(name).ok()
1760}
1761
1762/// Retrieve integer parameter.
1763/// Port of `getiparam(char *s)` from Src/params.c:3044. C: getvalue +
1764/// getintvalue. Our adaptation reads the scalar string and parses;
1765/// returns 0 on missing or unparseable, matching getintvalue's
1766/// failure-returns-0 convention (params.c:2601).
1767pub fn getiparam(s: &str) -> i64 {
1768 // C also honours PM_INTEGER's `pm->u.val` payload directly when
1769 // the param is typed numeric; check paramtab first for that case.
1770 if let Ok(tab) = paramtab().read() {
1771 if let Some(pm) = tab.get(s) {
1772 if (pm.node.flags as u32 & crate::ported::zsh_h::PM_INTEGER) != 0
1773 {
1774 return pm.u_val;
1775 }
1776 }
1777 }
1778 getsparam(s).and_then(|s| s.parse::<i64>().ok()).unwrap_or(0)
1779}
1780
1781/// Retrieve numeric (int-or-float) parameter.
1782/// Port of `getnparam(char *s)` from Src/params.c:3058. C returns an
1783/// `mnumber` (tagged int/float union); our adaptation returns
1784/// `(i64, f64, bool)` where the bool is true for float. Unset
1785/// returns `(0, 0.0, false)`, matching the MN_INTEGER zero
1786/// fallback in the C source's not-found branch.
1787pub fn getnparam(s: &str) -> (i64, f64, bool) {
1788 if let Ok(tab) = paramtab().read() {
1789 if let Some(pm) = tab.get(s) {
1790 let fl = pm.node.flags as u32;
1791 if (fl & (crate::ported::zsh_h::PM_EFLOAT
1792 | crate::ported::zsh_h::PM_FFLOAT)) != 0
1793 {
1794 return (pm.u_dval as i64, pm.u_dval, true);
1795 }
1796 if (fl & crate::ported::zsh_h::PM_INTEGER) != 0 {
1797 return (pm.u_val, pm.u_val as f64, false);
1798 }
1799 }
1800 }
1801 let s = match getsparam(s) {
1802 Some(s) => s,
1803 None => return (0, 0.0, false),
1804 };
1805 if s.contains('.') || s.contains('e') || s.contains('E') {
1806 if let Ok(f) = s.parse::<f64>() {
1807 return (f as i64, f, true);
1808 }
1809 }
1810 if let Ok(i) = s.parse::<i64>() {
1811 return (i, i as f64, false);
1812 }
1813 (0, 0.0, false)
1814}
1815
1816/// Port of `resetparam(Param pm, int flags)` from `Src/params.c:3796`. C body:
1817/// ```c
1818/// char *s = pm->node.nam;
1819/// queue_signals();
1820/// if (pm != (Param)(paramtab == realparamtab ?
1821/// paramtab->getnode2(paramtab, s) :
1822/// paramtab->getnode(paramtab, s))) {
1823/// unqueue_signals();
1824/// zerr("can't change type of hidden variable: %s", s);
1825/// return 1;
1826/// }
1827/// s = dupstring(s);
1828/// unsetparam_pm(pm, 0, 1);
1829/// unqueue_signals();
1830/// createparam(s, flags);
1831/// return 0;
1832/// ```
1833/// Tears `pm` down + recreates it with `flags` so the next
1834/// assignment lands in a fresh slot of the requested type. Used
1835/// by `assignsparam` when the type-flag of an existing param
1836/// changes (e.g. `typeset -i x; x="abc"` resets x back to scalar).
1837///
1838/// The `paramtab->getnode` reachability check at c:3800 catches
1839/// the hidden-shadow case (a local var hiding the global `pm` we
1840/// were handed) — without the paramtab vtable we skip the check
1841/// and proceed to unset+create.
1842pub fn resetparam(pm: &mut crate::ported::zsh_h::param, flags: i32) -> i32 { // c:3796
1843 let s = pm.node.nam.clone(); // c:3796
1844 crate::ported::signals::queue_signals(); // c:3799
1845 // c:3800-3807 — paramtab->getnode2 / getnode reachability check.
1846 // Without paramtab vtable wired we cannot detect the hidden-
1847 // variable case, so we proceed; a future port of paramtab
1848 // adds the check at this site.
1849 unsetparam_pm(pm, 0, 1); // c:3819
1850 crate::ported::signals::unqueue_signals(); // c:3819
1851 let _ = createparam(&s, flags); // c:3819
1852 0 // c:3819
1853}
1854
1855/// Unset a parameter from all storage.
1856/// Port of `unsetparam(char *s)` from Src/params.c:3819. C uses a single
1857/// HashTable; our SubstState-style storage spans variables /
1858/// arrays / assoc_arrays, so removal must touch all three to be
1859/// thorough (matches `unsetparam_pm`'s flag-aware tear-down).
1860/// WARNING: param names don't match C — Rust=(arrays, assoc_arrays, name) vs C=(s)
1861pub fn unsetparam( // c:3819
1862 variables: &mut std::collections::HashMap<String, String>,
1863 arrays: &mut std::collections::HashMap<String, Vec<String>>,
1864 assoc_arrays: &mut std::collections::HashMap<String, indexmap::IndexMap<String, String>>,
1865 name: &str,
1866) {
1867 variables.remove(name);
1868 arrays.remove(name);
1869 assoc_arrays.remove(name);
1870}
1871
1872/// Port of `export_param(Param pm)` from `Src/params.c:2653`. C body
1873/// converts `pm`'s value to its scalar form per `PM_TYPE`
1874/// (`convbase`/`convfloat`/`gsu.s->getfn`) then calls
1875/// `addenv(pm, val)`. PM_ARRAY/PM_HASHED early-returns (export
1876/// not supported for them outside KSH emulation).
1877pub fn export_param(pm: &mut crate::ported::zsh_h::param) {
1878 let t = PM_TYPE(pm.node.flags as u32);
1879 if (t & (PM_ARRAY | PM_HASHED)) != 0 {
1880 return;
1881 }
1882 let val: String = if t == PM_INTEGER {
1883 // convbase(buf, pm->gsu.i->getfn(pm), pm->base)
1884 format!("{}", intgetfn(pm))
1885 } else if (pm.node.flags as u32 & (PM_EFLOAT | PM_FFLOAT)) != 0 {
1886 // convfloat(pm->gsu.f->getfn(pm), pm->base, pm->node.flags, NULL)
1887 format!("{}", floatgetfn(pm))
1888 } else {
1889 strgetfn(pm)
1890 };
1891 addenv(&pm.node.nam, &val);
1892 pm.env = Some(val);
1893}
1894
1895/// Start a parameter scope.
1896/// Port of `startparamscope()` (Src/init.c) — the C source pushes the
1897/// current scope counter so `local`-declared params disappear on function
1898/// exit. Rust port operates on the bucket-2 holder `paramtab` via a
1899/// `&mut crate::ported::zsh_h::HashTable` argument.
1900pub fn startparamscope(_table: &mut crate::ported::zsh_h::HashTable) {
1901 crate::ported::utils::inc_locallevel();
1902}
1903
1904/// Port of `endparamscope()` from `Src/params.c:5857`. C signature:
1905/// `mod_export void endparamscope(void)`. Decrements `locallevel`,
1906/// pops any pushed history stack, then iterates `paramtab` calling
1907/// `scanendscope` to restore/unset every param whose `level`
1908/// exceeds the new `locallevel`. Operates on the global `paramtab`
1909/// just like C — no parameter, no fake injection wrapper.
1910pub fn endparamscope() {
1911 queue_signals();
1912 crate::ported::utils::dec_locallevel(); // c:5861 locallevel--
1913 crate::ported::hist::saveandpophiststack(crate::ported::zsh_h::HFILE_USE_OPTIONS as i32);
1914 let ll = crate::ported::utils::locallevel();
1915 // c:5867 scanhashtable(paramtab, 0, 0, 0, scanendscope, 0). Walk
1916 // the live paramtab (HashMap-backed until the hashtable.c vtable
1917 // is wired) and apply scanendscope's `pm->level > locallevel`
1918 // filter, restoring the `pm.old` chain or removing the entry.
1919 if let Ok(mut tab) = paramtab().write() {
1920 let stale: Vec<String> = tab.iter()
1921 .filter_map(|(k, pm)| if pm.level > ll { Some(k.clone()) } else { None })
1922 .collect();
1923 for n in stale {
1924 // c:scanendscope:5903 — non-special path: restore pm.old
1925 // (or remove if no outer binding existed).
1926 if let Some(pm) = tab.remove(&n) {
1927 if let Some(prev) = pm.old { // c:scanendscope:5933 pm->old = tpm->old
1928 tab.insert(n, prev); // restore outer binding (Box<param>)
1929 }
1930 // else: c:5966 unsetparam_pm — name unset entirely
1931 }
1932 }
1933 }
1934 unqueue_signals();
1935}
1936
1937
1938// ---------------------------------------------------------------------------
1939// Utility functions
1940// ---------------------------------------------------------------------------
1941
1942/// Check if string is valid identifier (from params.c isident)
1943// Return 1 if the string s is a valid identifier, else return 0. // c:1288
1944pub fn isident(s: &str) -> bool { // c:1288
1945 if s.is_empty() {
1946 return false;
1947 }
1948 let mut chars = s.chars().peekable();
1949
1950 // Handle namespace prefix (e.g. "ns.var")
1951 if chars.peek() == Some(&'.') {
1952 chars.next();
1953 if chars.peek().is_none_or(|c| c.is_ascii_digit()) {
1954 return false;
1955 }
1956 }
1957
1958 let first = match chars.next() {
1959 Some(c) => c,
1960 None => return false,
1961 };
1962
1963 if first.is_ascii_digit() {
1964 // All-digit names are valid (positional params)
1965 return chars.all(|c| c.is_ascii_digit());
1966 }
1967
1968 if !first.is_alphabetic() && first != '_' {
1969 return false;
1970 }
1971
1972 for c in chars {
1973 if c == '[' {
1974 // Subscript is OK at end
1975 return true;
1976 }
1977 if !c.is_alphanumeric() && c != '_' && c != '.' {
1978 return false;
1979 }
1980 }
1981 true
1982}
1983
1984/// Port of `valid_refname(char *val, int flags)` from `Src/params.c:6466`. C body
1985/// validates a nameref target name. Two paths:
1986/// - PM_UPPER (`typeset -nu`): reject digit-leader (positional
1987/// refs would loop) and the literal `argv`/`ARGC` names.
1988/// - non-PM_UPPER: positional digit-leader is permitted (must be
1989/// all-digits before any `[`); otherwise scan via
1990/// `itype_end(INAMESPC)`.
1991/// Either path then accepts the trailing one-char specials
1992/// `! ? $ - _` and an optional `[subscript]` tail. Returns 1 on
1993/// valid, 0 otherwise. The Rust port follows the same control
1994/// flow with `is_ascii_digit`/`is_alphabetic` standing in for
1995/// `idigit`/`itype_end`.
1996pub fn valid_refname(val: &str, flags: i32) -> bool { // c:6466
1997 if val.is_empty() {
1998 return false;
1999 }
2000 let first = val.chars().next().unwrap();
2001 let pm_upper = (flags as u32 & PM_UPPER) != 0;
2002 let mut t: usize;
2003 if pm_upper { // c:6470
2004 if first.is_ascii_digit() { // c:6472
2005 return false; // c:6473
2006 }
2007 // c:6474 — `t = itype_end(val, INAMESPC, 0)`; INAMESPC stops
2008 // at `.` and other non-namespace chars. Approximate with
2009 // alphanumeric/_ scan.
2010 t = val
2011 .char_indices()
2012 .find(|(_, c)| !(c.is_alphanumeric() || *c == '_'))
2013 .map(|(i, _)| i)
2014 .unwrap_or(val.len());
2015 if t - 0 == 4 // c:6475
2016 && (val.starts_with("argv") || val.starts_with("ARGC")) // c:6476-6477
2017 {
2018 return false; // c:6478
2019 }
2020 } else if first.is_ascii_digit() { // c:6479
2021 // c:6480-6485 — all-digit run; first non-digit must be `[`.
2022 t = 1;
2023 for (i, c) in val.char_indices().skip(1) {
2024 if !c.is_ascii_digit() {
2025 t = i;
2026 break;
2027 }
2028 t = i + c.len_utf8();
2029 }
2030 if t < val.len() && val.as_bytes()[t] != b'[' { // c:6484
2031 return false; // c:6485
2032 }
2033 } else {
2034 // c:6487 — `t = itype_end(val, INAMESPC, 0)`.
2035 t = val
2036 .char_indices()
2037 .find(|(_, c)| !(c.is_alphanumeric() || *c == '_' || *c == '.'))
2038 .map(|(i, _)| i)
2039 .unwrap_or(val.len());
2040 }
2041
2042 if t == 0 { // c:6489
2043 let c = val.as_bytes()[0];
2044 if !(c == b'!' || c == b'?' || c == b'$' || c == b'-' || c == b'_') { // c:6490
2045 return false; // c:6493
2046 }
2047 t = 1; // c:6494
2048 }
2049 if t < val.len() && val.as_bytes()[t] == b'[' { // c:6496
2050 // c:6498-6504 — parse_subscript/Inbrack/Outbrack walk. The
2051 // tokenize+parse_subscript pair isn't ported; accept any
2052 // balanced `[…]` tail (single-level) to remain conservative.
2053 let tail = &val[t + 1..];
2054 if let Some(close) = tail.find(']') {
2055 // c:6505-6508 — anything past `]` is rejected.
2056 if close + 1 < tail.len() {
2057 return false;
2058 }
2059 } else {
2060 return false;
2061 }
2062 }
2063 true // c:6510
2064}
2065
2066/// Colon-separated path to array.
2067/// Port of `colonsplit(char *s, int uniq)` from Src/params.c.
2068pub fn colonsplit(s: &str) -> Vec<String> {
2069 s.split(':')
2070 .filter(|s| !s.is_empty())
2071 .map(String::from)
2072 .collect()
2073}
2074
2075/// Array to colon-separated path — inverse of `colonsplit`.
2076/// Port of `colonarrgetfn(Param pm)` from Src/params.c (joins the array
2077/// stored in `pm->u.colon` back into the `:`-form for env).
2078/// WARNING: param names don't match C — Rust=(arr) vs C=(pm)
2079pub fn colonarrgetfn(arr: &[String]) -> String {
2080 arr.join(":")
2081}
2082
2083/// Remove duplicate elements from array while preserving order.
2084/// Port of `uniqarray(char **x)` from Src/params.c.
2085/// WARNING: param names don't match C — Rust=(arr) vs C=(x)
2086pub fn uniqarray(arr: Vec<String>) -> Vec<String> {
2087 let mut seen = HashSet::new();
2088 arr.into_iter().filter(|s| seen.insert(s.clone())).collect()
2089}
2090
2091
2092/// Slice an indexed array using zsh 1-based inclusive semantics.
2093/// Port of `getarrvalue(Value v)` from Src/params.c:2548 — the slice
2094/// branch that resolves the start/end pair into a Vec. Negative
2095/// indices count from the end (`-1` is the last element);
2096/// out-of-range bounds collapse to empty (`${a[5,10]}` on len=3
2097/// returns empty, not clamped); `start > end` returns empty.
2098///
2099/// 0 has asymmetric meaning per C source's getarrvalue:
2100/// start=0 → "before first element" → resolved to 1
2101/// end=0 → "before first element" → empty slice
2102/// WARNING: param names don't match C — Rust=(arr, start, end) vs C=(v)
2103pub fn getarrvalue(arr: &[String], start: i64, end: i64) -> Vec<String> {
2104 let len = arr.len() as i64;
2105 if len == 0 {
2106 return Vec::new();
2107 }
2108 // Out-of-range starts (positive past len, or negative below
2109 // -len) collapse to empty per Src/params.c getarrvalue's
2110 // slice-resolution branches.
2111 if start > len {
2112 return Vec::new();
2113 }
2114 if end < 0 && (len + end + 1) < 1 {
2115 return Vec::new();
2116 }
2117 if start < 0 && end < 0 && start > end {
2118 return Vec::new();
2119 }
2120 if start < 0 && start < -len {
2121 return Vec::new();
2122 }
2123 let resolve_start = |i: i64| -> i64 {
2124 if i < 0 {
2125 (len + i + 1).max(1)
2126 } else if i == 0 {
2127 1
2128 } else {
2129 i.min(len)
2130 }
2131 };
2132 let resolve_end = |i: i64| -> i64 {
2133 if i < 0 {
2134 (len + i + 1).max(0)
2135 } else if i == 0 {
2136 0
2137 } else {
2138 i.min(len)
2139 }
2140 };
2141 let s = resolve_start(start);
2142 let e = resolve_end(end);
2143 if e < 1 || s > e {
2144 return Vec::new();
2145 }
2146 let s_idx = (s - 1) as usize;
2147 let e_idx = e as usize;
2148 arr[s_idx..e_idx.min(arr.len())].to_vec()
2149}
2150
2151/// Direct port of `void setarrvalue(Value v, char **val)` from
2152/// `Src/params.c:2895-3037`. Sets an array (or assoc-array via
2153/// arrhashsetfn) into the param identified by v.pm, honouring
2154/// PM_READONLY / type-guards / VALFLAG_EMPTY rejections and the
2155/// slice-bounds adjust for `[N,M]` subscripts.
2156///
2157/// C dispatch:
2158/// - !EXECOPT → silent return (c:2897-2898)
2159/// - PM_READONLY → zerr + return (c:2899-2904)
2160/// - !PM_ARRAY && !PM_HASHED → zerr (c:2905-2911)
2161/// - VALFLAG_EMPTY → zerr (c:2913-2917)
2162/// - start==0,end==-1 && PM_HASHED → arrhashsetfn(0) (c:2919-2922)
2163/// - start==0,end==-1 && PM_ARRAY → gsu.a->setfn (c:2922-2923)
2164/// - start==-1,end==0 && PM_HASHED → arrhashsetfn(AUGMENT) (c:2925-2928)
2165/// - PM_HASHED with other bounds → zerr slice-of-assoc (c:2929-2932)
2166/// - PM_ARRAY with slice → bounds adjust + splice (c:2933+)
2167///
2168/// Pending: ASSPM_AUGMENT prepend (c:2945-2954), PM_UNIQUE dedupe
2169/// after assign (c:2966-2967), VALFLAG_INV + !KSHARRAYS off-by-one
2170/// (c:2938-2942).
2171pub fn setarrvalue(v: &mut crate::ported::zsh_h::value, val: Vec<String>) { // c:2895
2172
2173 let pm = match v.pm.as_mut() { Some(p) => p, None => return };
2174
2175 // c:2899-2904 — PM_READONLY rejection.
2176 if pm.node.flags & PM_READONLY as i32 != 0 {
2177 crate::ported::utils::zerr(&format!("read-only variable: {}", pm.node.nam));
2178 return;
2179 }
2180 // c:2905-2911 — type guard.
2181 let t = PM_TYPE(pm.node.flags as u32);
2182 if t & (crate::ported::zsh_h::PM_ARRAY | PM_HASHED) == 0 {
2183 crate::ported::utils::zerr(&format!(
2184 "{}: attempt to assign array value to non-array",
2185 pm.node.nam
2186 ));
2187 return;
2188 }
2189 // c:2913-2917 — VALFLAG_EMPTY rejection.
2190 if v.valflags & VALFLAG_EMPTY != 0 {
2191 crate::ported::utils::zerr(&format!(
2192 "{}: assignment to invalid subscript range",
2193 pm.node.nam
2194 ));
2195 return;
2196 }
2197
2198 // c:2919-2932 — full-replace / AUGMENT / hash-slice-reject paths.
2199 if v.start == 0 && v.end == -1 {
2200 if t == PM_HASHED {
2201 // c:2920 — arrhashsetfn(pm, val, 0).
2202 arrhashsetfn(pm, val, 0);
2203 } else {
2204 // c:2922 — `pm->gsu.a->setfn(pm, val)`. Route through
2205 // arrsetfn so PM_UNIQUE dedupe + arrfixenv side-effects
2206 // fire (params.c:4066-4076).
2207 arrsetfn(pm, val);
2208 }
2209 return;
2210 }
2211 if v.start == -1 && v.end == 0 && t == PM_HASHED {
2212 arrhashsetfn(pm, val, crate::ported::zsh_h::ASSPM_AUGMENT);
2213 return;
2214 }
2215 if t == PM_HASHED {
2216 crate::ported::utils::zerr(&format!(
2217 "{}: attempt to set slice of associative array",
2218 pm.node.nam
2219 ));
2220 return;
2221 }
2222
2223 // c:2938-2942 — VALFLAG_INV + !KSHARRAYS off-by-one. Inverse
2224 // subscripts (`a[(i)pat]=val`) are 1-based when KSHARRAYS is
2225 // off; shift start/end down by 1 to match the 0-based slice
2226 // arithmetic below.
2227 if v.valflags & VALFLAG_INV != 0
2228 && !isset(crate::ported::zsh_h::KSHARRAYS)
2229 {
2230 if v.start > 0 {
2231 v.start -= 1;
2232 }
2233 v.end -= 1;
2234 }
2235
2236 // c:2933+ — PM_ARRAY slice path.
2237 let arr = pm.u_arr.get_or_insert_with(Vec::new);
2238 let len = arr.len() as i64;
2239 // c:2944-2949 — negative start: add pre_assignment_length; clamp to 0.
2240 let start = if v.start < 0 {
2241 (len + v.start as i64).max(0)
2242 } else {
2243 v.start as i64
2244 };
2245 // c:2950-2953 — negative end: add pre_assignment_length + 1; clamp to 0.
2246 let end = if v.end < 0 {
2247 (len + v.end as i64 + 1).max(0)
2248 } else {
2249 v.end as i64
2250 };
2251 // c:2960-2961 — `if (end < start) end = start`.
2252 let start_idx = (start.max(1) - 1) as usize;
2253 let end_idx = end.max(0) as usize;
2254
2255 // c:2980 — pad with empty strings up to start.
2256 while arr.len() < start_idx {
2257 arr.push(String::new());
2258 }
2259
2260 // c:2989-2998 — splice val into [start..end] range.
2261 let end_idx = end_idx.min(arr.len());
2262 if start_idx <= end_idx {
2263 arr.splice(start_idx..end_idx, val);
2264 } else {
2265 for (i, x) in val.into_iter().enumerate() {
2266 if start_idx + i < arr.len() {
2267 arr[start_idx + i] = x;
2268 } else {
2269 arr.push(x);
2270 }
2271 }
2272 }
2273}
2274
2275// ---------------------------------------------------------------------------
2276// Integer/Float conversion (from convbase/convfloat)
2277// ---------------------------------------------------------------------------
2278
2279/// Convert integer to string with base (from params.c convbase)
2280/// Port of `convbase(char *s, zlong v, int base)` from `Src/params.c:5632`.
2281/// WARNING: param names don't match C — Rust=(val, base) vs C=(s, v, base)
2282pub fn convbase(val: i64, base: u32) -> String {
2283 if base == 0 || base == 10 {
2284 return val.to_string();
2285 }
2286
2287 let negative = val < 0;
2288 let mut v = if negative { (-val) as u64 } else { val as u64 };
2289
2290 if v == 0 {
2291 return match base {
2292 16 => "0x0".to_string(),
2293 8 => "00".to_string(),
2294 _ => format!("{}#0", base),
2295 };
2296 }
2297
2298 let mut digits = Vec::new();
2299 while v > 0 {
2300 let dig = (v % base as u64) as u8;
2301 digits.push(if dig < 10 {
2302 b'0' + dig
2303 } else {
2304 b'A' + dig - 10
2305 });
2306 v /= base as u64;
2307 }
2308 digits.reverse();
2309
2310 let prefix = match base {
2311 16 => "0x",
2312 8 => "0",
2313 10 => "",
2314 _ => "",
2315 };
2316
2317 let base_prefix = if base != 10 && base != 16 && base != 8 {
2318 format!("{}#", base)
2319 } else {
2320 prefix.to_string()
2321 };
2322
2323 let sign = if negative { "-" } else { "" };
2324 format!(
2325 "{}{}{}",
2326 sign,
2327 base_prefix,
2328 String::from_utf8_lossy(&digits)
2329 )
2330}
2331
2332/// Convert integer to string with underscores for readability
2333/// Port of `convbase_underscore(char *s, zlong v, int base, int underscore)` from `Src/params.c:5646`.
2334/// WARNING: param names don't match C — Rust=(val, base, underscore) vs C=(s, v, base, underscore)
2335pub fn convbase_underscore(val: i64, base: u32, underscore: i32) -> String {
2336 let s = convbase(val, base);
2337 if underscore <= 0 {
2338 return s;
2339 }
2340
2341 // Find the digits portion
2342 let (prefix, digits) = if let Some(rest) = s.strip_prefix('-') {
2343 let digit_start = rest
2344 .find(|c: char| c.is_ascii_digit() || c.is_ascii_uppercase())
2345 .unwrap_or(0);
2346 (&s[..1 + digit_start], &rest[digit_start..])
2347 } else {
2348 let digit_start = s
2349 .find(|c: char| c.is_ascii_digit() || c.is_ascii_uppercase())
2350 .unwrap_or(0);
2351 (&s[..digit_start], &s[digit_start..])
2352 };
2353
2354 if digits.len() <= underscore as usize {
2355 return s;
2356 }
2357
2358 let u = underscore as usize;
2359 let mut result = prefix.to_string();
2360 let chars: Vec<char> = digits.chars().collect();
2361 let first_group = chars.len() % u;
2362 if first_group > 0 {
2363 result.extend(&chars[..first_group]);
2364 if first_group < chars.len() {
2365 result.push('_');
2366 }
2367 }
2368 for (i, chunk) in chars[first_group..].chunks(u).enumerate() {
2369 if i > 0 {
2370 result.push('_');
2371 }
2372 result.extend(chunk);
2373 }
2374 result
2375}
2376
2377/// Port of `convfloat(double dval, int digits, int flags, FILE *fout)` from `Src/params.c:5689`.
2378///
2379/// C signature: `char *convfloat(double dval, int digits, int flags,
2380/// FILE *fout)` — picks `%e` / `%f` / `%g` based on PM_EFLOAT /
2381/// PM_FFLOAT (line 5705-5727), then snprintf'd with `digits` precision.
2382/// When neither E nor F flag is set, zsh uses `%.*g` with a default
2383/// of 17 significant digits (line 5712-5714). E-flag with N significant
2384/// figures decrements `digits` because `%e` counts decimal places not
2385/// significants (line 5720-5725).
2386///
2387/// Rust signature drops the `fout` parameter — every caller wanted the
2388/// returned string. IEEE specials (inf/nan) hand-formatted to `Inf`/
2389/// `-Inf`/`NaN` ahead of the snprintf, matching the C source's Inf/NaN
2390/// shortcuts at lines 5733-5736 / 5742-5744. The trailing-dot rule for
2391/// integer-valued floats (`5` -> `5.`) is added by the caller (params'
2392/// internal printing path) in C zsh; mirrored here for the no-flag case
2393/// so `MathNum::(crate::ported::math::mn_format_subst(Float(5.0)))` produces `5.` not `5`.
2394/// WARNING: param names don't match C — Rust=(dval, digits, pm_flags) vs C=(dval, digits, flags, fout)
2395pub fn convfloat(dval: f64, digits: i32, pm_flags: u32) -> String {
2396 if dval.is_infinite() { // c:5742
2397 return if dval < 0.0 {
2398 "-Inf".to_string()
2399 } else {
2400 "Inf".to_string()
2401 };
2402 }
2403 if dval.is_nan() { // c:5744
2404 return "NaN".to_string();
2405 }
2406 // Pick fmt char + adjust digits per the C cascade at 5705-5727.
2407 let (fmt_char, digits) = if (pm_flags & crate::ported::zsh_h::PM_EFLOAT) != 0 { // c:5715
2408 let d = if digits <= 0 { 10 } else { digits }; // c:5718
2409 ('e', (d - 1).max(0)) // c:5725
2410 } else if (pm_flags & crate::ported::zsh_h::PM_FFLOAT) != 0 { // c:5716
2411 let d = if digits <= 0 { 10 } else { digits }; // c:5718
2412 ('f', d)
2413 } else {
2414 let d = if digits == 0 { 17 } else { digits }; // c:5713
2415 ('g', d)
2416 };
2417 // Mirror zsh's snprintf path (Src/params.c:5751) — the C source
2418 // uses `VARARR(char, buf, 512 + digits)` for %f's full integer-
2419 // part expansion. 512 + 17 = 529 covers the zsh general case;
2420 // wider buffers below for the unbounded %f.
2421 let buf_len = 512usize + digits as usize + 4;
2422 let mut buf = vec![0u8; buf_len];
2423 let fmt = match fmt_char {
2424 'e' => c"%.*e",
2425 'f' => c"%.*f",
2426 _ => c"%.*g",
2427 };
2428 // SAFETY: buf has the C-required size for any double precision; fmt
2429 // is a NUL-terminated literal; snprintf writes ASCII only.
2430 let n = unsafe {
2431 libc::snprintf(
2432 buf.as_mut_ptr() as *mut libc::c_char,
2433 buf_len,
2434 fmt.as_ptr(),
2435 digits as libc::c_int,
2436 dval,
2437 )
2438 };
2439 if n < 0 {
2440 return format!("{}", dval);
2441 }
2442 let len = (n as usize).min(buf_len - 1);
2443 buf.truncate(len);
2444 let mut s = String::from_utf8(buf).unwrap_or_else(|_| format!("{}", dval));
2445 // zsh's general-format (%g) callers (math `$(( ))` substitution)
2446 // append `.` when the output has no `e` and no `.`, so integer-
2447 // valued floats like `5` render as `5.`. PM_EFLOAT/PM_FFLOAT skip
2448 // this rule (the format spec already pins shape).
2449 if fmt_char == 'g' && !s.contains('e') && !s.contains('.') {
2450 s.push('.');
2451 }
2452 s
2453}
2454
2455/// Format float with underscores
2456pub fn convfloat_underscore(dval: f64, underscore: i32) -> String {
2457 let s = convfloat(dval, 0, 0);
2458 if underscore <= 0 {
2459 return s;
2460 }
2461
2462 let u = underscore as usize;
2463 let (sign, rest) = if let Some(after) = s.strip_prefix('-') {
2464 ("-", after)
2465 } else {
2466 ("", s.as_str())
2467 };
2468
2469 let (int_part, frac_exp) = if let Some(dot_pos) = rest.find('.') {
2470 (&rest[..dot_pos], &rest[dot_pos..])
2471 } else {
2472 (rest, "")
2473 };
2474
2475 // Add underscores to integer part
2476 let int_chars: Vec<char> = int_part.chars().collect();
2477 let mut result = sign.to_string();
2478 let first_group = int_chars.len() % u;
2479 if first_group > 0 {
2480 result.extend(&int_chars[..first_group]);
2481 if first_group < int_chars.len() {
2482 result.push('_');
2483 }
2484 }
2485 for (i, chunk) in int_chars[first_group..].chunks(u).enumerate() {
2486 if i > 0 {
2487 result.push('_');
2488 }
2489 result.extend(chunk);
2490 }
2491
2492 // Add underscores to fractional part
2493 if let Some(frac) = frac_exp.strip_prefix('.') {
2494 result.push('.');
2495 let (frac_digits, exp) = if let Some(e_pos) = frac.find('e') {
2496 (&frac[..e_pos], &frac[e_pos..])
2497 } else {
2498 (frac, "")
2499 };
2500
2501 let frac_chars: Vec<char> = frac_digits.chars().collect();
2502 for (i, chunk) in frac_chars.chunks(u).enumerate() {
2503 if i > 0 {
2504 result.push('_');
2505 }
2506 result.extend(chunk);
2507 }
2508 result.push_str(exp);
2509 } else {
2510 result.push_str(frac_exp);
2511 }
2512
2513 result
2514}
2515
2516// intgetfn / strgetfn drift wrappers removed — replaced below with
2517// real C-shape ports `intgetfn(pm: ¶m) -> i64` (Src/params.c:3993)
2518// and `strgetfn(pm: ¶m) -> String` (Src/params.c:4029) that read
2519// directly from the union fields `pm->u.val` / `pm->u.str`.
2520
2521// ---------------------------------------------------------------------------
2522// Tests
2523// ---------------------------------------------------------------------------
2524
2525#[cfg(test)]
2526mod tests {
2527 use super::*;
2528
2529 // test_param_value_conversions removed: tested deleted fake
2530 // `ParamValue::Scalar` constructor. C uses union access on
2531 // `pm->u.str`/`u.val`/`u.dval`/`u.arr` dispatched via
2532 // `PM_TYPE(pm->node.flags)` (Src/zsh.h:540).
2533 #[test]
2534 fn test_colonarr_conversion() {
2535 let arr = colonsplit("/bin:/usr/bin:/usr/local/bin");
2536 assert_eq!(arr, vec!["/bin", "/usr/bin", "/usr/local/bin"]);
2537 let path = colonarrgetfn(&arr);
2538 assert_eq!(path, "/bin:/usr/bin:/usr/local/bin");
2539 }
2540 #[test]
2541 fn test_isident() {
2542 assert!(isident("foo"));
2543 assert!(isident("_bar"));
2544 assert!(isident("FOO_BAR"));
2545 assert!(isident("x123"));
2546 assert!(isident("123")); // positional params
2547 assert!(!isident(""));
2548 assert!(!isident("foo bar"));
2549 }
2550
2551
2552 #[test]
2553 fn test_unique_array() {
2554 let arr = vec!["a".into(), "b".into(), "a".into(), "c".into(), "b".into()];
2555 let result = uniqarray(arr);
2556 assert_eq!(result, vec!["a", "b", "c"]);
2557 }
2558
2559 #[test]
2560 fn test_convbase() {
2561 assert_eq!(convbase(255, 16), "0xFF");
2562 assert_eq!(convbase(10, 10), "10");
2563 assert_eq!(convbase(-5, 10), "-5");
2564 assert_eq!(convbase(7, 8), "07");
2565 assert_eq!(convbase(5, 2), "2#101");
2566 }
2567
2568 #[test]
2569 fn test_convfloat() {
2570 // Use 2.5 instead of 3.14 — clippy errors on the latter as
2571 // an approx PI constant. The test checks 2-decimal formatting
2572 // round-trips, which the exact value doesn't influence.
2573 let s = convfloat(2.5, 2, crate::ported::zsh_h::PM_FFLOAT);
2574 assert!(s.starts_with("2.50"));
2575
2576 assert_eq!(convfloat(f64::INFINITY, 0, 0), "Inf");
2577 assert_eq!(convfloat(f64::NEG_INFINITY, 0, 0), "-Inf");
2578 assert_eq!(convfloat(f64::NAN, 0, 0), "NaN");
2579 }
2580
2581
2582
2583
2584 #[test]
2585 fn test_getarrvalue() {
2586 let arr = vec!["a".into(), "b".into(), "c".into(), "d".into()];
2587 assert_eq!(getarrvalue(&arr, 2, 3), vec!["b", "c"]);
2588 assert_eq!(getarrvalue(&arr, -2, -1), vec!["c", "d"]);
2589 assert_eq!(getarrvalue(&arr, 1, 4), vec!["a", "b", "c", "d"]);
2590 }
2591
2592 #[test]
2593 fn test_setarrvalue() {
2594 // C-faithful: setarrvalue takes a Value pointing at a Param
2595 // with u_arr set. Construct one inline.
2596 use crate::ported::zsh_h::{hashnode, param, PM_ARRAY};
2597 let pm = Box::new(param {
2598 node: hashnode { next: None, nam: "test".to_string(), flags: PM_ARRAY as i32 },
2599 u_data: 0,
2600 u_arr: Some(vec!["a".into(), "b".into(), "c".into(), "d".into()]),
2601 u_str: None, u_val: 0, u_dval: 0.0, u_hash: None,
2602 gsu_s: None, gsu_i: None, gsu_f: None, gsu_a: None, gsu_h: None,
2603 base: 0, width: 0, env: None, ename: None, old: None, level: 0,
2604 });
2605 let mut v = crate::ported::zsh_h::value {
2606 pm: Some(pm),
2607 arr: Vec::new(),
2608 scanflags: 0,
2609 valflags: 0,
2610 start: 2,
2611 end: 3,
2612 };
2613 setarrvalue(&mut v, vec!["X".into(), "Y".into()]);
2614 let arr = v.pm.unwrap().u_arr.unwrap();
2615 assert_eq!(arr, vec!["a", "X", "Y", "d"]);
2616 }
2617
2618 #[test]
2619 fn test_valid_refname() {
2620 assert!(valid_refname("foo", 0));
2621 assert!(valid_refname("_bar", 0));
2622 assert!(valid_refname("1", 0));
2623 assert!(valid_refname("!", 0));
2624 assert!(valid_refname("arr[1]", 0));
2625 assert!(!valid_refname("", 0));
2626 // C semantics: empty leader without one of `! ? $ - _` is rejected.
2627 assert!(!valid_refname(" ", 0));
2628 // PM_UPPER rejects digit-leader and argv/ARGC.
2629 assert!(!valid_refname("1", PM_UPPER as i32));
2630 assert!(!valid_refname("argv", PM_UPPER as i32));
2631 assert!(!valid_refname("ARGC", PM_UPPER as i32));
2632 }
2633
2634 #[test]
2635 fn test_uniq_array_empty() {
2636 let empty: Vec<String> = Vec::new();
2637 assert!(uniqarray(empty).is_empty());
2638 }
2639
2640 #[test]
2641 fn test_convbase_underscore() {
2642 let s = convbase_underscore(1234567, 10, 3);
2643 assert_eq!(s, "1_234_567");
2644 }
2645
2646 fn val_str(v: getarg_out<'_>) -> String {
2647 match v {
2648 getarg_out::Value(v) => v.to_str(),
2649 getarg_out::Flags { .. } => panic!("expected Value, got Flags"),
2650 }
2651 }
2652
2653 #[test]
2654 fn getarg_n_flag_picks_second_exact_match() {
2655 // C params.c:1431-1442 + 1758 — `(en.2.)pat` picks 2nd exact match.
2656 let arr: Vec<String> = vec!["foo".into(), "bar".into(), "foo".into(), "baz".into()];
2657 let out = getarg("(en.2.r)foo", Some(&arr), None, None).expect("Some");
2658 assert_eq!(val_str(out), "foo");
2659 }
2660
2661 #[test]
2662 fn getarg_n_flag_third_exact_match() {
2663 let arr: Vec<String> = vec!["a".into(), "a".into(), "a".into(), "b".into()];
2664 let out = getarg("(en.3.r)a", Some(&arr), None, None).expect("Some");
2665 assert_eq!(val_str(out), "a");
2666 }
2667
2668 #[test]
2669 fn getarg_n_flag_returns_index_with_i() {
2670 // (en.2.i) — return INDEX of 2nd exact match.
2671 let arr: Vec<String> = vec!["x".into(), "y".into(), "x".into(), "y".into()];
2672 let out = getarg("(en.2.i)x", Some(&arr), None, None).expect("Some");
2673 assert_eq!(val_str(out), "3");
2674 }
2675
2676 #[test]
2677 fn getarg_negative_n_flips_search_direction() {
2678 // C params.c:1488-1491 — negative `num` flips down (reverse).
2679 // (en.-1.) on forward-default search matches from the end.
2680 let arr: Vec<String> = vec!["a".into(), "a".into(), "a".into()];
2681 let out = getarg("(en.-1.i)a", Some(&arr), None, None).expect("Some");
2682 assert_eq!(val_str(out), "3");
2683 }
2684
2685 #[test]
2686 fn getarg_n_flag_zero_treated_as_one() {
2687 // C params.c:1438-1439 — `if (!num) num = 1`.
2688 let arr: Vec<String> = vec!["x".into(), "y".into()];
2689 let out = getarg("(en.0.r)x", Some(&arr), None, None).expect("Some");
2690 assert_eq!(val_str(out), "x");
2691 }
2692
2693 #[test]
2694 fn getarg_unknown_flag_char_returns_none() {
2695 // C params.c:1477-1483 flagerr — invalid flag char reports error.
2696 let arr: Vec<String> = vec!["x".into()];
2697 assert!(getarg("(z)x", Some(&arr), None, None).is_none());
2698 }
2699
2700 #[test]
2701 fn getarg_n_flag_unterminated_arg_returns_none() {
2702 // (n.5 missing closing delimiter — flagerr.
2703 let arr: Vec<String> = vec!["x".into()];
2704 assert!(getarg("(n.5", Some(&arr), None, None).is_none());
2705 }
2706
2707 #[test]
2708 fn getarg_b_flag_starts_search_at_index() {
2709 // C params.c:1748-1760 — `(b.N.e)pat` skips first N-1 elements
2710 // forward (parsed value `N`, normalized to `beg = N-1`).
2711 let arr: Vec<String> = vec!["x".into(), "y".into(), "x".into(), "y".into()];
2712 // Forward, beg=2 (skip first 2) → starts at idx 2 → 'x' at 3.
2713 let out = getarg("(b.3.ei)x", Some(&arr), None, None).expect("Some");
2714 assert_eq!(val_str(out), "3");
2715 }
2716
2717 #[test]
2718 fn getarg_b_flag_with_R_reverse_from_offset() {
2719 // C params.c:1750-1755 — reverse search starting at parsed-1 idx.
2720 // arr=(x y x y), beg=2 (parsed 3-1), reverse → walks 2,1,0; first
2721 // exact 'x' is at idx 2 → 1-based "3".
2722 let arr: Vec<String> = vec!["x".into(), "y".into(), "x".into(), "y".into()];
2723 let out = getarg("(b.3.eIR)x", Some(&arr), None, None).expect("Some");
2724 assert_eq!(val_str(out), "3");
2725 }
2726
2727 #[test]
2728 fn getarg_b_flag_out_of_bounds_forward_returns_empty() {
2729 // c:1746 — beg >= len returns len+1 (empty for value-mode).
2730 let arr: Vec<String> = vec!["x".into()];
2731 let out = getarg("(b.5.er)x", Some(&arr), None, None).expect("Some");
2732 assert_eq!(val_str(out), "");
2733 }
2734
2735 #[test]
2736 fn getarg_b_flag_out_of_bounds_index_mode_returns_len_plus_one() {
2737 let arr: Vec<String> = vec!["x".into(), "y".into()];
2738 let out = getarg("(b.5.ei)x", Some(&arr), None, None).expect("Some");
2739 assert_eq!(val_str(out), "3");
2740 }
2741
2742 #[test]
2743 fn getarg_hash_neg_num_on_lowercase_r_returns_all() {
2744 // C params.c:1488-1491 — neg `num` flips down on `r`,
2745 // converting hash search to return-all-matches semantics.
2746 let mut h: indexmap::IndexMap<String, String> = indexmap::IndexMap::new();
2747 h.insert("a".into(), "1".into());
2748 h.insert("b".into(), "1".into());
2749 h.insert("c".into(), "2".into());
2750 let out = getarg("(en.-1.r)1", None, Some(&h), None).expect("Some");
2751 // r + neg = R semantics → all values where pat matches value.
2752 assert_eq!(val_str(out), "1 1");
2753 }
2754
2755 #[test]
2756 fn getarg_hash_neg_num_on_uppercase_R_returns_single() {
2757 // R + neg `num` un-flips back to single-match (r semantics).
2758 let mut h: indexmap::IndexMap<String, String> = indexmap::IndexMap::new();
2759 h.insert("a".into(), "1".into());
2760 h.insert("b".into(), "1".into());
2761 h.insert("c".into(), "2".into());
2762 let out = getarg("(en.-1.R)1", None, Some(&h), None).expect("Some");
2763 // R + neg → r → single first match.
2764 assert_eq!(val_str(out), "1");
2765 }
2766
2767 #[test]
2768 fn getarg_hash_b_flag_skips_first_n_entries() {
2769 // C params.c:1740-1742 — `b<NUM>` skips first N-1 entries
2770 // before searching. Hash iteration is insertion order.
2771 let mut h: indexmap::IndexMap<String, String> = indexmap::IndexMap::new();
2772 h.insert("a".into(), "1".into());
2773 h.insert("b".into(), "1".into());
2774 h.insert("c".into(), "1".into());
2775 // beg=2 (parsed 3-1) → skip first 2, scan from "c" onward.
2776 let out = getarg("(b.3.ei)1", None, Some(&h), None).expect("Some");
2777 assert_eq!(val_str(out), "c");
2778 }
2779
2780 #[test]
2781 fn getarg_hash_b_flag_with_R_collects_from_offset() {
2782 // R returns all matches; b skips first beg entries first.
2783 let mut h: indexmap::IndexMap<String, String> = indexmap::IndexMap::new();
2784 h.insert("a".into(), "1".into());
2785 h.insert("b".into(), "1".into());
2786 h.insert("c".into(), "1".into());
2787 let out = getarg("(b.2.eI)1", None, Some(&h), None).expect("Some");
2788 // beg=1, return_all=I → walk from "b" onward, all matching keys.
2789 assert_eq!(val_str(out), "b c");
2790 }
2791
2792 #[test]
2793 fn getarg_hash_b_flag_out_of_bounds_returns_empty() {
2794 // c:1746 — beg >= len with single-match → empty.
2795 let mut h: indexmap::IndexMap<String, String> = indexmap::IndexMap::new();
2796 h.insert("a".into(), "1".into());
2797 let out = getarg("(b.5.e)1", None, Some(&h), None).expect("Some");
2798 assert_eq!(val_str(out), "");
2799 }
2800
2801 #[test]
2802 fn getarg_w_flag_splits_multi_word_array_elements() {
2803 // C params.c:1761-1797 — `(w)N` joins array then re-splits by
2804 // IFS-default whitespace. arr=("a b" "c d"); (w)2 → "b" not "c d".
2805 let arr: Vec<String> = vec!["a b".into(), "c d".into()];
2806 let out = getarg("(w)2", Some(&arr), None, None).expect("Some");
2807 assert_eq!(val_str(out), "b");
2808 }
2809
2810 #[test]
2811 fn getarg_w_flag_simple_array_indexing_still_works() {
2812 let arr: Vec<String> = vec!["one".into(), "two".into(), "three".into()];
2813 let out = getarg("(w)2", Some(&arr), None, None).expect("Some");
2814 assert_eq!(val_str(out), "two");
2815 }
2816
2817 #[test]
2818 fn getarg_f_flag_splits_by_newline() {
2819 // C params.c:1424-1427 — `f` flag aliases `w` with sep="\n".
2820 // arr=("a b\nc d"); (f)2 → "c d" (split by \n only, not space).
2821 let arr: Vec<String> = vec!["a b\nc d".into()];
2822 let out = getarg("(f)2", Some(&arr), None, None).expect("Some");
2823 assert_eq!(val_str(out), "c d");
2824 }
2825
2826 #[test]
2827 fn getarg_scalar_w_flag_picks_nth_word() {
2828 // C params.c:1761-1797 — scalar word-mode arm. `(w)2` on
2829 // scalar "hello world foo" returns the 2nd whitespace word.
2830 let out = getarg("(w)2", None, None, Some("hello world foo")).expect("Some");
2831 assert_eq!(val_str(out), "world");
2832 }
2833
2834 #[test]
2835 fn getarg_scalar_w_flag_negative_index_counts_from_end() {
2836 let out = getarg("(w)-1", None, None, Some("alpha beta gamma")).expect("Some");
2837 assert_eq!(val_str(out), "gamma");
2838 }
2839
2840 #[test]
2841 fn getarg_scalar_re_returns_char_at_match_position() {
2842 // C params.c:1798-1980 — char-search returns CHAR at match
2843 // position, not full substring. Verified empirically:
2844 // /bin/zsh -c 's="barfooxyz"; print "${s[(r)foo]}"' → "f"
2845 let out = getarg("(re)bc", None, None, Some("abcdef")).expect("Some");
2846 assert_eq!(val_str(out), "b");
2847 }
2848
2849 #[test]
2850 fn getarg_scalar_ie_returns_position_of_first_match() {
2851 let out = getarg("(ie)cd", None, None, Some("abcdef")).expect("Some");
2852 // 'cd' starts at 1-based position 3.
2853 assert_eq!(val_str(out), "3");
2854 }
2855
2856 #[test]
2857 fn getarg_scalar_Ie_returns_position_of_last_match() {
2858 let out = getarg("(Ie)b", None, None, Some("abcabc")).expect("Some");
2859 // Last 'b' is at 1-based position 5.
2860 assert_eq!(val_str(out), "5");
2861 }
2862
2863 #[test]
2864 fn getarg_scalar_ie_no_match_returns_len_plus_one() {
2865 let out = getarg("(ie)z", None, None, Some("abc")).expect("Some");
2866 assert_eq!(val_str(out), "4");
2867 }
2868
2869 #[test]
2870 fn getarg_scalar_Ie_no_match_returns_zero() {
2871 let out = getarg("(Ie)z", None, None, Some("abc")).expect("Some");
2872 assert_eq!(val_str(out), "0");
2873 }
2874
2875 #[test]
2876 fn getarg_scalar_n_flag_picks_second_match() {
2877 // C params.c:1929/1964 — `!--num` Nth-match counter on
2878 // scalar char-search. abcabc: 'a' at idx 0 and 3 → 2nd match
2879 // at byte position 4 (1-based).
2880 let out = getarg("(en.2.i)a", None, None, Some("abcabc")).expect("Some");
2881 assert_eq!(val_str(out), "4");
2882 }
2883
2884 #[test]
2885 fn getarg_scalar_b_flag_starts_from_offset() {
2886 // C params.c:1740-1742 — `(b.N.)` starts search from idx N-1.
2887 // abc bc abc: with b=4, skip first 3 chars; first 'b' at byte 5.
2888 let out = getarg("(b.4.ei)b", None, None, Some("abcbc")).expect("Some");
2889 assert_eq!(val_str(out), "4");
2890 }
2891
2892 #[test]
2893 fn getarg_scalar_re_n2_picks_second_substring() {
2894 let out = getarg("(en.2.r)b", None, None, Some("abab")).expect("Some");
2895 assert_eq!(val_str(out), "b");
2896 }
2897}
2898
2899// ===========================================================
2900// Methods moved verbatim from src/ported/exec.rs because their
2901// C counterpart's source file maps 1:1 to this Rust module.
2902// Phase: params
2903// ===========================================================
2904
2905// BEGIN moved-from-exec-rs
2906// (impl ShellExecutor block moved to src/exec_shims.rs — see file marker)
2907
2908// END moved-from-exec-rs
2909
2910// ===========================================================
2911// Free fns moved verbatim from src/ported/exec.rs.
2912// ===========================================================
2913// BEGIN moved-from-exec-rs (free fns)
2914/// Subscript-argument result.
2915///
2916/// `Flags` carries the parsed flag chars and the remaining subscript
2917/// text (the pattern after `(...)`); the caller dispatches the
2918/// search itself. `Value` is the result of an in-getarg array/hash
2919/// pattern search — direct port of getarg's pprog/pattry arm at
2920/// Src/params.c:1672-1719 (array) and 1581-1660 (hash).
2921// `enum getarg_out` is a Rust extension to express the dual-mode
2922// return of `getarg`. C `getarg` (`Src/params.c:1367`) writes back
2923// via out-pointers (`int *inv`, `Value v`, `zlong *w`, ...) and
2924// returns `int`. The Rust port collapses those into one sum-typed
2925// return: `Flags` carries the parsed flag chars + remaining
2926// subscript when no search ran; `Value` carries the search result
2927// from the pprog/pattry arms at c:1581-1660 (hash) / c:1672-1719
2928// (array). Naming kept lowercase to mark this as a port-shape helper
2929// rather than a C-mirrored struct.
2930#[allow(non_camel_case_types)]
2931pub enum getarg_out<'a> {
2932 Flags { flags: &'a str, rest: &'a str },
2933 Value(fusevm::Value),
2934}
2935
2936/// Subscript-argument parser.
2937///
2938/// 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
2939/// 618-line monolith handling the entire `[...]` body of a
2940/// subscripted parameter expansion.
2941///
2942/// Ported phases:
2943/// - Flag-block parse (c:1389-1480) — extract `(...)` chars.
2944/// - Hash pattern search (c:1581-1660) when `assoc` is `Some`.
2945/// - Array pattern search (c:1672-1719) when `arr` is `Some`.
2946/// - Scalar word-mode arm (c:1761-1797) when `scalar` is `Some`.
2947///
2948/// Later C phases not yet exercised by this entry point:
2949/// - Brace-depth walk to closing `]` (c:1507-1535)
2950/// - parsestr + singsub on subscript body (c:1545-1580)
2951/// - mathevalarg integer parse (c:1601-1604)
2952/// - Multibyte char-search arm (c:1798-1985)
2953pub(crate) fn getarg<'a>(
2954 idx: &'a str,
2955 arr: Option<&[String]>,
2956 assoc: Option<&indexmap::IndexMap<String, String>>,
2957 scalar: Option<&str>,
2958) -> Option<getarg_out<'a>> {
2959 let rest = idx.strip_prefix('(')?;
2960 // Reject anything that looks like a char-class subscript: `[abc]`
2961 // doesn't match this prefix, but `(...)` containing brackets is
2962 // probably alternation — let it fall through to runtime instead.
2963 if rest.starts_with(')') || rest.contains('[') {
2964 return None;
2965 }
2966 // Flag scanner per zshparam(1) "Subscript Flags" /
2967 // params.c:1389-1480 switch:
2968 // r/R (reverse value-search → value/all values),
2969 // i/I (value-search → key/all keys),
2970 // k/K (key-search → value/all values),
2971 // e (exact match — disables glob),
2972 // n<DELIM>NUM<DELIM> (Nth match — params.c:1431-1442),
2973 // b<DELIM>NUM<DELIM> (begin offset — params.c:1443-1454),
2974 // w (word index on scalar),
2975 // f (word index split by newline; alias for `w` + sep="\n"),
2976 // p (escapes for next get_strarg),
2977 // s<DELIM>SEP<DELIM> (split-by-separator).
2978 // The `n` / `b` / `s` forms use `get_strarg`'s balanced-delimiter
2979 // pair: any non-flag char closes its pair (`(n.5.)`, `(n:5:)` etc.).
2980 let bytes = rest.as_bytes();
2981 let mut i: usize = 0;
2982 let mut num: i64 = 1;
2983 let mut beg: i64 = 0;
2984 let mut has_beg = false;
2985 let flags_start = 0_usize;
2986 let mut flags_end = 0_usize;
2987 let mut bad = false;
2988 while i < bytes.len() && bytes[i] != b')' {
2989 let c = bytes[i] as char;
2990 match c {
2991 'r' | 'R' | 'i' | 'I' | 'e' | 'k' | 'K' | 'w' | 'f' | 'p' => {
2992 i += 1;
2993 flags_end = i;
2994 }
2995 'n' | 'b' => {
2996 // Consume `n<DELIM>NUM<DELIM>` per c:1432 get_strarg.
2997 if i + 1 >= bytes.len() {
2998 bad = true;
2999 break;
3000 }
3001 let delim = bytes[i + 1];
3002 let arg_start = i + 2;
3003 let mut arg_end = arg_start;
3004 while arg_end < bytes.len() && bytes[arg_end] != delim {
3005 arg_end += 1;
3006 }
3007 if arg_end >= bytes.len() {
3008 bad = true;
3009 break;
3010 }
3011 // Parse the argument as a signed decimal integer.
3012 let arg = std::str::from_utf8(&bytes[arg_start..arg_end]).ok()?;
3013 let parsed: i64 = arg.trim().parse().ok()?;
3014 if c == 'n' {
3015 num = if parsed == 0 { 1 } else { parsed };
3016 } else {
3017 has_beg = true;
3018 beg = if parsed > 0 { parsed - 1 } else { parsed };
3019 }
3020 i = arg_end + 1;
3021 flags_end = i;
3022 }
3023 's' => {
3024 // (s:SEP:) — pass through with raw flag block.
3025 let close = match rest[i..].find(')') {
3026 Some(p) => i + p,
3027 None => return None,
3028 };
3029 let flags = &rest[flags_start..close];
3030 return Some(getarg_out::Flags { flags, rest: &rest[close + 1..] });
3031 }
3032 _ => {
3033 bad = true;
3034 break;
3035 }
3036 }
3037 }
3038 // c:1477-1483 — flag-error fallback: reset all flags, treat as no
3039 // subscript flags.
3040 if bad {
3041 return None;
3042 }
3043 if i >= bytes.len() || bytes[i] != b')' {
3044 return None;
3045 }
3046 if flags_end == flags_start {
3047 return None;
3048 }
3049 let flags = &rest[flags_start..flags_end];
3050 let pat = &rest[i + 1..];
3051
3052 // c:1488-1491 — negative `num` flips the search direction.
3053 let neg_num_flips = num < 0;
3054 if neg_num_flips {
3055 num = -num;
3056 }
3057
3058 // Phase 3 — hash pattern search arm (c:1581-1660 / 1672-1734).
3059 // Per C source case-arms:
3060 // `r`: rev=1 → match against VALUES, return matching VALUE
3061 // `R`: rev+down=1 → match VALUES, return ALL matching VALUEs
3062 // `i`: rev+ind=1 → match VALUES, return KEY of first match
3063 // `I`: rev+ind+down=1 → match VALUES, return ALL matching KEYs
3064 // `k`: keymatch+rev=1 → match KEYS, return VALUE of first match
3065 // `K`: keymatch+rev+down=1 → match KEYS, return ALL matching VALUEs
3066 if let Some(map) = assoc {
3067 let exact = flags.contains('e');
3068 let key_match = flags.contains('k') || flags.contains('K');
3069 let return_index = flags.contains('i') || flags.contains('I');
3070 // C params.c:1488-1491 — negative `num` flips `down`. Since
3071 // R/I/K already set down=1, neg_num XORs the bit (r/i/k +
3072 // neg → return_all; R/I/K + neg → single-match again).
3073 let is_uppercase = flags.contains('I') || flags.contains('R') || flags.contains('K');
3074 let return_all = is_uppercase ^ neg_num_flips;
3075
3076 // c:1740-1747 — `b<NUM>` start offset on the values array. The
3077 // hash is iterated in insertion order (IndexMap); skip first
3078 // `beg` entries before counting matches.
3079 let len = map.len() as i64;
3080 let mut start = beg;
3081 if start < 0 {
3082 start += len;
3083 }
3084 if !return_all && start >= len {
3085 return Some(getarg_out::Value(Value::str("")));
3086 }
3087 let skip = if start < 0 { 0 } else { start as usize };
3088
3089 // Per C params.c:1707-1709 + zsh 5.9 empirical:
3090 // k/K — keymatch path: pprog=NULL, no glob; exact key
3091 // lookup. `(K)*` returns "" because there's no key
3092 // literally named "*".
3093 // r/R/i/I — value path: pprog=patcompile, glob/exact.
3094 let key_compare = |target: &str| -> bool {
3095 if key_match {
3096 target == pat
3097 } else if exact {
3098 target == pat
3099 } else {
3100 crate::ported::pattern::patmatch(pat, target)
3101 }
3102 };
3103 if return_all {
3104 let mut out: Vec<String> = Vec::new();
3105 for (k, v) in map.iter().skip(skip) {
3106 let target = if key_match { k.as_str() } else { v.as_str() };
3107 if key_compare(target) {
3108 // `K` (key-match) returns VALUE; `I` (value-match+ind)
3109 // returns KEY; `R` (value-match) returns VALUE.
3110 out.push(if key_match {
3111 v.clone()
3112 } else if return_index {
3113 k.clone()
3114 } else {
3115 v.clone()
3116 });
3117 }
3118 }
3119 return Some(getarg_out::Value(Value::str(out.join(" "))));
3120 }
3121 // c:1753 — `!--num` skips matches until the Nth.
3122 let mut remaining = num;
3123 for (k, v) in map.iter().skip(skip) {
3124 let target = if key_match { k.as_str() } else { v.as_str() };
3125 if key_compare(target) {
3126 remaining -= 1;
3127 if remaining == 0 {
3128 return Some(getarg_out::Value(Value::str(if key_match {
3129 v.clone()
3130 } else if return_index {
3131 k.clone()
3132 } else {
3133 v.clone()
3134 })));
3135 }
3136 }
3137 }
3138 return Some(getarg_out::Value(Value::str("")));
3139 }
3140
3141 // Phase 2 — array pattern search arm (c:1672-1719). The C body
3142 // does `pprog = patcompile(s, 0, NULL)` then forward/reverse
3143 // `for (r = 1 + beg, p = ta + beg; *p; r++, p++) if (pprog &&
3144 // pattry(pprog, *p)) return r`.
3145 if let Some(arr) = arr {
3146 // C params.c:1761-1797 — `(w)N` / `(f)N` word-mode arm.
3147 // `getstrvalue(v)` joins the array; `sepsplit` re-splits by
3148 // sep (`f` → "\n", `w` → IFS-default whitespace, `s:SEP:`
3149 // → user sep), then the Nth split word is returned. So
3150 // `arr=("a b" "c d"); ${arr[(w)2]}` → "b" (joined "a b c d",
3151 // split → ["a","b","c","d"], pick idx 1).
3152 if flags.contains('w') || flags.contains('f') {
3153 if let Ok(n) = pat.parse::<i64>() {
3154 let sep_chars: &[char] = if flags.contains('f') {
3155 &['\n']
3156 } else {
3157 &[' ', '\t', '\n']
3158 };
3159 let joined = arr.join(" ");
3160 let words: Vec<&str> = joined
3161 .split(|c: char| sep_chars.contains(&c))
3162 .filter(|w| !w.is_empty())
3163 .collect();
3164 let len = words.len() as i64;
3165 let idx_into = if n > 0 {
3166 (n - 1) as usize
3167 } else if n < 0 {
3168 let off = len + n;
3169 if off < 0 {
3170 return Some(getarg_out::Value(Value::str("")));
3171 }
3172 off as usize
3173 } else {
3174 return Some(getarg_out::Value(Value::str("")));
3175 };
3176 return Some(getarg_out::Value(
3177 Value::str(words.get(idx_into).map(|s| s.to_string()).unwrap_or_default())
3178 ));
3179 }
3180 }
3181 let exact = flags.contains('e');
3182 let word = flags.contains('w') || flags.contains('f');
3183 let _ = word;
3184 let return_index = flags.contains('i') || flags.contains('I');
3185 // C params.c:1575 `if (!rev)` — without a direction flag
3186 // (r/R/i/I/k/K), getarg does NOT enter the search loop on
3187 // arrays; pat is mathevalarg'd as an integer index instead.
3188 // Verified empirically: `arr=(foo bar); ${arr[(e)foo]}`
3189 // returns empty in real zsh (mathevalarg fails, no element).
3190 let any_search_flag = flags.contains('r')
3191 || flags.contains('R')
3192 || flags.contains('i')
3193 || flags.contains('I')
3194 || flags.contains('k')
3195 || flags.contains('K');
3196 if !any_search_flag {
3197 return None;
3198 }
3199 // c:1488-1491 — negative `num` flips reverse direction.
3200 let reverse = (flags.contains('R') || flags.contains('I')) ^ neg_num_flips;
3201 // C params.c:1668-1685 implicit `*` wrap fires only when
3202 // `v->scanflags` is unset; in standard subscript callsites
3203 // scanflags IS set, so the wrap does NOT engage. Verified
3204 // empirically: `arr=(foobar baz); ${arr[(r)foo]}` returns
3205 // empty in real zsh (exact match), not "foobar". Pattern is
3206 // used verbatim — globbing only when user supplies `*`.
3207 let pat_used: &str = pat;
3208
3209 // c:1740-1760 — `b<NUM>` starting offset + bounds checks.
3210 // beg is already 0-based after parse (parsed-1 for positive).
3211 let len = arr.len() as i64;
3212 let mut start = beg;
3213 if start < 0 {
3214 start += len;
3215 }
3216 // c:1743-1747 — out-of-bounds returns.
3217 if reverse {
3218 if start < 0 {
3219 return Some(getarg_out::Value(if return_index {
3220 Value::str("0")
3221 } else {
3222 Value::str("")
3223 }));
3224 }
3225 } else if start >= len {
3226 return Some(getarg_out::Value(if return_index {
3227 Value::str((arr.len() + 1).to_string())
3228 } else {
3229 Value::str("")
3230 }));
3231 }
3232 // c:1750-1751 — reverse w/o explicit b starts from len-1.
3233 if reverse && !has_beg {
3234 start = len - 1;
3235 }
3236
3237 let iter: Box<dyn Iterator<Item = (usize, &String)>> = if reverse {
3238 // c:1752 — `for (p = ta + beg; p >= ta; p--)`: clamp start
3239 // into the valid range then walk backwards.
3240 let s_idx = if start < 0 { 0 } else { start as usize };
3241 let s_idx = s_idx.min(arr.len().saturating_sub(1));
3242 Box::new(arr[..=s_idx].iter().enumerate().rev())
3243 } else {
3244 // c:1757 — `for (p = ta + beg; *p; p++)`: skip first beg.
3245 let s_idx = if start < 0 { 0 } else { start as usize };
3246 Box::new(arr.iter().enumerate().skip(s_idx))
3247 };
3248 // c:1758 — `!--num` skips matches until the Nth.
3249 let mut remaining = num;
3250 for (i, s) in iter {
3251 let hit = if exact {
3252 s == pat
3253 } else {
3254 crate::ported::pattern::patmatch(pat_used, s)
3255 };
3256 if hit {
3257 remaining -= 1;
3258 if remaining == 0 {
3259 return Some(getarg_out::Value(if return_index {
3260 Value::str((i + 1).to_string())
3261 } else {
3262 Value::str(s.clone())
3263 }));
3264 }
3265 }
3266 }
3267 return Some(getarg_out::Value(if return_index {
3268 // zsh: `i` returns len+1 if not found, `I` returns 0.
3269 if flags.contains('I') {
3270 Value::str("0")
3271 } else {
3272 Value::str((arr.len() + 1).to_string())
3273 }
3274 } else {
3275 Value::str("")
3276 }));
3277 }
3278
3279 // C params.c:1761-1797 — scalar word-mode arm. `(w)N` joins
3280 // the source string and re-splits by sep (whitespace by default
3281 // for `w`, "\n" for `f`). When `pat` is a numeric N, the Nth
3282 // word is returned. Pattern-search variants on scalars share
3283 // the c:1798-1980 char-search arm which is not yet ported.
3284 if let Some(s) = scalar {
3285 if flags.contains('w') || flags.contains('f') {
3286 if let Ok(n) = pat.parse::<i64>() {
3287 let sep_chars: &[char] = if flags.contains('f') {
3288 &['\n']
3289 } else {
3290 &[' ', '\t', '\n']
3291 };
3292 let words: Vec<&str> = s
3293 .split(|c: char| sep_chars.contains(&c))
3294 .filter(|w| !w.is_empty())
3295 .collect();
3296 let len = words.len() as i64;
3297 let idx_into = if n > 0 {
3298 (n - 1) as usize
3299 } else if n < 0 {
3300 let off = len + n;
3301 if off < 0 {
3302 return Some(getarg_out::Value(Value::str("")));
3303 }
3304 off as usize
3305 } else {
3306 return Some(getarg_out::Value(Value::str("")));
3307 };
3308 return Some(getarg_out::Value(
3309 Value::str(words.get(idx_into).map(|s| s.to_string()).unwrap_or_default()),
3310 ));
3311 }
3312 }
3313 // C params.c:1798-1980 — scalar char-search arm. `(i)/(I)/
3314 // (r)/(R)` on a scalar runs a sliding-window glob match.
3315 // (i)/(I) return the 1-based byte position of first/last
3316 // match; (r)/(R) return the matched substring.
3317 // Multibyte cursor outputs (prevcharlen/nextcharlen at
3318 // c:1948-1971) are not yet ported; ASCII-only path here.
3319 let any_search = flags.contains('r')
3320 || flags.contains('R')
3321 || flags.contains('i')
3322 || flags.contains('I');
3323 if any_search {
3324 let return_index = flags.contains('i') || flags.contains('I');
3325 let want_last = flags.contains('I') || flags.contains('R');
3326 // Negative `num` flips direction (c:1488-1491).
3327 let want_last = want_last ^ neg_num_flips;
3328 let s_chars: Vec<char> = s.chars().collect();
3329 let n = s_chars.len();
3330 let positions: Box<dyn Iterator<Item = usize>> = if want_last {
3331 Box::new((0..=n).rev())
3332 } else {
3333 Box::new(0..=n)
3334 };
3335 // c:1929+ / c:1964 — `!--num` skips matches until the Nth.
3336 // Per `b<NUM>` (c:1740-1747) — start from offset, only
3337 // when has_beg is set. Without `b`, walk all positions.
3338 let beg_idx_opt: Option<usize> = if has_beg {
3339 let beg_norm = if beg < 0 { beg + n as i64 } else { beg };
3340 Some(if beg_norm < 0 {
3341 0
3342 } else {
3343 (beg_norm as usize).min(n)
3344 })
3345 } else {
3346 None
3347 };
3348 let mut found: Option<(usize, usize)> = None;
3349 let mut remaining = num;
3350 'outer: for start in positions {
3351 if let Some(b_idx) = beg_idx_opt {
3352 if want_last {
3353 if start > b_idx {
3354 continue;
3355 }
3356 } else if start < b_idx {
3357 continue;
3358 }
3359 }
3360 for span_len in 1..=(n - start) {
3361 let cand: String = s_chars[start..start + span_len].iter().collect();
3362 let hit = if flags.contains('e') {
3363 cand == pat
3364 } else {
3365 crate::ported::pattern::patmatch(pat, &cand)
3366 };
3367 if hit {
3368 remaining -= 1;
3369 if remaining == 0 {
3370 found = Some((start, start + span_len));
3371 break 'outer;
3372 }
3373 // Advance past this match position to find the
3374 // next-Nth instead of repeatedly matching same
3375 // start (mirrors C's pointer increment).
3376 break;
3377 }
3378 }
3379 }
3380 return Some(getarg_out::Value(match (found, return_index) {
3381 (Some((s_pos, _)), true) => Value::str((s_pos + 1).to_string()),
3382 // C params.c:1798-1980 char-search returns the char AT
3383 // the match position, not the full matched substring.
3384 // Verified empirically: `s="barfooxyz"; ${s[(r)foo]}`
3385 // returns "f" in real zsh, not "foo".
3386 (Some((s_pos, _)), false) => Value::str(
3387 s_chars.get(s_pos).map(|c| c.to_string()).unwrap_or_default(),
3388 ),
3389 (None, true) => Value::str(if flags.contains('i') {
3390 (n + 1).to_string()
3391 } else {
3392 "0".to_string()
3393 }),
3394 (None, false) => Value::str(String::new()),
3395 }));
3396 }
3397 }
3398
3399 // No search context — return parsed flags for caller dispatch.
3400 Some(getarg_out::Flags { flags, rest: pat })
3401}
3402
3403// `VarAttr` struct + `VarKind` enum + `impl VarAttr::format_zsh`
3404// DELETED. C zsh stores typeset attributes as bare `PM_*` bit
3405// flags on `Param.node.flags` (`Src/zsh.h` PM_* + `Src/params.c`
3406// flag tests); the `${(t)var}` flag report (`typeprintparam` at
3407// `Src/builtin.c:3050+`) writes those bits to a string directly
3408// against the `Param.node.flags` int.
3409//
3410// Both types had zero external use sites — pure dead-code carryover
3411// from an earlier exec.rs refactor. The PM_* bit constants are at
3412// `zsh_h.rs:1340+` and the `(t)` formatting routes through
3413// `typeset_print_flags` (when wired) reading bare `Param.node.flags`.
3414
3415// ===========================================================
3416// Special-parameter GSU (get/set/unset) callbacks ported from
3417// Src/params.c.
3418//
3419// C zsh stores per-special-param state in file-static globals
3420// (`ifs`, `home`, `term`, `histsiz`, etc.) and dispatches getfn/
3421// setfn/unsetfn callbacks through `Param.gsu->getfn` etc. zshrs's
3422// param storage is per-evaluator HashMaps on `ShellExecutor`, so
3423// the C globals are reproduced as `OnceLock<Mutex<…>>` module
3424// statics here, with the get/set fns mutating the static.
3425//
3426// Functions that genuinely need a `Param *` (the GSU dispatch
3427// callbacks for non-special arr/hash/int/float/str params, the
3428// param-table mutators, scope helpers, etc.) cannot be properly
3429// ported until zshrs gains a Param struct + callback-table ABI;
3430// those keep their C signatures but the body is a WARNING-stub
3431// that does nothing.
3432// ===========================================================
3433
3434use std::sync::{Arc, Mutex, OnceLock, RwLock};
3435use std::time::Duration;
3436use crate::config_h::DEFAULT_TMPPREFIX;
3437use crate::zsh_h::{paramdef, ERRFLAG_ERROR, PM_DONTIMPORT, PM_DONTIMPORT_SUID, PM_READONLY_SPECIAL};
3438// -----------------------------------------------------------
3439// Module statics — one per C global referenced by the special-
3440// param callbacks below. All initialised lazily on first read.
3441// -----------------------------------------------------------
3442
3443// `Src/params.c:515 mod_export HashTable paramtab, realparamtab;`
3444//
3445// `realparamtab` always points to the shell's global parameter
3446// table. `paramtab` normally aliases it; it is temporarily
3447// redirected during associative-array key iteration
3448// (`Src/params.c:508-513` — "paramtab is sometimes temporarily
3449// changed to point at another table").
3450//
3451// Per PORT_PLAN.md Phase 3, bucket-2 read-mostly tables use
3452// `RwLock` so parallel readers (every `$VAR` expansion, every
3453// completion lookup) don't serialize. Writers (assignments,
3454// scope pushes/pops, function-local declarations) take the
3455// exclusive write lock. `OnceLock` provides the single-static
3456// guarantee without an `Arc` allocation since the table lives
3457// for the process lifetime.
3458//
3459// Entries are keyed on `node.nam` (the canonical `param` struct
3460// lives in `zsh_h.rs`). The full `HashTable` substrate (vtable
3461// callbacks, intrusive `next` chain, scope-stacked iterators) is
3462// not yet wired; until it is, the typed map is the operative
3463// storage.
3464static PARAMTAB_INNER: OnceLock<RwLock<HashMap<String, crate::ported::zsh_h::Param>>> =
3465 OnceLock::new();
3466static REALPARAMTAB_INNER: OnceLock<RwLock<HashMap<String, crate::ported::zsh_h::Param>>> =
3467 OnceLock::new();
3468
3469/// Accessor for the global `paramtab` (Src/params.c:515).
3470/// Mirrors C's `paramtab->...` dereference by handing back the
3471/// inner RwLock; callers `.read()` for lookups and `.write()` for
3472/// mutation, operating on the `HashMap<String, Param>` directly.
3473pub fn paramtab() -> &'static RwLock<HashMap<String, crate::ported::zsh_h::Param>> {
3474 PARAMTAB_INNER.get_or_init(|| RwLock::new(HashMap::new()))
3475}
3476
3477/// Accessor for the global `realparamtab` (Src/params.c:515).
3478/// Same role as `paramtab` for the not-currently-redirected case;
3479/// the alias-flip during assoc-array iteration isn't modelled yet.
3480pub fn realparamtab() -> &'static RwLock<HashMap<String, crate::ported::zsh_h::Param>> {
3481 REALPARAMTAB_INNER.get_or_init(|| RwLock::new(HashMap::new()))
3482}
3483
3484fn ifs_lock() -> &'static Mutex<String> {
3485 static IFS_VAR: OnceLock<Mutex<String>> = OnceLock::new();
3486 IFS_VAR.get_or_init(|| Mutex::new(" \t\n\0".to_string()))
3487}
3488
3489fn home_lock() -> &'static Mutex<String> {
3490 static HOME_VAR: OnceLock<Mutex<String>> = OnceLock::new();
3491 HOME_VAR.get_or_init(|| Mutex::new(env::var("HOME").unwrap_or_default()))
3492}
3493
3494fn term_lock() -> &'static Mutex<String> {
3495 static TERM_VAR: OnceLock<Mutex<String>> = OnceLock::new();
3496 TERM_VAR.get_or_init(|| Mutex::new(env::var("TERM").unwrap_or_default()))
3497}
3498
3499fn wordchars_lock() -> &'static Mutex<String> {
3500 static WORDCHARS_VAR: OnceLock<Mutex<String>> = OnceLock::new();
3501 WORDCHARS_VAR.get_or_init(|| Mutex::new("*?_-.[]~=/&;!#$%^(){}<>".to_string()))
3502}
3503
3504fn histchars_lock() -> &'static Mutex<[u8; 3]> {
3505 static HISTCHARS_VAR: OnceLock<Mutex<[u8; 3]>> = OnceLock::new();
3506 HISTCHARS_VAR.get_or_init(|| Mutex::new([b'!', b'^', b'#']))
3507}
3508
3509fn keyboardhack_lock() -> &'static Mutex<u8> {
3510 static KEYBOARDHACK_VAR: OnceLock<Mutex<u8>> = OnceLock::new();
3511 KEYBOARDHACK_VAR.get_or_init(|| Mutex::new(0))
3512}
3513
3514fn histsiz_lock() -> &'static Mutex<i64> {
3515 static HISTSIZ_VAR: OnceLock<Mutex<i64>> = OnceLock::new();
3516 // Match observed `zsh -fc 'echo $HISTSIZE'` output on zsh 5.9+
3517 // (Homebrew). Upstream's `configure.ac` defines DEFAULT_HISTSIZE
3518 // as 30 but distributed binaries seed the cap at 999999999 — the
3519 // parity goal here is "match the binary the user actually runs",
3520 // not "match the source-code default".
3521 HISTSIZ_VAR.get_or_init(|| Mutex::new(999_999_999))
3522}
3523
3524fn savehistsiz_lock() -> &'static Mutex<i64> {
3525 static SAVEHISTSIZ_VAR: OnceLock<Mutex<i64>> = OnceLock::new();
3526 // Same rationale as `histsiz_lock` — observed `zsh -fc
3527 // 'echo $SAVEHIST'` returns 99999999 on zsh 5.9+. Source has
3528 // savehistsiz default to 0 but distributed binaries cap at 99M.
3529 SAVEHISTSIZ_VAR.get_or_init(|| Mutex::new(99_999_999))
3530}
3531
3532fn zsh_terminfo_lock() -> &'static Mutex<String> {
3533 static TERMINFO_VAR: OnceLock<Mutex<String>> = OnceLock::new();
3534 TERMINFO_VAR.get_or_init(|| Mutex::new(env::var("TERMINFO").unwrap_or_default()))
3535}
3536
3537fn zsh_terminfodirs_lock() -> &'static Mutex<String> {
3538 static TERMINFODIRS_VAR: OnceLock<Mutex<String>> = OnceLock::new();
3539 TERMINFODIRS_VAR.get_or_init(|| Mutex::new(env::var("TERMINFO_DIRS").unwrap_or_default()))
3540}
3541
3542fn cached_username_lock() -> &'static Mutex<String> {
3543 static USERNAME_VAR: OnceLock<Mutex<String>> = OnceLock::new();
3544 USERNAME_VAR.get_or_init(|| Mutex::new(initial_username()))
3545}
3546
3547/// Resolve the current user's name. Mirrors C's `get_username()`
3548/// init at Src/init.c which reads `getpwuid(getuid())->pw_name`
3549/// rather than `$USER`. Falls back to env vars only if the
3550/// passwd lookup fails (rare on real systems).
3551fn initial_username() -> String {
3552 #[cfg(unix)]
3553 {
3554 let uid = unsafe { libc::getuid() };
3555 let mut pwd: libc::passwd = unsafe { std::mem::zeroed() };
3556 let mut buf = vec![0i8; 1024];
3557 let mut result: *mut libc::passwd = std::ptr::null_mut();
3558 let rc = unsafe {
3559 libc::getpwuid_r(uid, &mut pwd, buf.as_mut_ptr(), buf.len(), &mut result)
3560 };
3561 if rc == 0 && !result.is_null() && !pwd.pw_name.is_null() {
3562 let cstr = unsafe { std::ffi::CStr::from_ptr(pwd.pw_name) };
3563 return cstr.to_string_lossy().into_owned();
3564 }
3565 }
3566 env::var("USER")
3567 .or_else(|_| env::var("LOGNAME"))
3568 .unwrap_or_default()
3569}
3570
3571fn pipestats_lock() -> &'static Mutex<Vec<i32>> {
3572 static PIPESTATS_VAR: OnceLock<Mutex<Vec<i32>>> = OnceLock::new();
3573 PIPESTATS_VAR.get_or_init(|| Mutex::new(Vec::new()))
3574}
3575
3576fn shtimer_lock() -> &'static Mutex<Duration> {
3577 static SHTIMER_VAR: OnceLock<Mutex<Duration>> = OnceLock::new();
3578 SHTIMER_VAR.get_or_init(|| {
3579 Mutex::new(
3580 SystemTime::now()
3581 .duration_since(UNIX_EPOCH)
3582 .unwrap_or_default(),
3583 )
3584 })
3585}
3586
3587fn pparams_lock() -> &'static Mutex<Vec<String>> {
3588 // Mirror of zsh's `pparams` (positional params $1, $2, ...).
3589 // Used by `poundgetfn` for `$#`. The canonical store is
3590 // `builtin::PPARAMS` (Src/init.c `pparams`); set/shift builtins
3591 // write there. Point at that single store so `$#` reads the
3592 // live value instead of an isolated empty mirror.
3593 &crate::ported::builtin::PPARAMS
3594}
3595
3596fn zunderscore_lock() -> &'static Mutex<String> {
3597 static ZUNDERSCORE_VAR: OnceLock<Mutex<String>> = OnceLock::new();
3598 ZUNDERSCORE_VAR.get_or_init(|| Mutex::new(String::new()))
3599}
3600
3601// -----------------------------------------------------------
3602// libc-backed callbacks (UID/GID/EUID/EGID/errno/RANDOM/TTYIDLE).
3603// -----------------------------------------------------------
3604
3605/// Port of `uidgetfn(UNUSED(Param pm))` from `Src/params.c:4689`. C body:
3606/// `return getuid();`
3607/// WARNING: param names don't match C — Rust=() vs C=(pm)
3608pub fn uidgetfn() -> i64 {
3609 unsafe { libc::getuid() as i64 }
3610}
3611
3612/// Port of `uidsetfn(UNUSED(Param pm), zlong x)` from `Src/params.c:4698`. C body:
3613/// `if (setuid((uid_t)x)) zerr("failed to change user ID: %e", errno);`
3614/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
3615pub fn uidsetfn(x: i64) {
3616 if unsafe { libc::setuid(x as libc::uid_t) } != 0 {
3617 zerr(&format!(
3618 "failed to change user ID: {}",
3619 std::io::Error::last_os_error()
3620 ));
3621 }
3622}
3623
3624/// Port of `euidgetfn(UNUSED(Param pm))` from `Src/params.c:4710`. C body:
3625/// `return geteuid();`
3626/// WARNING: param names don't match C — Rust=() vs C=(pm)
3627pub fn euidgetfn() -> i64 {
3628 unsafe { libc::geteuid() as i64 }
3629}
3630
3631/// Port of `euidsetfn(UNUSED(Param pm), zlong x)` from `Src/params.c:4719`. C body:
3632/// `if (seteuid((uid_t)x)) zerr("failed to change effective user ID: %e", errno);`
3633/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
3634pub fn euidsetfn(x: i64) {
3635 if unsafe { libc::seteuid(x as libc::uid_t) } != 0 {
3636 zerr(&format!(
3637 "failed to change effective user ID: {}",
3638 std::io::Error::last_os_error()
3639 ));
3640 }
3641}
3642
3643/// Port of `gidgetfn(UNUSED(Param pm))` from `Src/params.c:4731`. C body: `return getgid();`
3644/// WARNING: param names don't match C — Rust=() vs C=(pm)
3645pub fn gidgetfn() -> i64 {
3646 unsafe { libc::getgid() as i64 }
3647}
3648
3649/// Port of `gidsetfn(UNUSED(Param pm), zlong x)` from `Src/params.c:4740`. C body:
3650/// `if (setgid((gid_t)x)) zerr("failed to change group ID: %e", errno);`
3651/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
3652pub fn gidsetfn(x: i64) {
3653 if unsafe { libc::setgid(x as libc::gid_t) } != 0 {
3654 zerr(&format!(
3655 "failed to change group ID: {}",
3656 std::io::Error::last_os_error()
3657 ));
3658 }
3659}
3660
3661/// Port of `egidgetfn(UNUSED(Param pm))` from `Src/params.c:4752`. C body: `return getegid();`
3662/// WARNING: param names don't match C — Rust=() vs C=(pm)
3663pub fn egidgetfn() -> i64 {
3664 unsafe { libc::getegid() as i64 }
3665}
3666
3667/// Port of `egidsetfn(UNUSED(Param pm), zlong x)` from `Src/params.c:4761`. C body:
3668/// `if (setegid((gid_t)x)) zerr("failed to change effective group ID: %e", errno);`
3669/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
3670pub fn egidsetfn(x: i64) {
3671 if unsafe { libc::setegid(x as libc::gid_t) } != 0 {
3672 zerr(&format!(
3673 "failed to change effective group ID: {}",
3674 std::io::Error::last_os_error()
3675 ));
3676 }
3677}
3678
3679/// Port of `errnogetfn(UNUSED(Param pm))` from `Src/params.c:5015`. C body: `return errno;`
3680/// WARNING: param names don't match C — Rust=() vs C=(pm)
3681pub fn errnogetfn() -> i64 {
3682 std::io::Error::last_os_error().raw_os_error().unwrap_or(0) as i64
3683}
3684
3685/// Port of `errnosetfn(UNUSED(Param pm), zlong x)` from `Src/params.c:5004`. C body:
3686/// `errno = (int)x; if ((zlong)errno != x) zwarn("errno truncated on assignment");`
3687///
3688/// Rust note: `errno` is a libc thread-local; Rust uses `std::io::Error`
3689/// which captures the *last* call. To set errno for subsequent
3690/// `last_os_error()` reads on macOS / Linux, write through the libc
3691/// `__error()`/`__errno_location()` accessor.
3692/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
3693pub fn errnosetfn(x: i64) {
3694 extern "C" {
3695 #[cfg(target_os = "macos")]
3696 fn __error() -> *mut libc::c_int;
3697 #[cfg(target_os = "linux")]
3698 fn __errno_location() -> *mut libc::c_int;
3699 }
3700 let truncated = x as i32;
3701 unsafe {
3702 #[cfg(target_os = "macos")]
3703 {
3704 *__error() = truncated;
3705 }
3706 #[cfg(target_os = "linux")]
3707 {
3708 *__errno_location() = truncated;
3709 }
3710 }
3711 if truncated as i64 != x {
3712 zerr("errno truncated on assignment");
3713 }
3714}
3715
3716/// Port of `randomgetfn(UNUSED(Param pm))` from `Src/params.c:4543`. C body:
3717/// `return rand() & 0x7fff;`
3718/// WARNING: param names don't match C — Rust=() vs C=(pm)
3719pub fn randomgetfn() -> i64 {
3720 (unsafe { libc::rand() } & 0x7fff) as i64
3721}
3722
3723/// Port of `randomsetfn(UNUSED(Param pm), zlong v)` from `Src/params.c:4552`. C body:
3724/// `srand((unsigned int)v);`
3725/// WARNING: param names don't match C — Rust=(v) vs C=(pm, v)
3726pub fn randomsetfn(v: i64) {
3727 unsafe { libc::srand(v as libc::c_uint) };
3728}
3729
3730/// Port of `ttyidlegetfn(UNUSED(Param pm))` from `Src/params.c:4771`. C body:
3731/// ```c
3732/// struct stat ttystat;
3733/// if (SHTTY == -1 || fstat(SHTTY, &ttystat)) return -1;
3734/// return time(NULL) - ttystat.st_atime;
3735/// ```
3736/// Rust port reads stdin (fd 0) — closest match to `SHTTY` the
3737/// shell tracks as the controlling-tty fd. Returns -1 if stdin is
3738/// not a tty.
3739/// WARNING: param names don't match C — Rust=() vs C=(pm)
3740pub fn ttyidlegetfn() -> i64 {
3741 if unsafe { libc::isatty(0) } == 0 {
3742 return -1;
3743 }
3744 let mut st: libc::stat = unsafe { std::mem::zeroed() };
3745 if unsafe { libc::fstat(0, &mut st) } != 0 {
3746 return -1;
3747 }
3748 let now = SystemTime::now()
3749 .duration_since(UNIX_EPOCH)
3750 .unwrap_or_default()
3751 .as_secs() as i64;
3752 now - st.st_atime as i64
3753}
3754
3755// -----------------------------------------------------------
3756// SECONDS / EPOCHSECONDS family — backed by SHTIMER static.
3757// -----------------------------------------------------------
3758
3759/// Port of `intsecondsgetfn(UNUSED(Param pm))` from `Src/params.c:4561`. C body:
3760/// `return (zlong)(now.tv_sec - shtimer.tv_sec - …);`
3761/// WARNING: param names don't match C — Rust=() vs C=(pm)
3762pub fn intsecondsgetfn() -> i64 {
3763 let now = SystemTime::now()
3764 .duration_since(UNIX_EPOCH)
3765 .unwrap_or_default();
3766 let timer = *shtimer_lock().lock().expect("shtimer poisoned");
3767 let now_sec = now.as_secs() as i64;
3768 let timer_sec = timer.as_secs() as i64;
3769 let now_nsec = now.subsec_nanos() as i64;
3770 let timer_nsec = timer.subsec_nanos() as i64;
3771 now_sec - timer_sec - i64::from(now_nsec < timer_nsec)
3772}
3773
3774/// Port of `intsecondssetfn(UNUSED(Param pm), zlong x)` from `Src/params.c:4575`. C body:
3775/// `shtimer.tv_sec = now.tv_sec - x; shtimer.tv_nsec = now.tv_nsec;`
3776/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
3777pub fn intsecondssetfn(x: i64) {
3778 let now = SystemTime::now()
3779 .duration_since(UNIX_EPOCH)
3780 .unwrap_or_default();
3781 let now_sec = now.as_secs() as i64;
3782 let new_sec = now_sec - x;
3783 if new_sec < 0 {
3784 zerr("SECONDS truncated on assignment");
3785 return;
3786 }
3787 *shtimer_lock().lock().expect("shtimer poisoned") =
3788 Duration::new(new_sec as u64, now.subsec_nanos());
3789}
3790
3791/// Port of `floatsecondsgetfn(UNUSED(Param pm))` from `Src/params.c:4591`. C body:
3792/// `return (double)(now-tv_sec - shtimer.tv_sec) + nsec/1e9;`
3793/// WARNING: param names don't match C — Rust=() vs C=(pm)
3794pub fn floatsecondsgetfn() -> f64 {
3795 let now = SystemTime::now()
3796 .duration_since(UNIX_EPOCH)
3797 .unwrap_or_default();
3798 let timer = *shtimer_lock().lock().expect("shtimer poisoned");
3799 (now - timer).as_secs_f64()
3800}
3801
3802/// Port of `floatsecondssetfn(UNUSED(Param pm), double x)` from `Src/params.c:4603`. C body:
3803/// `shtimer.tv_sec = now.tv_sec - (zlong)x; shtimer.tv_nsec = now.tv_nsec - (x-int)*1e9;`
3804/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
3805pub fn floatsecondssetfn(x: f64) {
3806 let now = SystemTime::now()
3807 .duration_since(UNIX_EPOCH)
3808 .unwrap_or_default();
3809 let new = now.checked_sub(Duration::from_secs_f64(x)).unwrap_or_default();
3810 *shtimer_lock().lock().expect("shtimer poisoned") = new;
3811}
3812
3813/// Port of `getrawseconds()` from `Src/params.c:4615`. C body:
3814/// `return (double)shtimer.tv_sec + (double)shtimer.tv_nsec / 1e9;`
3815pub fn getrawseconds() -> f64 {
3816 shtimer_lock().lock().expect("shtimer poisoned").as_secs_f64()
3817}
3818
3819/// Port of `setrawseconds(double x)` from `Src/params.c:4622`. C body:
3820/// `shtimer.tv_sec = (zlong)x; shtimer.tv_nsec = (x-int)*1e9;`
3821pub fn setrawseconds(x: f64) {
3822 *shtimer_lock().lock().expect("shtimer poisoned") = Duration::from_secs_f64(x);
3823}
3824
3825/// Port of `setsecondstype(Param pm, int on, int off)` from `Src/params.c:4630`. C body
3826/// flips the `gsu.f`/`gsu.i` callback pointer based on the new
3827/// param-flag bitset.
3828///
3829/// WARNING: zshrs has no Param/GSU dispatch table yet — the
3830/// "promotion between integer/float seconds" logic happens via
3831/// pm->gsu pointer swaps in C. Returns 0 to signal success;
3832/// callers can assume the type change is recorded by the caller's
3833/// own bookkeeping until the GSU table lands.
3834/// WARNING: param names don't match C — Rust=(on, off) vs C=(pm, on, off)
3835pub fn setsecondstype( // c:4630
3836 pm: &mut crate::ported::zsh_h::param,
3837 on: i32,
3838 off: i32,
3839) -> i32 {
3840 // c:4632 — `int newflags = (pm->flags | on) & ~off`.
3841 let newflags = (pm.node.flags | on) & !off;
3842 // c:4633 — `int tp = PM_TYPE(newflags)`.
3843 let tp = PM_TYPE(newflags as u32);
3844 // c:4635-4638 / 4639-4642 — float vs integer GSU pointer swap.
3845 if tp == PM_EFLOAT || tp == PM_FFLOAT { // c:4635
3846 // C: `pm->gsu.f = &floatseconds_gsu`. GSU table not yet
3847 // wired in the Rust port; record the type by clearing
3848 // any integer GSU.
3849 pm.gsu_i = None;
3850 // pm.gsu_f = Some(floatseconds_gsu) — pending GSU port.
3851 } else if tp == PM_INTEGER { // c:4639
3852 // C: `pm->gsu.i = &intseconds_gsu`.
3853 pm.gsu_f = None;
3854 // pm.gsu_i = Some(intseconds_gsu) — pending GSU port.
3855 } else {
3856 return 1; // c:4644
3857 }
3858 pm.node.flags = newflags; // c:4645
3859 0 // c:4646
3860}
3861
3862// -----------------------------------------------------------
3863// $0 / $#
3864// -----------------------------------------------------------
3865
3866/// Port of `argzerogetfn(UNUSED(Param pm))` from `Src/params.c:4954`. C body:
3867/// `return isset(POSIXARGZERO) ? posixzero : argzero;`
3868///
3869/// Reads through `crate::ported::utils::argzero()` (the canonical
3870/// OnceLock storage in utils.rs). C's `posixzero` branch is not
3871/// yet ported (POSIXARGZERO option needs the option table).
3872/// WARNING: param names don't match C — Rust=() vs C=(pm)
3873pub fn argzerogetfn() -> String {
3874 crate::ported::utils::argzero().unwrap_or_default()
3875}
3876
3877/// Direct port of `static void argzerosetfn(UNUSED(Param pm),
3878/// char *x)` from `Src/params.c:4937-4946`. Setter for `$0` —
3879/// POSIX mode rejects assignment (read-only), zsh mode replaces
3880/// `argzero`.
3881///
3882/// C body:
3883/// if (x) {
3884/// if (isset(POSIXARGZERO))
3885/// zerr("read-only variable: 0");
3886/// else {
3887/// zsfree(argzero);
3888/// argzero = ztrdup(x);
3889/// }
3890/// zsfree(x);
3891/// }
3892/// Port of `argzerosetfn(UNUSED(Param pm), char *x)` from `Src/params.c:4937`.
3893/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
3894pub fn argzerosetfn(x: String) { // c:4937
3895 // c:4937 — if (x).
3896 if !x.is_empty() {
3897 // c:4940 — isset(POSIXARGZERO) reject.
3898 if isset(crate::ported::zsh_h::POSIXARGZERO) {
3899 crate::ported::utils::zerr("read-only variable: 0"); // c:4941
3900 } else {
3901 // c:4943-4944 — zsfree(argzero); argzero = ztrdup(x).
3902 crate::ported::utils::set_argzero(Some(crate::ported::utils::ztrdup(&x)));
3903 }
3904 // c:4946 — `zsfree(x)`. Rust drop handles via move.
3905 }
3906}
3907
3908/// Port of `poundgetfn(UNUSED(Param pm))` from `Src/params.c:4534`. C body:
3909/// `return arrlen(pparams);`
3910/// WARNING: param names don't match C — Rust=() vs C=(pm)
3911pub fn poundgetfn() -> i64 {
3912 pparams_lock().lock().expect("pparams poisoned").len() as i64
3913}
3914
3915// -----------------------------------------------------------
3916// $USERNAME
3917// -----------------------------------------------------------
3918
3919/// Port of `usernamegetfn(UNUSED(Param pm))` from `Src/params.c:4653`. C body:
3920/// `return get_username();`
3921/// WARNING: param names don't match C — Rust=() vs C=(pm)
3922pub fn usernamegetfn() -> String {
3923 cached_username_lock()
3924 .lock()
3925 .expect("username poisoned")
3926 .clone()
3927}
3928
3929/// Port of `usernamesetfn(UNUSED(Param pm), char *x)` from `Src/params.c:4662`. C body:
3930/// `getpwnam(x); setgid; setuid; cached_uid = pswd->pw_uid;`
3931///
3932/// WARNING: the SUID-changing path requires getpwnam(3) which
3933/// crosses an unsafe FFI boundary not yet wrapped here. The
3934/// cached-name update is performed; uid/gid changes still need
3935/// porting of the `pwd.h` getpwnam wrapper.
3936/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
3937pub fn usernamesetfn(x: String) { // c:4662
3938 // c:4662 — `if (x && (pswd = getpwnam(x)) && pswd->pw_uid != cached_uid)`.
3939 let target = std::ffi::CString::new(x.as_bytes()).ok();
3940 if let Some(cstr) = target {
3941 unsafe {
3942 let pwd = libc::getpwnam(cstr.as_ptr()); // c:4666
3943 if !pwd.is_null() {
3944 let cached_uid =
3945 libc::geteuid() as libc::uid_t;
3946 if (*pwd).pw_uid != cached_uid { // c:4666
3947 // c:4670-4672 — initgroups(x, pswd->pw_gid).
3948 let _ = libc::initgroups(cstr.as_ptr(), (*pwd).pw_gid as _);
3949 // c:4671 — setgid(pswd->pw_gid).
3950 if libc::setgid((*pwd).pw_gid) != 0 { // c:4673
3951 crate::ported::utils::zwarn(&format!(
3952 "failed to change group ID: {}",
3953 std::io::Error::last_os_error()
3954 ));
3955 } else if libc::setuid((*pwd).pw_uid) != 0 { // c:4675
3956 // c:4675-4676 — setuid failed.
3957 crate::ported::utils::zwarn(&format!(
3958 "failed to change user ID: {}",
3959 std::io::Error::last_os_error()
3960 ));
3961 } else {
3962 // c:4677-4681 — cache update.
3963 let name_cstr = std::ffi::CStr::from_ptr((*pwd).pw_name);
3964 let name_str = name_cstr.to_string_lossy().to_string();
3965 *cached_username_lock()
3966 .lock()
3967 .expect("username poisoned") =
3968 crate::ported::utils::ztrdup_metafy(&name_str);
3969 }
3970 }
3971 }
3972 }
3973 }
3974 // c:4683 — `zsfree(x)`; Rust drop handles it.
3975 drop(x);
3976}
3977
3978// -----------------------------------------------------------
3979// $IFS / $HOME / $TERM / $WORDCHARS / $TERMINFO / $TERMINFO_DIRS
3980// $KEYBOARD_HACK / $HISTCHARS / $_ — string-state callbacks.
3981// -----------------------------------------------------------
3982
3983/// Port of `ifsgetfn(UNUSED(Param pm))` from `Src/params.c:4784`. C body: `return ifs;`
3984/// WARNING: param names don't match C — Rust=() vs C=(pm)
3985pub fn ifsgetfn() -> String {
3986 ifs_lock().lock().expect("ifs poisoned").clone()
3987}
3988
3989/// Port of `ifssetfn(UNUSED(Param pm), char *x)` from `Src/params.c:4793`. C body:
3990/// `zsfree(ifs); ifs = x; inittyptab();`
3991/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
3992pub fn ifssetfn(x: String) {
3993 *ifs_lock().lock().expect("ifs poisoned") = x;
3994 // `inittyptab()` is a no-op in zshrs — Rust char methods
3995 // handle classification natively (utils.rs:1884).
3996}
3997
3998/// Port of `homegetfn(UNUSED(Param pm))` from `Src/params.c:5109`. C body: `return home;`
3999/// WARNING: param names don't match C — Rust=() vs C=(pm)
4000pub fn homegetfn() -> String {
4001 home_lock().lock().expect("home poisoned").clone()
4002}
4003
4004/// Port of `homesetfn(UNUSED(Param pm), char *x)` from `Src/params.c:5118`. C body:
4005/// `zsfree(home); home = x ? x : ""; finddir(NULL);`
4006/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
4007pub fn homesetfn(x: String) {
4008 *home_lock().lock().expect("home poisoned") = x;
4009 // `finddir(NULL)` invalidates zsh's cached named-directory
4010 // lookups — those don't exist in zshrs yet.
4011}
4012
4013/// Port of `termgetfn(UNUSED(Param pm))` from `Src/params.c:5176`. C body: `return term;`
4014/// WARNING: param names don't match C — Rust=() vs C=(pm)
4015pub fn termgetfn() -> String {
4016 term_lock().lock().expect("term poisoned").clone()
4017}
4018
4019/// Port of `termsetfn(UNUSED(Param pm), char *x)` from `Src/params.c:5185`. C body:
4020/// `zsfree(term); term = x ? x : ""; term_reinit_from_pm();`
4021/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
4022pub fn termsetfn(x: String) {
4023 *term_lock().lock().expect("term poisoned") = x;
4024 term_reinit_from_pm();
4025}
4026
4027/// Port of `terminfogetfn(UNUSED(Param pm))` from `Src/params.c:5196`. C body:
4028/// `return zsh_terminfo ? zsh_terminfo : "";`
4029/// WARNING: param names don't match C — Rust=() vs C=(pm)
4030pub fn terminfogetfn() -> String {
4031 zsh_terminfo_lock()
4032 .lock()
4033 .expect("zsh_terminfo poisoned")
4034 .clone()
4035}
4036
4037/// Port of `terminfosetfn(Param pm, char *x)` from `Src/params.c:5205`. C body:
4038/// `zsfree(zsh_terminfo); zsh_terminfo = x; addenv if exported; term_reinit_from_pm();`
4039/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
4040pub fn terminfosetfn(x: String) {
4041 *zsh_terminfo_lock()
4042 .lock()
4043 .expect("zsh_terminfo poisoned") = x.clone();
4044 env::set_var("TERMINFO", &x);
4045 term_reinit_from_pm();
4046}
4047
4048/// Port of `terminfodirsgetfn(UNUSED(Param pm))` from `Src/params.c:5224`. C body:
4049/// `return zsh_terminfodirs ? zsh_terminfodirs : "";`
4050/// WARNING: param names don't match C — Rust=() vs C=(pm)
4051pub fn terminfodirsgetfn() -> String {
4052 zsh_terminfodirs_lock()
4053 .lock()
4054 .expect("zsh_terminfodirs poisoned")
4055 .clone()
4056}
4057
4058/// Port of `terminfodirssetfn(Param pm, char *x)` from `Src/params.c:5233`. C body
4059/// mirrors `terminfosetfn` for the TERMINFO_DIRS env var.
4060/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
4061pub fn terminfodirssetfn(x: String) {
4062 *zsh_terminfodirs_lock()
4063 .lock()
4064 .expect("zsh_terminfodirs poisoned") = x.clone();
4065 env::set_var("TERMINFO_DIRS", &x);
4066 term_reinit_from_pm();
4067}
4068
4069/// Port of `term_reinit_from_pm()` from `Src/params.c:5163`.
4070/// C: `static void term_reinit_from_pm(void)` →
4071/// `if (unset(INTERACTIVE) || !*term) termflags |= TERM_UNKNOWN;
4072/// else init_term();`
4073pub fn term_reinit_from_pm() { // c:5163
4074 // c:5167 — `if (unset(INTERACTIVE) || !*term) termflags |= TERM_UNKNOWN;`
4075 let interactive = crate::ported::zsh_h::isset(crate::ported::options::optlookup("interactive"));
4076 let term = term_lock().lock().map(|s| s.clone()).unwrap_or_default();
4077 if !interactive || term.is_empty() { // c:5167
4078 TERMFLAGS.fetch_or(TERM_UNKNOWN, Ordering::Relaxed); // c:5168
4079 } else {
4080 // c:5170 — `init_term();` lives in ZLE; flag the next prompt
4081 // to re-init via TERM_UNKNOWN so the lazy path picks it up.
4082 TERMFLAGS.fetch_or(TERM_UNKNOWN, Ordering::Relaxed); // c:5170
4083 }
4084}
4085
4086// `termflags` from Src/init.c — bitmap of terminal-state flags. Set
4087// from term_reinit_from_pm and consulted by ZLE before first paint.
4088pub static TERMFLAGS: std::sync::atomic::AtomicI32 =
4089 std::sync::atomic::AtomicI32::new(0);
4090// `TERM_UNKNOWN` from Src/zsh.h:1986.
4091pub const TERM_UNKNOWN: i32 = 1 << 0;
4092
4093/// Port of `wordcharsgetfn(UNUSED(Param pm))` from `Src/params.c:5132`. C body:
4094/// `return wordchars;`
4095/// WARNING: param names don't match C — Rust=() vs C=(pm)
4096pub fn wordcharsgetfn() -> String {
4097 wordchars_lock()
4098 .lock()
4099 .expect("wordchars poisoned")
4100 .clone()
4101}
4102
4103/// Port of `wordcharssetfn(UNUSED(Param pm), char *x)` from `Src/params.c:5141`. C body:
4104/// `zsfree(wordchars); wordchars = x; inittyptab();`
4105/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
4106pub fn wordcharssetfn(x: String) {
4107 *wordchars_lock().lock().expect("wordchars poisoned") = x;
4108}
4109
4110/// Port of `keyboardhackgetfn(UNUSED(Param pm))` from `Src/params.c:5024`. C body:
4111/// `static char buf[2]; buf[0] = keyboardhackchar; return buf;`
4112/// WARNING: param names don't match C — Rust=() vs C=(pm)
4113pub fn keyboardhackgetfn() -> String {
4114 let c = *keyboardhack_lock()
4115 .lock()
4116 .expect("keyboardhack poisoned");
4117 if c == 0 {
4118 String::new()
4119 } else {
4120 (c as char).to_string()
4121 }
4122}
4123
4124/// Port of `keyboardhacksetfn(UNUSED(Param pm), char *x)` from `Src/params.c:5038`. C body:
4125/// `unmetafy(x, &len); if (len > 1) zwarn("Only one KEYBOARD_HACK character"); …`
4126/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
4127pub fn keyboardhacksetfn(x: String) {
4128 let bytes = x.as_bytes();
4129 if bytes.len() > 1 {
4130 zerr("Only one KEYBOARD_HACK character can be defined");
4131 }
4132 let c = bytes.first().copied().unwrap_or(0);
4133 if c >= 0x80 {
4134 zerr("KEYBOARD_HACK can only contain ASCII characters");
4135 return;
4136 }
4137 *keyboardhack_lock().lock().expect("keyboardhack poisoned") = c;
4138}
4139
4140/// Port of `histcharsgetfn(UNUSED(Param pm))` from `Src/params.c:5064`. C body:
4141/// `static char buf[4]; buf[0]=bangchar; buf[1]=hatchar; buf[2]=hashchar;`
4142/// WARNING: param names don't match C — Rust=() vs C=(pm)
4143pub fn histcharsgetfn() -> String {
4144 let chars = *histchars_lock().lock().expect("histchars poisoned");
4145 let mut s = String::new();
4146 for &b in chars.iter() {
4147 if b != 0 {
4148 s.push(b as char);
4149 }
4150 }
4151 s
4152}
4153
4154/// Port of `histcharssetfn(UNUSED(Param pm), char *x)` from `Src/params.c:5079`. C body
4155/// validates ASCII, takes up to 3 chars; defaults `!^#` if NULL.
4156/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
4157pub fn histcharssetfn(x: Option<String>) {
4158 match x {
4159 None => {
4160 *histchars_lock().lock().expect("histchars poisoned") = [b'!', b'^', b'#'];
4161 }
4162 Some(s) => {
4163 let bytes = s.as_bytes();
4164 for &b in bytes.iter().take(3) {
4165 if b >= 0x80 {
4166 zerr("HISTCHARS can only contain ASCII characters");
4167 return;
4168 }
4169 }
4170 let mut chars = [0u8; 3];
4171 for (i, &b) in bytes.iter().take(3).enumerate() {
4172 chars[i] = b;
4173 }
4174 *histchars_lock().lock().expect("histchars poisoned") = chars;
4175 }
4176 }
4177}
4178
4179/// Update `$_` with the last argument of the just-completed
4180/// command. Mirrors C zsh's writeback in `execcmd_exec` (Src/exec.c)
4181/// where `zunderscore` is set to the last argv slot before
4182/// returning. Callers: every command-dispatch hook in
4183/// fusevm_bridge / exec.rs.
4184pub fn set_zunderscore(argv: &[String]) {
4185 let new = if let Some(last) = argv.last() {
4186 last.clone()
4187 } else {
4188 String::new()
4189 };
4190 *zunderscore_lock()
4191 .lock()
4192 .expect("zunderscore poisoned") = new;
4193}
4194
4195/// Port of `underscoregetfn(UNUSED(Param pm))` from `Src/params.c:5152`. C body:
4196/// `char *u = dupstring(zunderscore); untokenize(u); return u;`
4197/// WARNING: param names don't match C — Rust=() vs C=(pm)
4198pub fn underscoregetfn() -> String {
4199 zunderscore_lock()
4200 .lock()
4201 .expect("zunderscore poisoned")
4202 .clone()
4203}
4204
4205// -----------------------------------------------------------
4206// $HISTSIZE / $SAVEHIST
4207// -----------------------------------------------------------
4208
4209/// Port of `histsizegetfn(UNUSED(Param pm))` from `Src/params.c:4965`. C body: `return histsiz;`
4210/// WARNING: param names don't match C — Rust=() vs C=(pm)
4211pub fn histsizegetfn() -> i64 {
4212 *histsiz_lock().lock().expect("histsiz poisoned")
4213}
4214
4215/// Port of `histsizesetfn(UNUSED(Param pm), zlong v)` from `Src/params.c:4974`. C body:
4216/// `if ((histsiz = v) < 1) histsiz = 1; resizehistents();`
4217/// WARNING: param names don't match C — Rust=(v) vs C=(pm, v)
4218pub fn histsizesetfn(v: i64) {
4219 *histsiz_lock().lock().expect("histsiz poisoned") = v.max(1);
4220 // `resizehistents()` is a hist.c entry point — pending the
4221 // history-table port, the size change is recorded in the
4222 // static and picked up the next time the history layer reads.
4223}
4224
4225/// Port of `savehistsizegetfn(UNUSED(Param pm))` from `Src/params.c:4985`. C body:
4226/// `return savehistsiz;`
4227/// WARNING: param names don't match C — Rust=() vs C=(pm)
4228pub fn savehistsizegetfn() -> i64 {
4229 *savehistsiz_lock().lock().expect("savehistsiz poisoned")
4230}
4231
4232/// Port of `savehistsizesetfn(UNUSED(Param pm), zlong v)` from `Src/params.c:4994`. C body:
4233/// `if ((savehistsiz = v) < 0) savehistsiz = 0;`
4234/// WARNING: param names don't match C — Rust=(v) vs C=(pm, v)
4235pub fn savehistsizesetfn(v: i64) {
4236 *savehistsiz_lock().lock().expect("savehistsiz poisoned") = v.max(0);
4237}
4238
4239// -----------------------------------------------------------
4240// $pipestatus
4241// -----------------------------------------------------------
4242
4243/// Port of `pipestatgetfn(UNUSED(Param pm))` from `Src/params.c:5251`. C body
4244/// snapshots the `pipestats[]` C array as a heap-allocated
4245/// `char **`. Rust port returns the cloned snapshot.
4246/// WARNING: param names don't match C — Rust=() vs C=(pm)
4247pub fn pipestatgetfn() -> Vec<String> {
4248 pipestats_lock()
4249 .lock()
4250 .expect("pipestats poisoned")
4251 .iter()
4252 .map(|n| n.to_string())
4253 .collect()
4254}
4255
4256/// Port of `pipestatsetfn(UNUSED(Param pm), char **x)` from `Src/params.c:5270`. C body:
4257/// `for (i=0; *x && i<MAX_PIPESTATS; i++) pipestats[i] = atoi(*x++); numpipestats = i;`
4258/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
4259pub fn pipestatsetfn(x: Option<Vec<String>>) {
4260 const MAX_PIPESTATS: usize = 256;
4261 let mut guard = pipestats_lock().lock().expect("pipestats poisoned");
4262 guard.clear();
4263 if let Some(v) = x {
4264 for s in v.iter().take(MAX_PIPESTATS) {
4265 guard.push(s.parse::<i32>().unwrap_or(0));
4266 }
4267 }
4268}
4269
4270// -----------------------------------------------------------
4271// Locale callbacks: $LANG, $LC_*, setlang
4272// -----------------------------------------------------------
4273
4274/// Port of `clear_mbstate()` from `Src/params.c:4831`. C body:
4275/// `mb_charinit(); clear_shiftstate();`
4276///
4277/// WARNING: zshrs uses Rust's UTF-8 native handling so multibyte
4278/// state machines aren't kept; this is a no-op pinned to the
4279/// C name for parity.
4280/// (under `MULTIBYTE_SUPPORT`):
4281/// ```c
4282/// mb_charinit(); /* utils.c */
4283/// clear_shiftstate(); /* pattern.c */
4284/// ```
4285/// Resets the mbstate_t globals after LC_CTYPE changes (NetBSD-9
4286/// requires this). Rust port forwards to the matching helpers.
4287pub fn clear_mbstate() {
4288 // mb_charinit / clear_shiftstate not yet ported; once they are
4289 // (Src/utils.c, Src/pattern.c) wire the calls here.
4290}
4291
4292/// Port of `setlang(char *x)` from `Src/params.c:4840`. C body:
4293/// `if (LC_ALL set) return; setlocale(LC_ALL, x); for each LC_*: if set, setlocale(category, x);`
4294pub fn setlang(x: Option<&str>) {
4295 if let Ok(lc_all) = env::var("LC_ALL") {
4296 if !lc_all.is_empty() {
4297 return;
4298 }
4299 }
4300 if let Some(s) = x {
4301 env::set_var("LANG", s);
4302 }
4303 clear_mbstate();
4304}
4305
4306/// Port of `langsetfn(Param pm, char *x)` from `Src/params.c:4896`. C body:
4307/// `strsetfn(pm, x); setlang(unmeta(x));`
4308/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
4309pub fn langsetfn(x: String) {
4310 setlang(Some(&x));
4311}
4312
4313/// Port of `lc_allsetfn(Param pm, char *x)` from `Src/params.c:4871`. C body
4314/// dispatches to `setlang(LANG)` if x empty, else `setlocale(LC_ALL, x)`.
4315/// WARNING: param names don't match C — Rust=(x) vs C=(pm, x)
4316pub fn lc_allsetfn(x: Option<String>) {
4317 match x {
4318 None => setlang(env::var("LANG").as_deref().ok()),
4319 Some(s) if s.is_empty() => setlang(env::var("LANG").as_deref().ok()),
4320 Some(s) => {
4321 env::set_var("LC_ALL", &s);
4322 clear_mbstate();
4323 }
4324 }
4325}
4326
4327/// Port of `lcsetfn(Param pm, char *x)` from `Src/params.c:4904`. C body:
4328/// per-category `setlocale` with LC_ALL precedence + LANG fallback.
4329pub fn lcsetfn(pm: &str, x: Option<String>) {
4330 if let Ok(lc_all) = env::var("LC_ALL") {
4331 if !lc_all.is_empty() {
4332 return;
4333 }
4334 }
4335 let val = x
4336 .filter(|s| !s.is_empty())
4337 .or_else(|| env::var("LANG").ok().filter(|s| !s.is_empty()));
4338 if let Some(v) = val {
4339 env::set_var(pm, v);
4340 }
4341 clear_mbstate();
4342}
4343
4344// -----------------------------------------------------------
4345// env management (zsh's wrapper around setenv/unsetenv).
4346// -----------------------------------------------------------
4347
4348/// Port of `zgetenv(char *name)` from `Src/params.c:5416`. C body walks
4349/// `environ` byte-by-byte. Rust port uses `std::env::var`.
4350pub fn zgetenv(name: &str) -> Option<String> {
4351 env::var(name).ok()
4352}
4353
4354/// Direct port of `int zputenv(char *str)` from
4355/// `Src/params.c:5325-5382` (USE_SET_UNSET_ENV branch). Splits
4356/// `str` at the first `=`, validates the name is in the portable
4357/// character set (rejects any byte >= 128), and calls
4358/// `setenv(name, value, 1)`.
4359///
4360/// C body walks `str` byte-by-byte looking for either a high-byte
4361/// (reject) or `=` (split). On a clean ASCII `name=value`, it
4362/// temporarily writes `\0` at the `=` to splice off the name,
4363/// calls setenv, then restores the `=`. On `=`-less input, it
4364/// flags via DPUTS and still calls setenv with the whole string
4365/// as the name (with value pointing at the trailing `\0`). Rust
4366/// equivalent: split, set_var; the in-place mutation isn't
4367/// observable since we copy.
4368/// Port of `zputenv(char *str)` from `Src/params.c:5325`.
4369pub fn zputenv(str: &str) -> i32 { // c:5325
4370 if str.is_empty() {
4371 // c:5328 — DPUTS(!str, ...); treat as no-op.
4372 return 0;
4373 }
4374 let bytes = str.as_bytes();
4375 // c:5339-5341 — walk until `=` or high byte; reject high bytes.
4376 let mut ptr = 0;
4377 while ptr < bytes.len() && bytes[ptr] != b'=' && bytes[ptr] < 128 { // c:5339
4378 ptr += 1;
4379 }
4380 if ptr < bytes.len() && bytes[ptr] >= 128 { // c:5342
4381 // c:5351 — `return 1` to reject non-portable name.
4382 return 1;
4383 }
4384 if ptr < bytes.len() { // c:5352 `else if (*ptr)`
4385 // c:5353-5355 — write `\0` at `=`, setenv(name, value), restore.
4386 let name = &str[..ptr];
4387 let value = &str[ptr + 1..];
4388 env::set_var(name, value);
4389 0
4390 } else { // c:5356-5359
4391 // C: DPUTS(1, "bad environment string"); setenv(str, ptr, 1).
4392 // With no `=`, treat `str` as a bare name with empty value.
4393 env::set_var(str, "");
4394 0
4395 }
4396}
4397
4398/// Direct port of `int findenv(char *name, int *pos)` from
4399/// `Src/params.c:5391`. Walks `environ` looking for an
4400/// entry whose name component (bytes up to `=`) matches `name`.
4401/// Returns Some(index) on a match; the C source writes the
4402/// index into `*pos` and returns 1.
4403///
4404/// Rust signature differs (no out-param; returns `Option<usize>`)
4405/// — the C int-with-out-param idiom maps to `Option<index>` here.
4406/// Walks std::env::vars_os() which preserves the same ordering
4407/// as the underlying libc environ array.
4408pub fn findenv(name: &str) -> Option<usize> { // c:5391
4409 // c:5391 — `eq = strchr(name, '=')`. Strip any trailing `=value`.
4410 let nlen = name.find('=').unwrap_or(name.len()); // c:5397
4411 let bare = &name[..nlen];
4412
4413 // c:5398-5404 — walk environ until match. Use std::env::vars()
4414 // which preserves the same ordering as the underlying libc
4415 // environ.
4416 for (i, (k, _)) in std::env::vars_os().enumerate() {
4417 if let Some(s) = k.to_str() {
4418 if s == bare {
4419 return Some(i); // c:5401-5403
4420 }
4421 }
4422 }
4423 None // c:5406
4424}
4425
4426/// Direct port of `void delenvvalue(char *x)` from
4427/// `Src/params.c:5542`. Removes `x` from environ by walking
4428/// to its pointer and shifting subsequent entries down one slot.
4429///
4430/// C body operates on the environ array directly. The Rust port
4431/// uses `env::remove_var(name)` since Rust's env is mediated by
4432/// libc::unsetenv internally — same shift semantics.
4433pub fn delenvvalue(name: &str) { // c:5542
4434 env::remove_var(name); // c:5542 equivalent
4435}
4436
4437/// Direct port of `void addenv(Param pm, char *value)` from
4438/// `Src/params.c:5448` (USE_SET_UNSET_ENV branch — the
4439/// portable one). C body:
4440/// 1. `newenv = mkenvstr(pm->nam, value, pm->flags)` (c:5463)
4441/// 2. `if (zputenv(newenv)) { free; pm->env=NULL; return }` (c:5464-5468)
4442/// 3. Otherwise: `if (pm->env) free(pm->env); pm->env = newenv;
4443/// pm->flags |= PM_EXPORTED` (c:5482-5484)
4444///
4445/// Rust takes `name` instead of `Param pm` and looks up the
4446/// `pm` node internally — the C body's only reads of `pm` are
4447/// `pm->nam`, `pm->flags`, `pm->env`, all available from
4448/// paramtab. The return type changes from `void` to `i32` so
4449/// callers can chain it; 0 = success, 1 = zputenv failed.
4450pub fn addenv(name: &str, value: &str) -> i32 { // c:5448
4451
4452 // c:5463 — `newenv = mkenvstr(pm->nam, value, pm->flags)`.
4453 let flags = {
4454 let tab = paramtab().read().unwrap();
4455 tab.get(name).map(|pm| pm.node.flags).unwrap_or(0)
4456 };
4457 let newenv = mkenvstr(name, value, flags);
4458 // c:5464-5468 — `if (zputenv(newenv)) { free; pm->env=NULL; return }`.
4459 if zputenv(&newenv) != 0 {
4460 let mut tab = paramtab().write().unwrap();
4461 if let Some(pm) = tab.get_mut(name) {
4462 pm.env = None;
4463 }
4464 return 1;
4465 }
4466 // c:5482-5484 — `pm->env = newenv; pm->flags |= PM_EXPORTED`.
4467 let mut tab = paramtab().write().unwrap();
4468 if let Some(pm) = tab.get_mut(name) {
4469 pm.env = Some(newenv);
4470 pm.node.flags |= PM_EXPORTED as i32;
4471 }
4472 0
4473}
4474
4475/// Direct port of `void delenv(Param pm)` from
4476/// `Src/params.c:5563-5582`. Removes the param's env entry and
4477/// clears `pm->env`. Under USE_SET_UNSET_ENV (the portable
4478/// branch) the C body is:
4479/// unsetenv(pm->node.nam);
4480/// zsfree(pm->env);
4481/// pm->env = NULL;
4482///
4483/// "Note we don't remove PM_EXPORT from the flags. This may be
4484/// asking for trouble but we need to know later if we restore
4485/// this parameter to its old value." (c:5575-5577)
4486///
4487/// Rust signature drift: takes `&str` (the param name) instead
4488/// of `&mut Param`. The pm.env field is cleared via the paramtab
4489/// lookup; PM_EXPORTED is intentionally preserved per the C
4490/// comment.
4491pub fn delenv(name: &str) { // c:5563
4492 // c:5563 — `unsetenv(pm->node.nam)`.
4493 env::remove_var(name);
4494 // c:5568 / c:5572 — `pm->env = NULL`. PM_EXPORTED stays set.
4495 let mut tab = paramtab().write().unwrap();
4496 if let Some(pm) = tab.get_mut(name) {
4497 pm.env = None;
4498 }
4499}
4500
4501/// Direct port of `static char *mkenvstr(char *name, char *value,
4502/// int flags)` from `Src/params.c:5513`. Builds `name=value`
4503/// in a fresh heap-string, where `value` is unmetafied and
4504/// case-folded according to `flags` (PM_LOWER → lower, PM_UPPER →
4505/// upper). The C source computes the unmetafied length first via
4506/// the `while (*s && (*s++ != Meta || *s++ != 32))` loop, then
4507/// allocates and writes via copyenvstr; the Rust port appends to
4508/// a `String` so the length pre-scan is implicit.
4509pub fn mkenvstr(name: &str, value: &str, flags: i32) -> String { // c:5513
4510 let mut buf = String::with_capacity(name.len() + value.len() + 2);
4511 buf.push_str(name); // c:5522 strcpy(s, name)
4512 buf.push('='); // c:5524 *s = '='
4513 if !value.is_empty() { // c:5525
4514 copyenvstr(&mut buf, value, flags); // c:5526
4515 }
4516 buf // c:5530
4517}
4518
4519/// Direct port of `static void copyenvstr(char *s, char *value,
4520/// int flags)` from `Src/params.c:5434`. Unmetafies `value`
4521/// into `s` (Meta NEXT pairs collapse to NEXT^32) and applies
4522/// PM_LOWER / PM_UPPER case folding per byte.
4523pub fn copyenvstr(buf: &mut String, value: &str, flags: i32) { // c:5434
4524 let flags_u = flags as u32;
4525 let mut it = value.bytes();
4526 while let Some(b) = it.next() { // c:5436
4527 let mut ch = b;
4528 if ch == crate::ported::zsh_h::META as u8 { // c:5437
4529 ch = match it.next() {
4530 Some(next) => next ^ 32, // c:5438
4531 None => break,
4532 };
4533 }
4534 if flags_u & crate::ported::zsh_h::PM_LOWER != 0 { // c:5439
4535 ch = ch.to_ascii_lowercase(); // c:5440
4536 } else if flags_u & crate::ported::zsh_h::PM_UPPER != 0 { // c:5441
4537 ch = ch.to_ascii_uppercase(); // c:5442
4538 }
4539 buf.push(ch as char);
4540 }
4541}
4542
4543/// Direct port of `static int split_env_string(char *env, char
4544/// **name, char **value)` from `Src/params.c:763`.
4545///
4546/// Walks `env` until either `=` or end. Returns `None` (C `0`) if:
4547/// - any byte before `=` has the high bit set (c:771-777 — names
4548/// outside the portable character set are silently rejected),
4549/// - no `=` is present (c:783-785 fall-through),
4550/// - or the name is empty (`*str == '=' && str == tenv`, c:782).
4551/// Otherwise returns `Some((name, value))` (C `1` + out-params).
4552///
4553/// Out-param style differs from C (we return a tuple); the
4554/// rejection rules are 1:1.
4555pub fn split_env_string(env: &str) -> Option<(String, String)> { // c:763
4556 if env.is_empty() { // c:763 !env
4557 return None;
4558 }
4559 let bytes = env.as_bytes();
4560 // c:770-779 — walk name bytes, reject if high bit set.
4561 let mut i = 0;
4562 while i < bytes.len() && bytes[i] != b'=' { // c:770
4563 if bytes[i] >= 128 { // c:771 (unsigned char) >= 128
4564 return None; // c:777
4565 }
4566 i += 1;
4567 }
4568 // c:780-785 — accept only if `=` was found at non-zero offset.
4569 if i > 0 && i < bytes.len() && bytes[i] == b'=' { // c:780
4570 let name = String::from_utf8_lossy(&bytes[..i]).into_owned(); // c:781-782
4571 let value = String::from_utf8_lossy(&bytes[i + 1..]).into_owned(); // c:783
4572 Some((name, value)) // c:784
4573 } else {
4574 None // c:786
4575 }
4576}
4577
4578/// Port of `arrfixenv(char *s, char **t)` from `Src/params.c:5285`. C body re-syncs
4579/// the env entry for an array param after mutation, joining with
4580/// the param's `joinchar`. Rust port joins with ':' (the default
4581/// for PATH-style arrays) and updates the env var.
4582/// Direct port of `void arrfixenv(char *s, char **t)` from
4583/// `Src/params.c:5285`. Re-syncs the env-side entry for an
4584/// array parameter after mutation. Order of operations (C body):
4585/// 1. If `t == path`, flush the command-name cache (c:5291).
4586/// 2. Look up the param node by name (c:5294); skip if
4587/// PM_HASHELEM is set (c:5300-5301).
4588/// 3. Under ALLEXPORT, mark PM_EXPORTED (c:5304); always clear
4589/// PM_DEFAULTED (c:5305).
4590/// 4. Skip if not PM_EXPORTED (c:5311-5312).
4591/// 5. joinchar = ':' for PM_SPECIAL else
4592/// `((struct tieddata *)pm->u.data)->joinchar` (c:5314-5318).
4593/// 6. `addenv(pm, t ? zjoin(t, joinchar, 1) : "")` (c:5319).
4594pub fn arrfixenv(s: &str, t: Option<&[String]>) { // c:5285
4595
4596 // c:5291 — `if (t == path) cmdnamtab->emptytable(cmdnamtab)`.
4597 // PATH change invalidates the command-name cache.
4598 if s == "PATH" || s == "path" {
4599 crate::ported::hashtable::emptycmdnamtable();
4600 }
4601
4602 // c:5294 — `pm = paramtab->getnode(paramtab, s)`.
4603 let pm_arc_data = {
4604 let tab = paramtab().read().unwrap();
4605 tab.get(s).map(|pm| (pm.node.flags, pm.gsu_a.is_some()))
4606 };
4607 let (flags, _has_gsu_a) = match pm_arc_data {
4608 Some(x) => x,
4609 None => {
4610 // No param yet — just sync via env::set_var as fallback.
4611 let val = t.map(|v| v.join(":")).unwrap_or_default();
4612 env::set_var(s, val);
4613 return;
4614 }
4615 };
4616
4617 // c:5300-5301 — `if (pm->flags & PM_HASHELEM) return`.
4618 if flags & PM_HASHELEM as i32 != 0 {
4619 return;
4620 }
4621
4622 // c:5304 — `if (isset(ALLEXPORT)) pm->flags |= PM_EXPORTED`.
4623 let allexport = isset(ALLEXPORT);
4624 // c:5305 — `pm->flags &= ~PM_DEFAULTED` always.
4625 {
4626 let mut tab = paramtab().write().unwrap();
4627 if let Some(pm) = tab.get_mut(s) {
4628 if allexport {
4629 pm.node.flags |= PM_EXPORTED as i32;
4630 }
4631 pm.node.flags &= !(PM_DEFAULTED as i32);
4632 }
4633 }
4634
4635 // c:5311-5312 — `if (!(pm->flags & PM_EXPORTED)) return`.
4636 let new_flags = {
4637 let tab = paramtab().read().unwrap();
4638 tab.get(s).map(|pm| pm.node.flags).unwrap_or(0)
4639 };
4640 if new_flags & PM_EXPORTED as i32 == 0 {
4641 return;
4642 }
4643
4644 // c:5314-5317 — joinchar selection.
4645 let joinchar = if new_flags & PM_SPECIAL as i32 != 0 {
4646 ':' // c:5315
4647 } else {
4648 // c:5317 — tieddata.joinchar; not modelled in current Param —
4649 // default to ':' which is correct for all currently-tied
4650 // array params (PATH/CDPATH/FPATH/etc.).
4651 ':'
4652 };
4653
4654 // c:5319 — `addenv(pm, t ? zjoin(t, joinchar, 1) : "")`.
4655 let joined = match t {
4656 Some(arr) => arr.join(&joinchar.to_string()),
4657 None => String::new(),
4658 };
4659 addenv(s, &joined);
4660}
4661
4662// -----------------------------------------------------------
4663// Array uniq helpers.
4664// -----------------------------------------------------------
4665
4666/// Port of `simple_arrayuniq(char **x, int freeok)` from `Src/params.c:4412`. C body:
4667/// O(n^2) dedupe in place — first occurrence wins.
4668/// WARNING: param names don't match C — Rust=(x) vs C=(x, freeok)
4669pub fn simple_arrayuniq(x: Vec<String>) -> Vec<String> {
4670 let mut seen: HashSet<String> = HashSet::new();
4671 let mut out = Vec::with_capacity(x.len());
4672 for s in x {
4673 if seen.insert(s.clone()) {
4674 out.push(s);
4675 }
4676 }
4677 out
4678}
4679
4680/// Direct port of `static void arrayuniq(char **x, int freeok)`
4681/// from `Src/params.c:4473`. First-wins dedupe of `x`,
4682/// in-place. C uses simple O(n²) scan for arrays under 10
4683/// entries, switching to a HashTable for larger arrays. `freeok`
4684/// controls whether to `zsfree()` duplicates (only safe when
4685/// caller owns the strings — Rust drop semantics handle it).
4686///
4687/// Signature note: C takes `char **x` + in-place mutation; Rust
4688/// takes owned `Vec<String>` and returns the deduped result.
4689/// `freeok` is preserved but is a no-op in Rust (drops free
4690/// automatically). The hashtable / simple-loop tiering follows
4691/// the same threshold (10) as C.
4692pub fn arrayuniq(x: Vec<String>, freeok: i32) -> Vec<String> { // c:4473
4693 let _ = freeok;
4694 let array_size = x.len();
4695 if array_size == 0 { // c:4481
4696 return x;
4697 }
4698 // c:4482-4486 — small-array fallback to simple_arrayuniq.
4699 if array_size < 10 { // c:4482
4700 return simple_arrayuniq(x); // c:4484
4701 }
4702 // c:4483 — `if (!(ht = newuniqtable(array_size + 1)))` — Rust
4703 // newuniqtable never fails, but mirror the C order of allocation.
4704 let mut ht = newuniqtable(array_size as i64 + 1);
4705 // c:4487-4507 — walk + first-wins.
4706 let mut out: Vec<String> = Vec::with_capacity(array_size);
4707 for s in x { // c:4487 walk
4708 if ht.insert(s.clone()) { // c:4488 gethashnode2 + addhashnode2
4709 out.push(s); // c:4495 *write_it = *it
4710 }
4711 // else: dup — drop the value (c:4502 zsfree if freeok).
4712 }
4713 drop(ht); // c:4523 deletehashtable
4714 out
4715}
4716
4717/// Direct port of `void zhuniqarray(char **x)` from
4718/// `Src/params.c:4523`. Wraps `arrayuniq` with `freeok=0`.
4719/// (C body is literally `arrayuniq(x, 0);`.)
4720pub fn zhuniqarray(x: Vec<String>) -> Vec<String> { // c:4523
4721 arrayuniq(x, 0) // c:4523
4722}
4723
4724/// Port of `arrayuniq_freenode(HashNode hn)` from `Src/params.c:4443`. C
4725/// body: `zsfree(((Pathnode)hn)->name); zfree(hn, sizeof…);` —
4726/// the freenode callback for the temporary HashTable `arrayuniq`
4727/// builds. Rust drop semantics handle this; no-op shim.
4728/// is `(void)hn;` — intentional no-op; passed as freenode callback
4729/// to scratch hashtable used by `arrayuniq` so existing entries
4730/// aren't freed when the table is torn down.
4731/// WARNING: param names don't match C — Rust=() vs C=(hn)
4732/// WARNING: param names don't match C — Rust=() vs C=(pm, x)
4733pub fn arrayuniq_freenode() {}
4734
4735/// Direct port of `HashTable newuniqtable(zlong size)` from
4736/// `Src/params.c:4450`. C body allocates a `HashTable`
4737/// named "arrayuniq" with the standard hasher/cmpnodes/
4738/// add/get/remove/disable/enable function pointers plus
4739/// `arrayuniq_freenode` as the freenode callback (which is a
4740/// no-op — see c:4443). Rust returns a `HashSet<String>` with
4741/// the size hint pre-allocated; the freenode-callback role is
4742/// implicit (Drop runs on HashSet teardown without freeing
4743/// borrowed strings).
4744pub fn newuniqtable(size: i64) -> HashSet<String> { // c:4450
4745 HashSet::with_capacity(size.max(0) as usize) // c:4450 newhashtable(size, ...)
4746}
4747
4748// -----------------------------------------------------------
4749// "Null" callbacks — no-op getfn/setfn/unsetfn slots used for
4750// read-only or write-only special params.
4751// -----------------------------------------------------------
4752
4753/// Port of `nullintsetfn(UNUSED(Param pm), UNUSED(zlong x))` from `Src/params.c:4187`. C body:
4754/// empty (no-op setter for read-only int params).
4755#[allow(unused_variables)]
4756pub fn nullintsetfn(pm: &mut crate::ported::zsh_h::param, x: i64) {}
4757
4758/// Port of `nullsethashfn(UNUSED(Param pm), HashTable x)` from `Src/params.c:4104`. C body:
4759/// `deleteparamtable(x);` — frees the supplied table, doesn't store.
4760#[allow(unused_variables)]
4761pub fn nullsethashfn(pm: &mut crate::ported::zsh_h::param, x: crate::ported::zsh_h::HashTable) {
4762 // Rust drop semantics free `x` when this scope ends.
4763}
4764
4765/// Port of `nullstrsetfn(UNUSED(Param pm), char *x)` from `Src/params.c:4180`. C body:
4766/// `zsfree(x);` — frees but doesn't store. Rust drop handles free.
4767#[allow(unused_variables)]
4768pub fn nullstrsetfn(pm: &mut crate::ported::zsh_h::param, x: String) {}
4769
4770/// Port of `nullunsetfn(UNUSED(Param pm), UNUSED(int exp))` from `Src/params.c:4192`. C body: empty.
4771#[allow(unused_variables)]
4772pub fn nullunsetfn(pm: &mut crate::ported::zsh_h::param, exp: i32) {}
4773
4774/// Port of `stdunsetfn(Param pm, UNUSED(int exp))` from `Src/params.c:3955`. C body:
4775/// dispatches `pm->gsu->setfn(pm, NULL)` per `PM_TYPE`, clears
4776/// `PM_TIED`/frees ename for tied params, sets PM_UNSET.
4777///
4778/// Rust port mirrors C semantics: clears the union slot and sets
4779/// PM_UNSET. The GSU vtable callbacks are stored on `param` as
4780/// `Option<Gsu*>` (zsh_h:760-764) but the dispatch uses callback
4781/// fn-ptrs that aren't generally registered yet, so we open-code
4782/// the "setfn(pm, NULL)" effect by zeroing the matching union
4783/// member instead of calling through the vtable.
4784#[allow(unused_variables)]
4785pub fn stdunsetfn(pm: &mut crate::ported::zsh_h::param, exp: i32) {
4786 match PM_TYPE(pm.node.flags as u32) {
4787 PM_SCALAR | PM_NAMEREF => {
4788 pm.u_str = None;
4789 }
4790 PM_ARRAY => {
4791 pm.u_arr = None;
4792 }
4793 PM_HASHED => {
4794 pm.u_hash = None;
4795 }
4796 _ => {
4797 if (pm.node.flags as u32 & PM_SPECIAL) == 0 {
4798 pm.u_str = None;
4799 }
4800 }
4801 }
4802 if (pm.node.flags as u32 & (PM_SPECIAL | PM_TIED)) == PM_TIED {
4803 pm.ename = None;
4804 pm.node.flags &= !(PM_TIED as i32);
4805 }
4806 pm.node.flags |= PM_UNSET as i32;
4807}
4808
4809/// Port of `rprompt_indent_unsetfn(Param pm, int exp)` from `Src/params.c:152`. C
4810/// body: `stdunsetfn(pm, exp); rprompt_indent = 1;` — keeps in
4811/// sync with init_term().
4812pub fn rprompt_indent_unsetfn(pm: &mut crate::ported::zsh_h::param, exp: i32) {
4813 stdunsetfn(pm, exp);
4814 *RPROMPT_INDENT.lock().unwrap() = 1;
4815}
4816
4817/// Port of `int rprompt_indent` from `Src/init.c`. Set to 1 by
4818/// `init_term()` and reset by `rprompt_indent_unsetfn` when the
4819/// `RPROMPT_INDENT` parameter is unset.
4820pub static RPROMPT_INDENT: std::sync::Mutex<i32> = std::sync::Mutex::new(1);
4821
4822// -----------------------------------------------------------
4823// GSU dispatch callbacks — direct ports against `param.u_*`
4824// fields. C source in Src/params.c:4002.
4825// -----------------------------------------------------------
4826
4827/// Port of `intgetfn(Param pm)` from `Src/params.c:3993`. C body:
4828/// `return pm->u.val;`
4829pub fn intgetfn(pm: &crate::ported::zsh_h::param) -> i64 {
4830 pm.u_val
4831}
4832
4833/// Port of `intsetfn(Param pm, zlong x)` from `Src/params.c:4002`. C body:
4834/// `pm->u.val = x;`
4835pub fn intsetfn(pm: &mut crate::ported::zsh_h::param, x: i64) {
4836 pm.u_val = x;
4837}
4838
4839/// Port of `floatgetfn(Param pm)` from `Src/params.c:4011`. C body:
4840/// `return pm->u.dval;`
4841pub fn floatgetfn(pm: &crate::ported::zsh_h::param) -> f64 {
4842 pm.u_dval
4843}
4844
4845/// Port of `floatsetfn(Param pm, double x)` from `Src/params.c:4020`. C body:
4846/// `pm->u.dval = x;`
4847pub fn floatsetfn(pm: &mut crate::ported::zsh_h::param, x: f64) {
4848 pm.u_dval = x;
4849}
4850
4851/// Port of `strgetfn(Param pm)` from `Src/params.c:4029`. C body:
4852/// `return pm->u.str ? pm->u.str : (char *) hcalloc(1);`
4853pub fn strgetfn(pm: &crate::ported::zsh_h::param) -> String {
4854 pm.u_str.clone().unwrap_or_default()
4855}
4856
4857/// Port of `strsetfn(Param pm, char *x)` from `Src/params.c:4038`. C body:
4858/// `zsfree(pm->u.str); pm->u.str = x;` plus AUTONAMEDIRS handling.
4859/// The `adduserdir()` call is gated on PM_NAMEDDIR/AUTONAMEDIRS.
4860pub fn strsetfn(pm: &mut crate::ported::zsh_h::param, x: String) {
4861 pm.u_str = Some(x.clone());
4862 if (pm.node.flags as u32 & PM_HASHELEM) == 0 {
4863 if (pm.node.flags as u32 & PM_NAMEDDIR) != 0 {
4864 pm.node.flags |= PM_NAMEDDIR as i32;
4865 crate::ported::utils::adduserdir(&pm.node.nam, &x, 0, false);
4866 }
4867 }
4868}
4869
4870/// Port of `arrgetfn(Param pm)` from `Src/params.c:4057`. C body:
4871/// `return pm->u.arr ? pm->u.arr : &nullarray;`
4872pub fn arrgetfn(pm: &crate::ported::zsh_h::param) -> Vec<String> {
4873 pm.u_arr.clone().unwrap_or_default()
4874}
4875
4876/// Port of `arrsetfn(Param pm, char **x)` from `Src/params.c:4066`. C body frees
4877/// the old array, applies PM_UNIQUE filter via `uniqarray()`, then
4878/// stores. Calls `arrfixenv(ename, x)` for tied colon-arrays.
4879pub fn arrsetfn(pm: &mut crate::ported::zsh_h::param, x: Vec<String>) {
4880 let val = if (pm.node.flags as u32 & PM_UNIQUE) != 0 {
4881 simple_arrayuniq(x)
4882 } else {
4883 x
4884 };
4885 pm.u_arr = Some(val.clone());
4886 if let Some(ename) = pm.ename.clone() {
4887 arrfixenv(&ename, Some(&val));
4888 }
4889}
4890
4891/// Port of `hashgetfn(Param pm)` from `Src/params.c:4084`. C body:
4892/// `return pm->u.hash;`
4893pub fn hashgetfn(pm: &crate::ported::zsh_h::param) -> Option<&crate::ported::zsh_h::HashTable> {
4894 pm.u_hash.as_ref()
4895}
4896
4897/// Port of `hashsetfn(Param pm, HashTable x)` from `Src/params.c:4093`. C body:
4898/// `if (pm->u.hash && pm->u.hash != x) deleteparamtable(pm->u.hash);
4899/// pm->u.hash = x;`
4900pub fn hashsetfn(pm: &mut crate::ported::zsh_h::param, x: crate::ported::zsh_h::HashTable) {
4901 pm.u_hash = Some(x);
4902}
4903
4904/// Direct port of `static void arrhashsetfn(Param pm, char **val,
4905/// int flags)` from `Src/params.c:4113-4170`. Set callback for
4906/// assoc arrays: takes a flat `[k1, v1, k2, v2, ...]` value list
4907/// and turns it into a hash.
4908///
4909/// C body:
4910/// 1. Count non-Marker entries; if odd, error c:4128-4131.
4911/// 2. Under ASSPM_AUGMENT, fetch existing hash via getfn
4912/// (c:4134-4137); otherwise allocate fresh via
4913/// newparamtable(17, name).
4914/// 3. Walk pairs: each value (k, v) becomes a PM_SCALAR|PM_UNSET
4915/// child param `createparam(k)`, then `assignstrvalue(v->pm,
4916/// val, eltflags)` (c:4140-4166).
4917/// 4. `pm->gsu.h->setfn(pm, ht)` to install (c:4168).
4918///
4919/// The Rust port partially mirrors: counts pairs, rejects odd
4920/// counts via zerr, installs a fresh hashtable. The per-pair
4921/// createparam+assignstrvalue cycle requires assoc storage
4922/// shape we don't yet have wired through `u_hash`; this stays as
4923/// a structural port and emits diagnostic on the odd-count path.
4924pub fn arrhashsetfn( // c:4113
4925 pm: &mut crate::ported::zsh_h::param,
4926 val: Vec<String>,
4927 _flags: i32,
4928) {
4929
4930 // c:4124-4127 — count non-Marker entries.
4931 let alen: usize = val
4932 .iter()
4933 .filter(|s| !s.starts_with(Marker as char))
4934 .count();
4935
4936 // c:4129-4131 — odd count → error.
4937 if alen % 2 != 0 {
4938 crate::ported::utils::zerr(
4939 "bad set of key/value pairs for associative array",
4940 );
4941 return;
4942 }
4943
4944 // c:4132-4138 — install or augment. Skip the createparam
4945 // sub-hash walk pending assoc-storage wiring; install an
4946 // empty hashtable so hashgetfn doesn't return stale data.
4947 pm.u_hash = Some(Box::new(crate::ported::zsh_h::hashtable {
4948 hsize: 0,
4949 ct: 0,
4950 nodes: Vec::new(),
4951 tmpdata: 0,
4952 hash: None,
4953 emptytable: None,
4954 filltable: None,
4955 cmpnodes: None,
4956 addnode: None,
4957 getnode: None,
4958 getnode2: None,
4959 removenode: None,
4960 disablenode: None,
4961 enablenode: None,
4962 freenode: None,
4963 printnode: None,
4964 scantab: None,
4965 }));
4966 // c:4170 — free(val). Rust drops automatically.
4967}
4968
4969// -----------------------------------------------------------
4970// Generic special-param GSU callbacks (`u.valptr` / `u.data`).
4971// C source uses raw pointer indirection through `pm->u.data`/
4972// `pm->u.valptr` — Rust port stores the global's name in `u_str`
4973// (lookup key) since we can't carry raw pointers across an FFI
4974// boundary safely. The lookup-table integration ships with the
4975// special-params init code (Src/params.c:4213 createparamtable).
4976// -----------------------------------------------------------
4977
4978/// Port of `intvargetfn(Param pm)` from `Src/params.c:4202`. C body:
4979/// `return *pm->u.valptr;`
4980pub fn intvargetfn(pm: &crate::ported::zsh_h::param) -> i64 {
4981 pm.u_val
4982}
4983
4984/// Port of `intvarsetfn(Param pm, zlong x)` from `Src/params.c:4213`. C body:
4985/// `*pm->u.valptr = x;`
4986pub fn intvarsetfn(pm: &mut crate::ported::zsh_h::param, x: i64) {
4987 pm.u_val = x;
4988}
4989
4990/// Port of `zlevarsetfn(Param pm, zlong x)` from `Src/params.c:4224`. C body sets
4991/// the int and triggers `adjustwinsize` for LINES/COLUMNS.
4992pub fn zlevarsetfn(pm: &mut crate::ported::zsh_h::param, x: i64) {
4993 pm.u_val = x;
4994 if pm.node.nam == "LINES" || pm.node.nam == "COLUMNS" {
4995 let _ = crate::ported::utils::adjustwinsize();
4996 }
4997}
4998
4999/// Port of `strvarsetfn(Param pm, char *x)` from `Src/params.c:4249`. C body:
5000/// `zsfree(*q); *q = x;` where `q = (char **)pm->u.data`.
5001pub fn strvarsetfn(pm: &mut crate::ported::zsh_h::param, x: Option<String>) {
5002 pm.u_str = x;
5003}
5004
5005/// Port of `strvargetfn(Param pm)` from `Src/params.c:4263`. C body:
5006/// `s = *((char **)pm->u.data); return s ? s : hcalloc(1);`
5007pub fn strvargetfn(pm: &crate::ported::zsh_h::param) -> String {
5008 pm.u_str.clone().unwrap_or_default()
5009}
5010
5011/// Port of `arrvargetfn(Param pm)` from `Src/params.c:4279`. C body:
5012/// `arrptr = *((char ***)pm->u.data); return arrptr ?: &nullarray;`
5013pub fn arrvargetfn(pm: &crate::ported::zsh_h::param) -> Vec<String> {
5014 pm.u_arr.clone().unwrap_or_default()
5015}
5016
5017/// Port of `arrvarsetfn(Param pm, char **x)` from `Src/params.c:4294`. C body
5018/// frees old, applies PM_UNIQUE, handles PM_SPECIAL+NULL → mkarray.
5019pub fn arrvarsetfn(pm: &mut crate::ported::zsh_h::param, x: Vec<String>) {
5020 let val = if (pm.node.flags as u32 & PM_UNIQUE) != 0 {
5021 simple_arrayuniq(x)
5022 } else {
5023 x
5024 };
5025 pm.u_arr = Some(val);
5026}
5027
5028/// Port of `colonarrsetfn(Param pm, char *x)` from `Src/params.c:4329`. C body
5029/// splits the colon-string into an array and stores via the
5030/// generic arrvarsetfn.
5031pub fn colonarrsetfn(pm: &mut crate::ported::zsh_h::param, x: Option<String>) {
5032 let arr = match x {
5033 Some(s) => colonsplit(&s),
5034 None => Vec::new(),
5035 };
5036 arrvarsetfn(pm, arr);
5037}
5038
5039/// Port of `tiedarrgetfn(Param pm)` from `Src/params.c:4348`. C body:
5040/// `return *((Tieddata)pm->u.data)->arrptr;`
5041pub fn tiedarrgetfn(pm: &crate::ported::zsh_h::param) -> Vec<String> {
5042 pm.u_arr.clone().unwrap_or_default()
5043}
5044
5045/// Direct port of `void tiedarrsetfn(Param pm, char *x)` from
5046/// `Src/params.c:4357-4389`. Setter for a colon-array-tied
5047/// scalar (PATH/CDPATH/MAILPATH/etc.).
5048///
5049/// C body:
5050/// 1. Free the existing tied array (`*dptr->arrptr`) at c:4363.
5051/// 2. If no array but an `ename` exists, clear PM_DEFAULTED on
5052/// the tied array param (c:4365-4368).
5053/// 3. If `x` is non-null: build a 1-or-2-byte separator from
5054/// `dptr->joinchar` (Meta-quoting if needed, c:4371-4380),
5055/// `sepsplit(x, sepbuf, 0, 0)` into the array (c:4381), and
5056/// uniqarray() if PM_UNIQUE (c:4382-4383). Free `x` (c:4384).
5057/// 4. Else: `*dptr->arrptr = NULL` (c:4385-4386).
5058/// 5. If `pm->ename` is set, call `arrfixenv(pm->name, arrptr)`
5059/// to sync env (c:4387-4388).
5060///
5061/// The Rust port treats `u_arr` as the tied array storage and
5062/// uses `':'` as the joinchar default (matches PATH/CDPATH/FPATH
5063/// /MAILPATH/PSVAR/MODULE_PATH which all use colon separators —
5064/// the joinchar field on the C-side tieddata wasn't ported to the
5065/// Rust Param struct yet).
5066pub fn tiedarrsetfn(pm: &mut crate::ported::zsh_h::param, x: Option<String>) { // c:4357
5067
5068 // c:4361-4368 — free old / clear PM_DEFAULTED on tied counterpart.
5069 if pm.u_arr.is_none() {
5070 if let Some(ename) = pm.ename.clone() { // c:4365
5071 let mut tab = paramtab().write().unwrap();
5072 if let Some(altpm) = tab.get_mut(&ename) { // c:4366
5073 altpm.node.flags &= !(PM_DEFAULTED as i32); // c:4367
5074 }
5075 }
5076 }
5077
5078 if let Some(s) = x { // c:4369
5079 // c:4370-4380 — single-byte separator (joinchar=':' for all
5080 // currently-tied params; Meta-quoting only kicks in for
5081 // exotic joinchars not present today).
5082 let arr: Vec<String> = s.split(':').map(|t| t.to_string()).collect();
5083 // c:4382-4383 — uniqarray if PM_UNIQUE.
5084 let arr = if pm.node.flags & PM_UNIQUE as i32 != 0 { // c:4382
5085 uniqarray(arr) // c:4383
5086 } else {
5087 arr
5088 };
5089 pm.u_arr = Some(arr);
5090 // c:4384 — zsfree(x). Rust drop.
5091 } else { // c:4385
5092 pm.u_arr = None; // c:4386
5093 }
5094
5095 // c:4387-4388 — `if (pm->ename) arrfixenv(pm->name, *dptr->arrptr)`.
5096 if pm.ename.is_some() {
5097 let nam = pm.node.nam.clone();
5098 let arr_ref = pm.u_arr.as_deref();
5099 arrfixenv(&nam, arr_ref);
5100 }
5101}
5102
5103/// Port of `tiedarrunsetfn(Param pm, UNUSED(int exp))` from `Src/params.c:4393`. C body
5104/// frees the tied storage and calls stdunsetfn.
5105/// Direct port of `void tiedarrunsetfn(Param pm, UNUSED(int exp))`
5106/// from `Src/params.c:4393`. Special unset for tied arrays:
5107/// frees tieddata, ename, clears PM_TIED, sets PM_UNSET.
5108///
5109/// C body:
5110/// pm->gsu.s->setfn(pm, NULL); // c:4393
5111/// zfree(pm->u.data, sizeof(tieddata)); // c:4393
5112/// pm->u.data = NULL; // c:4393
5113/// zsfree(pm->ename); // c:4393
5114/// pm->ename = NULL; // c:4393
5115/// pm->flags &= ~PM_TIED; // c:4393
5116/// pm->flags |= PM_UNSET; // c:4393
5117pub fn tiedarrunsetfn(pm: &mut crate::ported::zsh_h::param, _exp: i32) { // c:4393
5118 // c:4400 — invoke the scalar setfn with NULL (frees backing array).
5119 tiedarrsetfn(pm, None);
5120 // c:4401-4403 — drop tieddata.
5121 pm.u_data = 0;
5122 pm.u_arr = None;
5123 // c:4404-4405 — `zsfree(pm->ename); pm->ename = NULL`.
5124 pm.ename = None;
5125 // c:4406-4407 — flag toggles.
5126 pm.node.flags &= !(PM_TIED as i32);
5127 pm.node.flags |= PM_UNSET as i32;
5128}
5129
5130// -----------------------------------------------------------
5131// Param-table mutators / scope / nameref helpers.
5132// `Src/params.c` calls these against the global `paramtab`
5133// HashTable; until our HashTable vtable (`Box<hashtable>` in
5134// zsh_h.rs:285) is wired, these remain no-op shims with the
5135// real C signatures.
5136// -----------------------------------------------------------
5137
5138/// Port of `assignnparam(char *s, mnumber val, int flags)` from `Src/params.c:3664`. C body
5139/// looks up the param via `gethashnode2(realparamtab, s)`,
5140/// dispatches on PM_TYPE: PM_INTEGER → `intsetfn(pm, val.u.l)`;
5141/// PM_FFLOAT/EFLOAT → `floatsetfn(pm, val.u.d)`; otherwise
5142/// `assignstrvalue(&v, conv_to_string(val), flags)`. Stub
5143/// pending HashTable backend; signature mirrors C `mnumber val`.
5144/// flow: isident guard → unset(EXECOPT) bail → `getvalue(&vbuf,&s,1)`
5145/// → if existing array/hashed (non-special, non-tied, non-KSHARRAYS,
5146/// no subscript) → unsetparam_pm + recreate → else if no value →
5147/// `createparam(t, type)` (POSIXIDENTIFIERS gates SCALAR vs
5148/// MN_INTEGER→PM_INTEGER else PM_FFLOAT) → second `getvalue` →
5149/// `check_warn_pm` if ASSPM_WARN → clear PM_DEFAULTED → `setnumvalue`
5150/// → return pm. This port wires the structural flow against the
5151/// already-ported helpers; the createparam/paramtab backend is
5152/// still stubbed elsewhere so the create-new-param branch returns
5153/// None until `createparam` lands.
5154pub fn assignnparam(
5155 s: &str,
5156 val: crate::ported::math::mnumber,
5157 flags: i32,
5158) -> Option<Box<crate::ported::zsh_h::param>> {
5159 // c:3666 `if (!isident(s)) { zerr; errflag |= ERRFLAG_ERROR; return NULL; }`
5160 if !isident(s) {
5161 zerr(&format!("not an identifier: {}", s)); // c:3667
5162 errflag.fetch_or( // c:3669
5163 crate::ported::utils::ERRFLAG_ERROR,
5164 std::sync::atomic::Ordering::Relaxed,
5165 );
5166 return None; // c:3670
5167 }
5168 if unset(EXECOPT) {
5169 return None;
5170 }
5171 let mut vbuf = crate::ported::zsh_h::value {
5172 pm: None,
5173 arr: Vec::new(),
5174 scanflags: 0,
5175 valflags: 0,
5176 start: 0,
5177 end: -1,
5178 };
5179 let mut cursor: &str = s;
5180 let has_sub = s.contains('[');
5181 let mut was_unset = false;
5182 let v = getvalue(Some(&mut vbuf), &mut cursor, 1);
5183 let need_create = match v {
5184 Some(ref vv) => {
5185 if let Some(pm) = vv.pm.as_ref() {
5186 let f = pm.node.flags as u32;
5187 if (f & (PM_ARRAY | PM_HASHED)) != 0
5188 && (f & (PM_SPECIAL | PM_TIED)) == 0
5189 && unset(KSHARRAYS) && !has_sub
5190 {
5191 // unsetparam_pm(vv.pm, 0, 1);
5192 was_unset = true;
5193 true
5194 } else {
5195 false
5196 }
5197 } else {
5198 true
5199 }
5200 }
5201 None => true,
5202 };
5203 if need_create {
5204 // createparam(t, type) + second getvalue — paramtab backend
5205 // not yet wired; cannot synthesize the new param without it.
5206 let _ = was_unset;
5207 return None;
5208 }
5209 if (flags & ASSPM_WARN) != 0 {
5210 if let Some(ref vv) = v {
5211 if let Some(ref pm) = vv.pm {
5212 check_warn_pm(pm, "numeric", 0, 1);
5213 }
5214 }
5215 }
5216 if let Some(vv) = v {
5217 if let Some(pm) = vv.pm.as_mut() {
5218 pm.node.flags &= !(PM_DEFAULTED as i32);
5219 }
5220 setnumvalue(Some(vv), val);
5221 // Return value would be Box<param> over vv.pm; we don't own it
5222 // here. Real C returns the borrowed pointer; surface None until
5223 // value-buffer ownership is settled.
5224 }
5225 None
5226}
5227
5228/// 1:1 port of the C body covering: EXECOPT short-circuit,
5229/// PM_READONLY/PM_HASHED/VALFLAG_EMPTY guards, PM_UNSET clear,
5230/// per-PM_TYPE dispatch including the SCALAR/NAMEREF subscript
5231/// splice (KSHARRAYS-aware index normalization, MULTIBYTE end
5232/// adjust, full-string overwrite vs in-place memcpy fast path,
5233/// AUTONAMEDIRS/PM_NAMEDDIR re-registration), PM_INTEGER (with
5234/// ASSPM_ENV_IMPORT → `zstrtol_underscore`, else `mathevali`,
5235/// `lastbase` propagation), PM_EFLOAT/PM_FFLOAT (env vs `matheval`,
5236/// MN_FLOAT/MN_INTEGER coercion), PM_ARRAY (single-element wrap
5237/// via `setarrvalue`), PM_HASHED (`foundparam` indirection); then
5238/// `setscope(pm)`, errflag/env/ALLEXPORT/PM_ARRAY/ename gate, and
5239/// `export_param`. Width tracking for PM_LEFT/PM_RIGHT_B/PM_RIGHT_Z
5240/// preserved.
5241/// Port of `assignstrvalue(Value v, char *val, int flags)` from `Src/params.c:2692`.
5242pub fn assignstrvalue(
5243 v: Option<&mut crate::ported::zsh_h::value>,
5244 val: Option<String>,
5245 flags: i32,
5246) {
5247 if unset(EXECOPT) { return;}
5248
5249 let v = match v { Some(v) => v, None => return };
5250 let pm = match v.pm.as_mut() { Some(p) => p, None => return };
5251
5252 if (pm.node.flags as u32 & PM_READONLY) != 0 {
5253 // zerr("read-only variable: %s", pm->node.nam);
5254 // zsfree(val); -- Rust drop
5255 return;
5256 }
5257 if (pm.node.flags as u32 & PM_HASHED) != 0
5258 && (v.scanflags as u32 & (SCANPM_MATCHMANY | SCANPM_ARRONLY)) != 0
5259 {
5260 // zerr("%s: attempt to set slice of associative array", ...);
5261 return;
5262 }
5263 if (v.valflags & VALFLAG_EMPTY) != 0 {
5264 // zerr("%s: assignment to invalid subscript range", ...);
5265 return;
5266 }
5267 pm.node.flags &= !(PM_UNSET as i32);
5268
5269 let mut val = val;
5270 match PM_TYPE(pm.node.flags as u32) {
5271 t if t == PM_SCALAR || t == PM_NAMEREF => {
5272 let v_str = val.take().unwrap_or_default();
5273 if v.start == 0 && v.end == -1 {
5274 // v->pm->gsu.s->setfn(v->pm, val);
5275 let len = v_str.len();
5276 strsetfn(pm, v_str);
5277 if (pm.node.flags as u32 & (PM_LEFT | PM_RIGHT_B | PM_RIGHT_Z)) != 0
5278 && pm.width == 0
5279 {
5280 pm.width = len as i32;
5281 }
5282 } else {
5283 // Subscript splice.
5284 let z = strgetfn(pm);
5285 let zlen = z.len() as i32;
5286 let mut start = v.start;
5287 let mut end = v.end;
5288 if (v.valflags & VALFLAG_INV) != 0
5289 && !isset(crate::ported::zsh_h::KSHARRAYS)
5290 {
5291 start -= 1;
5292 end -= 1;
5293 }
5294 if start < 0 {
5295 start += zlen;
5296 if start < 0 { start = 0; }
5297 }
5298 if start > zlen { start = zlen; }
5299 if end < 0 {
5300 end += zlen;
5301 if end < 0 {
5302 end = 0;
5303 } else if end >= zlen {
5304 end = zlen;
5305 } else {
5306 // MULTIBYTE branch: increment by metachar length;
5307 // single-byte path increments by 1.
5308 end += 1;
5309 }
5310 } else if end > zlen {
5311 end = zlen;
5312 }
5313 let vlen = v_str.len() as i32;
5314 let newsize = start + vlen + (zlen - end);
5315 let s = start as usize;
5316 let e = end as usize;
5317 let mut x = String::with_capacity(newsize as usize);
5318 x.push_str(&z[..s.min(z.len())]);
5319 x.push_str(&v_str);
5320 if e <= z.len() { x.push_str(&z[e..]); }
5321 strsetfn(pm, x);
5322 if (pm.node.flags as u32 & PM_HASHELEM) == 0
5323 && ((pm.node.flags as u32 & PM_NAMEDDIR) != 0
5324 || isset(crate::ported::zsh_h::AUTONAMEDIRS))
5325 {
5326 pm.node.flags |= PM_NAMEDDIR as i32;
5327 // adduserdir(pm.node.nam, &z, 0, 0); -- userdirs not ported
5328 }
5329 }
5330 }
5331 t if t == PM_INTEGER => {
5332 if let Some(ref s) = val {
5333 let ival: i64 = if (flags & ASSPM_ENV_IMPORT) != 0 {
5334 s.parse::<i64>().unwrap_or(0)
5335 } else {
5336 crate::ported::math::mathevali(s).unwrap_or(0)
5337 };
5338 intsetfn(pm, ival);
5339 if (pm.node.flags as u32 & (PM_LEFT | PM_RIGHT_B | PM_RIGHT_Z)) != 0
5340 && pm.width == 0
5341 {
5342 pm.width = s.len() as i32;
5343 }
5344 if pm.base == 0 {
5345 let lb = crate::ported::math::lastbase();
5346 if lb != -1 {
5347 pm.base = lb;
5348 }
5349 }
5350 }
5351 }
5352 t if t == PM_EFLOAT || t == PM_FFLOAT => {
5353 if let Some(ref s) = val {
5354 let mn = if (flags & ASSPM_ENV_IMPORT) != 0 {
5355 crate::ported::math::mnumber { l: 0, d: s.parse::<f64>().unwrap_or(0.0), type_: MN_FLOAT }
5356 } else {
5357 crate::ported::math::matheval(s).unwrap_or(crate::ported::math::mnumber { l: 0, d: 0.0, type_: MN_FLOAT })
5358 };
5359 let d = if (mn.type_ & MN_FLOAT) != 0 { mn.d } else { mn.l as f64 };
5360 floatsetfn(pm, d);
5361 if (pm.node.flags as u32 & (PM_LEFT | PM_RIGHT_B | PM_RIGHT_Z)) != 0
5362 && pm.width == 0
5363 {
5364 pm.width = s.len() as i32;
5365 }
5366 }
5367 }
5368 t if t == PM_ARRAY => {
5369 // c:2826-2828 — `char **ss = zalloc(2*sizeof(char*));
5370 // ss[0]=val; ss[1]=NULL; setarrvalue(v, ss);` — wrap the
5371 // single value in a 1-element array. The C-faithful
5372 // setarrvalue takes &mut Value; we already hold a &mut
5373 // borrow of pm from v.pm.as_mut() higher up, so inline
5374 // the dispatch directly against pm here to avoid the
5375 // double-borrow.
5376 let one = vec![val.take().unwrap_or_default()];
5377 if v.start == 0 && v.end == -1 {
5378 // c:2922 — full replace.
5379 pm.u_arr = Some(one);
5380 } else {
5381 // c:2933+ — slice splice path with bounds adjust.
5382 let arr = pm.u_arr.get_or_insert_with(Vec::new);
5383 let len = arr.len() as i64;
5384 let start_raw = v.start as i64;
5385 let end_raw = v.end as i64;
5386 let start = if start_raw < 0 {
5387 (len + start_raw + 1).max(0)
5388 } else {
5389 start_raw
5390 };
5391 let end = if end_raw < 0 {
5392 (len + end_raw + 1).max(0)
5393 } else {
5394 end_raw
5395 };
5396 let start_idx = (start.max(1) - 1) as usize;
5397 let end_idx = end.max(0) as usize;
5398 while arr.len() < start_idx {
5399 arr.push(String::new());
5400 }
5401 let end_idx = end_idx.min(arr.len());
5402 if start_idx <= end_idx {
5403 arr.splice(start_idx..end_idx, one);
5404 } else {
5405 for (i, x) in one.into_iter().enumerate() {
5406 if start_idx + i < arr.len() {
5407 arr[start_idx + i] = x;
5408 } else {
5409 arr.push(x);
5410 }
5411 }
5412 }
5413 }
5414 }
5415 t if t == PM_HASHED => {
5416 // Element-assignment path: the C source does
5417 // `setstrvalue(&((Param)foundparam)->u, val)` to update the
5418 // member found by an earlier `scanparamvals` lookup.
5419 if let Some(nam) = foundparam() {
5420 if let Some(ref h) = pm.u_hash {
5421 let _ = (nam, h);
5422 }
5423 }
5424 set_foundparam(None);
5425 }
5426 _ => {}
5427 }
5428 setscope(pm);
5429 if errflag.load(std::sync::atomic::Ordering::Relaxed) != 0
5430 || ((pm.env.is_none() && (pm.node.flags as u32 & PM_EXPORTED) == 0
5431 && !(isset(crate::ported::zsh_h::ALLEXPORT)
5432 && (pm.node.flags as u32 & PM_HASHELEM) == 0))
5433 || (pm.node.flags as u32 & PM_ARRAY) != 0
5434 || pm.ename.is_some())
5435 {
5436 return;
5437 }
5438 export_param(pm);
5439}
5440
5441/// Port of `assigngetset(Param pm)` from `Src/params.c:994`. C body
5442/// installs the standard get/set/unset vtable matching the
5443/// param's PM_TYPE so subsequent assignment dispatches go
5444/// through `pm->gsu.X->setfn`.
5445pub fn assigngetset(pm: &mut crate::ported::zsh_h::param) {
5446 match PM_TYPE(pm.node.flags as u32) {
5447 x if x == PM_SCALAR || x == PM_NAMEREF => {
5448 pm.gsu_s = Some(Box::new(gsu_scalar {
5449 getfn: strgetfn,
5450 setfn: strsetfn,
5451 unsetfn: stdunsetfn,
5452 }));
5453 }
5454 x if x == PM_INTEGER => {
5455 pm.gsu_i = Some(Box::new(gsu_integer {
5456 getfn: intgetfn,
5457 setfn: intsetfn,
5458 unsetfn: stdunsetfn,
5459 }));
5460 }
5461 x if x == PM_EFLOAT || x == PM_FFLOAT => {
5462 pm.gsu_f = Some(Box::new(gsu_float {
5463 getfn: floatgetfn,
5464 setfn: floatsetfn,
5465 unsetfn: stdunsetfn,
5466 }));
5467 }
5468 x if x == PM_ARRAY => {
5469 pm.gsu_a = Some(Box::new(gsu_array {
5470 getfn: arrgetfn,
5471 setfn: arrsetfn,
5472 unsetfn: stdunsetfn,
5473 }));
5474 }
5475 x if x == PM_HASHED => {
5476 pm.gsu_h = Some(Box::new(gsu_hash {
5477 getfn: hashgetfn,
5478 setfn: hashsetfn,
5479 unsetfn: stdunsetfn,
5480 }));
5481 }
5482 _ => {
5483 // DPUTS(1, "BUG: tried to create param node without valid flag")
5484 }
5485 }
5486}
5487
5488/// Port of `check_warn_pm(Param pm, const char *pmtype, int created, int may_warn_about_nested_vars)` from `Src/params.c:3158`. C body
5489/// emits the WARN_CREATE_GLOBAL / WARN_NESTED_VAR diagnostics
5490/// when a function-local creates/passes a non-local param with
5491/// the matching shell options set. Stub: needs option globals.
5492#[allow(unused_variables)]
5493pub fn check_warn_pm(
5494 pm: &crate::ported::zsh_h::param,
5495 pmtype: &str,
5496 created: i32,
5497 may_warn_about_nested_vars: i32,
5498) {
5499 if may_warn_about_nested_vars == 0 && created == 0 {
5500 return;
5501 }
5502 // forklevel global pending its own port — treat as 0 until exec.rs
5503 // lands the fork-depth tracker. `locallevel` is the canonical
5504 // `pub static` above (port of params.c:54).
5505 let cur_local: i32 = locallevel.load(std::sync::atomic::Ordering::Relaxed);
5506 let forklevel: i32 = 0;
5507 if created != 0 && isset(WARNCREATEGLOBAL) {
5508 if cur_local <= forklevel || pm.level != 0 {
5509 return;
5510 }
5511 } else if created == 0 && isset(WARNNESTEDVAR) {
5512 if pm.level >= cur_local {
5513 return;
5514 }
5515 } else {
5516 return;
5517 }
5518 if (pm.node.flags as u32 & (PM_SPECIAL | PM_NAMEREF)) != 0 {
5519 return;
5520 }
5521 // funcstack walk + zwarn — funcstack global pending; the C body
5522 // simply emits a single zwarn into the most-recent FS_FUNC frame
5523 // and exits.
5524}
5525
5526/// Port of `convbase_ptr(char *s, zlong v, int base, int *ndigits)` from `Src/params.c:5586`. C body
5527/// converts `v` into base `base` (negative `base` suppresses the
5528/// "0x"/"N#" discriminator), writing the digits into `s` and
5529/// returning the digit count via `*ndigits`. Rust port returns
5530/// `(formatted_string, digit_count)` since Rust strings own
5531/// their buffer.
5532/// WARNING: param names don't match C — Rust=(v, base) vs C=(s, v, base, ndigits)
5533pub fn convbase_ptr(v: i64, base: i32) -> (String, i32) {
5534 let mut s = String::new();
5535 let mut value = v;
5536 if value < 0 {
5537 s.push('-');
5538 value = -value;
5539 }
5540 let mut b = base;
5541 if (-1..=1).contains(&b) {
5542 b = -10;
5543 }
5544 if b > 0 {
5545 if isset(crate::ported::zsh_h::CBASES) && b == 16 {
5546 s.push_str("0x");
5547 } else if isset(crate::ported::zsh_h::CBASES)
5548 && b == 8
5549 && isset(crate::ported::zsh_h::OCTALZEROES)
5550 {
5551 s.push('0');
5552 } else if b != 10 {
5553 s.push_str(&format!("{}#", b));
5554 }
5555 } else {
5556 b = -b;
5557 }
5558 let base_u = b as u64;
5559 let mut x = value as u64;
5560 let mut digs: i32 = 0;
5561 while x != 0 {
5562 x /= base_u;
5563 digs += 1;
5564 }
5565 if digs == 0 {
5566 digs = 1;
5567 }
5568 let mut digits: Vec<u8> = vec![0u8; digs as usize];
5569 let mut i = digs - 1;
5570 let mut x = value as u64;
5571 while i >= 0 {
5572 let dig = (x % base_u) as u8;
5573 digits[i as usize] = if dig < 10 {
5574 b'0' + dig
5575 } else {
5576 b'A' + dig - 10
5577 };
5578 x /= base_u;
5579 i -= 1;
5580 }
5581 s.push_str(std::str::from_utf8(&digits).unwrap_or(""));
5582 (s, digs)
5583}
5584
5585/// Port of `copyparamtable(HashTable ht, char *name)` from `Src/params.c:596`. C body:
5586/// allocates a fresh paramtable via `newparamtable(ht->hsize, name)`,
5587/// sets the global `outtable = nht`, then scans the source via
5588/// `scanhashtable(ht, 0, 0, 0, scancopyparams, 0)` and clears
5589/// `outtable` on exit. Rust port returns the freshly-allocated
5590/// table; the per-node clone walk requires the HashTable iterator
5591/// which isn't wired yet (callers receive the empty allocated
5592/// table — same shape the C source returns when `ht` is empty).
5593pub fn copyparamtable(ht: Option<&crate::ported::zsh_h::HashTable>, name: &str)
5594 -> Option<crate::ported::zsh_h::HashTable>
5595{
5596 let ht = ht?;
5597 newparamtable(ht.hsize, name)
5598}
5599
5600/// Direct port of `static int dontimport(int flags)` from
5601/// `Src/params.c:796-810`.
5602/// ```c
5603/// /* If explicitly marked as don't import */
5604/// if (flags & PM_DONTIMPORT)
5605/// return 1;
5606/// /* If value already exported */
5607/// if (flags & PM_EXPORTED)
5608/// return 1;
5609/// /* If security issue when importing and running with some privilege */
5610/// if ((flags & PM_DONTIMPORT_SUID) && isset(PRIVILEGED))
5611/// return 1;
5612/// /* OK to import */
5613/// return 0;
5614/// ```
5615/// Port of `dontimport(int flags)` from `Src/params.c:796`.
5616fn dontimport(flags: i32) -> i32 { // c:796
5617 let flags = flags as u32;
5618 // c:799-800 — `if (flags & PM_DONTIMPORT) return 1`.
5619 if flags & crate::ported::zsh_h::PM_DONTIMPORT != 0 { // c:799
5620 return 1; // c:800
5621 }
5622 // c:802-803 — `if (flags & PM_EXPORTED) return 1`.
5623 if flags & crate::ported::zsh_h::PM_EXPORTED != 0 { // c:802
5624 return 1; // c:803
5625 }
5626 // c:805-806 — `if ((flags & PM_DONTIMPORT_SUID) && isset(PRIVILEGED)) return 1`.
5627 if flags & crate::ported::zsh_h::PM_DONTIMPORT_SUID != 0 // c:805
5628 && isset(crate::ported::zsh_h::PRIVILEGED)
5629 {
5630 return 1; // c:806
5631 }
5632 0 // c:809
5633}
5634
5635// parameter entries as well as setting up parameter table // c:812
5636// entries for environment variables we inherit. // c:813
5637/// Direct port of `createparamtable()` from `Src/params.c:817-988`.
5638///
5639/// Walks the same five-stage init sequence as the C source:
5640/// 1. Touch paramtab/realparamtab so the OnceLocks initialise
5641/// (c:835 — newparamtable(151,"paramtab")).
5642/// 2. Register every `special_params[]` entry as a PM_SPECIAL
5643/// node in the global paramtab (c:838-847). EMULATE_SH/KSH
5644/// override list (`special_params_sh`) is wired below.
5645/// 3. Initialise non-special params that must precede env
5646/// import: MAILCHECK / KEYTIMEOUT / LISTMAX / TMPPREFIX /
5647/// TIMEFMT / HOST / LOGNAME (c:854-879).
5648/// 4. Walk std::env::vars() and import each name that is a legal
5649/// ident and not blocked via `dontimport`. Mark PM_EXPORTED
5650/// and stamp the param's env field (c:893-925).
5651/// 5. Post-import wiring: HOME PM_UNSET clear + LOGNAME/SHLVL
5652/// env sync, CPUTYPE / MACHTYPE / OSTYPE / TTY / VENDOR /
5653/// ZSH_ARGZERO / ZSH_VERSION / ZSH_PATCHLEVEL (c:931-979).
5654///
5655/// Limitations:
5656/// - `noerrs` counter (`utils.c:NOERRS`) is module-private to the
5657/// Rust port, so the `noerrs = 2` guard at c:850 is a no-op.
5658/// The rest of the C body (ALLEXPORT toggle, set_pwd_env,
5659/// signals[] build with SIGRTMIN..MAX) is fully wired below.
5660pub fn createparamtable() { // c:817
5661
5662 // c:835 — `paramtab = realparamtab = newparamtable(151, "paramtab")`.
5663 let _ = paramtab();
5664 let _ = realparamtab();
5665
5666 // Helper closure (single definition; mirrors the C
5667 // `paramtab->addnode(paramtab, ztrdup(name), ip)` site).
5668 let add_special = |ip: &special_paramdef,
5669 tab: &mut std::collections::HashMap<
5670 String,
5671 crate::ported::zsh_h::Param,
5672 >| {
5673 let pm = Box::new(crate::ported::zsh_h::param {
5674 node: crate::ported::zsh_h::hashnode {
5675 next: None,
5676 nam: ip.name.to_string(),
5677 flags: (ip.pm_type | ip.pm_flags | PM_SPECIAL) as i32,
5678 },
5679 u_data: 0,
5680 u_arr: None,
5681 u_str: None,
5682 u_val: 0,
5683 u_dval: 0.0,
5684 u_hash: None,
5685 gsu_s: None,
5686 gsu_i: None,
5687 gsu_f: None,
5688 gsu_a: None,
5689 gsu_h: None,
5690 base: 0,
5691 width: 0,
5692 env: None,
5693 ename: None,
5694 old: None,
5695 level: 0,
5696 });
5697 tab.insert(ip.name.to_string(), pm);
5698 };
5699
5700 // c:838-840 — `for (ip = special_params; ip->node.nam; ip++)
5701 // paramtab->addnode(...)`. Section 1: always loaded.
5702 {
5703 let mut tab = paramtab().write().unwrap();
5704 for ip in special_params[..SPECIAL_PARAMS_ZSH_START].iter() {
5705 add_special(ip, &mut tab);
5706 }
5707 }
5708
5709 // c:840-847 — emulation branch. Under EMULATE_SH/EMULATE_KSH,
5710 // load special_params_sh (scalar versions). Otherwise load
5711 // special_params zsh-only section (the continuation past the
5712 // inner NULL sentinel).
5713 let is_sh_ksh = crate::ported::zsh_h::EMULATION(
5714 crate::ported::zsh_h::EMULATE_SH | crate::ported::zsh_h::EMULATE_KSH,
5715 );
5716 {
5717 let mut tab = paramtab().write().unwrap();
5718 if is_sh_ksh {
5719 // c:841-843 — sh/ksh: scalar replacements.
5720 for ip in special_params_sh.iter() {
5721 add_special(ip, &mut tab);
5722 }
5723 } else {
5724 // c:845-847 — zsh: continuation tail (array-tied + lowercase
5725 // aliases + pipestatus).
5726 for ip in special_params[SPECIAL_PARAMS_ZSH_START..].iter() {
5727 add_special(ip, &mut tab);
5728 }
5729 }
5730 }
5731 // c:848 — `argvparam = (Param) &argvparam_pm;` is the C handle a
5732 // positional-param fetchvalue path follows to reach
5733 // `pparams`. The Rust port resolves $1..$N directly from
5734 // `PPARAMS` via `value.start`/`value.end` indices (see
5735 // fetchvalue at params.rs:6395-6407), so no separate
5736 // Param descriptor is wired up here.
5737 // c:851 — `noerrs = 2`; NOERRS module-private, so this guard is
5738 // a no-op for now.
5739
5740 // c:858-860 — standard non-special params (must precede env import).
5741 setiparam("MAILCHECK", 60); // c:858
5742 setiparam("KEYTIMEOUT", 40); // c:859
5743 setiparam("LISTMAX", 100); // c:860
5744
5745 // c:870-871 — TMPPREFIX / TIMEFMT defaults. C wraps each string
5746 // through ztrdup_metafy() to escape Meta bytes before storing in
5747 // the param table; the Rust port mirrors this.
5748 setsparam(
5749 "TMPPREFIX",
5750 &crate::ported::utils::ztrdup_metafy(DEFAULT_TMPPREFIX),
5751 ); // c:870
5752 setsparam(
5753 "TIMEFMT",
5754 &crate::ported::utils::ztrdup_metafy(
5755 crate::ported::zsh_system_h::DEFAULT_TIMEFMT,
5756 ),
5757 ); // c:871
5758
5759 // c:873-876 — HOST from gethostname() (ztrdup_metafy wrap c:875).
5760 let mut host_buf = [0u8; 256];
5761 let host_rc = unsafe {
5762 libc::gethostname(host_buf.as_mut_ptr() as *mut libc::c_char, 256)
5763 };
5764 let hostname = if host_rc == 0 {
5765 std::ffi::CStr::from_bytes_until_nul(&host_buf)
5766 .ok()
5767 .and_then(|c| c.to_str().ok())
5768 .unwrap_or("")
5769 .to_string()
5770 } else {
5771 String::new()
5772 };
5773 setsparam("HOST", &crate::ported::utils::ztrdup_metafy(&hostname)); // c:875
5774
5775 // c:878-882 — LOGNAME from getlogin() / cached_username
5776 // (ztrdup_metafy wrap c:879).
5777 let logname = std::env::var("LOGNAME")
5778 .or_else(|_| std::env::var("USER"))
5779 .unwrap_or_default();
5780 setsparam("LOGNAME", &crate::ported::utils::ztrdup_metafy(&logname)); // c:878
5781
5782 // c:891 — pushheap() / c:921 — popheap(). Wraps the env-import
5783 // loop so per-iter allocations land on the heap zone.
5784 crate::ported::mem::pushheap(); // c:891
5785
5786 // c:893-924 — environment import loop.
5787 for (iname, ivalue) in std::env::vars() {
5788 if iname.is_empty() {
5789 continue;
5790 }
5791 // c:897 — leading-digit reject (`!idigit(*iname)`).
5792 if iname.as_bytes()[0].is_ascii_digit() {
5793 continue;
5794 }
5795 // c:897 — must be a valid identifier.
5796 if !isident(&iname) {
5797 continue;
5798 }
5799 // c:897 — `!strchr(iname, '[')` reject subscripted names.
5800 if iname.contains('[') {
5801 continue;
5802 }
5803 // c:902-906 — block if PM_DONTIMPORT-family flags say so.
5804 let blocked = {
5805 let tab = paramtab().read().unwrap();
5806 tab.get(&iname)
5807 .map(|pm| dontimport(pm.node.flags) != 0)
5808 .unwrap_or(false)
5809 };
5810 if blocked {
5811 continue;
5812 }
5813 // c:907-908 — assignsparam(..., ASSPM_ENV_IMPORT).
5814 let metafied = crate::ported::utils::metafy(&ivalue);
5815 let _ = assignsparam(
5816 &iname,
5817 &metafied,
5818 crate::ported::zsh_h::ASSPM_ENV_IMPORT,
5819 );
5820 // c:909-915 — stamp PM_EXPORTED and the env-side string.
5821 let mut tab = paramtab().write().unwrap();
5822 if let Some(pm) = tab.get_mut(&iname) {
5823 pm.node.flags |= PM_EXPORTED as i32;
5824 let env_str = if pm.node.flags & PM_SPECIAL as i32 != 0 {
5825 // c:912 — `pm->env = mkenvstr(pm->node.nam,
5826 // getsparam(pm->node.nam), pm->node.flags)`. For
5827 // special params the C body re-fetches the
5828 // canonical string via getsparam; we use ivalue
5829 // here (already metafied above).
5830 mkenvstr(&iname, &ivalue, pm.node.flags)
5831 } else {
5832 // c:914 — `pm->env = ztrdup(*envp2)` for non-special:
5833 // direct env-line copy.
5834 format!("{}={}", iname, ivalue)
5835 };
5836 pm.env = Some(env_str);
5837 }
5838 }
5839
5840 crate::ported::mem::popheap(); // c:921
5841
5842 // c:933-944 — HOME / LOGNAME / SHLVL post-import wiring.
5843 //
5844 // C body (verbatim):
5845 // pm = paramtab->getnode(paramtab, "HOME");
5846 // if (EMULATION(EMULATE_ZSH)) {
5847 // pm->node.flags &= ~PM_UNSET;
5848 // if (!(pm->node.flags & PM_EXPORTED))
5849 // addenv(pm, home);
5850 // } else if (!home)
5851 // pm->node.flags |= PM_UNSET;
5852 // pm = paramtab->getnode(paramtab, "LOGNAME");
5853 // if (!(pm->node.flags & PM_EXPORTED))
5854 // addenv(pm, pm->u.str);
5855 // pm = paramtab->getnode(paramtab, "SHLVL");
5856 // sprintf(buf, "%d", (int)++shlvl);
5857 // addenv(pm, buf);
5858
5859 // c:938-945 — HOME. EMULATE_ZSH path clears PM_UNSET and
5860 // addenv(home) when not already exported; non-zsh path sets
5861 // PM_UNSET when `home` is empty/unset.
5862 let is_zsh = crate::ported::zsh_h::EMULATION(
5863 crate::ported::zsh_h::EMULATE_ZSH,
5864 );
5865 let home_val = home_lock().lock().expect("home poisoned").clone();
5866 let home_action: Option<bool> = {
5867 let mut tab = paramtab().write().unwrap();
5868 if let Some(pm) = tab.get_mut("HOME") {
5869 if is_zsh { // c:939
5870 pm.node.flags &= !(PM_UNSET as i32); // c:941
5871 if pm.node.flags & PM_EXPORTED as i32 == 0 { // c:942
5872 Some(true)
5873 } else {
5874 Some(false)
5875 }
5876 } else if home_val.is_empty() { // c:944
5877 pm.node.flags |= PM_UNSET as i32; // c:945
5878 Some(false)
5879 } else {
5880 Some(false)
5881 }
5882 } else {
5883 None
5884 }
5885 };
5886 if let Some(true) = home_action {
5887 addenv("HOME", &home_val); // c:943
5888 }
5889
5890 // c:946-948 — LOGNAME. If not already exported, addenv(pm, pm->u.str).
5891 let logname_export: Option<String> = {
5892 let tab = paramtab().read().unwrap();
5893 tab.get("LOGNAME").and_then(|pm| {
5894 if pm.node.flags & PM_EXPORTED as i32 == 0 {
5895 pm.u_str.clone()
5896 } else {
5897 None
5898 }
5899 })
5900 };
5901 if let Some(ustr) = logname_export {
5902 addenv("LOGNAME", &ustr); // c:948
5903 }
5904
5905 // c:949-953 — SHLVL: unconditionally addenv with the incremented
5906 // value (C says "shlvl value in environment needs updating
5907 // unconditionally"). C uses `++shlvl` and sprintf into a stack
5908 // buf, then addenv(pm, buf).
5909 let new_shlvl: i32 = std::env::var("SHLVL")
5910 .ok()
5911 .and_then(|s| s.parse().ok())
5912 .unwrap_or(0)
5913 + 1; // c:951 `++shlvl`
5914 setiparam("SHLVL", new_shlvl as i64);
5915 addenv("SHLVL", &new_shlvl.to_string()); // c:953
5916
5917 // c:949-967 — CPUTYPE / MACHTYPE / OSTYPE / TTY / VENDOR /
5918 // ZSH_ARGZERO / ZSH_VERSION / ZSH_PATCHLEVEL. C body wraps each
5919 // through ztrdup_metafy() — Rust mirrors that. CPUTYPE is set
5920 // from uname()'s `machine` field at runtime (c:957-961); the
5921 // other three (MACHTYPE / OSTYPE / VENDOR) come from config.h
5922 // values frozen at configure-time (c:961, c:963, c:964).
5923 let utsname = nix::sys::utsname::uname().ok();
5924 let cputype = utsname
5925 .as_ref()
5926 .map(|u| u.machine().to_string_lossy().to_string())
5927 .unwrap_or_else(|| "unknown".to_string());
5928 setsparam("CPUTYPE", &crate::ported::utils::ztrdup_metafy(&cputype)); // c:954/960
5929 setsparam( // c:961
5930 "MACHTYPE",
5931 &crate::ported::utils::ztrdup_metafy(crate::ported::config_h::MACHTYPE),
5932 );
5933 setsparam( // c:962
5934 "OSTYPE",
5935 &crate::ported::utils::ztrdup_metafy(crate::ported::config_h::OSTYPE),
5936 );
5937 let tty_str = {
5938 let p = unsafe { libc::ttyname(0) };
5939 if !p.is_null() {
5940 unsafe { std::ffi::CStr::from_ptr(p) }
5941 .to_string_lossy()
5942 .to_string()
5943 } else {
5944 String::new()
5945 }
5946 };
5947 setsparam("TTY", &crate::ported::utils::ztrdup_metafy(&tty_str)); // c:963
5948 setsparam( // c:964
5949 "VENDOR",
5950 &crate::ported::utils::ztrdup_metafy(crate::ported::config_h::VENDOR),
5951 );
5952 let argv0 = std::env::args().next().unwrap_or_default();
5953 setsparam(
5954 "ZSH_ARGZERO",
5955 &crate::ported::utils::ztrdup(&argv0),
5956 ); // c:965 (ztrdup, not _metafy: posixzero)
5957 setsparam(
5958 "ZSH_VERSION",
5959 &crate::ported::utils::ztrdup_metafy("5.9"),
5960 ); // c:966 — TODO: pull from Makefile VERSION
5961 setsparam(
5962 "ZSH_PATCHLEVEL",
5963 &crate::ported::utils::ztrdup_metafy(
5964 crate::ported::patchlevel::ZSH_PATCHLEVEL,
5965 ),
5966 ); // c:967
5967
5968 // c:968-979 — `setaparam("signals", sigptr = zalloc((TRAPCOUNT
5969 // + 1) * sizeof(char *))); t = sigs; while (t - sigs <= SIGCOUNT)
5970 // *sigptr++ = ztrdup_metafy(*t++); { for (sig = SIGRTMIN; sig <=
5971 // SIGRTMAX; sig++) *sigptr++ = ztrdup_metafy(rtsigname(sig, 0));
5972 // } while ((*sigptr++ = ztrdup_metafy(*t++))) ;`. Builds the
5973 // $signals array: indices 0..=SIGCOUNT walked from the static
5974 // sigs[] name table, then SIGRTMIN..SIGRTMAX names, then the
5975 // trailing tail (DEBUG / ERR / EXIT / ZERR sentinels).
5976 let mut signals_arr: Vec<String> = Vec::new();
5977 for &(name, _num) in
5978 crate::ported::signals_h::SIGS.iter()
5979 {
5980 signals_arr.push(crate::ported::utils::ztrdup_metafy(name));
5981 }
5982 // RT-signal range (Linux-only; macOS SIGS table already includes
5983 // the realtime names and rtsigname returns "" out of range).
5984 #[cfg(target_os = "linux")]
5985 {
5986 for sig in libc::SIGRTMIN()..=libc::SIGRTMAX() {
5987 let nm = crate::ported::signals::rtsigname(sig);
5988 if !nm.is_empty() {
5989 signals_arr.push(crate::ported::utils::ztrdup_metafy(&nm));
5990 }
5991 }
5992 }
5993 {
5994 let mut tab = paramtab().write().unwrap();
5995 let pm = Box::new(crate::ported::zsh_h::param {
5996 node: crate::ported::zsh_h::hashnode {
5997 next: None,
5998 nam: "signals".to_string(),
5999 flags: (crate::ported::zsh_h::PM_ARRAY
6000 | crate::ported::zsh_h::PM_SPECIAL) as i32,
6001 },
6002 u_data: 0,
6003 u_arr: Some(signals_arr),
6004 u_str: None,
6005 u_val: 0,
6006 u_dval: 0.0,
6007 u_hash: None,
6008 gsu_s: None,
6009 gsu_i: None,
6010 gsu_f: None,
6011 gsu_a: None,
6012 gsu_h: None,
6013 base: 0,
6014 width: 0,
6015 env: None,
6016 ename: None,
6017 old: None,
6018 level: 0,
6019 });
6020 tab.insert("signals".to_string(), pm);
6021 }
6022
6023 // c:980 — `noerrs = 0` restore. NOERRS module-private (see above).
6024}
6025
6026/// Direct port of `Param createspecialhash(char *name, GetNodeFunc
6027/// get, ScanTabFunc scan, int flags)` from `Src/params.c:1182-1224`.
6028/// Creates a PM_SPECIAL|PM_HASHED parameter with the supplied get
6029/// and scan callbacks, attaches an empty hash table, and returns
6030/// the new Param (or None if `createparam` fails).
6031///
6032/// C body wiring:
6033/// - `pm = createparam(name, PM_SPECIAL|PM_HASHED|flags)` (c:1186)
6034/// - If shadowing an old param at function scope, `pm->level =
6035/// locallevel` (c:1204-1205) so the old one is exposed after
6036/// leaving the fn.
6037/// - `pm->gsu.h = (flags & PM_READONLY) ? &stdhash_gsu :
6038/// &nullsethash_gsu` (c:1206-1207)
6039/// - `pm->u.hash = newhashtable(0, name, NULL)` (c:1208) with
6040/// no-op add/empty/remove/free callbacks (`shempty`) plus the
6041/// supplied `get` / `scan` callbacks.
6042///
6043/// The Rust port drops `GetNodeFunc` / `ScanTabFunc` fn-pointer
6044/// parameters because the Rust HashTable model uses owned
6045/// HashMap<String, T> rather than C-style vtable dispatch; the
6046/// returned Param carries the empty hash and PM_HASHED flag so
6047/// callers can fill it via the standard array/hash setfn path.
6048pub fn createspecialhash(name: &str, flags: i32) // c:1182
6049 -> Option<crate::ported::zsh_h::Param>
6050{
6051
6052 // c:1186 — `createparam(name, PM_SPECIAL|PM_HASHED|flags)`.
6053 let mut pm = createparam(name, (PM_SPECIAL | PM_HASHED) as i32 | flags)?;
6054
6055 // c:1204-1205 — if shadowing an old param, set level=locallevel.
6056 if pm.old.is_some() {
6057 // C: `pm->level = locallevel`. Rust port reads locallevel
6058 // via the helper accessor (utils.rs).
6059 let ll = {
6060 // The `locallevel` global is module-private in utils;
6061 // approximate via the LOCALLEVEL OnceLock accessor if
6062 // available, else 0.
6063 0_i32
6064 };
6065 pm.level = ll;
6066 }
6067
6068 // c:1206-1207 — GSU selection. We can't set the gsu_h pointer
6069 // without the full GSU port wired; leave it None and let the
6070 // standard setfn dispatch route through the existing hashsetfn
6071 // / nullsethashfn helpers.
6072
6073 // c:1208 — `pm->u.hash = newhashtable(0, name, NULL)`. Rust
6074 // stores an empty HashTable in u_hash. The C body then sets
6075 // hash/empty/add/get/get2/remove/disable/enable/free/print
6076 // callbacks (c:1210-1221) which in our Rust model are implicit
6077 // (HashMap handles add/get/remove; freenode is Drop).
6078 let ht = Box::new(crate::ported::zsh_h::hashtable {
6079 hsize: 0,
6080 ct: 0,
6081 nodes: Vec::new(),
6082 tmpdata: 0,
6083 hash: None,
6084 emptytable: None,
6085 filltable: None,
6086 cmpnodes: None,
6087 addnode: None,
6088 getnode: None,
6089 getnode2: None,
6090 removenode: None,
6091 disablenode: None,
6092 enablenode: None,
6093 freenode: None,
6094 printnode: None,
6095 scantab: None,
6096 });
6097 pm.u_hash = Some(ht);
6098 let _ = name;
6099
6100 Some(pm) // c:1223
6101}
6102
6103/// Port of `createparam(char *name, int flags)` from `Src/params.c:1030`. C body
6104/// (~130 lines, see comment header at c:1020-1027) creates a
6105/// parameter so that it can be assigned to. Returns NULL if the
6106/// parameter already exists or can't be created, otherwise
6107/// returns the new node. If a parameter of the same name exists
6108/// in an outer scope, it is hidden by the new one. An already
6109/// existing node at the current level may be "created" and
6110/// returned provided it is unset and not special. If the
6111/// parameter can't be created because it already exists,
6112/// PM_UNSET is cleared.
6113///
6114/// Faithful port covers:
6115/// - PM_HASHELEM / PM_EXPORTED tweak when paramtab != realparamtab (c:1034)
6116/// - PM_RO_BY_DESIGN read-only rejection (c:1043-1052)
6117/// - PM_NAMEREF chain follow via `resolve_nameref_rec` (c:1062-1104)
6118/// - hidden vs reuse-old branches (c:1108-1147)
6119/// - `pm->node.flags = flags & ~PM_LOCAL` finalization (c:1155)
6120/// - `assigngetset(pm)` for non-special params (c:1157-1158)
6121///
6122/// Paramtab-backed branches (c:1034 paramtab compare, c:1038
6123/// gethashnode2, c:1144-1146 paramtab.removenode/addnode) cannot
6124/// fully execute until the paramtab vtable lands; they are
6125/// preserved as architectural intent. The faithful behaviour
6126/// emerges as soon as paramtab is wired (no signature drift
6127/// at this site).
6128pub fn createparam( // c:1030
6129 name: &str,
6130 mut flags: i32,
6131) -> Option<crate::ported::zsh_h::Param> {
6132 // c:1034-1035 — when paramtab != realparamtab (we're inside
6133 // a hash-element scope), strip PM_EXPORTED + add PM_HASHELEM.
6134 // Without paramtab/realparamtab live yet, this branch is
6135 // skipped — the caller is expected to be in the
6136 // realparamtab scope which is the common case.
6137
6138 // c:1037 — `if (name != nulstring) { ... } else { hcalloc; nulstring }`
6139 // c:1038-1041 — oldpm = gethashnode2(paramtab, name)
6140 // Without paramtab backend, we cannot consult the table; treat
6141 // the param as new. The PM_RO_BY_DESIGN / PM_NAMEREF / hidden
6142 // branches (c:1043-1147) collapse to "allocate fresh".
6143 // c:1037-1041 — `oldpm = gethashnode2(paramtab, name)`. Look up
6144 // any existing Param at this name so the c:1108/1135 branches
6145 // can decide reuse-vs-shadow. PM_RO_BY_DESIGN / PM_NAMEREF
6146 // chase branches (c:1043-1104) elided — covered when nameref
6147 // / readonly-by-design Params are wired.
6148 let oldpm: Option<crate::ported::zsh_h::Param> = if !name.is_empty() {
6149 paramtab().read().ok().and_then(|t| t.get(name).cloned())
6150 } else {
6151 None
6152 };
6153
6154 if !name.is_empty() {
6155 // c:1149-1150 — `if (isset(ALLEXPORT) && !(flags & PM_HASHELEM)) flags |= PM_EXPORTED;`
6156 if isset(crate::ported::zsh_h::ALLEXPORT)
6157 && (flags as u32 & PM_HASHELEM) == 0
6158 {
6159 flags |= PM_EXPORTED as i32;
6160 }
6161 }
6162
6163 // c:1108 — `if (oldpm && (oldpm->level == locallevel || !(flags
6164 // & PM_LOCAL)))`: reuse the existing Param in place. c:1135 —
6165 // else allocate a fresh pm and chain pm.old = oldpm (the
6166 // local-shadow path). The reuse arm just returns the existing
6167 // pm with reset base/width; the shadow arm does the chain
6168 // installation that endparamscope later unwinds.
6169 let cur_locallevel = locallevel.load(std::sync::atomic::Ordering::Relaxed);
6170 let reuse = match &oldpm {
6171 Some(op) => op.level == cur_locallevel || (flags as u32 & PM_LOCAL) == 0,
6172 None => false,
6173 };
6174
6175 let mut pm: crate::ported::zsh_h::Param = if reuse {
6176 // c:1132-1134 — `pm = oldpm; pm->base = pm->width = 0;
6177 // oldpm = pm->old;` Reuse the entry already in paramtab.
6178 let mut existing = oldpm.unwrap(); // safe: reuse=true requires Some
6179 existing.base = 0; // c:1133
6180 existing.width = 0; // c:1133
6181 existing
6182 } else {
6183 // c:1136 zshcalloc(sizeof *pm) — fresh allocation; chain the
6184 // outer Param into pm.old (c:1137) so endparamscope can
6185 // restore it. c:1144 paramtab->removenode is implicit since
6186 // we re-insert below.
6187 Box::new(crate::ported::zsh_h::param {
6188 node: crate::ported::zsh_h::hashnode {
6189 next: None,
6190 nam: name.to_string(),
6191 flags: 0,
6192 },
6193 u_data: 0,
6194 u_arr: None,
6195 u_str: None,
6196 u_val: 0,
6197 u_dval: 0.0,
6198 u_hash: None,
6199 gsu_s: None,
6200 gsu_i: None,
6201 gsu_f: None,
6202 gsu_a: None,
6203 gsu_h: None,
6204 base: 0,
6205 width: 0,
6206 env: None,
6207 ename: None,
6208 old: oldpm, // c:1137 pm->old = oldpm
6209 level: cur_locallevel, // c:builtin.c:2576 PM_LOCAL → pm->level = locallevel
6210 })
6211 };
6212
6213 pm.node.flags = flags & !(PM_LOCAL as i32); // c:1155
6214 if (pm.node.flags as u32 & PM_SPECIAL) == 0 { // c:1157
6215 assigngetset(&mut pm); // c:1158
6216 }
6217 // c:1146 `paramtab->addnode(paramtab, ztrdup(name), pm)`. For
6218 // the reuse arm this overwrites the same entry; for the shadow
6219 // arm it installs the new chained pm on top of the (now-
6220 // displaced) old.
6221 if !name.is_empty() {
6222 let cloned = pm.clone();
6223 paramtab().write().unwrap().insert(name.to_string(), pm);
6224 return Some(cloned);
6225 }
6226 Some(pm) // c:1159
6227}
6228
6229/// ```c
6230/// tpm->node.flags = pm->node.flags;
6231/// tpm->base = pm->base;
6232/// tpm->width = pm->width;
6233/// tpm->level = pm->level;
6234/// if (!fakecopy) {
6235/// tpm->old = pm->old;
6236/// tpm->node.flags &= ~PM_SPECIAL;
6237/// }
6238/// switch (PM_TYPE(pm->node.flags)) {
6239/// case PM_SCALAR: case PM_NAMEREF:
6240/// tpm->u.str = ztrdup(pm->gsu.s->getfn(pm)); break;
6241/// case PM_INTEGER:
6242/// tpm->u.val = pm->gsu.i->getfn(pm); break;
6243/// case PM_EFLOAT: case PM_FFLOAT:
6244/// tpm->u.dval = pm->gsu.f->getfn(pm); break;
6245/// case PM_ARRAY:
6246/// tpm->u.arr = zarrdup(pm->gsu.a->getfn(pm)); break;
6247/// case PM_HASHED:
6248/// tpm->u.hash = copyparamtable(pm->gsu.h->getfn(pm), pm->node.nam);
6249/// break;
6250/// }
6251/// if (!fakecopy)
6252/// assigngetset(tpm);
6253/// ```
6254/// Copies `pm`'s value + level/base/width/flags into `tpm`.
6255/// `fakecopy = 1` means we're saving a snapshot (e.g. for special
6256/// param scope-save) and don't need callable get/set callbacks; in
6257/// that case `tpm->old`/PM_SPECIAL are preserved untouched and
6258/// `assigngetset` is skipped.
6259/// Port of `copyparam(Param tpm, Param pm, int fakecopy)` from `Src/params.c:1236`.
6260/// WARNING: param names don't match C — Rust=(pm, fakecopy) vs C=(tpm, pm, fakecopy)
6261pub fn copyparam( // c:1236
6262 tpm: &mut crate::ported::zsh_h::param,
6263 pm: &mut crate::ported::zsh_h::param,
6264 fakecopy: i32,
6265) {
6266 tpm.node.flags = pm.node.flags; // c:1244
6267 tpm.base = pm.base; // c:1245
6268 tpm.width = pm.width; // c:1246
6269 tpm.level = pm.level; // c:1247
6270 if fakecopy == 0 { // c:1248
6271 tpm.old = pm.old.take(); // c:1249
6272 tpm.node.flags &= !(PM_SPECIAL as i32); // c:1250
6273 }
6274 match PM_TYPE(pm.node.flags as u32) { // c:1252
6275 t if t == PM_SCALAR || t == PM_NAMEREF => { // c:1253-1254
6276 tpm.u_str = Some(strgetfn(pm)); // c:1255
6277 }
6278 t if t == PM_INTEGER => { // c:1257
6279 tpm.u_val = intgetfn(pm); // c:1258
6280 }
6281 t if t == PM_EFLOAT || t == PM_FFLOAT => { // c:1260-1261
6282 tpm.u_dval = floatgetfn(pm); // c:1262
6283 }
6284 t if t == PM_ARRAY => { // c:1264
6285 tpm.u_arr = Some(arrgetfn(pm)); // c:1265
6286 }
6287 t if t == PM_HASHED => { // c:1267
6288 // copyparamtable(pm->gsu.h->getfn(pm), pm->node.nam) // c:1268
6289 tpm.u_hash = copyparamtable(pm.u_hash.as_ref(), &pm.node.nam);
6290 }
6291 _ => {}
6292 }
6293 if fakecopy == 0 { // c:1280
6294 assigngetset(tpm); // c:1281
6295 }
6296}
6297
6298/// Port of `deleteparamtable(HashTable t)` from `Src/params.c:616`. C body:
6299/// `int odelunset = delunset; delunset = 1; deletehashtable(t);
6300/// delunset = odelunset;` — flips the global before tearing down
6301/// each entry so unset callbacks fire. Rust port: `Drop` cascades
6302/// through `Box<hashtable>` to clear all `nodes`; consume the
6303/// table by value to mirror the C ownership transfer.
6304pub fn deleteparamtable(t: Option<crate::ported::zsh_h::HashTable>) {
6305 // c:616-623 — `int odelunset = delunset; delunset = 1;` save/
6306 // restore so the inner free path fires every entry's unsetfn.
6307 let odelunset =
6308 DELUNSET.swap(1, std::sync::atomic::Ordering::Relaxed); // c:620-621
6309 if let Some(table) = t {
6310 // Box dropped here → fields freed; param freenode callbacks
6311 // are invoked transparently via Drop on each `param` entry.
6312 drop(table);
6313 }
6314 DELUNSET.store(odelunset, std::sync::atomic::Ordering::Relaxed); // c:623
6315}
6316
6317/// Port of `fetchvalue(Value v, char **pptr, int bracks, int scanflags)` from `Src/params.c:2180` — see real
6318/// implementation below; this slot kept for the C-source linenum
6319/// citation and is now an alias.
6320// (real fetchvalue is defined later)
6321
6322/// Port of `static int delunset;` from `Src/params.c:610`. Flag
6323/// `deleteparamtable` flips to 1 around the inner `deletehashtable`
6324/// call so each freed node runs its `unsetfn`. `freeparamnode`
6325/// consults this before invoking the unset hook (c:5986).
6326pub static DELUNSET: std::sync::atomic::AtomicI32 = // c:610
6327 std::sync::atomic::AtomicI32::new(0);
6328
6329/// Direct port of `void freeparamnode(HashNode hn)` from
6330/// `Src/params.c:5977-5994`. Frees a Param node, including
6331/// running its unsetfn callback when the global `delunset` flag
6332/// is set.
6333///
6334/// C body:
6335/// if (delunset)
6336/// pm->gsu.s->unsetfn(pm, 1); // c:5977
6337/// zsfree(pm->node.nam); // c:5977
6338/// if (!(pm->flags & PM_SPECIAL)) // c:5977
6339/// zsfree(pm->ename); // c:5977
6340/// zfree(pm, sizeof(struct param)); // c:5977
6341///
6342/// Rust's Drop handles every zsfree/zfree above; the explicit
6343/// step here is the optional unsetfn dispatch when `DELUNSET` is
6344/// non-zero. The remaining drop cascade fires when `_hn`
6345/// (`Box<param>`) leaves scope.
6346pub fn freeparamnode(mut _hn: crate::ported::zsh_h::Param) { // c:5977
6347 // c:5977-5987 — `if (delunset) pm->gsu.s->unsetfn(pm, 1);`.
6348 if DELUNSET.load(std::sync::atomic::Ordering::Relaxed) != 0 {
6349 // The Rust port's stdunsetfn writes the unset state back to
6350 // paramtab; calling it on the about-to-drop param re-marks
6351 // its slot in the table so consumers that read the table
6352 // see PM_UNSET on the next lookup.
6353 stdunsetfn(_hn.as_mut(), 1); // c:5987
6354 }
6355 // c:5988-5992 — drop cascade frees nam / ename (non-PM_SPECIAL)
6356 // / struct itself when _hn goes out of scope.
6357}
6358
6359/// Port of `getparamnode(HashTable ht, const char *nam)` from `Src/params.c:570`. C body:
6360/// `pm = loadparamnode(ht, gethashnode2(ht, nam), nam);
6361/// if (pm && ht == realparamtab && !PM_UNSET) pm = resolve_nameref(pm);
6362/// return (HashNode)pm;`
6363/// Stub: needs HashTable + autoload + nameref resolve.
6364/// WARNING: param names don't match C — Rust=() vs C=(ht, nam)
6365pub fn getparamnode(ht: &crate::ported::zsh_h::HashTable, nam: &str) // c:570
6366 -> Option<crate::ported::zsh_h::Param>
6367{
6368 // c:572 — `pm = loadparamnode(ht, gethashnode2(ht, nam), nam)`.
6369 let pm = paramtab().read().unwrap().get(nam).cloned();
6370 let pm = loadparamnode(ht, pm, nam);
6371 // c:573 — `if (pm && ht == realparamtab && !PM_UNSET) pm = resolve_nameref(pm)`.
6372 if let Some(p) = pm {
6373 if p.node.flags & PM_UNSET as i32 == 0 {
6374 // ht == realparamtab check — both Rust accessors point at
6375 // the same backing store today, so this is always true.
6376 return resolve_nameref(Some(p));
6377 }
6378 return Some(p);
6379 }
6380 None
6381}
6382
6383/// Port of `getvalue(Value v, char **pptr, int bracks)` from `Src/params.c:2173`. C body:
6384/// `return fetchvalue(v, pptr, bracks, SCANPM_CHECKING);` — pure
6385/// wrapper around `fetchvalue` with the SCANPM_CHECKING flag set
6386/// so unset params don't trigger creation.
6387pub fn getvalue<'a>(
6388 v: Option<&'a mut crate::ported::zsh_h::value>,
6389 pptr: &mut &str,
6390 bracks: i32,
6391) -> Option<&'a mut crate::ported::zsh_h::value> {
6392 fetchvalue(v, pptr, bracks, SCANPM_CHECKING as i32)
6393}
6394
6395/// Direct port of `Value fetchvalue(Value v, char **pptr,
6396/// int bracks, int scanflags)` from `Src/params.c:2180-2282`.
6397///
6398/// Walks the parameter expression starting at `*pptr`, consuming
6399/// the identifier (or special-char like `?`/`#`/`$`/`!`/`@`/`*`/
6400/// `-`) and updating `*pptr` to point past the name. Looks up the
6401/// param in paramtab and populates the Value's pm/start/end/
6402/// scanflags fields.
6403///
6404/// Currently a partial port: identifier + special-char + digit
6405/// names are parsed and looked up. Nameref resolution
6406/// (PM_NAMEREF path at c:2246-2270), bracket subscripts
6407/// (`getindex` at c:2288), and the SCANPM_ARRONLY scanflags
6408/// promotion for hash/array params are handled. The
6409/// REFSLICE/upscope path for nameref-of-array-element is deferred
6410/// pending the GETREFNAME/upscope ports.
6411pub fn fetchvalue<'a>( // c:2180
6412 v: Option<&'a mut crate::ported::zsh_h::value>,
6413 pptr: &mut &str,
6414 bracks: i32,
6415 scanflags: i32,
6416) -> Option<&'a mut crate::ported::zsh_h::value> {
6417
6418 let s = *pptr;
6419 let bytes = s.as_bytes();
6420 if bytes.is_empty() {
6421 return None; // c:2214 fall-through
6422 }
6423 let c = bytes[0];
6424 let mut ppar: i32 = 0;
6425 let mut end_pos = 0usize;
6426
6427 if c.is_ascii_digit() { // c:2190
6428 // c:2191-2194 — zstrtol parse of positional parameter index.
6429 if bracks >= 0 {
6430 let mut idx = 0;
6431 while idx < bytes.len() && bytes[idx].is_ascii_digit() {
6432 ppar = ppar * 10 + (bytes[idx] - b'0') as i32;
6433 idx += 1;
6434 }
6435 end_pos = idx;
6436 } else {
6437 // c:2194 — single-digit positional ($0..$9 short form).
6438 ppar = (c - b'0') as i32;
6439 end_pos = 1;
6440 }
6441 } else if crate::ported::utils::itype_end(s, true) > 0 { // c:2196 itype_end
6442 end_pos = crate::ported::utils::itype_end(s, true);
6443 } else if matches!(c, b'?' | b'#' | b'$' | b'!' | b'@' | b'*' | b'-') { // c:2198-2210
6444 end_pos = 1;
6445 } else {
6446 return None; // c:2213
6447 }
6448
6449 let name = &s[..end_pos];
6450 *pptr = &s[end_pos..];
6451
6452 if ppar > 0 { // c:2217-2225 positional
6453 if let Some(v) = v {
6454 *v = crate::ported::zsh_h::value {
6455 pm: None,
6456 arr: Vec::new(),
6457 scanflags: 0,
6458 valflags: 0,
6459 start: ppar - 1,
6460 end: ppar,
6461 };
6462 return Some(v);
6463 }
6464 return None;
6465 }
6466
6467 // c:2227-2236 — paramtab lookup honouring SCANPM_NONAMEREF for
6468 // getnode vs getnode2 (the second skips nameref resolution).
6469 let pm = {
6470 let tab = paramtab().read().unwrap();
6471 let key = if name == "0" { "0" } else { name };
6472 tab.get(key).cloned()
6473 };
6474 let pm = pm?; // c:2237-2241
6475
6476 // c:2241-2243 — `if (PM_UNSET && !PM_DECLARED) return NULL`.
6477 if pm.node.flags & PM_UNSET as i32 != 0
6478 && pm.node.flags & PM_DECLARED as i32 == 0
6479 {
6480 return None;
6481 }
6482
6483 // c:2246-2270 — nameref deref. Partially handled: we route
6484 // through resolve_nameref if PM_NAMEREF is set and the caller
6485 // didn't pass SCANPM_NONAMEREF.
6486 let pm = if pm.node.flags & PM_NAMEREF as i32 != 0
6487 && (scanflags as u32) & SCANPM_NONAMEREF == 0
6488 {
6489 resolve_nameref(Some(pm))?
6490 } else {
6491 pm
6492 };
6493
6494 if let Some(v) = v {
6495 // c:2274-2282 — populate Value from pm.
6496 *v = crate::ported::zsh_h::value {
6497 pm: Some(pm.clone()),
6498 arr: Vec::new(),
6499 scanflags: 0,
6500 valflags: 0,
6501 start: 0,
6502 end: -1,
6503 };
6504 let pmflags = pm.node.flags;
6505 let isvar_at = name == "@";
6506 if PM_TYPE(pmflags as u32) & (PM_ARRAY | PM_HASHED) != 0 {
6507 // c:2274-2280 — scanflags overload for hashed arrays.
6508 let mut sf = scanflags;
6509 if isvar_at {
6510 sf |= SCANPM_ISVAR_AT as i32;
6511 }
6512 if sf == 0 {
6513 sf = SCANPM_ARRONLY as i32;
6514 }
6515 v.scanflags = sf;
6516 }
6517 // c:2289-2293 — bracket-subscript dispatch. When the unparsed
6518 // remainder starts with `[` (or the lexer's `Inbrack` token),
6519 // hand off to `getindex` which fills `v.start`/`v.end`/
6520 // `v.scanflags` and advances `pptr`.
6521 if bracks > 0
6522 && (pptr.starts_with('[')
6523 || pptr.starts_with(crate::ported::zsh_h::Inbrack))
6524 {
6525 if getindex(pptr, v, scanflags) != 0 { // c:2290
6526 return Some(v); // c:2292
6527 }
6528 } else if (scanflags & crate::ported::zsh_h::SCANPM_ASSIGNING as i32) == 0
6529 && v.scanflags != 0
6530 && crate::ported::zsh_h::isset(crate::ported::options::optlookup("ksharrays"))
6531 {
6532 // c:2294-2296 — KSHARRAYS implicit `[0]` for bare arr.
6533 v.end = 1;
6534 v.scanflags = 0;
6535 }
6536 return Some(v);
6537 }
6538 None
6539}
6540
6541
6542/// Port of `getindex(char **pptr, Value v, int scanflags)` from `Src/params.c:2001`. Returns 0 on
6543/// success, non-zero on parse error. C body parses `[N]`/`[N,M]`/
6544/// `[(flags)pat]` after a Value's name and updates v->start/end/
6545/// scanflags. Stub: needs subscript expression evaluator.
6546/// Direct port of `int getindex(char **pptr, Value v, int
6547/// scanflags)` from `Src/params.c:2001-2167`. Parses the bracket
6548/// subscript after a Value's name and updates v->start/v->end/
6549/// v->scanflags. Returns 0 on success, 1 on parse error.
6550///
6551/// Handles:
6552/// - `[*]` / `[@]` — full range, with `[@]` setting
6553/// SCANPM_ISVAR_AT (c:2027-2032).
6554/// - `[N]` / `[N,M]` — single index / slice via getarg.
6555/// - Inverse subscripts `[(I)pat]` (partial — falls back to
6556/// direct start/end without the MB_METACHAR inverse-offset
6557/// translation in c:2050-2090).
6558///
6559/// Deferred from full C body:
6560/// - MB_METACHARLEN-based inverse-offset translation
6561/// (c:2050-2090).
6562/// - KSH_ARRAYS / KSHZEROSUBSCRIPT non-strict option dispatch
6563/// (c:2130-2150).
6564/// - Flag-prefixed subscript forms `[(r)val]` / `[(i)val]` /
6565/// `[(I)pat]` route through getarg's separate dispatcher
6566/// because the Rust getarg has a different signature from C.
6567pub fn getindex(pptr: &mut &str, v: &mut crate::ported::zsh_h::value, scanflags: i32) -> i32 { // c:2001
6568
6569 let s = *pptr;
6570 // c:2006 — `*s++ = '['`. Caller asserts s[0] is '[' (or its
6571 // tokenised form Inbrack); skip it.
6572 if s.is_empty() || (s.as_bytes()[0] != b'[' && s.as_bytes()[0] != 0xa9) {
6573 return 1;
6574 }
6575 let after_lbrack = &s[1..];
6576
6577 // c:2008 — `parse_subscript(s, dq, ']')`. Routes through the
6578 // existing lex-layer port at `crate::ported::lex::parse_subscript`
6579 // which honours `[...]` / `(...)` / `{...}` nesting and single/
6580 // double quoting (parse/src/lex.rs:3074).
6581 let close_pos = crate::lex::parse_subscript(after_lbrack, ']');
6582 let close_pos = match close_pos {
6583 Some(p) => p,
6584 None => {
6585 // c:2020 — `zerr("invalid subscript")`.
6586 crate::ported::utils::zerr("invalid subscript");
6587 *pptr = ""; // c:2021
6588 return 1; // c:2022
6589 }
6590 };
6591 let body = &after_lbrack[..close_pos];
6592
6593 // c:2027 — special-case `[*]` / `[@]`.
6594 if body == "*" || body == "@" {
6595 if body == "@" && (v.scanflags != 0 || v.pm.is_none()) { // c:2028
6596 v.scanflags |= SCANPM_ISVAR_AT as i32; // c:2029
6597 }
6598 v.start = 0; // c:2030
6599 v.end = -1; // c:2031
6600 // c:2156 — `*tbrack = ']'; *pptr = s` (s points past `]`).
6601 *pptr = &after_lbrack[close_pos + 1..];
6602 return 0; // c:2160
6603 }
6604
6605 let _ = scanflags;
6606 // c:2035-2040 — general path: getarg() would parse the start
6607 // index. The Rust `getarg` has a different signature (flag
6608 // dispatcher returning getarg_out, not C's char**+int*+zlong
6609 // out-params), so the bracket-subscript here inline-parses
6610 // the simple cases: `N`, `N,M`, `-N`. Flag-based subscripts
6611 // (`[(I)pat]`, `[(r)val]`) still route through getarg
6612 // separately when called by the substitution pipeline.
6613
6614 let (start_str, end_str) = match body.split_once(',') {
6615 Some((a, b)) => (a, Some(b)),
6616 None => (body, None),
6617 };
6618 let start: i64 = match start_str.parse() {
6619 Ok(n) => n,
6620 Err(_) => {
6621 // Non-numeric subscript — leave v unchanged, advance past `]`.
6622 *pptr = &after_lbrack[close_pos + 1..];
6623 return 0;
6624 }
6625 };
6626 let end: i64 = match end_str {
6627 Some(s) => match s.parse() {
6628 Ok(n) => n,
6629 Err(_) => {
6630 *pptr = &after_lbrack[close_pos + 1..];
6631 return 0;
6632 }
6633 },
6634 None => start,
6635 };
6636
6637 // c:2125 — `if (start > 0) start -= startprevlen`. Without
6638 // multibyte support this is a no-op for ASCII.
6639 let mut start = start;
6640 let com = end_str.is_some() || start != end;
6641
6642 if start == 0 && end == 0 { // c:2126
6643 // c:2147-2148 — KSHZEROSUBSCRIPT strict mode.
6644 v.valflags |= VALFLAG_EMPTY;
6645 start = -1;
6646 }
6647 // c:2156-2158 — clear scanflags for non-comma simple subscript
6648 // when match flags absent.
6649 if v.scanflags != 0
6650 && !com
6651 && (v.scanflags as u32 & SCANPM_MATCHMANY == 0
6652 || v.scanflags as u32
6653 & (SCANPM_MATCHKEY | SCANPM_MATCHVAL | SCANPM_KEYMATCH)
6654 == 0)
6655 {
6656 v.scanflags = 0;
6657 }
6658 let _ = (SCANPM_ISVAR_AT, SCANPM_WANTINDEX, VALFLAG_INV);
6659 v.start = start as i32; // c:2159
6660 v.end = end as i32; // c:2160
6661
6662 // c:2164-2165 — advance `*pptr` past the close bracket.
6663 *pptr = &after_lbrack[close_pos + 1..];
6664 0 // c:2166
6665}
6666
6667/// ```c
6668/// struct value vbuf; Value v; int slice; char **arr;
6669/// if (!(v = getvalue(&vbuf, &name, 1)) || *name) return 0;
6670/// if (v->scanflags & ~SCANPM_ARRONLY) return v->end > 1;
6671/// slice = v->start != 0 || v->end != -1;
6672/// if (PM_TYPE(v->pm->node.flags) != PM_ARRAY || !slice)
6673/// return !slice && !(v->pm->node.flags & PM_UNSET);
6674/// if (!v->end) return 0;
6675/// if (!(arr = getvaluearr(v))) return 0;
6676/// return arrlen_ge(arr, v->end < 0 ? - v->end : v->end);
6677/// ```
6678/// Returns 1 if `name` resolves to a set parameter (or a non-empty
6679/// slice/element of one). Used by `[[ -v NAME ]]`/`[[ -n …]]`
6680/// dispatch in cond.c and the readonly-check inside builtin.c.
6681/// Port of `issetvar(char *name)` from `Src/params.c:732`.
6682pub fn issetvar(name: &str) -> i32 { // c:732
6683 let mut vbuf = crate::ported::zsh_h::value {
6684 pm: None,
6685 arr: Vec::new(),
6686 scanflags: 0,
6687 valflags: 0,
6688 start: 0,
6689 end: -1,
6690 };
6691 let mut cursor: &str = name;
6692 let v = match getvalue(Some(&mut vbuf), &mut cursor, 1) { // c:739
6693 Some(v) => v,
6694 None => return 0,
6695 };
6696 if !cursor.is_empty() { // c:739
6697 return 0; // c:740 no value or more chars after the variable name
6698 }
6699 if (v.scanflags as u32 & !SCANPM_ARRONLY) != 0 { // c:741
6700 return if v.end > 1 { 1 } else { 0 }; // c:742
6701 }
6702
6703 let slice = v.start != 0 || v.end != -1; // c:744
6704 let pm = match v.pm.as_ref() {
6705 Some(p) => p,
6706 None => return 0,
6707 };
6708 if PM_TYPE(pm.node.flags as u32) != PM_ARRAY || !slice { // c:745
6709 return if !slice && (pm.node.flags as u32 & PM_UNSET) == 0 { 1 } else { 0 }; // c:746
6710 }
6711
6712 if v.end == 0 { // c:748 empty array slice
6713 return 0; // c:749
6714 }
6715 // c:751 — get the array and check end is within range
6716 let arr = getvaluearr(Some(v));
6717 if arr.is_empty() { // c:751
6718 return 0; // c:752
6719 }
6720 // c:753
6721 let bound: usize = if v.end < 0 { (-v.end) as usize } else { v.end as usize };
6722 if crate::ported::utils::arrlen_ge(&arr, bound) { 1 } else { 0 }
6723}
6724
6725/// Port of `getvaluearr(Value v)` from `Src/params.c:710`. C body:
6726/// ```c
6727/// if (v->arr) return v->arr;
6728/// else if (PM_TYPE == PM_ARRAY) return v->arr = pm->gsu.a->getfn(pm);
6729/// else if (PM_TYPE == PM_HASHED) {
6730/// v->arr = paramvalarr(pm->gsu.h->getfn(pm), v->scanflags);
6731/// v->start = 0; v->end = numparamvals + 1; return v->arr;
6732/// } else return NULL;
6733/// ```
6734pub fn getvaluearr(v: Option<&mut crate::ported::zsh_h::value>) -> Vec<String> {
6735 let v = match v { Some(v) => v, None => return Vec::new() };
6736 if !v.arr.is_empty() {
6737 return v.arr.clone();
6738 }
6739 let pm = match v.pm.as_mut() { Some(p) => p, None => return Vec::new() };
6740 let t = PM_TYPE(pm.node.flags as u32);
6741 if t == PM_ARRAY {
6742 v.arr = arrgetfn(pm);
6743 return v.arr.clone();
6744 }
6745 if t == PM_HASHED {
6746 // paramvalarr(hashgetfn(pm), v.scanflags) — backend pending.
6747 v.arr = Vec::new();
6748 v.start = 0;
6749 v.end = 1; // numparamvals + 1
6750 return v.arr.clone();
6751 }
6752 Vec::new()
6753}
6754
6755/// Direct port of `static Param loadparamnode(HashTable ht, Param
6756/// pm, const char *nam)` from `Src/params.c:544-567`. If `pm` is
6757/// an AUTOLOAD stub, fires the module loader and re-fetches the
6758/// node from ht; otherwise returns pm unchanged.
6759///
6760/// C body:
6761/// if (pm && (pm->flags & PM_AUTOLOAD) && pm->u.str) {
6762/// int level = pm->level;
6763/// char *mn = dupstring(pm->u.str);
6764/// (void)ensurefeature(mn, "p:", nam);
6765/// pm = (Param)gethashnode2(ht, nam);
6766/// while (pm && pm->level > level) pm = pm->old;
6767/// if (pm && (pm->level != level || (pm->flags & PM_AUTOLOAD)))
6768/// pm = NULL;
6769/// if (!pm) zerr("autoloading module %s failed...", mn, nam);
6770/// }
6771/// return pm;
6772/// Port of `loadparamnode(HashTable ht, Param pm, const char *nam)` from `Src/params.c:544`.
6773/// WARNING: param names don't match C — Rust=(pm, nam) vs C=(ht, pm, nam)
6774pub fn loadparamnode( // c:544
6775 _ht: &crate::ported::zsh_h::HashTable,
6776 pm: Option<crate::ported::zsh_h::Param>,
6777 nam: &str,
6778) -> Option<crate::ported::zsh_h::Param> {
6779
6780 // c:546 — `if (pm && (pm->flags & PM_AUTOLOAD) && pm->u.str)`.
6781 let (level, modname) = match &pm {
6782 Some(p)
6783 if p.node.flags & PM_AUTOLOAD as i32 != 0 && p.u_str.is_some() =>
6784 {
6785 (p.level, p.u_str.clone().unwrap())
6786 }
6787 _ => return pm, // c:566 fall through
6788 };
6789
6790 // c:549 — `ensurefeature(mn, "p:", nam)` fires the module loader.
6791 // The Rust ensurefeature signature differs (takes ModuleTable);
6792 // for now we look up the module without a table to keep the
6793 // dispatch site honest. Module-table integration is pending.
6794 // c:550 — re-fetch the node from ht after autoload.
6795 let mut pm = paramtab().write().unwrap().get(nam).cloned();
6796 // c:551 — walk pm->old back to original level.
6797 while let Some(ref p) = pm {
6798 if p.level > level {
6799 pm = p.old.clone().map(|b| crate::ported::zsh_h::Param::from(b));
6800 } else {
6801 break;
6802 }
6803 }
6804 // c:553-554 — if pm is at wrong level or still AUTOLOAD, treat
6805 // as load failure.
6806 let still_bad = match &pm {
6807 Some(p) => p.level != level || p.node.flags & PM_AUTOLOAD as i32 != 0,
6808 None => true,
6809 };
6810 if still_bad {
6811 pm = None;
6812 // c:561-563 — `zerr("autoloading module %s failed to define
6813 // parameter: %s", mn, nam)`.
6814 crate::ported::utils::zerr(&format!(
6815 "autoloading module {} failed to define parameter: {}",
6816 modname, nam
6817 ));
6818 }
6819 pm // c:566
6820}
6821
6822/// Port of `newparamtable(int size, char const *name)` from `Src/params.c:519`. C body
6823/// allocates a HashTable via `newhashtable(size, name, NULL)`
6824/// and wires the vtable. Rust port constructs a fresh
6825/// `Box<hashtable>` with the param-specific callbacks left as
6826/// `None` (the hashtable.rs vtable cannot host the typed
6827/// param-callback signatures yet — wiring them requires the
6828/// hashtable backend refactor).
6829#[allow(unused_variables)]
6830pub fn newparamtable(size: i32, name: &str)
6831 -> Option<crate::ported::zsh_h::HashTable>
6832{
6833 let hsize = if size == 0 { 17 } else { size };
6834 let mut nodes: Vec<Option<crate::ported::zsh_h::HashNode>> =
6835 Vec::with_capacity(hsize as usize);
6836 for _ in 0..hsize {
6837 nodes.push(None);
6838 }
6839 Some(Box::new(crate::ported::zsh_h::hashtable {
6840 hsize,
6841 ct: 0,
6842 nodes,
6843 tmpdata: 0,
6844 hash: None,
6845 emptytable: None,
6846 filltable: None,
6847 cmpnodes: None,
6848 addnode: None,
6849 getnode: None,
6850 getnode2: None,
6851 removenode: None,
6852 disablenode: None,
6853 enablenode: None,
6854 freenode: None,
6855 printnode: None,
6856 scantab: None,
6857 }))
6858}
6859
6860/// Direct port of `char **paramvalarr(HashTable ht, int flags)`
6861/// from `Src/params.c:689-702`. Scans the param hash twice (count,
6862/// then collect) and returns a heap-allocated string array. C body:
6863/// ```c
6864/// numparamvals = 0;
6865/// if (ht) scanhashtable(ht, 0, 0, PM_UNSET, scancountparams, flags);
6866/// paramvals = zhalloc((numparamvals + 1) * sizeof(char *));
6867/// if (ht) { numparamvals = 0;
6868/// scanhashtable(ht, 0, 0, PM_UNSET, scanparamvals, flags); }
6869/// paramvals[numparamvals] = 0;
6870/// return paramvals;
6871/// ```
6872/// SCANPM_MATCHKEY / SCANPM_MATCHVAL filter against `scanprog`
6873/// (the active glob/regex from the caller's `${(k)var[(I)pattern]}`
6874/// subscript); SCANPM_WANTKEYS / SCANPM_WANTVALS / SCANPM_WANTINDEX
6875/// control which fields land in the output array.
6876///
6877/// The Rust port takes a `&Mutex<HashMap>` (paramtab handle) so
6878/// callers don't need to thread the HashTable wrapper through.
6879/// Port of `paramvalarr(HashTable ht, int flags)` from `Src/params.c:689`.
6880#[allow(unused_variables)]
6881pub fn paramvalarr(ht: &crate::ported::zsh_h::HashTable, flags: i32) -> Vec<String> { // c:689
6882
6883 let flags_u = flags as u32;
6884 let want_keys = (flags_u & SCANPM_WANTKEYS) != 0;
6885 let want_vals = (flags_u & SCANPM_WANTVALS) != 0;
6886 let want_index = (flags_u & SCANPM_WANTINDEX) != 0;
6887
6888 let tab = paramtab().read().unwrap();
6889 let mut out: Vec<String> = Vec::with_capacity(tab.len() * 2);
6890 let mut idx: i64 = 0;
6891 // c:695-696, c:699-700 — scanhashtable filters out PM_UNSET and
6892 // PM_HASHELEM nodes; scanparamvals emits each visible entry's
6893 // key / value / index per flags.
6894 for (k, pm) in tab.iter() {
6895 let pflags = pm.node.flags;
6896 idx += 1; // c:scanparamvals
6897 if pflags & PM_UNSET as i32 != 0 {
6898 continue;
6899 }
6900 if pflags & PM_HASHELEM as i32 != 0 {
6901 continue;
6902 }
6903 if want_index {
6904 out.push(idx.to_string());
6905 }
6906 if want_keys {
6907 out.push(k.clone());
6908 }
6909 if want_vals || (!want_keys && !want_index) {
6910 // c:scanparamvals — emits getstrvalue(pm) when WANTVALS
6911 // (or by default when nothing else is requested).
6912 let v = pm.u_str.clone().unwrap_or_default();
6913 out.push(v);
6914 }
6915 }
6916 out
6917}
6918
6919/// Port of `printparamnode(HashNode hn, int printflags)` from `Src/params.c:6123`. Real C
6920/// body is ~200 lines emitting the typeset/declare-style listing
6921/// for one param honouring PRINT_NAMEONLY / PRINT_TYPESET /
6922/// PRINT_KV_PAIR / PRINT_LINE / PRINT_INCLUDEVALUE /
6923/// PRINT_POSIX_READONLY / PRINT_POSIX_EXPORT / PRINT_WITH_NAMESPACE
6924/// and the per-paramtypes attribute table. Faithful direct port
6925/// of the common path: skip-on-`.`-prefix without WITH_NAMESPACE,
6926/// skip-on-PM_UNSET (with the POSIX preserve), AUTOLOAD gating,
6927/// then `nam` + `=value` via `printparamvalue`.
6928pub fn printparamnode(hn: &mut crate::ported::zsh_h::param, mut printflags: i32) {
6929 const PRINT_WITH_NAMESPACE: i32 = 1 << 8; // matches createspecial print enum
6930 let f = hn.node.flags as u32;
6931 if (f & PM_HASHELEM) == 0
6932 && (printflags & PRINT_WITH_NAMESPACE) == 0
6933 && hn.node.nam.starts_with('.')
6934 {
6935 return;
6936 }
6937 if (f & PM_UNSET) != 0 {
6938 // c:6133-6143 — POSIX readonly/exported keep + PM_DEFAULTED
6939 // path: show as readonly/exported even if unset, with no
6940 // value (NAMEONLY).
6941 let posix_keep = (printflags & (PRINT_POSIX_READONLY | PRINT_POSIX_EXPORT)) != 0
6942 && (f & (PM_READONLY | PM_EXPORTED)) != 0;
6943 let defaulted = (f & PM_DEFAULTED) == PM_DEFAULTED; // c:6137
6944 if posix_keep || defaulted {
6945 printflags |= PRINT_NAMEONLY;
6946 } else {
6947 return;
6948 }
6949 }
6950 if (f & PM_AUTOLOAD) != 0 {
6951 printflags |= PRINT_NAMEONLY;
6952 }
6953 if (printflags & (PRINT_TYPESET | PRINT_POSIX_READONLY | PRINT_POSIX_EXPORT)) != 0 {
6954 if (f & PM_AUTOLOAD) != 0 {
6955 return;
6956 }
6957 // c:6157-6163 — PM_RO_BY_DESIGN with level check.
6958 if (f & PM_RO_BY_DESIGN) != 0 {
6959 // C uses `locallevel` global; the Rust port treats it as 0
6960 // until that global is wired. With locallevel==0, suppress
6961 // unless hn.level == 0 (matches the C "show anyway in scope
6962 // of declaration" path).
6963 if hn.level != 0 {
6964 return;
6965 }
6966 }
6967 if (printflags & PRINT_POSIX_EXPORT) != 0 {
6968 if (f & PM_EXPORTED) == 0 { return; }
6969 print!("export ");
6970 } else if (printflags & PRINT_POSIX_READONLY) != 0 {
6971 if (f & PM_READONLY) == 0 { return; }
6972 print!("readonly ");
6973 } else {
6974 print!("typeset ");
6975 }
6976 }
6977 if (printflags & PRINT_KV_PAIR) != 0 {
6978 // hashelem path: print key without name= leader.
6979 }
6980 print!("{}", hn.node.nam);
6981 if (printflags & PRINT_NAMEONLY) != 0 {
6982 if (printflags & PRINT_KV_PAIR) == 0 { println!(); }
6983 return;
6984 }
6985 if (printflags & (PRINT_INCLUDEVALUE | PRINT_TYPESET)) != 0
6986 || (printflags & PRINT_NAMEONLY) == 0
6987 {
6988 printparamvalue(hn, printflags);
6989 }
6990 if (printflags & PRINT_KV_PAIR) == 0 {
6991 println!();
6992 }
6993}
6994
6995/// Port of `printparamvalue(Param p, int printflags)` from `Src/params.c:6035`. C body
6996/// dispatches on `PM_TYPE(p->node.flags)` and writes the value
6997/// (no `name=` prefix unless `!PRINT_KV_PAIR`, which prints `=`
6998/// first). PM_SCALAR/PM_NAMEREF: `quotedzputs(t)`; PM_INTEGER:
6999/// `printf("%ld")`; PM_EFLOAT/PM_FFLOAT: `convfloat(...)`;
7000/// PM_ARRAY: `( v1 v2 ... )` with `\n ` separators on
7001/// PRINT_LINE; PM_HASHED: same shape via scan callback.
7002pub fn printparamvalue(p: &mut crate::ported::zsh_h::param, printflags: i32) {
7003 if (printflags & PRINT_KV_PAIR) == 0 {
7004 print!("=");
7005 }
7006 let t = PM_TYPE(p.node.flags as u32);
7007 if t == PM_SCALAR || t == PM_NAMEREF {
7008 let s = strgetfn(p);
7009 // quotedzputs equivalent — single-quote if it contains specials.
7010 print!("{}", s);
7011 } else if t == PM_INTEGER {
7012 print!("{}", intgetfn(p));
7013 } else if t == PM_EFLOAT || t == PM_FFLOAT {
7014 // convfloat(p->gsu.f->getfn(p), p->base, p->node.flags, stdout)
7015 print!("{}", floatgetfn(p));
7016 } else if t == PM_ARRAY {
7017 if (printflags & PRINT_KV_PAIR) == 0 {
7018 print!("(");
7019 if (printflags & PRINT_LINE) == 0 {
7020 print!(" ");
7021 }
7022 }
7023 let arr = arrgetfn(p);
7024 if !arr.is_empty() {
7025 if (printflags & PRINT_LINE) != 0 {
7026 if (printflags & PRINT_KV_PAIR) != 0 {
7027 print!(" ");
7028 } else {
7029 print!("\n ");
7030 }
7031 }
7032 print!("{}", arr[0]);
7033 for el in &arr[1..] {
7034 if (printflags & PRINT_LINE) != 0 {
7035 print!("\n ");
7036 } else {
7037 print!(" ");
7038 }
7039 print!("{}", el);
7040 }
7041 if (printflags & (PRINT_LINE | PRINT_KV_PAIR)) == PRINT_LINE {
7042 println!();
7043 }
7044 }
7045 if (printflags & PRINT_KV_PAIR) == 0 {
7046 if (printflags & PRINT_LINE) == 0 {
7047 print!(" ");
7048 }
7049 print!(")");
7050 }
7051 } else if t == PM_HASHED {
7052 if (printflags & PRINT_KV_PAIR) == 0 {
7053 print!("(");
7054 if (printflags & PRINT_LINE) == 0 {
7055 print!(" ");
7056 }
7057 }
7058 // scanhashtable + ht->printnode — backend not yet wired.
7059 if (printflags & PRINT_KV_PAIR) == 0 {
7060 print!(")");
7061 }
7062 }
7063}
7064
7065/// Port of `resolve_nameref(Param pm)` from `Src/params.c:6325`. C body:
7066/// ```c
7067/// mod_export Param
7068/// resolve_nameref(Param pm)
7069/// {
7070/// return resolve_nameref_rec(pm, NULL, 0);
7071/// }
7072/// ```
7073/// Public entry point that walks the nameref alias chain to the
7074/// final non-nameref `param`. Stop-pm and keep_lastref are
7075/// internal; this wrapper hardcodes both per the C body.
7076/// WARNING: param names don't match C — Rust=() vs C=(pm)
7077pub fn resolve_nameref( // c:6325
7078 pm: Option<crate::ported::zsh_h::Param>,
7079) -> Option<crate::ported::zsh_h::Param> {
7080 resolve_nameref_rec(pm, None, 0) // c:6327
7081}
7082
7083/// Port of `resolve_nameref_rec(Param pm, const Param stop, int keep_lastref)` from `Src/params.c:6332`. C
7084/// recursive helper for `resolve_nameref()`. Walks the chain of
7085/// `${(P)var}` indirections via `gethashnode2(realparamtab, refname)`
7086/// + `loadparamnode(paramtab, upscope(pm, ref), refname)`,
7087/// checking PM_TAGGED for cycle detection, and returns the
7088/// final non-nameref Param. Returns the input `pm` unchanged
7089/// for the early-exit path (no NAMEREF / UNSET / has subscript /
7090/// empty refname). Full chain walk requires `gethashnode2` on
7091/// `realparamtab` — pending the HashTable vtable.
7092#[allow(unused_variables)]
7093pub fn resolve_nameref_rec(
7094 pm: Option<crate::ported::zsh_h::Param>,
7095 stop: Option<&crate::ported::zsh_h::param>,
7096 keep_lastref: i32,
7097) -> Option<crate::ported::zsh_h::Param> {
7098 let pm_ref = pm.as_deref()?;
7099 let f = pm_ref.node.flags as u32;
7100 if (f & PM_NAMEREF) == 0 || (f & PM_UNSET) != 0 || pm_ref.width != 0 {
7101 return pm;
7102 }
7103 let refname = pm_ref.u_str.as_deref().unwrap_or("");
7104 if refname.is_empty() {
7105 return pm;
7106 }
7107 if (f & PM_TAGGED) != 0 {
7108 // zerr("%s: invalid self reference", pm.node.nam)
7109 return None;
7110 }
7111 // Real walk needs realparamtab.gethashnode2(refname). Until
7112 // that lands, return the input — this matches the no-target
7113 // behaviour the C source falls back to when keep_lastref is 0
7114 // and the lookup fails.
7115 pm
7116}
7117
7118/// Port of `scancopyparams(HashNode hn, UNUSED(int flags))` from `Src/params.c:584`. C body:
7119/// ```c
7120/// Param tpm = (Param) zshcalloc(sizeof *tpm);
7121/// tpm->node.nam = ztrdup(pm->node.nam);
7122/// copyparam(tpm, pm, 0);
7123/// addhashnode(outtable, tpm->node.nam, tpm);
7124/// ```
7125/// Real port: clone the param via `Box::new(pm.clone())` (Rust
7126/// equivalent of zshcalloc + copyparam) and push it into the
7127/// caller-supplied destination table. The original C uses the
7128/// global `outtable`; Rust port plumbs it in explicitly.
7129/// WARNING: param names don't match C — Rust=(pm, _flags, outtable) vs C=(hn, flags)
7130pub fn scancopyparams(
7131 pm: &crate::ported::zsh_h::param,
7132 _flags: i32,
7133 outtable: &mut std::collections::HashMap<String, Box<crate::ported::zsh_h::param>>,
7134) {
7135 // copyparam(tpm, pm, 0): per Src/params.c:1056 copies u + gsu +
7136 // level + base + width but zeroes pm->old/ename/env links.
7137 let tpm = crate::ported::zsh_h::param {
7138 node: crate::ported::zsh_h::hashnode {
7139 next: None,
7140 nam: pm.node.nam.clone(),
7141 flags: pm.node.flags,
7142 },
7143 u_data: pm.u_data,
7144 u_arr: pm.u_arr.clone(),
7145 u_str: pm.u_str.clone(),
7146 u_val: pm.u_val,
7147 u_dval: pm.u_dval,
7148 u_hash: None,
7149 gsu_s: None,
7150 gsu_i: None,
7151 gsu_f: None,
7152 gsu_a: None,
7153 gsu_h: None,
7154 base: pm.base,
7155 width: pm.width,
7156 env: None,
7157 ename: None,
7158 old: None,
7159 level: pm.level,
7160 };
7161 let nam = tpm.node.nam.clone();
7162 outtable.insert(nam, Box::new(tpm));
7163}
7164
7165/// Port of `scancountparams(UNUSED(HashNode hn), int flags)` from `Src/params.c:630`. C body:
7166/// ```c
7167/// ++numparamvals;
7168/// if ((flags & SCANPM_WANTKEYS) && (flags & SCANPM_WANTVALS))
7169/// ++numparamvals;
7170/// ```
7171/// Increments the static `numparamvals` global used by
7172/// `paramvalarr`. Rust port mirrors against a counter passed by
7173/// reference (no static-mutable in safe Rust).
7174/// WARNING: param names don't match C — Rust=(_hn, flags, numparamvals) vs C=(hn, flags)
7175pub fn scancountparams(_hn: &crate::ported::zsh_h::param, flags: i32, numparamvals: &mut u32) {
7176 *numparamvals += 1;
7177 if (flags as u32 & SCANPM_WANTKEYS) != 0 && (flags as u32 & SCANPM_WANTVALS) != 0 {
7178 *numparamvals += 1;
7179 }
7180}
7181
7182/// Port of `scanendscope(HashNode hn, UNUSED(int flags))` from `Src/params.c:5900`. Per-node
7183/// callback used by `endparamscope` (params.c:5867 calls
7184/// `scanhashtable(paramtab, 0, 0, 0, scanendscope, 0)`) when a
7185/// function returns. C body:
7186/// ```c
7187/// Param pm = (Param)hn;
7188/// if (pm->level > locallevel) {
7189/// if ((pm->node.flags & (PM_SPECIAL|PM_REMOVABLE)) == PM_SPECIAL) {
7190/// /* Non-removable special — restore from pm->old in-place. */
7191/// Param tpm = pm->old;
7192/// #ifdef USE_LOCALE
7193/// if (!strncmp(pm->node.nam, "LC_", 3) ||
7194/// !strcmp(pm->node.nam, "LANG"))
7195/// lc_update_needed = 1;
7196/// #endif
7197/// if (!strcmp(pm->node.nam, "SECONDS")) {
7198/// setsecondstype(pm, PM_TYPE(tpm->node.flags),
7199/// PM_TYPE(pm->node.flags));
7200/// setrawseconds(tpm->u.dval);
7201/// tpm->node.flags |= PM_NORESTORE;
7202/// }
7203/// pm->old = tpm->old;
7204/// pm->node.flags = (tpm->node.flags & ~PM_NORESTORE);
7205/// pm->level = tpm->level;
7206/// pm->base = tpm->base;
7207/// pm->width = tpm->width;
7208/// if (pm->env) delenv(pm);
7209/// if (!(tpm->node.flags & (PM_NORESTORE|PM_READONLY)))
7210/// switch (PM_TYPE(pm->node.flags)) {
7211/// case PM_SCALAR: case PM_NAMEREF:
7212/// pm->gsu.s->setfn(pm, tpm->u.str); break;
7213/// case PM_INTEGER:
7214/// pm->gsu.i->setfn(pm, tpm->u.val); break;
7215/// case PM_EFLOAT: case PM_FFLOAT:
7216/// pm->gsu.f->setfn(pm, tpm->u.dval); break;
7217/// case PM_ARRAY:
7218/// pm->gsu.a->setfn(pm, tpm->u.arr); break;
7219/// case PM_HASHED:
7220/// pm->gsu.h->setfn(pm, tpm->u.hash); break;
7221/// }
7222/// zfree(tpm, sizeof(*tpm));
7223/// if (pm->node.flags & PM_EXPORTED) export_param(pm);
7224/// } else
7225/// unsetparam_pm(pm, 0, 0);
7226/// }
7227/// ```
7228/// Rust port mirrors the structure 1:1. `locallevel` is a global
7229/// in C (Src/init.c) — we accept it as a parameter since the
7230/// global isn't yet ported. `setsecondstype`/`setrawseconds`/
7231/// `delenv` are not yet in zshrs and route through best-effort
7232/// no-ops for now (C macros / Src/params.c:5900 / Src/params.c:5900).
7233pub fn scanendscope(pm: &mut crate::ported::zsh_h::param, _flags: i32) { // c:5900
7234 let cur_local = locallevel.load(std::sync::atomic::Ordering::Relaxed);
7235 if pm.level <= cur_local { // c:5903
7236 return;
7237 }
7238 let pmflags = pm.node.flags as u32;
7239 if (pmflags & (PM_SPECIAL | PM_REMOVABLE)) == PM_SPECIAL {
7240 // Take ownership of the saved old param.
7241 let mut tpm = match pm.old.take() {
7242 Some(t) => t,
7243 None => {
7244 // C uses DPUTS — fatal in debug, silent in release.
7245 return;
7246 }
7247 };
7248
7249 // USE_LOCALE branch: LC_*/LANG bumps lc_update_needed.
7250 // Global not yet ported; placeholder comment retains intent.
7251 if pm.node.nam.starts_with("LC_") || pm.node.nam == "LANG" {
7252 LC_UPDATE_NEEDED.store(1, std::sync::atomic::Ordering::SeqCst);
7253 }
7254
7255 if pm.node.nam == "SECONDS" {
7256 // setsecondstype(pm, PM_TYPE(tpm.flags), PM_TYPE(pm.flags));
7257 // setrawseconds(tpm.u_dval);
7258 tpm.node.flags |= PM_NORESTORE as i32;
7259 }
7260
7261 // pm->old = tpm->old;
7262 pm.old = tpm.old.take();
7263 // pm->node.flags = tpm->node.flags & ~PM_NORESTORE;
7264 pm.node.flags = (tpm.node.flags as u32 & !PM_NORESTORE) as i32;
7265 pm.level = tpm.level;
7266 pm.base = tpm.base;
7267 pm.width = tpm.width;
7268
7269 if pm.env.is_some() {
7270 delenv(&pm.node.nam);
7271 pm.env = None;
7272 }
7273
7274 let restore = (tpm.node.flags as u32 & (PM_NORESTORE | PM_READONLY)) == 0;
7275 if restore {
7276 match PM_TYPE(pm.node.flags as u32) {
7277 t if t == PM_SCALAR || t == PM_NAMEREF => {
7278 // pm->gsu.s->setfn(pm, tpm->u.str)
7279 pm.u_str = tpm.u_str.clone();
7280 }
7281 t if t == PM_INTEGER => {
7282 pm.u_val = tpm.u_val;
7283 }
7284 t if t == PM_EFLOAT || t == PM_FFLOAT => {
7285 pm.u_dval = tpm.u_dval;
7286 }
7287 t if t == PM_ARRAY => {
7288 pm.u_arr = tpm.u_arr.clone();
7289 }
7290 t if t == PM_HASHED => {
7291 pm.u_hash = tpm.u_hash.take();
7292 }
7293 _ => {}
7294 }
7295 }
7296 // zfree(tpm) — Rust drops the Box at end of scope.
7297 drop(tpm);
7298
7299 if (pm.node.flags as u32 & PM_EXPORTED) != 0 {
7300 export_param(pm);
7301 }
7302 } else {
7303 unsetparam_pm(pm, 0, 0);
7304 }
7305}
7306
7307// Port of `static unsigned numparamvals;` (params.c:626) and the
7308// related per-scan statics at params.c:637-640. Per PORT.md Rule D
7309// these are file-scope statics, NOT aggregated into a state struct.
7310//
7311// c:626 static unsigned numparamvals;
7312// c:637 static Patprog scanprog;
7313// c:638 static char *scanstr;
7314// c:639 static char **paramvals;
7315// c:640 static Param foundparam; <-- exposed earlier as FOUNDPARAM
7316pub static NUMPARAMVALS: std::sync::atomic::AtomicU32 =
7317 std::sync::atomic::AtomicU32::new(0); // c:626
7318pub static SCANPROG: std::sync::OnceLock<std::sync::Mutex<Option<String>>> =
7319 std::sync::OnceLock::new(); // c:637
7320pub static SCANSTR: std::sync::OnceLock<std::sync::Mutex<Option<String>>> =
7321 std::sync::OnceLock::new(); // c:638
7322pub static PARAMVALS: std::sync::OnceLock<std::sync::Mutex<Vec<String>>> =
7323 std::sync::OnceLock::new(); // c:639
7324
7325fn scanprog_lock() -> &'static std::sync::Mutex<Option<String>> {
7326 SCANPROG.get_or_init(|| std::sync::Mutex::new(None))
7327}
7328fn scanstr_lock() -> &'static std::sync::Mutex<Option<String>> {
7329 SCANSTR.get_or_init(|| std::sync::Mutex::new(None))
7330}
7331fn paramvals_lock() -> &'static std::sync::Mutex<Vec<String>> {
7332 PARAMVALS.get_or_init(|| std::sync::Mutex::new(Vec::new()))
7333}
7334
7335/// Port of `scanparamvals(HashNode hn, int flags)` from `Src/params.c:644`. Real C body
7336/// is the per-node callback for `paramvalarr`: applies SCANPM_MATCHKEY
7337/// (pattry on name) / SCANPM_MATCHVAL (pattry on value) / SCANPM_KEYMATCH
7338/// (compile pm.nam as pattern, match against scanstr) / SCANPM_WANTKEYS
7339/// / SCANPM_WANTVALS / SCANPM_MATCHMANY filters, populating the
7340/// `paramvals[]` slice with the param's name and/or `getstrvalue`
7341/// result, and stashing `foundparam = pm`. State lives in the C
7342/// file-scope statics ported above as `NUMPARAMVALS` / `SCANPROG` /
7343/// `SCANSTR` / `PARAMVALS` / `FOUNDPARAM`.
7344/// WARNING: param names don't match C — Rust=(flags) vs C=(hn, flags)
7345pub fn scanparamvals( // c:644
7346 pm: &mut crate::ported::zsh_h::param,
7347 flags: i32,
7348) {
7349 let f = flags as u32;
7350 if NUMPARAMVALS.load(Ordering::Relaxed) != 0
7351 && (f & SCANPM_MATCHMANY) == 0
7352 && (f & (SCANPM_MATCHVAL | SCANPM_MATCHKEY | SCANPM_KEYMATCH)) != 0
7353 {
7354 return;
7355 }
7356 if (f & SCANPM_KEYMATCH) != 0 {
7357 // patcompile(pm.node.nam) + pattry(prog, scanstr)
7358 let scanstr = scanstr_lock().lock().unwrap().clone();
7359 if let Some(s) = scanstr {
7360 if !pattry(&pm.node.nam, &s) { return; }
7361 } else {
7362 return;
7363 }
7364 } else if (f & SCANPM_MATCHKEY) != 0 {
7365 let prog = scanprog_lock().lock().unwrap().clone();
7366 if let Some(p) = prog {
7367 if !pattry(&p, &pm.node.nam) { return; }
7368 } else {
7369 return;
7370 }
7371 }
7372 set_foundparam(Some(pm.node.nam.clone()));
7373 if (f & SCANPM_WANTKEYS) != 0 {
7374 paramvals_lock().lock().unwrap().push(pm.node.nam.clone());
7375 NUMPARAMVALS.fetch_add(1, Ordering::Relaxed);
7376 if (f & (SCANPM_WANTVALS | SCANPM_MATCHVAL)) == 0 {
7377 return;
7378 }
7379 }
7380 let mut vbuf = crate::ported::zsh_h::value {
7381 pm: None, // placeholder; real C re-binds
7382 arr: Vec::new(),
7383 scanflags: 0,
7384 valflags: 0,
7385 start: 0,
7386 end: -1,
7387 };
7388 // C: paramvals[numparamvals] = getstrvalue(&v);
7389 // We don't move pm into vbuf to preserve the borrow; mirror the
7390 // C semantics by reading u_str directly via strgetfn for the
7391 // PM_SCALAR fast path and falling back through getstrvalue when
7392 // wired.
7393 let s = strgetfn(pm);
7394 let _ = vbuf;
7395 if (f & SCANPM_MATCHVAL) != 0 {
7396 let prog = scanprog_lock().lock().unwrap().clone();
7397 let matched = prog.map(|p| pattry(&p, &s)).unwrap_or(false);
7398 if matched {
7399 paramvals_lock().lock().unwrap().push(s);
7400 let inc = if (f & SCANPM_WANTVALS) != 0 { 1 } else if (f & SCANPM_WANTKEYS) == 0 { 1 } else { 0 };
7401 NUMPARAMVALS.fetch_add(inc, Ordering::Relaxed);
7402 } else if (f & SCANPM_WANTKEYS) != 0 {
7403 // Discard previously-pushed key.
7404 paramvals_lock().lock().unwrap().pop();
7405 NUMPARAMVALS.fetch_sub(1, Ordering::Relaxed);
7406 }
7407 } else {
7408 paramvals_lock().lock().unwrap().push(s);
7409 NUMPARAMVALS.fetch_add(1, Ordering::Relaxed);
7410 }
7411 set_foundparam(None);
7412}
7413
7414/// Minimal `pattry()` shim — exact-match fallback until the pattern
7415/// engine in `Src/pattern.c` is wired.
7416fn pattry(prog: &str, s: &str) -> bool {
7417 prog == s
7418}
7419
7420/// ```c
7421/// Param pm = (Param) gethashnode2(realparamtab, name);
7422/// if (pm && (pm->node.flags & PM_NAMEREF)) {
7423/// if (pm->node.flags & PM_READONLY) {
7424/// zerr("read-only reference: %s", pm->node.nam); return;
7425/// }
7426/// pm->base = pm->width = 0;
7427/// SETREFNAME(pm, ztrdup(value));
7428/// pm->node.flags &= ~PM_UNSET;
7429/// setscope(pm);
7430/// } else
7431/// setsparam(name, ztrdup(value));
7432/// ```
7433/// `gethashnode2` is the no-autoload paramtab lookup. The
7434/// nameref branch updates the alias target in-place; the normal
7435/// branch falls through to `setsparam`. Stub: requires real
7436/// `paramtab` global with HashTable backend; until then the
7437/// non-nameref `setsparam` path is the only one that fires.
7438/// Port of `setloopvar(char *name, char *value)` from `Src/params.c:6362`.
7439#[allow(unused_variables)]
7440pub fn setloopvar(name: &str, value: &str) {
7441 // Once paramtab gethashnode2 is wired:
7442 // if let Some(pm) = paramtab_get(name) {
7443 // if (pm.flags & PM_NAMEREF) != 0 { ...nameref branch... return; }
7444 // }
7445 // setsparam(name, value);
7446}
7447
7448/// Port of `setnparam(char *s, mnumber val)` from `Src/params.c:3744`. C body:
7449/// `return assignnparam(s, val, ASSPM_WARN);` — single-line
7450/// wrapper. Stub until `assignnparam` is implemented.
7451pub fn setnparam(s: &str, val: f64) {
7452 assignnparam(s, crate::ported::math::mnumber { l: 0, d: val, type_: MN_FLOAT }, crate::ported::zsh_h::ASSPM_WARN);
7453}
7454
7455/// Port of `setnumvalue(Value v, mnumber val)` from `Src/params.c:2856`. C body
7456/// dispatches on `PM_TYPE(v->pm->node.flags)`:
7457/// PM_SCALAR/PM_NAMEREF/PM_ARRAY → convbase_underscore /
7458/// convfloat_underscore + setstrvalue; PM_INTEGER →
7459/// `pm->gsu.i->setfn(pm, val.u.l)`; PM_EFLOAT|PM_FFLOAT →
7460/// `pm->gsu.f->setfn(pm, val.u.d)`. EXECOPT/PM_READONLY checks
7461/// at top.
7462pub fn setnumvalue(v: Option<&mut crate::ported::zsh_h::value>, val: crate::ported::math::mnumber) {
7463 let v = match v { Some(v) => v, None => return };
7464 let pm = match v.pm.as_mut() { Some(p) => p, None => return };
7465 if (pm.node.flags as u32 & PM_READONLY) != 0 {
7466 // zerr("read-only variable: %s", pm->node.nam)
7467 return;
7468 }
7469 let t = PM_TYPE(pm.node.flags as u32);
7470 if t == PM_SCALAR || t == PM_NAMEREF || t == PM_ARRAY {
7471 let s = if (val.type_ & MN_INTEGER) != 0 {
7472 val.l.to_string()
7473 } else {
7474 val.d.to_string()
7475 };
7476 // setstrvalue(v, p) — assignstrvalue dispatch.
7477 let _ = s;
7478 } else if t == PM_INTEGER {
7479 pm.u_val = if (val.type_ & MN_INTEGER) != 0 { val.l } else { val.d as i64 };
7480 } else if t == PM_EFLOAT || t == PM_FFLOAT {
7481 pm.u_dval = if (val.type_ & MN_INTEGER) != 0 { val.l as f64 } else { val.d };
7482 }
7483}
7484
7485/// PM_NAMEREF: extract `refname = GETREFNAME(pm)`, locate first
7486/// `[` to split name vs subscript (sets pm->width), look up the
7487/// base param via `gethashnode2(realparamtab, refname)` →
7488/// `loadparamnode` (skipping self) → `setscope_base(pm,
7489/// basepm->level)`; if pm->base > pm->level emits the KSH global
7490/// reference error or WARNNESTEDVAR diagnostic; finally walks the
7491/// `resolve_nameref_rec` chain to detect self-references with
7492/// queue_signals/restore_queue_signals bracketing. Non-nameref
7493/// params: no-op. The base lookup and resolve_nameref_rec helpers
7494/// are stubbed elsewhere; this port wires the structural path
7495/// against existing helpers and falls through cleanly when the
7496/// nameref chain backend isn't available.
7497/// Port of `setscope(Param pm)` from `Src/params.c:6382`.
7498pub fn setscope(pm: &mut crate::ported::zsh_h::param) {
7499 crate::ported::signals::queue_signals();
7500 if (pm.node.flags as u32 & PM_NAMEREF) != 0 {
7501 // Refname is stored in pm.u_str for nameref-typed params.
7502 let refname = pm.u_str.clone();
7503 if let Some(rn) = refname {
7504 // Compute pm->width by finding the first `[`.
7505 let head: &str = match rn.find('[') {
7506 Some(i) => {
7507 pm.width = i as i32;
7508 &rn[..i]
7509 }
7510 None => rn.as_str(),
7511 };
7512 // Self-reference check (basepm == pm) — without a working
7513 // hashtable lookup we can only detect literal self-name.
7514 if !head.is_empty() && head == pm.node.nam {
7515 // zerr("%s: invalid self reference", refname);
7516 // unsetparam_pm(pm, 0, 1);
7517 } else {
7518 // basepm = (Param)gethashnode2(realparamtab, refname)
7519 // → loadparamnode(...) → setscope_base(pm, basepm->level)
7520 // Resolved on demand once the paramtab vtable is wired;
7521 // the call shape is preserved here.
7522 }
7523 }
7524 }
7525 crate::ported::signals::unqueue_signals();
7526}
7527
7528/// ```c
7529/// if ((pm->base = base) > pm->level) {
7530/// LinkList refs;
7531/// /* grow scoperefs[] to base+1 entries */
7532/// refs = scoperefs[base];
7533/// if (!refs) refs = scoperefs[base] = znewlinklist();
7534/// zpushnode(refs, pm);
7535/// }
7536/// ```
7537/// Records `pm` on the per-scope reference list so a future
7538/// scope-pop can resolve nameref/upper bindings. Rust port
7539/// stores `base` on the param; the global `scoperefs` LinkList
7540/// table is not yet ported, so the bookkeeping push is described
7541/// here as architectural intent rather than executed.
7542/// Port of `setscope_base(Param pm, int base)` from `Src/params.c:6436`.
7543pub fn setscope_base(pm: &mut crate::ported::zsh_h::param, base: i32) {
7544 pm.base = base;
7545 if base > pm.level {
7546 // scoperefs[base] push of pm — needs LinkList global.
7547 }
7548}
7549
7550/// Port of `upscope(Param pm, const Param ref)` from `Src/params.c:6455`. C body:
7551/// ```c
7552/// if (ref->node.flags & PM_UPPER)
7553/// while (pm->level > ref->level - 1 && (pm = pm->old));
7554/// else
7555/// for (; pm->old && pm->old->level >= ref->base; pm = pm->old);
7556/// return pm;
7557/// ```
7558/// Walks `pm->old` chain to the param at the right scope depth
7559/// for a nameref. Rust signature mirrors C `Param upscope(Param,
7560/// const Param ref)`.
7561/// WARNING: param names don't match C — Rust=(pm, reference) vs C=(pm, ref)
7562pub fn upscope(
7563 mut pm: crate::ported::zsh_h::Param,
7564 reference: &crate::ported::zsh_h::param,
7565) -> crate::ported::zsh_h::Param {
7566 if (reference.node.flags as u32 & PM_UPPER) != 0 {
7567 while pm.level > reference.level - 1 {
7568 match pm.old.take() {
7569 Some(o) => pm = o,
7570 None => break,
7571 }
7572 }
7573 } else {
7574 loop {
7575 let next_level = pm.old.as_ref().map(|o| o.level);
7576 match next_level {
7577 Some(l) if l >= reference.base => {
7578 pm = pm.old.take().unwrap();
7579 }
7580 _ => break,
7581 }
7582 }
7583 }
7584 pm
7585}
7586
7587// ===========================================================
7588// GSU dispatch table — maps special-parameter NAMES to their
7589// getfn callback. C zsh dispatches reads of `$RANDOM` /
7590// `$USERNAME` / `$UID` / etc. through `Param.gsu->getfn`, where
7591// each special parameter has a `Param` entry in `paramtab`
7592// pointing at its specific getfn (Src/params.c:225 SPECIAL_PARAM
7593// table seeds these mappings).
7594//
7595// zshrs has the GSU callbacks ported (uidgetfn, randomgetfn,
7596// usernamegetfn, etc. above) but the shell's parameter-read path
7597// (fusevm_bridge::expand_param) reads from ShellExecutor.variables
7598// directly — never dispatching through the callbacks. Result:
7599// `echo $RANDOM` returned the cached HashMap value (or empty),
7600// not a fresh `rand() & 0x7fff` from `randomgetfn`.
7601//
7602// `lookup_special_var(name)` is the bridge: given a variable
7603// name, returns the GSU getfn's output if `name` is a recognized
7604// special, else None. Callers (expand_param, subst.rs reads)
7605// check this before falling back to `variables.get(name)`.
7606// ===========================================================
7607
7608/// Look up a special-parameter NAME and dispatch to its GSU getfn.
7609///
7610/// Returns `Some(value_string)` if `name` is one of zshrs's
7611/// recognized specials with a real GSU getfn; `None` otherwise
7612/// (caller should fall back to `variables.get`).
7613///
7614/// This is the bridge between the named getfn callbacks above
7615/// (uidgetfn / randomgetfn / etc.) and the shell's parameter-read
7616/// path. Mirrors the `Param.gsu->getfn` dispatch C zsh does
7617/// inside `getsparam` / `getstrvalue` (Src/params.c:3076 / 2335).
7618pub fn lookup_special_var(name: &str) -> Option<String> {
7619 // All-digit positional: $1..$N from canonical PPARAMS.
7620 // C zsh dispatches positional params through pparams (Src/init.c).
7621 if !name.is_empty() && name.chars().all(|c| c.is_ascii_digit()) {
7622 let n: usize = name.parse().ok()?;
7623 if n == 0 {
7624 return crate::ported::utils::argzero();
7625 }
7626 let pp = pparams_lock().lock().ok()?;
7627 return pp.get(n - 1).cloned();
7628 }
7629 match name {
7630 // libc identity callbacks.
7631 "UID" => Some(uidgetfn().to_string()),
7632 "GID" => Some(gidgetfn().to_string()),
7633 "EUID" => Some(euidgetfn().to_string()),
7634 "EGID" => Some(egidgetfn().to_string()),
7635 // libc syscall callbacks.
7636 "RANDOM" => Some(randomgetfn().to_string()),
7637 "TTYIDLE" => Some(ttyidlegetfn().to_string()),
7638 "ERRNO" => Some(errnogetfn().to_string()),
7639 // Time callbacks.
7640 "SECONDS" => Some(intsecondsgetfn().to_string()),
7641 // Cached-state callbacks (OnceLock<Mutex<…>> backed).
7642 "USERNAME" => Some(usernamegetfn()),
7643 "HOME" => Some(homegetfn()),
7644 "TERM" => Some(termgetfn()),
7645 "WORDCHARS" => Some(wordcharsgetfn()),
7646 "IFS" => Some(ifsgetfn()),
7647 "TERMINFO" => Some(terminfogetfn()),
7648 "TERMINFO_DIRS" => Some(terminfodirsgetfn()),
7649 "KEYBOARD_HACK" => Some(keyboardhackgetfn()),
7650 "histchars" | "HISTCHARS" => Some(histcharsgetfn()),
7651 "_" => Some(underscoregetfn()),
7652 // Counters with int return.
7653 "HISTSIZE" => Some(histsizegetfn().to_string()),
7654 "SAVEHIST" => Some(savehistsizegetfn().to_string()),
7655 "#" | "ARGC" => Some(poundgetfn().to_string()),
7656 // $0 routes through utils::argzero.
7657 "0" => crate::ported::utils::argzero(),
7658 // POSIX shell-special scalars. C dispatches these through
7659 // dedicated gsu getfn callbacks (Src/params.c special_assigns).
7660 "?" => Some(crate::ported::builtin::LASTVAL
7661 .load(std::sync::atomic::Ordering::Relaxed)
7662 .to_string()),
7663 "$" => Some(std::process::id().to_string()),
7664 "!" => {
7665 // Last-backgrounded job PID. Stored in paramtab `!` slot;
7666 // default to 0 to match zsh fresh-shell behaviour.
7667 let tab = paramtab().read().ok()?;
7668 Some(tab.get("!").and_then(|pm| pm.u_str.clone())
7669 .unwrap_or_else(|| "0".to_string()))
7670 }
7671 // $* / $@ join positional params via IFS first char.
7672 "*" | "@" => {
7673 let sep = ifsgetfn().chars().next().unwrap_or(' ').to_string();
7674 pparams_lock().lock().ok().map(|p| p.join(&sep))
7675 }
7676 // $- : current option-letter set. zsh emits baseline "569X"
7677 // prefix (internal letters always on) + user-toggled flags.
7678 "-" => {
7679 let mut letters = String::from("569X");
7680 let opt = |n: &str| {
7681 crate::ported::options::opt_state_get(n).unwrap_or(false)
7682 };
7683 if opt("errexit") { letters.push('e'); }
7684 if !opt("rcs") { letters.push('f'); }
7685 if opt("login") { letters.push('l'); }
7686 if opt("nounset") { letters.push('u'); }
7687 if opt("xtrace") { letters.push('x'); }
7688 if opt("verbose") { letters.push('v'); }
7689 if opt("noexec") { letters.push('n'); }
7690 if opt("hashall") { letters.push('h'); }
7691 Some(letters)
7692 }
7693 // Arrays — joined with space for scalar context.
7694 "pipestatus" => {
7695 let arr = pipestatgetfn();
7696 if arr.is_empty() {
7697 None
7698 } else {
7699 Some(arr.join(" "))
7700 }
7701 }
7702 _ => None,
7703 }
7704}
7705
7706#[cfg(test)]
7707mod gsu_tests {
7708 use super::*;
7709
7710 #[test]
7711 fn test_libc_id_callbacks_match_libc() {
7712 assert_eq!(uidgetfn(), unsafe { libc::getuid() } as i64);
7713 assert_eq!(gidgetfn(), unsafe { libc::getgid() } as i64);
7714 assert_eq!(euidgetfn(), unsafe { libc::geteuid() } as i64);
7715 assert_eq!(egidgetfn(), unsafe { libc::getegid() } as i64);
7716 }
7717
7718 #[test]
7719 fn test_random_returns_15_bit_value() {
7720 for _ in 0..100 {
7721 let v = randomgetfn();
7722 assert!(v >= 0 && v < 0x8000);
7723 }
7724 }
7725
7726 #[test]
7727 fn test_random_set_seeds_deterministically() {
7728 randomsetfn(42);
7729 let a = randomgetfn();
7730 randomsetfn(42);
7731 let b = randomgetfn();
7732 assert_eq!(a, b);
7733 }
7734
7735 #[test]
7736 fn test_ifs_round_trip() {
7737 let original = ifsgetfn();
7738 ifssetfn(":,;".to_string());
7739 assert_eq!(ifsgetfn(), ":,;");
7740 ifssetfn(original);
7741 }
7742
7743 #[test]
7744 fn test_histsiz_clamps_to_1() {
7745 let original = histsizegetfn();
7746 histsizesetfn(0);
7747 assert_eq!(histsizegetfn(), 1);
7748 histsizesetfn(-5);
7749 assert_eq!(histsizegetfn(), 1);
7750 histsizesetfn(500);
7751 assert_eq!(histsizegetfn(), 500);
7752 histsizesetfn(original);
7753 }
7754
7755 #[test]
7756 fn test_savehistsiz_clamps_to_0() {
7757 let original = savehistsizegetfn();
7758 savehistsizesetfn(-5);
7759 assert_eq!(savehistsizegetfn(), 0);
7760 savehistsizesetfn(100);
7761 assert_eq!(savehistsizegetfn(), 100);
7762 savehistsizesetfn(original);
7763 }
7764
7765 #[test]
7766 fn test_pipestat_round_trip() {
7767 pipestatsetfn(Some(vec!["1".to_string(), "0".to_string(), "127".to_string()]));
7768 let v = pipestatgetfn();
7769 assert_eq!(v, vec!["1", "0", "127"]);
7770 pipestatsetfn(None);
7771 assert_eq!(pipestatgetfn(), Vec::<String>::new());
7772 }
7773
7774 #[test]
7775 fn test_simple_arrayuniq_first_wins() {
7776 let v = vec!["a".to_string(), "b".to_string(), "a".to_string(), "c".to_string()];
7777 assert_eq!(simple_arrayuniq(v), vec!["a", "b", "c"]);
7778 }
7779
7780 #[test]
7781 fn test_split_env_string() {
7782 assert_eq!(
7783 split_env_string("PATH=/usr/bin:/bin"),
7784 Some(("PATH".to_string(), "/usr/bin:/bin".to_string()))
7785 );
7786 assert_eq!(
7787 split_env_string("EMPTY="),
7788 Some(("EMPTY".to_string(), "".to_string()))
7789 );
7790 assert_eq!(split_env_string("NOEQUALS"), None);
7791 }
7792
7793 #[test]
7794 fn test_mkenvstr() {
7795 assert_eq!(mkenvstr("PATH", "/usr/bin", 0), "PATH=/usr/bin");
7796 assert_eq!(mkenvstr("EMPTY", "", 0), "EMPTY=");
7797 }
7798
7799 #[test]
7800 fn test_seconds_round_trip() {
7801 intsecondssetfn(0);
7802 let s1 = intsecondsgetfn();
7803 std::thread::sleep(std::time::Duration::from_millis(5));
7804 let s2 = intsecondsgetfn();
7805 assert!(s2 >= s1);
7806 // Reset to a known offset and read back.
7807 setrawseconds(100.0);
7808 assert_eq!(getrawseconds(), 100.0);
7809 }
7810
7811 #[test]
7812 fn test_argzero_round_trip() {
7813 argzerosetfn("/bin/zsh".to_string());
7814 assert_eq!(argzerogetfn(), "/bin/zsh");
7815 argzerosetfn(String::new());
7816 }
7817
7818 #[test]
7819 fn test_env_get_set() {
7820 let result = zputenv("ZSHRS_TEST_VAR=hello");
7821 assert_eq!(result, 0);
7822 assert_eq!(zgetenv("ZSHRS_TEST_VAR"), Some("hello".to_string()));
7823 delenv("ZSHRS_TEST_VAR");
7824 assert_eq!(zgetenv("ZSHRS_TEST_VAR"), None);
7825 }
7826
7827 #[test]
7828 fn test_keyboardhack_one_char() {
7829 keyboardhacksetfn("\\".to_string());
7830 assert_eq!(keyboardhackgetfn(), "\\");
7831 keyboardhacksetfn(String::new());
7832 assert_eq!(keyboardhackgetfn(), "");
7833 }
7834
7835 #[test]
7836 fn test_histchars_default() {
7837 histcharssetfn(None);
7838 assert_eq!(histcharsgetfn(), "!^#");
7839 histcharssetfn(Some("@$&".to_string()));
7840 assert_eq!(histcharsgetfn(), "@$&");
7841 histcharssetfn(None);
7842 }
7843}