Skip to main content

zsh/ported/modules/
zutil.rs

1//! Zsh utility builtins - port of Modules/zutil.c
2//!
3//! Style stuff.                                                             // c:82
4//! Hash table of styles and associated functions.                           // c:104
5//! Format stuff.                                                            // c:800
6//! Zregexparse stuff.                                                       // c:1091
7//!
8//! Provides zstyle, zformat, zparseopts builtins.
9
10use crate::ported::builtin::PPARAMS;
11use crate::ported::glob::tokenize;
12use crate::ported::mem::{popheap, pushheap};
13use crate::ported::options::opt_state_set;
14use crate::ported::params::{
15    assignaparam, getaparam, getsparam, paramtab, setaparam, sethparam, setsparam, unsetparam,
16};
17use crate::ported::pattern::{patcompile, pattry};
18use crate::ported::signals_h::{queue_signals, unqueue_signals};
19use crate::ported::utils::{errflag, zwarnnam};
20use crate::ported::zsh_h::PAT_HEAPDUP;
21use crate::ported::zsh_h::{
22    eprog, features, hashnode, hashtable, isset, module, opt_name, options, param, Eprog, HashNode,
23    HashTable, Param, Patprog, ERRFLAG_INT, EXTENDEDGLOB, MAX_OPS, OPT_ARG, OPT_ISSET, PAT_STATIC, PM_ARRAY,
24};
25use std::collections::HashMap;
26use std::io::Write;
27use std::sync::atomic::Ordering;
28use std::sync::{Mutex, OnceLock};
29
30/// Port of `savematch(MatchData *m)` from Src/Modules/zutil.c:40.
31/// C: `static void savematch(MatchData *m)` — snapshot $match/$mbegin/
32/// $mend into the MatchData struct.
33#[allow(non_snake_case)]
34pub fn savematch(m: &mut MatchData) {
35    // c:40
36    queue_signals(); // c:44
37                     // c:45-50 — three `a = getaparam("X"); m->X = a ? zarrdup(a) : NULL`
38                     // captures. The previous Rust port hardcoded `a = None` for all
39                     // three because the fabricated `getaparam(Option<&mut value>)` sig
40                     // couldn't take a name string. Now that `getaparam(&str)` matches
41                     // C, real reads from paramtab work end-to-end.
42    m.r#match = getaparam("match"); // c:45-46
43    m.mbegin = getaparam("mbegin"); // c:47-48
44    m.mend = getaparam("mend"); // c:49-50
45    unqueue_signals(); // c:51
46}
47
48/// Port of `static void restorematch(MatchData *m)` from
49/// `Src/Modules/zutil.c:55`.
50///
51/// C body (c:57-68):
52/// ```c
53/// if (m->match)  setaparam("match",  m->match);
54/// else           unsetparam("match");
55/// if (m->mbegin) setaparam("mbegin", m->mbegin);
56/// else           unsetparam("mbegin");
57/// if (m->mend)   setaparam("mend",   m->mend);
58/// else           unsetparam("mend");
59/// ```
60///
61/// Restores `$match`/`$mbegin`/`$mend` from a snapshot. Critical:
62/// when the saved field is NULL/None, C **unsets** the param. The
63/// previous Rust port left it alone — comment claimed "the Rust
64/// paramtab API doesn't yet expose unsetparam-by-string" but
65/// `unsetparam` HAS been ported (`params::unsetparam` at
66/// params.rs:4731). Skipping the unset means a regex callout that
67/// set `$match` from an originally-unset state would leave `$match`
68/// set after restorematch — the OPPOSITE of the documented contract.
69pub fn restorematch(m: &MatchData) {
70    // c:55
71    // c:57-68 — C uses `setaparam` which routes through `assignaparam`
72    // with `ASSPM_WARN` (params.c:3766). The warn flag tells
73    // assignaparam to emit "scalar parameter X created globally in
74    // function" / "read-only variable" diagnostics on write attempts.
75    //
76    // Prior Rust port called `assignaparam` directly with `flags = 0`,
77    // dropping the WARN bit — a user who pinned `typeset -r match`
78    // upstream would see the readonly assignment silently swallowed,
79    // where C zsh prints `zsh: read-only variable: match` and bails.
80    // Route through `setaparam` (port at params.rs:6023 already does
81    // the `assignaparam(name, val, ASSPM_WARN)` wrap) so the
82    // diagnostic surface matches C bit-for-bit.
83    if let Some(v) = m.r#match.as_ref() {
84        setaparam("match", v.clone()); // c:58
85    } else {
86        unsetparam("match"); // c:60
87    }
88    if let Some(v) = m.mbegin.as_ref() {
89        setaparam("mbegin", v.clone()); // c:62
90    } else {
91        unsetparam("mbegin"); // c:64
92    }
93    if let Some(v) = m.mend.as_ref() {
94        setaparam("mend", v.clone()); // c:66
95    } else {
96        unsetparam("mend"); // c:68
97    }
98}
99
100/// Port of `freematch(Cmatch m, int nbeg, int nend)` from Src/Modules/zutil.c:72.
101/// C: `static void freematch(MatchData *m)` — drops the captured arrays.
102#[allow(non_snake_case)]
103pub fn freematch(m: &mut MatchData) {
104    // c:72
105    // c:72
106    // c:74-81 — freearray(m->match/mbegin/mend) when non-NULL. Rust
107    // path: take() drops the inner Vec, mirroring freearray + NULL set.
108    m.r#match.take();
109    m.mbegin.take();
110    m.mend.take();
111}
112// `MatchData` is defined above (line 23) — Option<Vec<String>> per field
113// matches the C `char **match`/`mbegin`/`mend` semantics where NULL means
114// the variable was unset. The savematch/restorematch/freematch ports
115// below operate on that existing struct.
116
117/// `Stypat` mirroring Src/Modules/zutil.c:97-104.
118///
119/// `Clone` is a RUST-ONLY addition (C moves pointers): `(...)` subshells
120/// are in-process in zshrs, so `subshell_begin` has to deep-copy the
121/// zstyle table the way `fork()` copies it for C — see the
122/// `SubshellSnapshot::zstyles` field in fusevm_bridge.rs.
123#[allow(non_camel_case_types)]
124#[derive(Clone)]
125pub struct stypat {
126    pub next: Option<Box<stypat>>, // c:98 Stypat next
127    pub pat: String,               // c:99 char *pat
128    pub prog: Option<crate::ported::pattern::Patprog>, // c:100 Patprog prog (compiled)
129    pub weight: u64,               // c:101 zulong weight
130    pub eval: Option<Eprog>,       // c:102 Eprog eval
131    pub vals: Vec<String>,         // c:103 char **vals
132}
133/// `Stypat` type alias.
134pub type Stypat = Box<stypat>;
135
136/// `Style` mirroring Src/Modules/zutil.c:91-94.
137#[allow(non_camel_case_types)]
138pub struct style {
139    pub node: hashnode,       // c:92 struct hashnode node
140    pub pats: Option<Stypat>, // c:93 Stypat pats (sorted by weight)
141}
142/// `Style` type alias.
143pub type Style = Box<style>;
144
145/// Global `zstyletab` mirror — port of the static
146/// `static HashTable zstyletab` in Src/Modules/zutil.c:209.
147/// C allocates this via `newzstyletable()` (c:270) during
148/// module setup; the Rust port uses a `LazyLock<Mutex<>>`
149/// since the table is process-global and `bin_zstyle` /
150/// `lookupstyle` / `testforstyle` all need to share it.
151#[allow(non_upper_case_globals)]
152pub static zstyletab: std::sync::LazyLock<Mutex<style_table>> =
153    std::sync::LazyLock::new(|| Mutex::new(style_table::new())); // c:209
154
155/// Port of `freestylepatnode(Stypat p)` from Src/Modules/zutil.c:111.
156/// C: `static void freestylepatnode(Stypat p)` — drops pat/prog/vals/eval.
157#[allow(non_snake_case)]
158pub fn freestylepatnode(p: Stypat) {
159    // c:111
160    // c:111 zsfree(p->pat) — String drop
161    // c:114 freepatprog(p->prog) — Option<()> drop
162    // c:115-116 if (p->vals) freearray(p->vals) — Vec<String> drop
163    // c:117-118 if (p->eval) freeeprog(p->eval) — Option<()> drop
164    // c:119 zfree(p, sizeof(*p)) — Box<stypat> drop
165    drop(p);
166}
167
168/// Port of `freestylenode(HashNode hn)` from Src/Modules/zutil.c:123.
169/// C: `static void freestylenode(HashNode hn)` — walk pats list freeing
170/// each via freestylepatnode, then free node name + Style.
171#[allow(non_snake_case)]
172pub fn freestylenode(hn: HashNode) {
173    // c:123
174    // c:123 — Style s = (Style) hn; (C uses hashnode-prefix
175    // inheritance; the Rust HashNode and Style are separate Boxes so
176    // the cast collapses to dropping hn — its underlying style.pats
177    // chain drops with it.)
178    let s: HashNode = hn;
179    // c:111 — Stypat p, pn;
180    // c:111-133 — while (p) { pn = p->next; freestylepatnode(p); p = pn; }
181    // Rust: dropping s drops style.pats recursively.
182    drop(s);
183    // c:135 zsfree(s->node.nam) + c:136 zfree(s) — Rust Drop handles.
184}
185
186/// Port of `freestypat(Stypat p, Style s, Stypat prev)` from Src/Modules/zutil.c:151.
187/// C: `static void freestypat(Stypat p, Style s, Stypat prev)` — unlink
188/// from style.pats list, then freestylepatnode. If style empties,
189/// remove from zstyletab too.
190#[allow(non_snake_case)]
191pub fn freestypat(mut p: Stypat, s: Option<&mut style>, prev: Option<&mut stypat>) {
192    // c:151
193    // c:151-158 — relink prev->next to p->next (or s->pats if no prev).
194    // Use Option::take() to move the chain pointer out of p, since
195    // stypat doesn't derive Clone (matching C's pointer-move semantics).
196    let next = p.next.take(); // c:155 capture p->next
197    let s_has_some = s.is_some();
198    if let Some(s_ref) = s {
199        // c:153
200        if let Some(prev_ref) = prev {
201            // c:154
202            prev_ref.next = next; // c:155 prev->next = p->next
203        } else {
204            s_ref.pats = next; // c:157 s->pats = p->next
205        }
206    }
207    // c:160 — freestylepatnode(p);
208    freestylepatnode(p);
209    // c:162-167 — if (s && !s->pats) { zstyletab->removenode + zsfree(name) + zfree(s) }
210    // Static-link path: zstyletab access lives outside src/ported; the
211    // removal is a no-op until the style table accessor is wired.
212    let _ = s_has_some;
213}
214
215impl style_table {
216    /// WARNING: NOT IN ZUTIL.C — method on Rust-only `style_table` wrapper.
217    /// C inlines this pattern at every callsite; Rust factors it onto the wrapper.
218    pub fn new() -> Self {
219        Self::default()
220    }
221
222    /// Port of `setstypat(Style s, char *pat, Patprog prog, char **vals, int eval)` from `Src/Modules/zutil.c:295`.
223    /// Insert or replace a pattern→values mapping for a style.
224    /// Mirrors Src/Modules/zutil.c:295 `setstypat` + c:403 `addstyle`
225    /// — find or create the style's pats list, replace if pattern
226    /// already present, else insert in weight-descending order.
227    pub fn set(
228        &mut self,
229        pattern: &str,
230        style: &str,
231        values: Vec<String>,
232        eval_prog: Option<Eprog>,
233    ) {
234        if let Ok(m) = self.memo.get_mut() {
235            m.clear(); // the patterns change: drop memoized matches
236        }
237        let style_patterns = self.styles.entry(style.to_string()).or_default();
238        // c:319-333 — Exists → replace.
239        if let Some(existing) = style_patterns.iter_mut().find(|p| p.pat == pattern) {
240            existing.vals = values; // c:328
241            existing.eval = eval_prog; // c:329 p->eval = eprog (the parsed program)
242            return;
243        }
244        // c:344-385 — Calculate weight: high 32 bits = colon-component
245        // count, low 32 bits = sum of per-component specificity (0/1/2).
246        //
247        // Scoring per component:
248        //   `*` (alone in component, must be followed by NUL or `:`) → 0
249        //   contains a pattern metachar (`( | * [ < ? # ^`) → 1
250        //   plain literal → 2
251        //
252        // Prior Rust port omitted the c:365 lookahead `(!str[1] ||
253        // str[1] == ':')` on the wildcard-component check, so patterns
254        // like `*foo:bar` mis-scored their first component as a bare
255        // wildcard (0) instead of a metachar-containing pattern (1).
256        // The miscount produced incorrect zstyle ordering — more-
257        // specific patterns lost out to less-specific siblings.
258        let mut weight: u64 = 0;
259        let mut tmp: u64 = 2;
260        let mut first = true;
261        let bytes = pattern.as_bytes();
262        let mut i = 0;
263        while i < bytes.len() {
264            let ch = bytes[i] as char;
265            let next: Option<u8> = bytes.get(i + 1).copied();
266            // c:365 — `if (first && *str == '*' && (!str[1] || str[1] == ':'))`
267            //   "alone-star component": star is the first char AND the
268            //   next char ends the component (NUL or `:`).
269            if first && ch == '*' && (next.is_none() || next == Some(b':')) {
270                tmp = 0;
271                i += 1;
272                continue;
273            }
274            first = false; // c:370
275            if matches!(ch, '(' | '|' | '*' | '[' | '<' | '?' | '#' | '^') {
276                // c:372
277                tmp = 1;
278            }
279            if ch == ':' {
280                // c:377
281                weight += 1u64 << 32; // c:379
282                first = true; // c:381
283                weight += tmp; // c:382
284                tmp = 2; // c:383
285            }
286            i += 1;
287        }
288        weight += tmp; // c:386
289                       // c:337-342 — New pattern: build stypat.
290                       // c:339 — `p->prog = prog;`. bin_zstyle compiles the pattern
291                       // once, when the style is set (c:525-527 `tokenize(pat)`,
292                       // `patcompile(pat, PAT_ZDUP, NULL)`), and every lookup reuses
293                       // it. Compiling at lookup time instead re-ran patcompile for
294                       // every pattern of a style on every zstyle query.
295        let prog = {
296            let mut tok = pattern.to_string();
297            crate::ported::glob::tokenize(&mut tok); // c:526
298            patcompile(&tok, crate::ported::zsh_h::PAT_ZDUP, None) // c:527
299        };
300        let sp = stypat {
301            next: None,               // c:342
302            pat: pattern.to_string(), // c:338
303            prog,                     // c:339
304            weight,                   // c:386
305            eval: eval_prog,          // c:341 p->eval = eprog (the parsed program)
306            vals: values,             // c:340
307        };
308        // c:388-396 — insert q in weight-descending order (highest first).
309        let pos = style_patterns
310            .iter()
311            .position(|p| p.weight < weight)
312            .unwrap_or(style_patterns.len());
313        style_patterns.insert(pos, sp);
314    }
315
316    /// Port of `bin_zstyle(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))` from `Src/Modules/zutil.c:487`.
317    /// Look up the values for (context, style). Mirrors
318    /// Src/Modules/zutil.c:443 `lookupstyle` — walk the style's pats
319    /// list, return values from the first weight-sorted entry whose
320    /// pat matches the context.
321    pub fn get(&self, context: &str, style: &str) -> Option<&[String]> {
322        let patterns = self.styles.get(style)?;
323        self.matched_pat(patterns, context, style)
324            .map(|i| patterns[i].vals.as_slice())
325    }
326
327    /// c:Src/Modules/zutil.c:768-779 — `bin_zstyle -g` retrieval. Unlike
328    /// `lookupstyle`/`get` (which `pattry`-match the CONTEXT against every
329    /// stored pattern), `-g` does an EXACT pattern-string compare
330    /// (`if (!strcmp(args[2], p->pat))`). So after `zstyle ':s:*' k v`,
331    /// `zstyle -g out ':s:sub' k` returns NOTHING (":s:sub" != ":s:*"),
332    /// whereas `zstyle -s ':s:sub' k out` matches and yields "v".
333    pub fn get_exact(&self, pattern: &str, style: &str) -> Option<&[String]> {
334        self.styles.get(style).and_then(|pats| {
335            pats.iter()
336                .find(|p| p.pat == pattern)
337                .map(|p| p.vals.as_slice())
338        })
339    }
340
341    /// WARNING: NOT IN ZUTIL.C — method on the Rust-only `style_table`
342    /// wrapper. Same best-pattern-match walk as `get`, but returns the
343    /// matched entry's values AND whether it is an `-e` (eval) style, so
344    /// `lookupstyle` can decide to execute the body (C reads the matched
345    /// `Stypat`'s `eval` field inline; the wrapper keeps the map private).
346    pub fn get_match(&self, context: &str, style: &str) -> Option<(Vec<String>, bool)> {
347        let patterns = self.styles.get(style)?;
348        self.matched_pat(patterns, context, style)
349            .map(|i| (patterns[i].vals.clone(), patterns[i].eval.is_some()))
350    }
351
352    /// WARNING: NOT IN ZUTIL.C — method on the Rust-only `style_table`
353    /// wrapper. The c:452-458 walk shared by `get` and `get_match`: the index
354    /// of the first pattern, in weight order, whose prog matches `context`
355    /// (c:455 `pattry(p->prog, context)`, on the prog compiled when the style
356    /// was set). Answers are memoized per (style, context); see `memo`.
357    fn matched_pat(&self, patterns: &[stypat], context: &str, style: &str) -> Option<usize> {
358        if let Some(hit) = self
359            .memo
360            .lock()
361            .ok()
362            .and_then(|m| m.get(style).and_then(|c| c.get(context)).copied())
363        {
364            return hit;
365        }
366        let found = patterns
367            .iter()
368            .position(|p| p.pat == "*" || p.prog.as_ref().is_some_and(|prog| pattry(prog, context)));
369        if let Ok(mut m) = self.memo.lock() {
370            m.entry(style.to_string())
371                .or_default()
372                .insert(context.to_string(), found);
373        }
374        found
375    }
376
377    /// WARNING: NOT IN ZUTIL.C — method on Rust-only `style_table` wrapper.
378    /// C inlines this pattern at every callsite; Rust factors it onto the wrapper.
379    /// Remove style/pattern entries from the table. Mirrors the
380    /// `-d` dispatch arms of `bin_zstyle` (Src/Modules/zutil.c:487).
381    pub fn delete(&mut self, pattern: Option<&str>, style: Option<&str>) {
382        if let Ok(m) = self.memo.get_mut() {
383            m.clear(); // the patterns change: drop memoized matches
384        }
385        match (pattern, style) {
386            (None, None) => self.styles.clear(),
387            (Some(pat), None) => {
388                for patterns in self.styles.values_mut() {
389                    patterns.retain(|p| p.pat != pat);
390                }
391                self.styles.retain(|_, v| !v.is_empty());
392            }
393            (Some(pat), Some(sty)) => {
394                if let Some(patterns) = self.styles.get_mut(sty) {
395                    patterns.retain(|p| p.pat != pat);
396                    if patterns.is_empty() {
397                        self.styles.remove(sty);
398                    }
399                }
400            }
401            (None, Some(sty)) => {
402                self.styles.remove(sty);
403            }
404        }
405    }
406
407    /// Port of `setstypat(Style s, char *pat, Patprog prog, char **vals, int eval)` from `Src/Modules/zutil.c:295`.
408    /// Return `(pattern, style, values)` triples for `zstyle -L` /
409    /// `zstyle -a` listing. Mirrors bin_zstyle list dispatch
410    /// (Src/Modules/zutil.c:487 -L/-a arms).
411    pub fn list(&self, context: Option<&str>) -> Vec<(String, String, Vec<String>)> {
412        let mut result = Vec::new();
413        // c:Src/Modules/zutil.c:558 / :751 / :756 — every listing consumer
414        // (`printstyle` for the plain/`-L` listing, `scanpatstyles` for both
415        // `-g` shapes) calls `scanhashtable(zstyletab, 1, …)`, and that
416        // leading `1` is the SORTED flag: the style table is walked in
417        // strcmp order of the style NAME. `zstyle -g out` therefore returns
418        // its contexts in a stable order in zsh, while iterating the Rust
419        // HashMap directly produced a different order on every run.
420        let mut style_names: Vec<&String> = self.styles.keys().collect();
421        style_names.sort(); // c:hashtable.c scanhashtable sorted arm
422        for style in style_names {
423            let patterns = &self.styles[style];
424            for pat in patterns {
425                if let Some(ctx) = context {
426                    let matches =
427                        pat.pat == "*" || pat.prog.as_ref().is_some_and(|prog| pattry(prog, ctx));
428                    if !matches {
429                        continue;
430                    }
431                }
432                result.push((pat.pat.clone(), style.clone(), pat.vals.clone()));
433            }
434        }
435        result
436    }
437
438    /// WARNING: NOT IN ZUTIL.C — method on Rust-only `style_table` wrapper.
439    /// Every entry as `(pattern, style, values, is_eval)`, in [`list`]'s
440    /// order (style name sorted, each style's patterns by weight) — what
441    /// `zstyle -L` prints, `-e` included (c:Src/Modules/zutil.c:201
442    /// `printf("zstyle %s", (p->eval ? "-e " : ""))`). The recorder snapshots it so a
443    /// replay can call `setstypat` with the same arguments.
444    ///
445    /// [`list`]: style_table::list
446    pub fn entries(&self) -> Vec<(String, String, Vec<String>, bool)> {
447        let mut style_names: Vec<&String> = self.styles.keys().collect();
448        style_names.sort();
449        style_names
450            .into_iter()
451            .flat_map(|style| {
452                self.styles[style]
453                    .iter()
454                    .map(move |p| (p.pat.clone(), style.clone(), p.vals.clone(), p.eval.is_some()))
455            })
456            .collect()
457    }
458
459    /// WARNING: NOT IN ZUTIL.C — method on Rust-only `style_table` wrapper.
460    /// C inlines this pattern at every callsite; Rust factors it onto the wrapper.
461    /// List all registered style names (bin_zstyle -g without args).
462    pub fn list_styles(&self) -> Vec<&str> {
463        self.styles.keys().map(|s| s.as_str()).collect()
464    }
465
466    /// WARNING: NOT IN ZUTIL.C — method on Rust-only `style_table` wrapper.
467    /// C inlines this pattern at every callsite; Rust factors it onto the wrapper.
468    /// List all distinct patterns across every style (bin_zstyle -g
469    /// with a single pattern arg).
470    pub fn list_patterns(&self) -> Vec<&str> {
471        let mut patterns = Vec::new();
472        for pats in self.styles.values() {
473            for pat in pats {
474                if !patterns.contains(&pat.pat.as_str()) {
475                    patterns.push(pat.pat.as_str());
476                }
477            }
478        }
479        patterns
480    }
481
482    /// WARNING: NOT IN ZUTIL.C — method on Rust-only `style_table` wrapper.
483    /// C inlines this pattern at every callsite; Rust factors it onto the wrapper.
484    /// Boolean-truthy `zstyle -T` / `zstyle -t` check.
485    /// Mirrors bin_zstyle -t / -T arms in Src/Modules/zutil.c:487.
486    pub fn test(&self, context: &str, style: &str, values: Option<&[&str]>) -> bool {
487        if let Some(found) = self.get(context, style) {
488            if let Some(test_vals) = values {
489                test_vals.iter().any(|v| found.contains(&v.to_string()))
490            } else {
491                matches!(
492                    found.first().map(|s| s.as_str()),
493                    Some("true" | "yes" | "on" | "1")
494                )
495            }
496        } else {
497            false
498        }
499    }
500
501    /// WARNING: NOT IN ZUTIL.C — method on Rust-only `style_table` wrapper.
502    /// C inlines this pattern at every callsite; Rust factors it onto the wrapper.
503    /// Single-value "yes/no" interrogation of a style. The `bin_zstyle`
504    /// -b arm of Src/Modules/zutil.c:487.
505    pub fn test_bool(&self, context: &str, style: &str) -> Option<bool> {
506        self.get(context, style).and_then(|vals| {
507            if vals.len() == 1 {
508                match vals[0].as_str() {
509                    "yes" | "true" | "on" | "1" => Some(true),
510                    "no" | "false" | "off" | "0" => Some(false),
511                    _ => None,
512                }
513            } else {
514                None
515            }
516        })
517    }
518}
519
520/// Port of `static Patprog zstyle_contprog;` from Src/Modules/zutil.c:178 —
521/// the pattern `zstyle -L [context]` filters printed style patterns with.
522thread_local! {
523    static ZSTYLE_CONTPROG: std::cell::RefCell<Option<crate::ported::pattern::Patprog>> = const { std::cell::RefCell::new(None) }; // c:178
524}
525
526/// Port of `printstylenode(HashNode hn, int printflags)` from Src/Modules/zutil.c:184.
527/// C: `static void printstylenode(HashNode hn, int printflags)` — emit
528/// `zstyle -L` / basic-list output for one style entry. The context filter
529/// is the file-static `zstyle_contprog`, set by `bin_zstyle` before the scan
530/// (c:562-570), exactly as in C.
531#[allow(non_snake_case)]
532pub fn printstylenode(hn: &HashNode, printflags: i32) {
533    // c:184
534    let nam: String = hn.nam.clone();
535    let mut stdout = std::io::stdout().lock();
536    use crate::ported::utils::quotedzputs;
537    let t = match zstyletab.lock() {
538        Ok(t) => t,
539        Err(_) => return,
540    };
541    let patterns = match t.styles.get(&nam) {
542        Some(p) => p,
543        None => return,
544    };
545    if printflags == 1 {
546        // c:190-193 — ZSLIST_BASIC header: the style name on its own line.
547        // Only emitted when at least one pattern will survive the filter,
548        // matching C (the header prints unconditionally, but the contprog
549        // filter path is only reached via the syntax listing; bare `zstyle`
550        // passes no context so every pattern survives here).
551        let _ = writeln!(stdout, "{}", quotedzputs(&nam)); // c:191-192
552    }
553    for p in patterns {
554        // c:196-197 — skip patterns that don't match the context filter.
555        if ZSTYLE_CONTPROG.with(|c| {
556            c.borrow()
557                .as_ref()
558                .is_some_and(|prog| !pattry(prog, &p.pat))
559        }) {
560            continue;
561        }
562        let is_eval = p.eval.is_some();
563        if printflags == 1 {
564            // c:198-199 — `printf("%s  %s", eval ? "(eval)" : "      ", p->pat);`
565            let prefix = if is_eval { "(eval)" } else { "      " };
566            let _ = write!(stdout, "{}  {}", prefix, p.pat); // c:199
567        } else {
568            // c:201-204 — `printf("zstyle %s", eval ? "-e " : "");
569            //              quotedzputs(p->pat); putchar(' '); quotedzputs(style);`
570            let eflag = if is_eval { "-e " } else { "" };
571            let _ = write!(stdout, "zstyle {}", eflag); // c:201
572            let _ = write!(stdout, "{}", quotedzputs(&p.pat)); // c:202
573            let _ = write!(stdout, " "); // c:203
574            let _ = write!(stdout, "{}", quotedzputs(&nam)); // c:204
575        }
576        // c:206-209 — per-value: ` `, quotedzputs(v).
577        for v in &p.vals {
578            let _ = write!(stdout, " {}", quotedzputs(v)); // c:207-208
579        }
580        let _ = writeln!(stdout); // c:210
581    }
582}
583
584/// Port of `scanpatstyles(HashNode hn, int spatflags)` from Src/Modules/zutil.c:229.
585/// C: `static void scanpatstyles(HashNode hn, int spatflags)` — iterate
586/// every pattern of `hn`'s style, switching on `spatflags` (ZSPAT_NAME /
587/// ZSPAT_PAT / ZSPAT_REMOVE).
588#[allow(non_snake_case)]
589pub fn scanpatstyles(hn: HashNode, spatflags: i32) {
590    // c:229
591    // c:229 — Style s = (Style)hn;
592    let _s: HashNode = hn;
593    // c:232 — Stypat p, q;
594    // c:233 — LinkNode n;
595    // c:235-265 — for (q = NULL, p = s->pats; p; q = p, p = p->next)
596    // walks the pattern list and dispatches on spatflags. Rust port:
597    // the HashNode→Style cast doesn't yield the pats list directly
598    // (separate Boxes), so the body switches on spatflags and exits
599    // each branch without traversal until the cast is wired.
600    match spatflags {
601        // c:236
602        0 => { // c:237 ZSPAT_NAME
603             // c:238-241 — if pat matches zstyle_patname, addlinknode + return
604        }
605        1 => { // c:244 ZSPAT_PAT
606             // c:246-251 — addlinknode unless already present
607        }
608        2 => { // c:253 ZSPAT_REMOVE
609             // c:254-262 — if pat matches, freestypat(p, s, q) + return
610        }
611        _ => {}
612    }
613}
614
615impl ZFormat {
616    /// Recursive walker for zformat. Returns the index of the
617    /// terminator (`endchar`). idx is mutated in place.
618    /// Direct port of `zformat_substring()` from Src/Modules/zutil.c:814 —
619    /// the recursive descent over the format string with `%c` substitution
620    /// and `%(?...)` ternary blocks.
621    fn substring(
622        bytes: &[char],
623        idx: &mut usize,
624        out: &mut String,
625        endchar: char,
626        specs: &HashMap<char, String>,
627        presence: bool,
628        quote: bool,
629        qspecs: &HashMap<char, usize>,
630        skip: bool,
631    ) -> Option<()> {
632        while *idx < bytes.len() {
633            let c = bytes[*idx];
634            // Stop at endchar (zutil.c:820 `*s != endchar`).
635            if endchar != '\0' && c == endchar {
636                return Some(());
637            }
638            if c != '%' {
639                // Plain text — emit unless skipping (zutil.c:937-948).
640                if !skip {
641                    out.push(c);
642                }
643                *idx += 1;
644                continue;
645            }
646            // `%` — parse the spec.
647            let start = *idx;
648            *idx += 1;
649            // Optional `-` for right-align (zutil.c:825-826).
650            let mut right = false;
651            if *idx < bytes.len() && bytes[*idx] == '-' {
652                right = true;
653                *idx += 1;
654            }
655            // Optional digit run for min (zutil.c:828-831).
656            let mut min: Option<i64> = None;
657            if *idx < bytes.len() && bytes[*idx].is_ascii_digit() {
658                let mut n: i64 = 0;
659                while *idx < bytes.len() && bytes[*idx].is_ascii_digit() {
660                    n = n * 10 + bytes[*idx].to_digit(10).unwrap() as i64;
661                    *idx += 1;
662                }
663                min = Some(n);
664            }
665            // Ternary detection: `(` at this position (zutil.c:834-840).
666            let testit = *idx < bytes.len() && bytes[*idx] == '(';
667            // `%(-...` allows leading `-` after the paren (zutil.c:835-840).
668            if testit && *idx + 1 < bytes.len() && bytes[*idx + 1] == '-' {
669                right = true;
670                *idx += 1;
671            }
672            // Optional `.MAX` or just `.` after (zutil.c:841-845).
673            let mut max: Option<i64> = None;
674            if *idx < bytes.len()
675                && (bytes[*idx] == '.' || testit)
676                && *idx + 1 < bytes.len()
677                && bytes[*idx + 1].is_ascii_digit()
678            {
679                *idx += 1; // skip `.` or `(`
680                let mut n: i64 = 0;
681                while *idx < bytes.len() && bytes[*idx].is_ascii_digit() {
682                    n = n * 10 + bytes[*idx].to_digit(10).unwrap() as i64;
683                    *idx += 1;
684                }
685                max = Some(n);
686            } else if *idx < bytes.len() && (bytes[*idx] == '.' || testit) {
687                *idx += 1;
688            }
689
690            // c:849-856 — "next char isn't a legal spec char -- unwind, treat
691            // the sequence literally":
692            //   if (!testit && (!*s || *s == '%' || *s == ')' || *s == '-' || *s == '.')) {
693            //       if (!quote) start += (s - start == 1 && (*s == '%' || *s == ')'));
694            //       s = start;
695            //   }
696            // After the unwind `*s` is `%` (or `)`), never a registered spec, so
697            // the raw-copy arm below emits exactly one character.
698            let mut start = start;
699            if !testit {
700                let at = bytes.get(*idx).copied();
701                if matches!(at, None | Some('%') | Some(')') | Some('-') | Some('.')) {
702                    // c:853 — `-q` keeps `%%` and `%)` intact for the later
703                    // prompt expansion the quoted output is destined for.
704                    if !quote && *idx - start == 1 && matches!(at, Some('%') | Some(')')) {
705                        start += 1; // c:854
706                    }
707                    *idx = start; // c:855
708                }
709            }
710
711            if testit && *idx < bytes.len() {
712                // Ternary expression — zutil.c:858-902.
713                let testval: i64 = min.or(max).unwrap_or(0);
714                let spec_char = bytes[*idx];
715                let actval: bool;
716                let spec_val = specs.get(&spec_char);
717                if let Some(sv) = spec_val.filter(|s| !s.is_empty()) {
718                    if presence {
719                        let cmp_val: i64 = if testval != 0 {
720                            sv.chars().count() as i64
721                        } else {
722                            1
723                        };
724                        // c:875-876 — "don't count extra %s from quoting when
725                        // testing this"; applied even to the bare-presence
726                        // value 1, so a `-q` spec holding a `%` with no test
727                        // width tests as absent, as in C.
728                        let cmp_val = cmp_val - qspecs.get(&spec_char).copied().unwrap_or(0) as i64;
729                        actval = if right {
730                            testval < cmp_val
731                        } else {
732                            testval >= cmp_val
733                        };
734                    } else {
735                        let signed_test = if right { -testval } else { testval };
736                        // c:882 — `actval = (int) mathevali(specs[(unsigned
737                        // char) *s]) - testval;`. The spec's VALUE is an
738                        // arithmetic expression, not a bare integer: the
739                        // documented `%18(s.math.)` with `s:6*3` is true.
740                        // The previous `sv.parse()` returned 0 for any
741                        // non-literal, so every arithmetic test was false.
742                        //
743                        // C's `mathevali` REPORTS a bad expression on the way
744                        // out (checkunary -> zerr) and leaves `errflag` set,
745                        // yielding 0 (c:Src/math.c:1505-1509); `bin_zformat`
746                        // never checks it, so the shell aborts on the flag.
747                        // Rust's `mathevali` returns that message in `Err`
748                        // instead, and the port discarded it with
749                        // `.unwrap_or(0)` — so `zformat -f R '%(%.yes.no)'`
750                        // printed `yes` where zsh 5.9.2 prints `bad math
751                        // expression: operand expected at `%'` and aborts.
752                        // Surface it the way bin_let (builtin.rs:17151) and
753                        // paramsubst (subst.rs:3171) already do.
754                        let n: i64 = match crate::ported::math::mathevali(sv) {
755                            Ok(v) => v,
756                            Err(msg) => {
757                                crate::ported::utils::zerr(&msg);
758                                errflag.store(
759                                    errflag.load(Ordering::Relaxed)
760                                        | crate::ported::zsh_h::ERRFLAG_ERROR,
761                                    Ordering::Relaxed,
762                                );
763                                0 // c:Src/math.c:1509 — error yields 0
764                            }
765                        };
766                        actval = (n - signed_test) != 0;
767                    }
768                } else {
769                    actval = if presence { !right } else { testval != 0 };
770                }
771                // Skip past the spec char to find the delimiter
772                // (zutil.c:888 `if (!(endcharl = *++s))`).
773                *idx += 1;
774                if *idx >= bytes.len() {
775                    return None;
776                }
777                let endcharl = bytes[*idx];
778                *idx += 1;
779                // First branch (true-text) — emit only if actval is true,
780                // i.e. skip = skip || !actval. Wait, C says
781                // `skip || actval` for the FIRST sub-call meaning: if
782                // actval is true SKIP the first branch?
783                // Re-reading zutil.c:880-884 — comment says "Either skip
784                // true text and output false text, or vice versa". The
785                // pattern `skip || actval` for the first call means: if
786                // actval, skip the first text. So the FIRST text
787                // (between `(` and the delim) is the FALSE branch, the
788                // SECOND text (between delim and `)`) is the TRUE.
789                //     if (!(s = zformat_substring(s+1, ..., endcharl, ...,
790                //                 skip || actval)) || !*s)
791                //         return NULL;
792                // c:882-884 — a branch that runs off the end of the format
793                // without meeting its delimiter makes the whole format
794                // malformed; the previous port kept going and returned the
795                // partial text, so `zformat -F R '%(..)'` succeeded.
796                ZFormat::substring(
797                    bytes, idx, out, endcharl, specs, presence, quote, qspecs, skip || actval,
798                )?;
799                if *idx >= bytes.len() {
800                    return None; // c:884
801                }
802                *idx += 1; // c:882 `s+1` — past the delimiter
803                ZFormat::substring(
804                    bytes, idx, out, ')', specs, presence, quote, qspecs, skip || !actval,
805                )?;
806                if *idx >= bytes.len() {
807                    return None; // c:887
808                }
809                *idx += 1; // c:821 `s++` — past the closing `)`
810                continue;
811            }
812
813            if skip {
814                // In skip mode — advance past spec char and continue.
815                if *idx < bytes.len() {
816                    *idx += 1;
817                }
818                continue;
819            }
820
821            // Plain `%X` spec (zutil.c:890-922).
822            if *idx >= bytes.len() {
823                // c:952-964 with `*s == '\0'` — no spec char (`%(` or `%-`
824                // at the end): `len = s - start + 1` copies the sequence up
825                // to the terminator verbatim.
826                for &c in &bytes[start..] {
827                    out.push(c);
828                }
829                continue;
830            }
831            {
832                let spec_char = bytes[*idx];
833                *idx += 1;
834                if let Some(spec_val) = specs.get(&spec_char).filter(|_| *idx - 1 != start) {
835                    let mut val_chars: Vec<char> = spec_val.chars().collect();
836                    // c:908-920 — "the assumption with quoted specs is that the
837                    // output will be subject to further % expansion -- adjust
838                    // width specifiers so that the result will be correct
839                    // *after* that expansion": each doubled `%` inside the
840                    // width widens it by one.
841                    let (mut min, mut max) = (min, max);
842                    if (min > Some(0) || max > Some(0))
843                        && qspecs.get(&spec_char).copied().unwrap_or(0) > 0
844                    {
845                        let mut i = 0usize;
846                        while i < val_chars.len() {
847                            if val_chars[i] == '%' {
848                                let i64i = i as i64;
849                                if let Some(m) = min.filter(|&m| m > 0 && i64i < m) {
850                                    min = Some(m + 1);
851                                }
852                                if let Some(m) = max.filter(|&m| m > 0 && i64i < m) {
853                                    max = Some(m + 1);
854                                }
855                                i += 1;
856                            }
857                            i += 1;
858                        }
859                    }
860                    let len = val_chars.len() as i64;
861                    let len = match max {
862                        Some(m) if m >= 0 && len > m => {
863                            val_chars.truncate(m as usize);
864                            m
865                        }
866                        _ => len,
867                    };
868                    let outl = match min {
869                        Some(m) if m >= 0 && m > len => m,
870                        _ => len,
871                    };
872                    if len >= outl {
873                        for &c in val_chars.iter().take(outl as usize) {
874                            out.push(c);
875                        }
876                    } else {
877                        let diff = (outl - len) as usize;
878                        if right {
879                            for _ in 0..diff {
880                                out.push(' ');
881                            }
882                            for &c in val_chars.iter() {
883                                out.push(c);
884                            }
885                        } else {
886                            for &c in val_chars.iter() {
887                                out.push(c);
888                            }
889                            for _ in 0..diff {
890                                out.push(' ');
891                            }
892                        }
893                    }
894                } else {
895                    // Unknown spec — emit raw segment back
896                    // (zutil.c:923-936).
897                    for &c in &bytes[start..*idx] {
898                        out.push(c);
899                    }
900                }
901            }
902        }
903        Some(())
904    }
905} // impl ZFormat
906
907/// Port of `newzstyletable(int size, char const *name)` from Src/Modules/zutil.c:270.
908/// C: `static HashTable newzstyletable(int size, char const *name)` —
909/// `newhashtable` plus the style table's function-pointer slots.
910#[allow(non_snake_case)]
911pub fn newzstyletable(size: i32, name: &str) -> HashTable {
912    // c:270
913    let mut ht = crate::ported::hashtable::newhashtable(size, name); // c:273
914
915    ht.hash = Some(crate::ported::hashtable::hasher); // c:275
916    ht.emptytable = Some(hashtable::emptyhashtable); // c:276
917    ht.filltable = None; // c:277
918    ht.cmpnodes = Some(|a, b| a.cmp(b) as i32); // c:278 strcmp
919    ht.addnode = Some(hashtable::addhashnode); // c:279
920    /* DISABLED is not supported */
921    ht.getnode = Some(hashtable::gethashnode2); // c:281
922    ht.getnode2 = Some(hashtable::gethashnode2); // c:282
923    ht.removenode = Some(hashtable::removehashnode); // c:283
924    ht.disablenode = None; // c:284
925    ht.enablenode = None; // c:285
926    ht.freenode = Some(freestylenode); // c:286
927    ht.printnode = Some(printstylenode); // c:287
928
929    ht // c:289
930}
931
932/// Port of `setstypat(Style s, char *pat, Patprog prog, char **vals, int eval)` from Src/Modules/zutil.c:295.
933/// C: `static int setstypat(Style s, char *pat, Patprog prog,
934/// char **vals, int eval)` — store/replace a (pat, vals) entry on
935/// the Style's pat list. Returns 1 on parse error, 0 on success.
936///
937/// Static-link path routes through style_table::set on the global
938/// zstyletab. The `style_name` arg replaces the C `Style s` since
939/// Rust's style_table is keyed by name. The `prog` (Patprog) arg is
940/// ignored because style_table::set compiles at lookup-time via patmatch.
941#[allow(non_snake_case)]
942/// WARNING: param names don't match C — Rust=(style_name, pat, vals, eval) vs C=(s, pat, prog, vals, eval)
943pub fn setstypat(
944    style_name: &str,
945    pat: &str, // c:295
946    _prog: Option<Patprog>,
947    vals: Vec<String>,
948    eval: i32,
949) -> i32 {
950    // c:304-318 — `-e` (eval) style: parse the joined values into an
951    // Eprog now, so lookup can execute it. C saves/restores errflag
952    // around the parse (keeping any user-interrupt bit); on parse
953    // failure it frees `prog` and returns 1. Here `_prog` drops on the
954    // early return, which is the Rust-idiom equivalent of freepatprog.
955    let eval_prog: Option<Eprog> = if eval != 0 {
956        let joined = crate::ported::utils::zjoin(&vals, ' '); // c:309 zjoin(vals, ' ', 1)
957        match crate::ported::exec::parse_string(&joined, 0) {
958            // c:309
959            None => return 1, // c:311-314 freepatprog(prog); return 1
960            Some(ep) => Some(Box::new(crate::ported::parse::dupeprog(&ep, false))), // c:317
961        }
962    } else {
963        None
964    };
965    if let Ok(mut t) = zstyletab.lock() {
966        t.set(pat, style_name, vals, eval_prog); // c:319 set/replace
967        0
968    } else {
969        1
970    }
971}
972
973/// Port of `addstyle(char *name)` from Src/Modules/zutil.c:403.
974/// C: `static Style addstyle(char *name)` — alloc a new Style node and
975/// install in zstyletab.
976#[allow(non_snake_case)]
977/// C body (3 lines):
978///     `Style s = (Style) zshcalloc(sizeof(*s));
979///      zstyletab->addnode(zstyletab, ztrdup(name), s);
980///      return s;`
981pub fn addstyle(name: &str) -> Option<Style> {
982    // c:403
983    Some(Box::new(style {
984        // c:405 zshcalloc + return
985        node: hashnode {
986            next: None,
987            nam: name.to_string(),
988            flags: 0,
989        },
990        pats: None,
991    }))
992    // c:407 addnode — zstyletab integration is the caller's job; the
993    //                 Box is returned for them to install.
994}
995
996/// Port of `evalstyle(Stypat p)` from Src/Modules/zutil.c:413.
997/// Runs `p.eval` and reads `$reply` (array first, falling back to
998/// scalar form). Returns empty Vec on error or unset.
999/// `code` is the joined eval body (the values an `-e` style was
1000/// registered with, e.g. `reply=(computed-$((1+1)))`). C runs the
1001/// pre-parsed `p->eval` Eprog; zshrs re-runs the stored source through
1002/// the live executor, which is equivalent for setting `$reply`.
1003pub fn evalstyle(code: &str) -> Vec<String> {
1004    // c:413
1005
1006    // c:415 — int ef = errflag;
1007    let ef = errflag.load(Ordering::Relaxed);
1008    // c:418 — unsetparam("reply");
1009    unsetparam("reply");
1010    // c:419 — execode(p->eval, 1, 0, "style"): execute the style body so
1011    // it can set `$reply`. Runs on the live session executor.
1012    {
1013        // c:Src/Modules/zutil.c:419 — `execode(p->eval, 1, 0, "style");`.
1014        // A `zstyle -e` body runs with `style` appended to the eval context.
1015        // Popped on every return path. Bug #1069.
1016        let sync_eval_ctx = |stack: &[String]| {
1017            let joined = stack.join(":");
1018            if let Ok(mut tab) = crate::ported::params::paramtab().write() {
1019                if let Some(pm) = tab.get_mut("zsh_eval_context") {
1020                    pm.u_arr = Some(stack.to_vec());
1021                    pm.node.flags &= !(crate::ported::zsh_h::PM_UNSET as i32);
1022                }
1023                if let Some(pm) = tab.get_mut("ZSH_EVAL_CONTEXT") {
1024                    pm.u_str = Some(joined);
1025                    pm.node.flags &= !(crate::ported::zsh_h::PM_UNSET as i32);
1026                }
1027            }
1028        };
1029        if let Ok(mut ctx) = crate::ported::exec::zsh_eval_context.lock() {
1030            ctx.push("style".to_string());
1031            sync_eval_ctx(&ctx);
1032        }
1033        struct StyleCtxGuard<F: Fn(&[String])>(F);
1034        impl<F: Fn(&[String])> Drop for StyleCtxGuard<F> {
1035            fn drop(&mut self) {
1036                if let Ok(mut ctx) = crate::ported::exec::zsh_eval_context.lock() {
1037                    ctx.pop();
1038                    (self.0)(&ctx);
1039                }
1040            }
1041        }
1042        let _ctx_guard = StyleCtxGuard(sync_eval_ctx);
1043        let _ = crate::ported::exec::execute_script_zsh_pipeline(code);
1044    }
1045    // c:420-425 — restore errflag preserving INT bit only.
1046    let cur = errflag.load(Ordering::Relaxed);
1047    errflag.store(ef | (cur & ERRFLAG_INT), Ordering::Relaxed);
1048    if (cur & !ERRFLAG_INT) != 0 {
1049        return Vec::new(); // c:423
1050    }
1051    // c:427-433 — `if ((ret = getaparam("reply"))) ret = arrdup(ret);
1052    //              else if ((str = getsparam("reply"))) ret = [str];`
1053    queue_signals();
1054    let ret = if let Some(arr) = getaparam("reply") {
1055        arr
1056    } else if let Some(s) = getsparam("reply") {
1057        vec![s]
1058    } else {
1059        Vec::new()
1060    };
1061    unqueue_signals();
1062    // c:435 — unsetparam("reply");
1063    unsetparam("reply");
1064    ret
1065}
1066
1067/// Port of `lookupstyle(char *ctxt, char *style)` from Src/Modules/zutil.c:443.
1068/// C: `static char **lookupstyle(char *ctxt, char *style)` — find best
1069/// pat-style match against the style entry; return its vals.
1070#[allow(non_snake_case)]
1071pub fn lookupstyle(ctxt: &str, style: &str) -> Vec<String> {
1072    // c:443
1073    // c:443-463 — zstyletab->getnode2 + savematch/pattry/restorematch
1074    // loop. style_table::get_match() encapsulates the pat-walk and also
1075    // reports whether the matched entry is an `-e` (eval) style; weight
1076    // order is enforced at insert time so first-match wins.
1077    //
1078    // c:450-451 + c:459 — the pat-walk is bracketed by
1079    // `savematch(&match)` / `restorematch(&match)`. That bracket is NOT
1080    // cosmetic: `restorematch` UNSETS `$match`/`$mbegin`/`$mend`
1081    // whenever the snapshot came back NULL (zutil.c:57-68), and
1082    // `savematch` reads them with `getaparam`, which returns NULL for a
1083    // non-array. So a plain `local match` (a scalar — `_main_complete`
1084    // declares exactly that at its sh:27 `local` line) is UNSET by the
1085    // first zstyle lookup in zsh. Without the bracket zshrs left the
1086    // scalar in place and `$parameters` reported one name zsh does not
1087    // have.
1088    // c:449-450 — `s = (Style)zstyletab->getnode2(zstyletab, style);
1089    // if (s) {`: the pattern walk AND its savematch/restorematch bracket
1090    // run ONLY when a style of that NAME exists. Bracketing every lookup
1091    // unconditionally unset `$match`/`$mbegin`/`$mend` (c:57-68) even
1092    // where C returns immediately — with no zstyles defined, C never
1093    // touches them, so `_main_complete`'s sh:27 `local match` scalar
1094    // survives the whole completion in zsh and was disappearing here.
1095    let style_exists = zstyletab
1096        .lock()
1097        .map(|t| t.styles.contains_key(style))
1098        .unwrap_or(false);
1099    if !style_exists {
1100        return Vec::new(); // c:447 `found = NULL` → c:461 `return found`
1101    }
1102    let mut saved = MatchData {
1103        r#match: None,
1104        mbegin: None,
1105        mend: None,
1106    };
1107    savematch(&mut saved); // c:450
1108    let matched = match zstyletab.lock() {
1109        Ok(t) => t.get_match(ctxt, style), // (vals, is_eval)
1110        Err(_) => None,
1111    };
1112    // Lock released before evalstyle so the body can touch zstyle/params
1113    // without re-entering the table lock.
1114    let found = match matched {
1115        // c:455-456 — `found = (p->eval ? evalstyle(p) : p->vals);`
1116        // C runs evalstyle INSIDE the loop, i.e. BEFORE restorematch, so
1117        // the `-e` body still sees the match vars the pattern set.
1118        Some((vals, true)) => evalstyle(&crate::ported::utils::zjoin(&vals, ' ')),
1119        Some((vals, false)) => vals,
1120        None => Vec::new(),
1121    };
1122    restorematch(&saved); // c:459
1123    found
1124}
1125
1126// =====================================================================
1127// static struct features module_features                            c:2143
1128// =====================================================================
1129
1130/// Port of `testforstyle(char *ctxt, char *style)` from Src/Modules/zutil.c:465.
1131/// C: `static int testforstyle(char *ctxt, char *style)` — non-empty
1132/// match check for context+style. Returns `!found` so 0 == success.
1133#[allow(non_snake_case)]
1134pub fn testforstyle(ctxt: &str, style: &str) -> i32 {
1135    // c:465
1136    // c:465-484 — zstyletab lookup + pattern match against ctxt,
1137    // bracketed by savematch/restorematch exactly as `lookupstyle` is
1138    // (c:473 / c:481). See the note there for why the bracket is
1139    // load-bearing rather than cosmetic.
1140    // c:471-472 — same `if (s)` gate as lookupstyle above.
1141    let style_exists = zstyletab
1142        .lock()
1143        .map(|t| t.styles.contains_key(style))
1144        .unwrap_or(false);
1145    if !style_exists {
1146        return 1; // c:483 `return !found` with found == 0
1147    }
1148    let mut saved = MatchData {
1149        r#match: None,
1150        mbegin: None,
1151        mend: None,
1152    };
1153    savematch(&mut saved); // c:473
1154    let found = match zstyletab.lock() {
1155        // c:471
1156        Ok(t) => t.get(ctxt, style).is_some(), // c:476 pattry
1157        Err(_) => false,
1158    };
1159    restorematch(&saved); // c:481
1160    if found {
1161        0
1162    } else {
1163        1
1164    } // c:485 return !found
1165}
1166
1167/// Direct port of `bin_zstyle(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))` from `Src/Modules/zutil.c:487`.
1168/// C body (c:490-952): switch over -L/-l/-d/-s/-b/-t/-T/-m/-a/-g/-e
1169/// flags + per-mode handlers.
1170///
1171/// The per-context lookups (-s/-b/-t/-T/-m/-a/-g/-e) go through
1172/// `lookupstyle`/`testforstyle` over the global `zstyletab`.
1173/// WARNING: param names don't match C — Rust=(nam, args, _func) vs C=(nam, args, ops, func)
1174pub fn bin_zstyle(
1175    nam: &str,
1176    args: &[String], // c:487
1177    _ops: &options,
1178    _func: i32,
1179) -> i32 {
1180    // c:Src/Modules/zutil.c:487 — bin_zstyle parses args[0] directly
1181    // (BUILTIN spec has NULL optstr at c:2139) so the dispatch order
1182    // and unknown-flag diagnostic match zsh exactly. Build a local
1183    // `options` struct here, mirroring what execbuiltin's option
1184    // parser would have produced if there had been an optstr — then
1185    // the existing OPT_ISSET-driven flag arms below run unchanged.
1186    //
1187    // C flow at c:491-512 + c:587-600:
1188    //   - !args[0]                         → bare list mode
1189    //   - args[0] == "-" or "--"           → lone dash; positional follows
1190    //   - args[0] starts with -X + extra   → "invalid argument: %s"
1191    //   - args[0] == "-L"                  → list mode (ZSLIST_SYNTAX)
1192    //   - args[0] == "-e"                  → eval+add (handled like setstyle)
1193    //   - args[0] == "-d|-s|-b|-a|-t|-T|-m|-g" → action mode (positional follows)
1194    //   - else any other "-X"              → "invalid option: -X" rc=1
1195    let mut ops_local = options {
1196        ind: [0u8; MAX_OPS],
1197        args: Vec::new(),
1198        argscount: 0,
1199        argsalloc: 0,
1200    };
1201    let mut positional_start = 0;
1202    if let Some(first) = args.first() {
1203        let fb = first.as_bytes();
1204        if fb.first() == Some(&b'-') && fb.len() >= 2 && fb[1] != b'-' {
1205            if fb.len() > 2 {
1206                // c:497-499 — extra chars after the option letter
1207                // (e.g. `-Lx`) → "invalid argument: -Lx" rc=1.
1208                crate::ported::utils::zwarnnam(nam, &format!("invalid argument: {}", first));
1209                return 1;
1210            }
1211            let oc = fb[1];
1212            if matches!(
1213                oc,
1214                b'L'
1215                    | b'e'
1216                    | b'd'
1217                    | b's'
1218                    | b'b'
1219                    | b'a'
1220                    | b't'
1221                    | b'T'
1222                    | b'm'
1223                    | b'q'
1224                    | b'g'
1225            ) {
1226                ops_local.ind[oc as usize] = 1;
1227                positional_start = 1;
1228            } else {
1229                // c:597-599 default arm — "bad option: -X".
1230                crate::ported::utils::zwarnnam(nam, &format!("bad option: {}", first));
1231                return 1;
1232            }
1233        } else if fb == b"-" || fb == b"--" {
1234            // c:508-511 — lone `-` or `--` with no following positional
1235            // is "not enough arguments" (the lone dash implies set-style
1236            // mode which requires ≥2 positionals).
1237            if args.len() == 1 {
1238                crate::ported::utils::zwarnnam(nam, "not enough arguments");
1239                return 1;
1240            }
1241            positional_start = 1;
1242        }
1243    }
1244    let args: &[String] = &args[positional_start..];
1245    let ops: &options = &ops_local;
1246    // c:Src/Modules/zutil.c:588-604 — min-args check per action flag.
1247    // After the args[0] option is consumed, the remaining positionals
1248    // must satisfy each action's min count or "not enough arguments"
1249    // fires. Without this gate `zstyle -g`, `-s`, `-t`, `-T` (etc.)
1250    // silently returned rc=1 instead of emitting the canonical
1251    // diagnostic.
1252    let min_args = if OPT_ISSET(ops, b'd') {
1253        0
1254    } else if OPT_ISSET(ops, b's')
1255        || OPT_ISSET(ops, b'b')
1256        || OPT_ISSET(ops, b'a')
1257        || OPT_ISSET(ops, b'm')
1258    {
1259        3
1260    } else if OPT_ISSET(ops, b't') || OPT_ISSET(ops, b'T') || OPT_ISSET(ops, b'q') {
1261        // c:592-595 — t/T min 2; q min 2 max 2
1262        2
1263    } else if OPT_ISSET(ops, b'g') {
1264        1
1265    } else {
1266        0 // L/e or no flag — no min check at this layer
1267    };
1268    if args.len() < min_args {
1269        crate::ported::utils::zwarnnam(nam, "not enough arguments");
1270        return 1;
1271    }
1272    // c:565-573 — the same switch's `max`: s 4, b/a/m 3, g 3, q 2
1273    // (d/t/T unbounded). c:582-584 `else if (max >= 0 && n > max)
1274    // { zwarnnam(nam, "too many arguments"); return 1; }`.
1275    let max_args = if OPT_ISSET(ops, b'd') || OPT_ISSET(ops, b't') || OPT_ISSET(ops, b'T') {
1276        None
1277    } else if OPT_ISSET(ops, b's') {
1278        Some(4)
1279    } else if OPT_ISSET(ops, b'b') || OPT_ISSET(ops, b'a') || OPT_ISSET(ops, b'm') {
1280        Some(3)
1281    } else if OPT_ISSET(ops, b'g') {
1282        Some(3)
1283    } else if OPT_ISSET(ops, b'q') {
1284        Some(2)
1285    } else if OPT_ISSET(ops, b'L') {
1286        // c:539-555 — `-L [context [style]]`: three or more args is
1287        // "too many arguments".
1288        Some(2)
1289    } else {
1290        None
1291    };
1292    if max_args.is_some_and(|m| args.len() > m) {
1293        crate::ported::utils::zwarnnam(nam, "too many arguments");
1294        return 1;
1295    }
1296    // c:491-492 — C reaches the bare-list arm only when `!args[0]`, i.e.
1297    // NO argument at all was given. `positional_start == 1` means an
1298    // option letter was consumed off args[0], so this is `zstyle -X`
1299    // (with X's own positionals exhausted), NOT a bare `zstyle`.
1300    // Without this gate, `zstyle -d` (delete everything, c:639-640
1301    // `zstyletab->emptytable`) fell into the listing arm and returned 0
1302    // having deleted nothing — every V05styles chunk after the
1303    // `zstyle -d` in chunk 1 then saw the leftover styles.
1304    if args.is_empty()
1305        && positional_start == 0
1306        && !OPT_ISSET(ops, b'L')
1307        && !OPT_ISSET(ops, b'e')
1308    {
1309        // c:491-492 + c:580-581 — bare `zstyle` invocation:
1310        // `list = ZSLIST_BASIC; scanhashtable(zstyletab, ..., printstylenode, list);`
1311        //
1312        // Route through printstylenode (zutil.rs:184 port) so the
1313        // (eval) prefix per pattern lands consistently. Prior bare-list
1314        // implementation duplicated the format inline with the eval-bit
1315        // hardcoded off — eval styles printed identical to literal
1316        // styles under bare `zstyle`.
1317        let names: Vec<String> = match zstyletab.lock() {
1318            Ok(t) => t.styles.keys().cloned().collect(),
1319            Err(_) => return 1,
1320        };
1321        let mut sorted = names;
1322        sorted.sort();
1323        ZSTYLE_CONTPROG.with(|c| *c.borrow_mut() = None); // c:572 zstyle_contprog = NULL
1324        for nam in sorted {
1325            // c:580 — scanhashtable callback dispatch per style.
1326            let hn: HashNode = Box::new(hashnode {
1327                next: None,
1328                nam,
1329                flags: 0,
1330            });
1331            printstylenode(&hn, 1); // c:580-581 — ZSLIST_BASIC
1332        }
1333        return 0; // c:585
1334    }
1335    if OPT_ISSET(ops, b'L') {
1336        // c:544-583 — `zstyle -L [context [stylename]]`: list = ZSLIST_SYNTAX,
1337        // optionally filtered by a context pattern and/or an exact style name.
1338        //   args[0] = context (glob matched against each stored pattern),
1339        //   args[1] = stylename (only that style is listed; error if absent).
1340        let context = args.first().map(|s| s.as_str()); // c:551/556 context
1341        let stylename = args.get(1).map(|s| s.as_str()); // c:552 stylename
1342                                                         // c:562-570 — validate the context pattern up front (invalid → rc 1).
1343        if let Some(c) = context {
1344            let mut pat = c.to_string();
1345            crate::ported::glob::tokenize(&mut pat); // c:563 tokenize(context)
1346            let prog = patcompile(&pat, crate::ported::zsh_h::PAT_STATIC, None); // c:564
1347            if prog.is_none() {
1348                return 1; // c:566-568
1349            }
1350            ZSTYLE_CONTPROG.with(|cp| *cp.borrow_mut() = prog);
1351        } else {
1352            ZSTYLE_CONTPROG.with(|cp| *cp.borrow_mut() = None); // c:572
1353        }
1354        // c:573-582 — a named style lists just that node (error if it does
1355        // not exist); otherwise scan every style.
1356        let names: Vec<String> = if let Some(sn) = stylename {
1357            let exists = zstyletab
1358                .lock()
1359                .map(|t| t.styles.contains_key(sn))
1360                .unwrap_or(false);
1361            if !exists {
1362                return 1; // c:575-577 — `if (!s) return 1;`
1363            }
1364            vec![sn.to_string()]
1365        } else {
1366            match zstyletab.lock() {
1367                Ok(t) => {
1368                    let mut v: Vec<String> = t.styles.keys().cloned().collect();
1369                    v.sort();
1370                    v
1371                }
1372                Err(_) => return 1,
1373            }
1374        };
1375        for nam in names {
1376            let hn: HashNode = Box::new(hashnode {
1377                next: None,
1378                nam,
1379                flags: 0,
1380            });
1381            printstylenode(&hn, 2); // c:501 — ZSLIST_SYNTAX
1382        }
1383        return 0; // c:585
1384    }
1385    if OPT_ISSET(ops, b'd') {
1386        // c:610-641 — three -d forms:
1387        //
1388        //     if (args[1]) {
1389        //         if (args[2]) {
1390        //             char *pat = args[1];
1391        //             for (args += 2; *args; args++) { ... freestypat per style ... }
1392        //         } else {
1393        //             zstyle_patname = args[1];
1394        //             scanhashtable(zstyletab, 0, 0, 0, scanpatstyles, ZSPAT_REMOVE);
1395        //         }
1396        //     } else
1397        //         zstyletab->emptytable(zstyletab);
1398        //
1399        // Form 1 takes pattern + MULTIPLE style names (the c:618 loop
1400        // walks args+2 to the end). Prior port read only args[1] —
1401        // `zstyle -d pat sty1 sty2 sty3` silently dropped sty2/sty3.
1402        let pat = args.first().map(|s| s.as_str());
1403        if let Ok(mut t) = zstyletab.lock() {
1404            if args.len() > 1 {
1405                // c:615-631 — per-style deletion of one pattern.
1406                for sty in &args[1..] {
1407                    t.delete(pat, Some(sty.as_str())); // c:626 freestypat
1408                }
1409            } else {
1410                // c:632-638 pattern-only / c:639-640 wipe-all.
1411                t.delete(pat, None);
1412            }
1413        }
1414        return 0; // c:524
1415    }
1416    // c:541-942 — -s/-b/-t/-T/-m/-a/-e per-context lookup arms.
1417    // -g has different arg layout (args[0] = output name, not context)
1418    // so it gets its own block below.
1419    if OPT_ISSET(ops, b's')
1420        || OPT_ISSET(ops, b'b')
1421        || OPT_ISSET(ops, b't')
1422        || OPT_ISSET(ops, b'T')
1423        || OPT_ISSET(ops, b'a')
1424        || OPT_ISSET(ops, b'm')
1425        || OPT_ISSET(ops, b'q')
1426    {
1427        if args.len() < 2 {
1428            return 1;
1429        }
1430        let ctxt = &args[0]; // c:541
1431        let style = &args[1];
1432        // c:749-757 — `case 'q': {
1433        //                  int success;
1434        //                  queue_signals();	/* Protect PAT_STATIC */
1435        //                  success = testforstyle(args[1], args[2]);
1436        //                  unqueue_signals();
1437        //                  return success;
1438        //              }`
1439        // Prior port had no -q arm at all — the option-letter matcher
1440        // rejected `zstyle -q ctx sty` with "invalid option: -q".
1441        if OPT_ISSET(ops, b'q') {
1442            queue_signals(); // c:752
1443            let success = testforstyle(ctxt, style); // c:753
1444            unqueue_signals(); // c:754
1445            return success; // c:755
1446        }
1447        let vals = lookupstyle(ctxt, style); // c:443
1448                                             // c:559-732 — per-flag return semantics: just check found vs not.
1449                                             // For -t: 0 if found AND first value matches one of the "true"
1450                                             // tokens (when arg given) or first ∈ {true,yes,on,1}.
1451        if OPT_ISSET(ops, b't') || OPT_ISSET(ops, b'T') {
1452            // c:700-724 — shared t/T arm:
1453            //
1454            //     if ((vals = lookupstyle(args[1], args[2])) && vals[0]) {
1455            //         if (args[3]) {
1456            //             char **ap = args + 3, **p;
1457            //             while (*ap) {
1458            //                 p = vals;
1459            //                 while (*p)
1460            //                     if (!strcmp(*ap, *p++))
1461            //                         return 0;
1462            //                 ap++;
1463            //             }
1464            //             return 1;
1465            //         } else
1466            //             return !(!strcmp(vals[0], "true") ||
1467            //                      !strcmp(vals[0], "yes") ||
1468            //                      !strcmp(vals[0], "on") ||
1469            //                      !strcmp(vals[0], "1"));
1470            //     }
1471            //     return (args[0][1] == 't' ? (vals ? 1 : 2) : 0);
1472            //
1473            // Two contracts the prior arms missed:
1474            //   1. Extra args = value-membership test: exit 0 when ANY
1475            //      style value string-equals ANY extra arg. Both arms
1476            //      ignored args[3..] entirely.
1477            //   2. -t's tri-state exit: 2 when the style is UNDEFINED
1478            //      for the context, 1 when defined-but-empty (or
1479            //      non-boolean first value). Prior -t collapsed both
1480            //      to 1.
1481            if !vals.is_empty() {
1482                // c:706 vals && vals[0]
1483                if args.len() > 2 {
1484                    // c:707-717
1485                    for ap in &args[2..] {
1486                        if vals.iter().any(|v| v == ap) {
1487                            return 0; // c:714
1488                        }
1489                    }
1490                    return 1; // c:717
1491                }
1492                // c:719-722 — boolean first value.
1493                return if matches!(vals[0].as_str(), "true" | "yes" | "on" | "1") {
1494                    0
1495                } else {
1496                    1
1497                };
1498            }
1499            // c:724 — `return (args[0][1] == 't' ? (vals ? 1 : 2) : 0);`
1500            // vals here is the C pointer: non-NULL when a pattern
1501            // matched but carried zero values. Probe the table for the
1502            // defined-but-empty vs undefined distinction.
1503            if OPT_ISSET(ops, b't') {
1504                let defined = match zstyletab.lock() {
1505                    Ok(t) => t.get(ctxt, style).is_some(),
1506                    Err(_) => false,
1507                };
1508                return if defined { 1 } else { 2 }; // c:724
1509            }
1510            return 0; // c:724 -T arm
1511        }
1512        // -m PATTERN: pattern-match args[2] against each value, return
1513        // 0 if any matches. C: zutil.c:727-747.
1514        if OPT_ISSET(ops, b'm') {
1515            // c:727
1516            if args.len() < 3 {
1517                return 1;
1518            }
1519            queue_signals(); // c:732 — Protect PAT_STATIC
1520            let mut pat = args[2].clone();
1521            tokenize(&mut pat); // c:734 — shell metachar → pattern token
1522            let prog = match patcompile(&pat, PAT_STATIC, None) {
1523                // c:737
1524                Some(p) => p,
1525                None => {
1526                    unqueue_signals(); // c:745
1527                    return 1;
1528                }
1529            };
1530            for v in &vals {
1531                // c:738
1532                if pattry(&prog, v) {
1533                    // c:739
1534                    unqueue_signals(); // c:740
1535                    return 0; // c:741
1536                }
1537            }
1538            unqueue_signals(); // c:745
1539            return 1; // c:746
1540        }
1541        // -s CONTEXT STYLE NAME [SEP]: join vals with SEP (default " "),
1542        // setsparam(NAME, joined). Return 0 if found else 1 (empty str).
1543        // C: zutil.c:643-658.
1544        if OPT_ISSET(ops, b's') {
1545            // c:643
1546            if args.len() < 3 {
1547                return 1;
1548            }
1549            let pname = &args[2];
1550            if !vals.is_empty() {
1551                let sep = args.get(3).map(|s| s.as_str()).unwrap_or(" "); // c:649
1552                let ret = vals.join(sep);
1553                setsparam(pname, &ret);
1554                return 0; // c:650
1555            }
1556            setsparam(pname, ""); // c:652
1557            return 1; // c:653
1558        }
1559        // -b CONTEXT STYLE NAME: coerce single bool-ish val to "yes"/"no".
1560        // C: zutil.c:660-680.
1561        if OPT_ISSET(ops, b'b') {
1562            // c:660
1563            if args.len() < 3 {
1564                return 1;
1565            }
1566            let pname = &args[2];
1567            let truthy = vals.len() == 1                                     // c:665-670
1568                && matches!(vals[0].as_str(),
1569                            "yes" | "true" | "on" | "1");
1570            let (ret, code) = if truthy { ("yes", 0) } else { ("no", 1) };
1571            setsparam(pname, ret); // c:677
1572            return code; // c:672/675
1573        }
1574        // -a CONTEXT STYLE NAME: setaparam(NAME, vals).
1575        // C: zutil.c:682-699:
1576        //
1577        //     if ((vals = lookupstyle(args[1], args[2]))) {
1578        //         ret = zarrdup(vals);
1579        //         val = 0;
1580        //     } else {
1581        //         char *dummy = NULL;
1582        //         ret = zarrdup(&dummy);
1583        //         val = 1;
1584        //     }
1585        //     setaparam(args[3], ret);
1586        //
1587        // Exit code keys on the lookupstyle POINTER, not vals[0]: a
1588        // pattern that matched with ZERO values still exits 0 (array
1589        // set empty). Rust lookupstyle collapses NULL and empty to
1590        // one Vec, so probe the table for the defined/undefined
1591        // distinction — same shape as the -t tri-state fix.
1592        if OPT_ISSET(ops, b'a') {
1593            // c:682
1594            if args.len() < 3 {
1595                return 1;
1596            }
1597            let pname = &args[2];
1598            let defined = match zstyletab.lock() {
1599                Ok(t) => t.get(ctxt, style).is_some(), // c:687 vals != NULL
1600                Err(_) => false,
1601            };
1602            // c:696 — `setaparam(args[3], ret)`. C's setaparam routes a
1603            // PM_HASHED (associative) target through the hashed-assign
1604            // path, treating the flat value list as alternating
1605            // key/value pairs. zshrs's setaparam clobbers the assoc into
1606            // an indexed array, so detect a PM_HASHED target and use
1607            // sethparam (key/val interleave) instead — `typeset -A h;
1608            // zstyle -a ctx style h` now populates h's keys.
1609            let is_assoc = crate::ported::params::paramtab()
1610                .read()
1611                .ok()
1612                .and_then(|t| {
1613                    t.get(pname.as_str())
1614                        .map(|p| (p.node.flags as u32 & crate::ported::zsh_h::PM_HASHED) != 0)
1615                })
1616                .unwrap_or(false);
1617            if is_assoc {
1618                sethparam(pname, vals); // c:696 (assoc: key/val pairs)
1619            } else {
1620                setaparam(pname, vals); // c:696 (empty when undefined)
1621            }
1622            return if defined { 0 } else { 1 }; // c:689/694
1623        }
1624        // -g: handled below (different arg layout).
1625        // -e: NOT a per-context lookup arm. C c:504-507 routes -e
1626        // through the add path (eval = add = 1), handled in the
1627        // canonical setstypat block below.
1628        if vals.is_empty() {
1629            return 1;
1630        }
1631        return 0;
1632    }
1633    // -g NAME [PATTERN [STYLE]]: collect into array NAME.
1634    // C: zutil.c:758-795. Distinct arg layout: args[0]=NAME (not ctxt).
1635    if OPT_ISSET(ops, b'g') {
1636        // c:758
1637        if args.is_empty() {
1638            return 1;
1639        }
1640        let pname = &args[0]; // c:792 args[1]→args[0]
1641        let pat_arg = args.get(1).map(|s| s.as_str()); // c:766
1642        let sty_arg = args.get(2).map(|s| s.as_str()); // c:767
1643        let mut out: Vec<String> = Vec::new();
1644        let t = match zstyletab.lock() {
1645            Ok(g) => g,
1646            Err(_) => return 1,
1647        };
1648        // c:759 — `int ret = 1;`. Only the exact-pattern lookup can fail
1649        // (return 1); the two listing branches always succeed (ret = 0).
1650        let mut ret = 1;
1651        match (pat_arg, sty_arg) {
1652            (None, _) => {
1653                // Collect distinct context patterns. c:788
1654                let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
1655                for (p, _s, _v) in t.list(None) {
1656                    if seen.insert(p.clone()) {
1657                        out.push(p);
1658                    }
1659                }
1660                ret = 0; // c:789
1661            }
1662            (Some(pat), None) => {
1663                // Collect style names attached to context = pat. c:783
1664                let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
1665                for (p, s, _v) in t.list(None) {
1666                    if p == pat && seen.insert(s.clone()) {
1667                        out.push(s);
1668                    }
1669                }
1670                ret = 0; // c:784
1671            }
1672            (Some(pat), Some(sty)) => {
1673                // c:768-779 — `-g` matches the pattern EXACTLY (strcmp),
1674                // NOT via pattry: it retrieves the value stored for that
1675                // precise pattern key, not the value a context would
1676                // resolve to. Use get_exact, not get (pattry). c:770-777:
1677                // ret stays 1 unless the exact pattern is found.
1678                if let Some(v) = t.get_exact(pat, sty) {
1679                    out.extend(v.iter().cloned());
1680                    ret = 0; // c:776
1681                }
1682            }
1683        }
1684        drop(t);
1685        setaparam(pname, out); // c:792
1686        return ret; // c:793
1687    }
1688
1689    // c:515-534 — add path: zstyle [-e] PATTERN STYLE [VALUES...]
1690    if args.len() < 2 {
1691        zwarnnam(nam, "not enough arguments"); // c:521
1692        return 1;
1693    }
1694    let ctxt = &args[0]; // c:524
1695    let style = &args[1];
1696    let values: Vec<String> = if args.len() >= 3 {
1697        args[2..].to_vec() // c:533 args+2
1698    } else {
1699        Vec::new()
1700    };
1701    // c:524-530 — tokenize + patcompile validation. Reject invalid
1702    // patterns with the canonical "invalid pattern: %s" diagnostic
1703    // before they reach the style table.
1704    {
1705        let mut pat = ctxt.clone(); // c:524 dupstring
1706        tokenize(&mut pat); // c:525
1707        if patcompile(&pat, crate::ported::zsh_h::PAT_ZDUP, None).is_none() {
1708            // c:527
1709            zwarnnam(nam, &format!("invalid pattern: {}", ctxt)); // c:528
1710            return 1; // c:529
1711        }
1712    }
1713    let eval = OPT_ISSET(ops, b'e'); // c:505 eval = add = 1
1714                                     // c:533 — `setstypat(s, pat, prog, args + 2, eval)`. Route through
1715                                     // setstypat (which parses the `-e` value and locks zstyletab itself)
1716                                     // rather than locking + `t.set` here, matching C and avoiding a
1717                                     // re-entrant lock on the non-reentrant zstyletab mutex.
1718    setstypat(style, ctxt, None, values.clone(), eval as i32); // c:533
1719                                                               // PFA-SMR: one event per zstyle call. `rest` carries the style
1720                                                               // name + values so replay can re-emit the full setter.
1721    #[cfg(feature = "recorder")]
1722    if crate::recorder::is_enabled() {
1723        let ctx = crate::recorder::recorder_ctx_global();
1724        let rest = if values.is_empty() {
1725            style.clone()
1726        } else {
1727            format!("{} {}", style, values.join(" "))
1728        };
1729        crate::recorder::emit_zstyle(ctxt, &rest, ctx);
1730    }
1731    0 // c:951
1732}
1733
1734/// Port of `bin_zformat(char *nam, char **args, Options ops, UNUSED(int func))` from `Src/Modules/zutil.c:984`.
1735/// BUILTIN spec at c:2151 is `BUILTIN("zformat", 0, bin_zformat, 2, -1,
1736/// 0, "afFqQ", NULL)`: execbuiltin parses `-a`/`-f`/`-F` (mode) and
1737/// `-q`/`-Q` (auto-quote `%` in spec values) into `ops`, so `args[0]` is
1738/// the target parameter and `args[1]` the format / separator.
1739pub fn bin_zformat(
1740    nam: &str,
1741    args: &[String], // c:984
1742    ops: &options,
1743    _func: i32,
1744) -> i32 {
1745    // c:986 — `qopt = OPT_ISSET(ops,'q') ? 'q' : OPT_ISSET(ops,'Q') ? 'Q' : 0`
1746    let qopt: u8 = if OPT_ISSET(ops, b'q') {
1747        b'q'
1748    } else if OPT_ISSET(ops, b'Q') {
1749        b'Q'
1750    } else {
1751        0
1752    };
1753    let mut presence = false; // c:987
1754
1755    if OPT_ISSET(ops, b'q') && OPT_ISSET(ops, b'Q') {
1756        zwarnnam(nam, "only one of -qQ allowed"); // c:990
1757        return 1;
1758    }
1759    let modes = OPT_ISSET(ops, b'a') as i32 + OPT_ISSET(ops, b'f') as i32 + OPT_ISSET(ops, b'F') as i32;
1760    if modes < 1 {
1761        zwarnnam(nam, "one of -afF expected"); // c:994
1762        return 1;
1763    }
1764    if modes > 1 {
1765        zwarnnam(nam, "only one of -afF allowed"); // c:998
1766        return 1;
1767    }
1768    if OPT_ISSET(ops, b'a') && qopt != 0 {
1769        zwarnnam(nam, "-qQ not allowed with -a"); // c:1002
1770        return 1;
1771    }
1772
1773    // c:1006 — `switch (OPT_ISSET(ops,'a') ? 'a' : OPT_ISSET(ops,'f') ? 'f' : 'F')`
1774    let opt: u8 = if OPT_ISSET(ops, b'a') {
1775        b'a'
1776    } else if OPT_ISSET(ops, b'f') {
1777        b'f'
1778    } else {
1779        b'F'
1780    };
1781
1782    match opt {
1783        b'F' | b'f' => {
1784            if opt == b'F' {
1785                presence = true; // c:1008 (fall-through into 'f')
1786            }
1787            // c:1012-1014 — `char *specs[256] = {0}; int qspecs[256] = {0};`
1788            // zero-initialised: `%` and `)` are never specs (upstream
1789            // a04c944804 dropped the old seeding; `%%`/`%)` are handled by
1790            // the unwind in zformat_substring instead).
1791            let mut specs: HashMap<char, String> = HashMap::new();
1792            let mut qspecs: HashMap<char, usize> = HashMap::new();
1793            for ap in &args[2..] {
1794                // c:1017 — "quote by default (spec like d:...) with -qQ"
1795                let mut quote = qopt != 0;
1796                let mut arg: &str = ap;
1797                if arg.starts_with("%%") {
1798                    // c:1021-1023 — `%%d:...` explicitly disables quoting
1799                    quote = false;
1800                    arg = &arg[2..];
1801                } else if arg.starts_with('%') {
1802                    // c:1025-1027 — `%d:...` explicitly enables quoting
1803                    quote = true;
1804                    arg = &arg[1..];
1805                }
1806                let ab = arg.as_bytes();
1807                if ab.is_empty()
1808                    || ab[0] == b'-'
1809                    || ab[0] == b'.'
1810                    || ab[0] == b'%'
1811                    || ab[0] == b')'
1812                    || ab[0].is_ascii_digit()
1813                    || ab.len() < 2
1814                    || ab[1] != b':'
1815                {
1816                    zwarnnam(nam, &format!("invalid spec: {}", ap)); // c:1033
1817                    return 1;
1818                }
1819                let key = ab[0] as char;
1820                let val = &arg[2..];
1821                if quote {
1822                    // c:1037-1058 — double every `%` and remember how many
1823                    // were added, for the width/presence corrections.
1824                    let pct = val.matches('%').count();
1825                    specs.insert(key, val.replace('%', "%%"));
1826                    qspecs.insert(key, pct);
1827                } else {
1828                    // c:1060 — qspecs[c] is left as an earlier `-q` spec of
1829                    // the same letter set it.
1830                    specs.insert(key, val.to_string());
1831                }
1832            }
1833
1834            // c:1064-1069 — `if (!zformat_substring(args[1], specs, &out,
1835            // &oused, &olen, '\0', presence, qopt == 'q', qspecs, 0))`
1836            let chars: Vec<char> = args[1].chars().collect();
1837            let mut out = String::with_capacity(chars.len() + 16);
1838            let mut idx = 0;
1839            if ZFormat::substring(
1840                &chars,
1841                &mut idx,
1842                &mut out,
1843                '\0',
1844                &specs,
1845                presence,
1846                qopt == b'q',
1847                &qspecs,
1848                false,
1849            )
1850            .is_none()
1851            {
1852                zwarnnam(nam, &format!("malformed format string: {}", args[1])); // c:1071
1853                return 1;
1854            }
1855            setsparam(&args[0], &out); // c:1076
1856            return 0; // c:1077
1857        }
1858        b'a' => {
1859            // c:1080 — -a column-format branch.
1860            let mut pre = 0usize; // c:1000
1861            let mut suf = 0usize; // c:1000
1862                                  // First pass: compute max prefix/suffix widths.
1863            for ap in &args[2..] {
1864                // c:1005
1865                let mut nbc = 0usize; // c:1006
1866                let bytes = ap.as_bytes();
1867                let mut cp_idx = 0usize;
1868                while cp_idx < bytes.len() && bytes[cp_idx] != b':' {
1869                    // c:1007
1870                    if bytes[cp_idx] == b'\\' && cp_idx + 1 < bytes.len() {
1871                        // c:1008
1872                        cp_idx += 1;
1873                        nbc += 1;
1874                    }
1875                    cp_idx += 1;
1876                }
1877                if cp_idx < bytes.len() && bytes[cp_idx] == b':'              // c:1010
1878                    && cp_idx + 1 < bytes.len()
1879                {
1880                    let d = cp_idx.saturating_sub(nbc); // c:1015
1881                    if d > pre {
1882                        pre = d;
1883                    } // c:1016
1884                      // multi-byte width branch (c:1017-1029) collapses to
1885                      // ASCII byte count for the common case in Rust.
1886                    let s = bytes.len() - cp_idx - 1; // c:1030
1887                    if s > suf {
1888                        suf = s;
1889                    } // c:1031
1890                }
1891            }
1892            // Second pass: build formatted columns + setaparam.
1893            let middle = &args[1]; // c:1037
1894            let sl = middle.len(); // c:1037
1895            let mut ret: Vec<String> = Vec::new(); // c:1043
1896            for ap in &args[2..] {
1897                // c:1051
1898                let bytes = ap.as_bytes();
1899                let mut copy: Vec<u8> = Vec::with_capacity(bytes.len()); // c:1052
1900                let mut k = 0usize;
1901                let mut sep_at: Option<usize> = None;
1902                while k < bytes.len() {
1903                    // c:1053
1904                    if bytes[k] == b':' {
1905                        sep_at = Some(copy.len());
1906                        break;
1907                    }
1908                    if bytes[k] == b'\\' && k + 1 < bytes.len() {
1909                        // c:1054
1910                        k += 1;
1911                    }
1912                    copy.push(bytes[k]); // c:1055
1913                    k += 1;
1914                }
1915                // c:1058 — `((cpp==cp && oldc==':') || *cp==':') && cp[1]`:
1916                // align ONLY when a colon is present AND the value after it
1917                // is non-empty. `empty:` (colon, no value) and `nocolon`
1918                // (no colon) both fall to the else and emit just the key —
1919                // no padding, no separator. zshrs previously aligned any
1920                // colon-bearing spec, leaving a dangling `empty -- `.
1921                if sep_at.is_some() && k + 1 < bytes.len() {
1922                    let left_len = sep_at.unwrap();
1923                    // c:1058
1924                    let after = std::str::from_utf8(&bytes[(k + 1)..]).unwrap_or("");
1925                    let mut buf = String::with_capacity(pre + sl + after.len());
1926                    let prefix = std::str::from_utf8(&copy[..left_len]).unwrap_or("");
1927                    buf.push_str(prefix); // c:1072 memcpy(buf, copy, cpp-copy)
1928                                          // c:1071 memset(buf, ' ', pre) — pad up to byte count
1929                                          // `pre` (matches C !MULTIBYTE_SUPPORT byte-counting; the
1930                                          // first pass already computed `pre = cp - *ap - nbc`
1931                                          // in bytes c:1015). Using prefix.len() (bytes) not
1932                                          // chars().count() — for non-ASCII labels chars<bytes,
1933                                          // so the prior char-count version over-padded.
1934                    for _ in prefix.len()..pre {
1935                        buf.push(' ');
1936                    }
1937                    buf.push_str(middle); // c:1073 strcpy past `suf` shift
1938                    buf.push_str(after); // c:1073 (tail after `:`)
1939                    ret.push(buf); // c:1075 ztrdup
1940                } else {
1941                    ret.push(String::from_utf8_lossy(&copy).into_owned()); // c:1082
1942                }
1943            }
1944            let _ = sl;
1945            setaparam(&args[0], ret); // c:1081 setaparam(args[0], ret)
1946            return 0; // c:1082
1947        }
1948        _ => {}
1949    }
1950    // c:1176 — `DPUTS(1, "BUG: unhandled option"); return 1;`
1951    1
1952}
1953
1954/// Port of `connectstates(LinkList out, LinkList in)` from `Src/Modules/zutil.c:1119`.
1955/// For every (outbranch, inbranch) pair, create a new RParseBranch
1956/// whose target is inbranch.state and whose actions are
1957/// outbranch.actions ++ inbranch.actions, then add it to the
1958/// outbranch.state's branches list.
1959pub fn connectstates(
1960    out: &[std::rc::Rc<std::cell::RefCell<RParseBranch>>], // c:1119
1961    in_: &[std::rc::Rc<std::cell::RefCell<RParseBranch>>],
1962) {
1963    // c:1123 — for (outnode = firstnode(out); outnode; ...)
1964    for outnode in out.iter() {
1965        // c:1126 — for (innode = firstnode(in); innode; ...)
1966        for innode in in_.iter() {
1967            let outbranch = outnode.borrow();
1968            let inbranch = innode.borrow();
1969            // c:1128 — `br = hcalloc`; c:1130-1135 — populate.
1970            let mut new_actions: Vec<String> =
1971                Vec::with_capacity(outbranch.actions.len() + inbranch.actions.len());
1972            new_actions.extend(outbranch.actions.iter().cloned()); // c:1132-1133
1973            new_actions.extend(inbranch.actions.iter().cloned()); // c:1134-1135
1974            let br = std::rc::Rc::new(std::cell::RefCell::new(RParseBranch {
1975                state: inbranch.state.clone(), // c:1130
1976                actions: new_actions,
1977            }));
1978            // c:1136 — addlinknode(outbranch->state->branches, br);
1979            outbranch.state.borrow_mut().branches.push(br);
1980        }
1981    }
1982}
1983
1984/// Port of `static int rparseelt(RParseResult *result, jmp_buf *perr)`
1985/// from `Src/Modules/zutil.c:1142`. Atom in the zregexparse grammar:
1986///   `/pat/[+/-]` \[`%lookahead%`\] \[`-guard`\] \[`:action`\]    — pattern atom
1987///   `(` ... `)`                                            — grouped alt
1988pub fn rparseelt(result: &mut RParseResult) -> i32 {
1989    // c:1142
1990    // c:1145 — s = *rparseargs;
1991    let s = match RPARSEARGS.with(|q| q.borrow().front().cloned()) {
1992        Some(s) => s,
1993        None => return 1, // c:1147-1148
1994    };
1995    let first = s.chars().next();
1996    match first {
1997        Some('/') => {
1998            // c:1151
1999            // c:1157 — l = strlen(s);
2000            // c:1158-1161 — require `/.../` or `/.../[+-]`.
2001            let l = s.len();
2002            let last = s.chars().last().unwrap_or(' ');
2003            let prevlast = if l >= 2 { s.as_bytes()[l - 2] } else { 0 };
2004            let ok_close = (l >= 2 && last == '/')
2005                || (l >= 3 && prevlast == b'/' && (last == '+' || last == '-'));
2006            if !ok_close {
2007                return 1;
2008            }
2009            // c:1162-1164 — alloc state, set cutoff.
2010            let mut st = RParseState::default();
2011            st.cutoff = last as i32;
2012            // c:1165-1171 — pattern slice between '/' and final '/[+-]?'.
2013            let (pattern, _patternlen) = if last == '/' {
2014                (&s[1..l - 1], l - 2)
2015            } else {
2016                (&s[1..l - 2], l - 3)
2017            };
2018            // c:1172 — rparseargs++;
2019            RPARSEARGS.with(|q| q.borrow_mut().pop_front());
2020            // c:1173-1180 — optional `%lookahead%` next arg.
2021            let lookahead: Option<String> = {
2022                let nxt = RPARSEARGS.with(|q| q.borrow().front().cloned());
2023                if let Some(la) = nxt {
2024                    if la.starts_with('%') && la.len() >= 2 && la.ends_with('%') {
2025                        RPARSEARGS.with(|q| q.borrow_mut().pop_front());
2026                        Some(la[1..la.len() - 1].to_string())
2027                    } else {
2028                        None
2029                    }
2030                } else {
2031                    None
2032                }
2033            };
2034            // c:1181-1202 — assemble compiled pattern string
2035            //   "[]"        → no pattern (NULL)
2036            //   else        → "(#b)((#B)<pat>)<lookahead?>*"
2037            if pattern == "[]" {
2038                st.pattern = None; // c:1182
2039            } else {
2040                let mut buf = String::with_capacity(pattern.len() + 16);
2041                buf.push_str("(#b)((#B)"); // c:1189-1190
2042                buf.push_str(pattern); // c:1191-1192
2043                buf.push(')'); // c:1193
2044                if let Some(la) = lookahead.as_ref() {
2045                    buf.push_str("(#B)"); // c:1196
2046                    buf.push_str(la); // c:1198
2047                }
2048                buf.push('*'); // c:1201
2049                st.pattern = Some(buf);
2050            }
2051            st.patprog = None; // c:1203
2052                               // c:1204-1211 — optional `-guard` arg.
2053            let nxt = RPARSEARGS.with(|q| q.borrow().front().cloned());
2054            if let Some(g) = nxt {
2055                if g.starts_with('-') {
2056                    RPARSEARGS.with(|q| q.borrow_mut().pop_front());
2057                    st.guard = Some(g[1..].to_string());
2058                }
2059            }
2060            // c:1212-1219 — optional `:action` arg.
2061            let nxt = RPARSEARGS.with(|q| q.borrow().front().cloned());
2062            if let Some(a) = nxt {
2063                if a.starts_with(':') {
2064                    RPARSEARGS.with(|q| q.borrow_mut().pop_front());
2065                    st.action = Some(a[1..].to_string());
2066                }
2067            }
2068            // Wrap state for sharing, register in RPARSESTATES.
2069            let st_rc = std::rc::Rc::new(std::cell::RefCell::new(st));
2070            RPARSESTATES.with(|s| s.borrow_mut().push(st_rc.clone()));
2071
2072            // c:1220-1230 — result->in = [br(st)], result->out = [br(st)].
2073            result.nullacts = None; // c:1220
2074            let in_br = std::rc::Rc::new(std::cell::RefCell::new(RParseBranch {
2075                state: st_rc.clone(),
2076                actions: Vec::new(),
2077            }));
2078            result.in_ = vec![in_br]; // c:1221-1225
2079            let out_br = std::rc::Rc::new(std::cell::RefCell::new(RParseBranch {
2080                state: st_rc,
2081                actions: Vec::new(),
2082            }));
2083            result.out = vec![out_br]; // c:1226-1230
2084            0 // c:1248
2085        }
2086        Some('(') if s.len() == 1 => {
2087            // c:1233-1235
2088            // c:1236 — rparseargs++;
2089            RPARSEARGS.with(|q| q.borrow_mut().pop_front());
2090            // c:1237 — if (rparsealt(result, perr)) longjmp(*perr, 2);
2091            if rparsealt(result) != 0 {
2092                return 1; // longjmp surrogate
2093            }
2094            // c:1239-1241 — require closing `)`.
2095            let nxt = RPARSEARGS.with(|q| q.borrow().front().cloned());
2096            if nxt.as_deref() != Some(")") {
2097                return 1;
2098            }
2099            // c:1242 — rparseargs++;
2100            RPARSEARGS.with(|q| q.borrow_mut().pop_front());
2101            0
2102        }
2103        _ => 1, // c:1244-1245
2104    }
2105}
2106
2107/// Port of `static int rparseclo(RParseResult *result, jmp_buf *perr)`
2108/// from `Src/Modules/zutil.c:1252`. Closure: atom followed by `#`
2109/// (zero-or-more); a string of `#`s collapses to one.
2110pub fn rparseclo(result: &mut RParseResult) -> i32 {
2111    // c:1252
2112    // c:1254 — if (rparseelt(result, perr)) return 1;
2113    if rparseelt(result) != 0 {
2114        return 1;
2115    }
2116    // c:1257-1264 — `if (*rparseargs && !strcmp(*rparseargs, "#")) { ... }`
2117    let mut saw = false;
2118    loop {
2119        let nxt = RPARSEARGS.with(|q| q.borrow().front().cloned());
2120        if nxt.as_deref() != Some("#") {
2121            break;
2122        }
2123        RPARSEARGS.with(|q| q.borrow_mut().pop_front());
2124        saw = true;
2125    }
2126    if saw {
2127        // c:1262 — connectstates(result->out, result->in)
2128        // Borrow result.out and result.in_ via cloned Rc lists to avoid
2129        // double-borrow (connectstates writes branches inside the states).
2130        let out_snap = result.out.clone();
2131        let in_snap = result.in_.clone();
2132        connectstates(&out_snap, &in_snap);
2133        // c:1263 — result->nullacts = newlinklist();
2134        result.nullacts = Some(Vec::new());
2135    }
2136    0 // c:1265
2137}
2138
2139// === auto-generated stubs ===
2140// Direct ports of static helpers from Src/Modules/zutil.c not
2141// yet covered above. zshrs links modules statically; live
2142// state owned by the module's typed struct. Name-parity shims.
2143
2144/// Port of `prependactions(LinkList acts, LinkList branches)` from `Src/Modules/zutil.c:1269`.
2145/// For each branch, pushnode (insert at HEAD) each action from `acts`
2146/// in reverse — net effect: branch.actions gets `acts` prepended.
2147pub fn prependactions(
2148    acts: &[String], // c:1269
2149    branches: &[std::rc::Rc<std::cell::RefCell<RParseBranch>>],
2150) {
2151    // c:1273 — for (bln = firstnode(branches); bln; ...)
2152    for bln in branches.iter() {
2153        let mut br = bln.borrow_mut();
2154        // c:1276 — for (aln = lastnode(acts); aln != (LinkNode)acts; aln = prevnode(aln))
2155        //   pushnode(br->actions, getdata(aln));
2156        // pushnode inserts at the HEAD of br->actions. C iterates acts
2157        // back-to-front and pushes each — net effect is acts in order
2158        // become the new prefix of br.actions.
2159        for aln in acts.iter().rev() {
2160            br.actions.insert(0, aln.clone());
2161        }
2162    }
2163}
2164
2165/// Port of `appendactions(LinkList acts, LinkList branches)` from `Src/Modules/zutil.c:1282`.
2166/// For each branch, append each action from `acts` to br.actions.
2167pub fn appendactions(
2168    acts: &[String], // c:1282
2169    branches: &[std::rc::Rc<std::cell::RefCell<RParseBranch>>],
2170) {
2171    // c:1285 — for (bln = firstnode(branches); bln; ...)
2172    for bln in branches.iter() {
2173        let mut br = bln.borrow_mut();
2174        // c:1288 — for (aln = firstnode(acts); aln; ...) addlinknode(br->actions, getdata(aln));
2175        for aln in acts.iter() {
2176            br.actions.push(aln.clone());
2177        }
2178    }
2179}
2180
2181/// Port of `rparseseq(RParseResult *result, jmp_buf *perr)` from Src/Modules/zutil.c:1294.
2182///
2183/// Part of the `zregexparse` builtin's recursive-descent
2184/// parser family (zutil.c:1140-1250 + helpers). zshrs's
2185/// Port of `static int rparseseq(RParseResult *result, jmp_buf *perr)`
2186/// from `Src/Modules/zutil.c:1294`. Recursive-descent parser for the
2187/// `zregexparse` builtin's sequence rule: walks `rparseargs` consuming
2188/// `{action}` blocks (paste into nullacts + every branch's actions
2189/// list) and `rparseclo` sub-results (connectstates / prependactions
2190/// / appendactions splice).
2191///
2192/// ```c
2193/// static int
2194/// rparseseq(RParseResult *result, jmp_buf *perr)
2195/// {
2196///     int l;
2197///     char *s;
2198///     RParseResult sub;
2199///     result->nullacts = newlinklist();
2200///     result->in = newlinklist();
2201///     result->out = newlinklist();
2202///     while (1) {
2203///         if ((s = *rparseargs) && s[0] == '{' && s[(l=strlen(s))-1] == '}') {
2204///             char *action = hcalloc(l - 1);
2205///             LinkNode ln;
2206///             rparseargs++;
2207///             memcpy(action, s + 1, l - 2);
2208///             action[l - 2] = '\0';
2209///             if (result->nullacts) addlinknode(result->nullacts, action);
2210///             for (ln = firstnode(result->out); ln; ln = nextnode(ln)) {
2211///                 RParseBranch *br = getdata(ln);
2212///                 addlinknode(br->actions, action);
2213///             }
2214///         } else if (!rparseclo(&sub, perr)) {
2215///             connectstates(result->out, sub.in);
2216///             if (result->nullacts) { prependactions(result->nullacts, sub.in);
2217///                 insertlinklist(sub.in, lastnode(result->in), result->in); }
2218///             if (sub.nullacts) { appendactions(sub.nullacts, result->out);
2219///                 insertlinklist(sub.out, lastnode(result->out), result->out); }
2220///             else result->out = sub.out;
2221///             if (result->nullacts && sub.nullacts)
2222///                 insertlinklist(sub.nullacts, lastnode(result->nullacts), result->nullacts);
2223///             else result->nullacts = NULL;
2224///         } else break;
2225///     }
2226///     return 0;
2227/// }
2228/// ```
2229///
2230/// Per PORT.md Rule 9: body executes. Struct field divergence
2231/// (Rust `RParseResult.args` vs C `in/out`) is a pre-existing port
2232/// gap tracked separately; the action-consumption branch operates
2233/// on the available `nullacts` field.
2234pub fn rparseseq(result: &mut RParseResult) -> i32 {
2235    // c:1294
2236    // c:1300-1302 — initialize result with empty lists.
2237    result.nullacts = Some(Vec::new()); // c:1300
2238    result.in_ = Vec::new(); // c:1301
2239    result.out = Vec::new(); // c:1302
2240
2241    loop {
2242        // c:1304
2243        // c:1305 — `if ((s = *rparseargs) && s[0]=='{' && s[l-1]=='}')`
2244        let s = RPARSEARGS.with(|q| q.borrow().front().cloned());
2245        let action_arg = match s {
2246            Some(ref arg) if arg.len() >= 2 && arg.starts_with('{') && arg.ends_with('}') => {
2247                Some(arg.clone())
2248            }
2249            _ => None,
2250        };
2251        if let Some(arg) = action_arg {
2252            // c:1306 — char *action = hcalloc(l - 1);
2253            // c:1307-1311 — strip braces.
2254            let action = arg[1..arg.len() - 1].to_string();
2255            // c:1309 — rparseargs++;
2256            RPARSEARGS.with(|q| q.borrow_mut().pop_front());
2257            // c:1312-1313 — if (result->nullacts) addlinknode(result->nullacts, action);
2258            if let Some(ref mut na) = result.nullacts {
2259                na.push(action.clone());
2260            }
2261            // c:1314-1317 — for each branch in result->out: addlinknode(br->actions, action);
2262            for br in result.out.iter() {
2263                br.borrow_mut().actions.push(action.clone());
2264            }
2265            continue;
2266        }
2267        // c:1319 — `else if (!rparseclo(&sub, perr))`
2268        let mut sub = RParseResult::default();
2269        if rparseclo(&mut sub) == 0 {
2270            // c:1319
2271            // c:1320 — connectstates(result->out, sub.in);
2272            {
2273                let out_snap = result.out.clone();
2274                let in_snap = sub.in_.clone();
2275                connectstates(&out_snap, &in_snap);
2276            }
2277            // c:1322-1325 — `if (result->nullacts)
2278            //                   { prependactions(result->nullacts, sub.in);
2279            //                     insertlinklist(sub.in, lastnode(result->in), result->in); }`
2280            if let Some(ref na) = result.nullacts.clone() {
2281                prependactions(na, &sub.in_);
2282                // insertlinklist(src, after, dst): splice src into dst
2283                // AT the end (lastnode). Equivalent to append.
2284                result.in_.extend(sub.in_.iter().cloned());
2285            }
2286            // c:1326-1330 — sub.nullacts splice (or just steal sub.out).
2287            if let Some(ref sub_na) = sub.nullacts.clone() {
2288                appendactions(sub_na, &result.out);
2289                result.out.extend(sub.out.iter().cloned());
2290            } else {
2291                result.out = sub.out.clone();
2292            }
2293            // c:1332-1336 — combine nullacts (or NULL them).
2294            match (result.nullacts.as_mut(), sub.nullacts.as_ref()) {
2295                (Some(rna), Some(sna)) => {
2296                    rna.extend(sna.iter().cloned());
2297                }
2298                _ => {
2299                    result.nullacts = None;
2300                }
2301            }
2302        } else {
2303            // c:1338
2304            break; // c:1339
2305        }
2306    }
2307    0 // c:1341
2308}
2309
2310/// Port of `static int rparsealt(RParseResult *result, jmp_buf *perr)`
2311/// from `Src/Modules/zutil.c:1345`. Alternation: one or more `seq`
2312/// terms separated by `|`.
2313pub fn rparsealt(result: &mut RParseResult) -> i32 {
2314    // c:1345
2315    // c:1349-1350 — if (rparseseq(result, perr)) return 1;
2316    if rparseseq(result) != 0 {
2317        return 1;
2318    }
2319    // c:1352 — while (*rparseargs && !strcmp(*rparseargs, "|"))
2320    loop {
2321        let nxt = RPARSEARGS.with(|q| q.borrow().front().cloned());
2322        if nxt.as_deref() != Some("|") {
2323            break;
2324        }
2325        // c:1353 — rparseargs++;
2326        RPARSEARGS.with(|q| q.borrow_mut().pop_front());
2327        let mut sub = RParseResult::default();
2328        // c:1354 — if (rparseseq(&sub, perr)) longjmp(*perr, 2);
2329        if rparseseq(&mut sub) != 0 {
2330            return 1;
2331        }
2332        // c:1356-1357 — if (!result->nullacts && sub.nullacts) result->nullacts = sub.nullacts;
2333        if result.nullacts.is_none() {
2334            if let Some(sn) = sub.nullacts.take() {
2335                result.nullacts = Some(sn);
2336            }
2337        }
2338        // c:1359-1360 — insertlinklist(sub.in,  lastnode(result->in),  result->in)
2339        //               insertlinklist(sub.out, lastnode(result->out), result->out)
2340        result.in_.extend(sub.in_.into_iter());
2341        result.out.extend(sub.out.into_iter());
2342    }
2343    0 // c:1362
2344}
2345
2346/// Direct port of `static int rmatch(RParseResult *sm, char *subj,
2347/// char *var1, char *var2, int comp)` from `Src/Modules/zutil.c:1366-
2348/// 1474`. Executes the zregexparse state machine against `subj`:
2349/// drives transitions through the branch graph compiled by
2350/// `rparsealt`/`rparsestate`, runs guard predicates + action
2351/// strings via `execstring`, and binds the parse cursor to
2352/// `$var1`/`$var2`.
2353///
2354/// Returns:
2355///   - 0 if a complete parse path matches subj end-to-end (or in
2356///     completion mode + no out-edges constraint)
2357///   - 1 if no next-state candidates after exhausting subj (the
2358///     nextslist has at least one set of pending branches)
2359///   - 2 if there were no next-state candidates at all (nexts empty)
2360///   - 3 if a pattern fails to compile
2361pub fn rmatch(sm: &RParseResult, subj: &str, var1: &str, var2: &str, comp: i32) -> i32 {
2362    // c:1368-1373 — `LinkNode ln, lnn; LinkList nexts; LinkList nextslist;
2363    //                RParseBranch *br; RParseState *st = NULL; int point1=0, point2=0;`
2364    let mut st: Option<std::rc::Rc<std::cell::RefCell<RParseState>>> = None;
2365    let mut point1: i64 = 0; // c:1373
2366    let mut point2: i64 = 0; // c:1373
2367
2368    // c:1375-1376 — `setiparam(var1, point1); setiparam(var2, point2);`
2369    crate::ported::params::setiparam(var1, point1);
2370    crate::ported::params::setiparam(var2, point2);
2371
2372    // c:1378 — `if (!comp && !*subj && sm->nullacts)` — empty subj
2373    // matches nullacts directly when not in completion mode.
2374    if comp == 0 && subj.is_empty() {
2375        if let Some(nullacts) = sm.nullacts.as_ref() {
2376            // c:1379-1384 — `for (ln = firstnode(sm->nullacts); ...)
2377            //                    execstring(action, 1, 0, "zregexparse-action");`
2378            for action in nullacts {
2379                // c:1379
2380                if !action.is_empty() {
2381                    // c:1382
2382                    crate::ported::exec::execstring(action, 1, 0, "zregexparse-action");
2383                    // c:1383
2384                }
2385            }
2386            return 0; // c:1385
2387        }
2388    }
2389
2390    // c:1388 — `nextslist = newlinklist();`
2391    let mut nextslist: Vec<Vec<std::rc::Rc<std::cell::RefCell<RParseBranch>>>> = Vec::new();
2392    // c:1389-1390 — `nexts = sm->in; addlinknode(nextslist, nexts);`
2393    let mut nexts: Vec<std::rc::Rc<std::cell::RefCell<RParseBranch>>> = sm.in_.clone();
2394    nextslist.push(nexts.clone()); // c:1390
2395
2396    // `subj` is C's mutable char* cursor advancing through the input.
2397    // Mirror with a byte-index into the input.
2398    let subj_bytes = subj.as_bytes();
2399    let mut subj_pos: usize = 0; // c:1366 — initial cursor position.
2400
2401    // c:1391-1449 — `do { ... } while (ln);` — the outer loop continues
2402    // as long as the inner for-loop FOUND a match (C's `ln` non-NULL
2403    // after `break`). In Rust we use an explicit `matched` flag.
2404    loop {
2405        // c:1392-1394 — `MatchData match1, match2; savematch(&match1);`
2406        let mut match1 = MatchData {
2407            r#match: None,
2408            mbegin: None,
2409            mend: None,
2410        };
2411        savematch(&mut match1);
2412
2413        let mut matched = false; // mirror of C's `ln` truthiness after break.
2414                                 // c:1396 — `for (ln = firstnode(nexts); ln; ln = nextnode(ln))`
2415        for br_rc in &nexts.clone() {
2416            // c:1400 — `br = getdata(ln); next = br->state;`
2417            let br = br_rc.borrow();
2418            let next_rc = br.state.clone();
2419            drop(br); // release the immutable borrow before patprog mutation
2420
2421            // c:1402-1406 — pattern compile on first match.
2422            //   `if (next->pattern && !next->patprog) {
2423            //        tokenize(next->pattern);
2424            //        if (!(next->patprog = patcompile(...))) return 3;`
2425            let (has_pattern, need_compile) = {
2426                let n = next_rc.borrow();
2427                (
2428                    n.pattern.is_some(),
2429                    n.pattern.is_some() && n.patprog.is_none(),
2430                )
2431            };
2432            if need_compile {
2433                let mut pat_str = {
2434                    let n = next_rc.borrow();
2435                    n.pattern.clone().unwrap_or_default()
2436                };
2437                crate::ported::glob::tokenize(&mut pat_str); // c:1403
2438                let prog = patcompile(&pat_str, 0, None); // c:1404
2439                let mut n = next_rc.borrow_mut();
2440                n.pattern = Some(pat_str);
2441                match prog {
2442                    Some(p) => n.patprog = Some(p), // c:1404
2443                    None => return 3,               // c:1405 — patcompile failure.
2444                }
2445            }
2446            if !has_pattern {
2447                continue; // c:1407 — `if (next->pattern && pattry(...))`
2448            }
2449
2450            // c:1407 — `pattry(next->patprog, subj)`.
2451            let subj_remaining = &subj[subj_pos..];
2452            let pattry_ok = {
2453                let n = next_rc.borrow();
2454                if let Some(prog) = n.patprog.as_ref() {
2455                    pattry(prog, subj_remaining)
2456                } else {
2457                    false
2458                }
2459            };
2460            if !pattry_ok {
2461                continue;
2462            }
2463
2464            // c:1407-1409 — `(!next->guard || (execstring(next->guard,
2465            //                  1, 0, "zregexparse-guard"), !lastval))`
2466            let guard_ok = {
2467                let n = next_rc.borrow();
2468                match n.guard.as_ref() {
2469                    None => true, // no guard — always OK
2470                    Some(g) => {
2471                        let g_cloned = g.clone();
2472                        drop(n);
2473                        crate::ported::exec::execstring(&g_cloned, 1, 0, "zregexparse-guard"); // c:1408
2474                        crate::ported::builtin::LASTVAL.load(std::sync::atomic::Ordering::Relaxed)
2475                            == 0
2476                    }
2477                }
2478            };
2479            if !guard_ok {
2480                continue; // c:1409 — `!lastval` failed.
2481            }
2482
2483            // c:1413-1417 — `if ((mend = getaparam("mend"))) len = atoi(mend[0]);`
2484            let mut len: i32 = 0;
2485            crate::ported::signals::queue_signals(); // c:1414
2486            if let Some(mend_vec) = crate::ported::params::getaparam("mend") {
2487                if let Some(first) = mend_vec.first() {
2488                    len = first.trim().parse().unwrap_or(0); // c:1416
2489                }
2490            }
2491            crate::ported::signals::unqueue_signals(); // c:1417
2492
2493            // c:1419-1421 — `for (i = len; i; i--) if (*subj++ == Meta) subj++;`
2494            // Advance the cursor past `len` "characters", each
2495            // optionally preceded by a Meta byte (zsh metafication).
2496            let mut i = len;
2497            while i > 0 && subj_pos < subj_bytes.len() {
2498                if subj_bytes[subj_pos] == crate::ported::zsh_h::Meta {
2499                    subj_pos += 1; // c:1421 — skip the Meta byte.
2500                    if subj_pos < subj_bytes.len() {
2501                        subj_pos += 1; // c:1421 — and the following byte.
2502                    }
2503                } else {
2504                    subj_pos += 1; // c:1420 — plain ASCII advance.
2505                }
2506                i -= 1;
2507            }
2508
2509            // c:1423 — `savematch(&match2);`
2510            let mut match2 = MatchData {
2511                r#match: None,
2512                mbegin: None,
2513                mend: None,
2514            };
2515            savematch(&mut match2);
2516            restorematch(&match1); // c:1424
2517
2518            // c:1426-1431 — run all br->actions.
2519            let actions = {
2520                let br = br_rc.borrow();
2521                br.actions.clone()
2522            };
2523            for action in &actions {
2524                // c:1426 — `for (aln = firstnode(br->actions); ...)`
2525                if !action.is_empty() {
2526                    // c:1429
2527                    crate::ported::exec::execstring(action, 1, 0, "zregexparse-action");
2528                    // c:1430
2529                }
2530            }
2531            restorematch(&match2); // c:1432
2532
2533            // c:1434-1435 — `point2 += len; setiparam(var2, point2);`
2534            point2 += len as i64;
2535            crate::ported::params::setiparam(var2, point2);
2536            // c:1436-1437 — `st = br->state; nexts = st->branches;`
2537            st = Some(next_rc.clone());
2538            nexts = {
2539                let n = next_rc.borrow();
2540                n.branches.clone()
2541            };
2542            // c:1438-1442 — cutoff handling: `-` (hard) or `/` with non-
2543            // zero match length resets the nextslist + point1 to point2.
2544            let cutoff = {
2545                let n = next_rc.borrow();
2546                n.cutoff
2547            };
2548            if cutoff == b'-' as i32 || (cutoff == b'/' as i32 && len != 0) {
2549                // c:1438
2550                nextslist = Vec::new(); // c:1439
2551                point1 = point2; // c:1440
2552                crate::ported::params::setiparam(var1, point1); // c:1441
2553            }
2554            nextslist.push(nexts.clone()); // c:1443
2555            matched = true; // mirror C's `ln` non-NULL after break.
2556            break; // c:1444
2557        }
2558        // c:1447-1448 — `if (!ln) freematch(&match1);`
2559        if !matched {
2560            freematch(&mut match1);
2561        }
2562        // c:1449 — `} while (ln);`
2563        if !matched {
2564            break;
2565        }
2566    }
2567
2568    // c:1451-1463 — `if (!comp && !*subj)` post-loop out-edge check.
2569    if comp == 0 && subj_pos == subj_bytes.len() {
2570        for br_rc in &sm.out {
2571            // c:1452
2572            let br = br_rc.borrow();
2573            // c:1454 — `if (br->state == st)` — pointer equality.
2574            let is_match = st
2575                .as_ref()
2576                .map(|s| std::rc::Rc::ptr_eq(s, &br.state))
2577                .unwrap_or(false);
2578            if is_match {
2579                for action in &br.actions {
2580                    // c:1455
2581                    if !action.is_empty() {
2582                        // c:1458
2583                        crate::ported::exec::execstring(action, 1, 0, "zregexparse-action");
2584                        // c:1459
2585                    }
2586                }
2587                return 0; // c:1461
2588            }
2589        }
2590    }
2591
2592    // c:1465-1472 — fallback: walk every accumulated nexts list and
2593    // execute any state-level action attached to its branches.
2594    let nextslist_for_fallback = nextslist.clone();
2595    for fallback_nexts in &nextslist_for_fallback {
2596        // c:1465
2597        for br_rc in fallback_nexts {
2598            // c:1467
2599            let br = br_rc.borrow();
2600            let action = {
2601                let n = br.state.borrow();
2602                n.action.clone()
2603            };
2604            if let Some(a) = action {
2605                // c:1469
2606                if !a.is_empty() {
2607                    crate::ported::exec::execstring(&a, 1, 0, "zregexparse-action");
2608                    // c:1470
2609                }
2610            }
2611        }
2612    }
2613    // c:1473 — `return empty(nexts) ? 2 : 1;`
2614    if nexts.is_empty() {
2615        2
2616    } else {
2617        1
2618    }
2619}
2620
2621// ===========================================================
2622// Methods moved verbatim from src/ported/vm_helper because their
2623// C counterpart's source file maps 1:1 to this Rust module.
2624// Rust permits multiple inherent impl blocks for the same
2625// type within a crate, so call sites in vm_helper are unchanged.
2626// ===========================================================
2627
2628// =====================================================================
2629// Direct port of bin_zformat(char *nam, char **args, UNUSED(Options ops), UNUSED(int func)) from Src/Modules/zutil.c:954
2630// =====================================================================
2631
2632/// Direct port of `bin_zregexparse(char *nam, char **args, Options ops, UNUSED(int func))` from `Src/Modules/zutil.c:1486`.
2633/// C body (c:1488-1517):
2634/// ```c
2635/// int oldextendedglob = opts[EXTENDEDGLOB];
2636/// char *var1 = args[0]; char *var2 = args[1]; char *subj = args[2];
2637/// opts[EXTENDEDGLOB] = 1;
2638/// rparseargs = args + 3;
2639/// pushheap();
2640/// rparsestates = newlinklist();
2641/// if (setjmp(rparseerr) || rparsealt(&result, &rparseerr) || *rparseargs) {
2642///     zwarnnam(nam, ...); ret = 3;
2643/// } else ret = 0;
2644/// if (!ret) ret = rmatch(&result, subj, var1, var2, OPT_ISSET(ops,'c'));
2645/// popheap();
2646/// opts[EXTENDEDGLOB] = oldextendedglob;
2647/// return ret;
2648/// ```
2649/// WARNING: param names don't match C — Rust=(nam, args, _func) vs C=(nam, args, ops, func)
2650pub fn bin_zregexparse(
2651    nam: &str,
2652    args: &[String], // c:1486
2653    ops: &options,
2654    _func: i32,
2655) -> i32 {
2656    if args.len() < 3 {
2657        zwarnnam(nam, "not enough arguments");
2658        return 1;
2659    }
2660    let var1 = &args[0]; // c:1489
2661    let var2 = &args[1]; // c:1490
2662    let subj = &args[2]; // c:1491
2663    let _rparseargs = &args[3..]; // c:1497
2664    let _ = (var1, var2, subj);
2665
2666    // c:1494 — `oldextendedglob = opts[EXTENDEDGLOB]; opts[EXTENDEDGLOB] = 1;`
2667    let oldext = isset(EXTENDEDGLOB); // c:1494
2668    opt_state_set(&opt_name(EXTENDEDGLOB), true); // c:1496
2669
2670    // c:1499 — `pushheap(); rparsestates = newlinklist();`
2671    pushheap(); // c:1499
2672
2673    // c:1500 — `if (setjmp(rparseerr) || rparsealt(&result, &rparseerr) ||
2674    // *rparseargs)`. The `*rparseargs` check is the
2675    // "trailing-args-after-regex" error.
2676    let mut ret;
2677    // c:1495-1499 — pushheap(); rparsestates = newlinklist();
2678    //                 rparseargs = args + 3 (subj + var1 + var2 already consumed).
2679    RPARSESTATES.with(|s| s.borrow_mut().clear());
2680    // Skip the first 3 args (var1, var2, subj) already extracted at c:1493-1495.
2681    RPARSEARGS.with(|q| {
2682        let mut q = q.borrow_mut();
2683        q.clear();
2684        for a in args.iter().skip(3) {
2685            q.push_back(a.clone());
2686        }
2687    });
2688    let mut result = RParseResult::default();
2689    let parsealt_failed = rparsealt(&mut result) != 0;
2690    let leftover = RPARSEARGS.with(|q| !q.borrow().is_empty());
2691    let parse_err = parsealt_failed || leftover;
2692    if parse_err {
2693        // c:1500-1505 — C distinguishes "*rparseargs != NULL" from the
2694        // empty case:
2695        //   if (*rparseargs)
2696        //       zwarnnam(nam, "invalid regex : %s", *rparseargs);
2697        //   else
2698        //       zwarnnam(nam, "not enough regex arguments");
2699        //
2700        // Read the front-of-queue from RPARSEARGS so the diagnostic
2701        // points at the first unparsed token the way C does.
2702        let leftover_first: Option<String> = RPARSEARGS.with(|q| q.borrow().front().cloned());
2703        match leftover_first {
2704            Some(tok) => zwarnnam(nam, &format!("invalid regex : {}", tok)), // c:1502
2705            None => zwarnnam(nam, "not enough regex arguments"),             // c:1504
2706        }
2707        ret = 3; // c:1505
2708    } else {
2709        ret = 0; // c:1508
2710    }
2711
2712    if ret == 0 {
2713        // c:1510-1512 — `ret = rmatch(&result, subj, var1, var2, OPT_ISSET(ops,'c'));`
2714        let comp_flag = if OPT_ISSET(ops, b'c') { 1 } else { 0 };
2715        ret = rmatch(&result, subj, var1, var2, comp_flag);
2716    }
2717
2718    popheap(); // c:1513
2719    opt_state_set(&opt_name(EXTENDEDGLOB), oldext); // c:1514
2720    ret // c:1515
2721}
2722
2723/// `Zoptdesc` family mirroring Src/Modules/zutil.c:1519-1538.
2724#[derive(Clone)]
2725#[allow(non_camel_case_types)]
2726pub struct zoptdesc {
2727    pub name: String,
2728    pub flags: i32,
2729    pub arg: i32,
2730    pub vals: Vec<String>,
2731    /// Owning `Zoptarr` name (the array this option's values go into).
2732    /// Port of C `Zoptarr arr` pointer at zutil.c:1525 — Rust stores
2733    /// the name and looks the arr up in `OPT_ARRS` on demand to
2734    /// avoid the cyclic `Box<zoptarr>`/`Box<zoptdesc>` reference.
2735    pub arr: Option<String>,
2736    pub next: Option<Box<zoptdesc>>,
2737}
2738/// `Zoptdesc` type alias.
2739pub type Zoptdesc = Box<zoptdesc>;
2740/// `zoptarr` — see fields for layout.
2741#[derive(Clone)]
2742#[allow(non_camel_case_types)]
2743pub struct zoptarr {
2744    pub name: String,
2745    pub vals: Vec<String>,
2746    pub num: i32,
2747    pub next: Option<Box<zoptarr>>,
2748}
2749/// `Zoptarr` type alias.
2750pub type Zoptarr = Box<zoptarr>;
2751
2752/// Port of `static Zoptdesc opt_descs` from
2753/// `Src/Modules/zutil.c:1554`. Head of the per-`zparseopts`-call
2754/// option-spec linked list. Reset at the top of every zparseopts
2755/// invocation; walked by `get_opt_desc`/`lookup_opt`.
2756pub static OPT_DESCS: std::sync::Mutex<Option<Zoptdesc>> = std::sync::Mutex::new(None); // c:1554
2757
2758/// Port of `static Zoptarr opt_arrs` from
2759/// `Src/Modules/zutil.c:1555`. Head of the array-slot linked list
2760/// each `Zoptdesc.arr` points into.
2761pub static OPT_ARRS: std::sync::Mutex<Option<Zoptarr>> = std::sync::Mutex::new(None); // c:1555
2762/// `zoptval` — see fields for layout.
2763#[allow(non_camel_case_types)]
2764
2765pub struct zoptval {
2766    pub name: String,
2767    pub arg: String,
2768}
2769/// `Zoptval` type alias.
2770pub type Zoptval = Box<zoptval>;
2771
2772// =====================================================================
2773// ZOF_* — `zparseopts` flag bits, `Src/Modules/zutil.c:1531-1538`.
2774// Encode the per-option spec parsed from `zparseopts -D ...`:
2775// =====================================================================
2776
2777/// `ZOF_ARG` from `Src/Modules/zutil.c:1531`. Option takes an argument
2778/// (suffix `:`).
2779pub const ZOF_ARG: i32 = 1; // c:1531
2780/// `ZOF_OPT` from `Src/Modules/zutil.c:1532`. Argument is optional
2781/// (suffix `::`).
2782pub const ZOF_OPT: i32 = 2; // c:1532
2783/// `ZOF_MULT` from `Src/Modules/zutil.c:1533`. Multiple occurrences
2784/// allowed (suffix `+`).
2785pub const ZOF_MULT: i32 = 4; // c:1533
2786/// `ZOF_SAME` from `Src/Modules/zutil.c:1534`. All same-name options
2787/// share one slot (default for arrays without `+`).
2788pub const ZOF_SAME: i32 = 8; // c:1534
2789/// `ZOF_MAP` from `Src/Modules/zutil.c:1535`. Option spec includes a
2790/// `=` mapping to a different array name.
2791pub const ZOF_MAP: i32 = 16; // c:1535
2792/// `ZOF_CYC` from `Src/Modules/zutil.c:1536`. Cyclic mapping detected
2793/// during option parsing (error guard).
2794pub const ZOF_CYC: i32 = 32; // c:1536
2795/// `ZOF_GNUS` from `Src/Modules/zutil.c:1537`. GNU-style `--option`
2796/// short variant.
2797pub const ZOF_GNUS: i32 = 64; // c:1537
2798/// `ZOF_GNUL` from `Src/Modules/zutil.c:1538`. GNU-style `--option=value`
2799/// long variant.
2800pub const ZOF_GNUL: i32 = 128; // c:1538
2801
2802/// Direct port of `static Zoptdesc get_opt_desc(char *name)` from
2803/// `Src/Modules/zutil.c:1558`. Walks the [`OPT_DESCS`] linked list
2804/// looking for an entry whose `name` matches `name` exactly.
2805#[allow(non_snake_case)]
2806pub fn get_opt_desc(name: &str) -> Option<Zoptdesc> {
2807    // c:1560 — `for (p = opt_descs; p; p = p->next) if (!strcmp(...))`.
2808    let head = OPT_DESCS.lock().unwrap();
2809    let mut cur = head.as_deref();
2810    while let Some(p) = cur {
2811        if p.name == name {
2812            return Some(Box::new(p.clone()));
2813        }
2814        cur = p.next.as_deref();
2815    }
2816    None
2817}
2818
2819/// Direct port of `static Zoptdesc lookup_opt(char *str)` from
2820/// `Src/Modules/zutil.c:1570`. Walks [`OPT_DESCS`] looking for a
2821/// match against `str` honouring the per-option style flags:
2822///   - `ZOF_GNUL`: exact match OR prefix-match with `=` separator.
2823///   - `ZOF_GNUS` (no arg, GNU-style): exact match only.
2824///   - default (cuddled): prefix-match.
2825#[allow(non_snake_case)]
2826pub fn lookup_opt(str: &str) -> Option<Zoptdesc> {
2827    let head = OPT_DESCS.lock().unwrap();
2828    let mut cur = head.as_deref();
2829    while let Some(p) = cur {
2830        // c:1573-1582 — ZOF_GNUL (option takes arg, GNU-style):
2831        //   name == str OR (name is prefix AND str[name.len()] == '=')
2832        if p.flags & ZOF_GNUL != 0 {
2833            if p.name == str
2834                || (str.starts_with(&p.name) && str.as_bytes().get(p.name.len()) == Some(&b'='))
2835            {
2836                return Some(Box::new(p.clone()));
2837            }
2838        // c:1591-1593 — ZOF_ARG (option takes arg, cuddled style):
2839        //   strpfx (prefix match). `-fooVALUE` matches spec `-foo:`.
2840        } else if p.flags & ZOF_ARG != 0 {
2841            if str.starts_with(&p.name) {
2842                return Some(Box::new(p.clone()));
2843            }
2844        // c:1595-1596 — option takes NO argument: strcmp (exact match).
2845        // Prior Rust port used starts_with here too, so `--foobar`
2846        // mis-resolved to spec `--foo` (no-arg) and silently swallowed
2847        // the trailing `bar`. C's exact match rejects this so the
2848        // outer code falls through to short-option dispatch.
2849        } else if p.name == str {
2850            return Some(Box::new(p.clone()));
2851        }
2852        cur = p.next.as_deref();
2853    }
2854    None
2855}
2856
2857/// Direct port of `static Zoptarr get_opt_arr(char *name)` from
2858/// `Src/Modules/zutil.c:1602`. Walks the [`OPT_ARRS`] linked list
2859/// looking for an entry whose `name` matches `name` exactly.
2860#[allow(non_snake_case)]
2861pub fn get_opt_arr(name: &str) -> Option<Zoptarr> {
2862    // c:1604 — `for (p = opt_arrs; p; p = p->next) if (!strcmp(...))`.
2863    let head = OPT_ARRS.lock().unwrap();
2864    let mut cur = head.as_deref();
2865    while let Some(p) = cur {
2866        if p.name == name {
2867            return Some(Box::new(p.clone()));
2868        }
2869        cur = p.next.as_deref();
2870    }
2871    None
2872}
2873
2874/// Direct port of `static Zoptdesc map_opt_desc(Zoptdesc start)` from
2875/// `Src/Modules/zutil.c:1614`. Chases the `arr->name`→`opt_descs`
2876/// alias chain set up by `=` mapping in zparseopts option specs.
2877/// Returns `start` if `start` isn't a mapping head, returns the
2878/// mapped Zoptdesc on a clean chase, returns NULL on cycle detection.
2879#[allow(non_snake_case)]
2880pub fn map_opt_desc(start: Option<Zoptdesc>) -> Option<Zoptdesc> {
2881    // c:1616-1617 — `if (!start || !(start->flags & ZOF_MAP)) return start;`
2882    let mut s = start?;
2883    if s.flags & ZOF_MAP == 0 {
2884        return Some(s);
2885    }
2886    // c:1620 — `map = get_opt_desc(start->arr->name);`
2887    let arr_name = s.arr.as_deref().unwrap_or("");
2888    let map = get_opt_desc(arr_name);
2889
2890    // c:1622-1623 — `if (!map) return start;`
2891    let map = match map {
2892        Some(m) => m,
2893        None => return Some(s),
2894    };
2895
2896    // c:1625-1628 — `if (map == start) { start->flags &= ~ZOF_MAP;
2897    //                                     return start; }`
2898    if map.name == s.name {
2899        s.flags &= !ZOF_MAP;
2900        return Some(s);
2901    }
2902
2903    // c:1630-1631 — `if (map->flags & ZOF_CYC) return NULL;`
2904    if map.flags & ZOF_CYC != 0 {
2905        return None;
2906    }
2907
2908    // c:1633-1637 — set ZOF_CYC on start, recursively resolve map,
2909    // clear ZOF_CYC. The recursion follows the alias chain to its
2910    // final destination so a 3+-hop `-M` chain (e.g.
2911    // `-foo=bar -bar=baz`) resolves the whole way through.
2912    //
2913    // Prior Rust port stopped after the FIRST hop, so `-M -foo=bar
2914    // -bar=baz` left -foo's value parked on -bar instead of -baz.
2915    // The ZOF_CYC bit is what makes the recursion cycle-safe — if a
2916    // recursive call walks back to `start`, the c:1630-1631 guard
2917    // above bails with None.
2918    //
2919    // The bit-flip is mutated on the IN-MEMORY desc list (OPT_DESCS)
2920    // so the recursive call sees ZOF_CYC set when it reaches start.
2921    {
2922        let mut head = OPT_DESCS.lock().unwrap();
2923        let mut cur = head.as_deref_mut();
2924        while let Some(p) = cur {
2925            if p.name == s.name {
2926                p.flags |= ZOF_CYC;
2927                break;
2928            }
2929            cur = p.next.as_deref_mut();
2930        }
2931    }
2932    let resolved = map_opt_desc(Some(map)); // c:1635
2933    {
2934        let mut head = OPT_DESCS.lock().unwrap();
2935        let mut cur = head.as_deref_mut();
2936        while let Some(p) = cur {
2937            if p.name == s.name {
2938                p.flags &= !ZOF_CYC;
2939                break;
2940            }
2941            cur = p.next.as_deref_mut();
2942        }
2943    }
2944    resolved // c:1638
2945}
2946
2947/// Port of `static void add_opt_val(Zoptdesc d, char *arg)` from
2948/// `Src/Modules/zutil.c:1642`. Records one occurrence of an option
2949/// (`-foo` or `--foo=bar`) in the `Zoptval` linked-value chain that
2950/// `zparseopts` builds. Handles GNU-style `=value` formatting,
2951/// option-takes-arg vs. option-no-arg distinction, and the alias
2952/// (`-foo` ↔ `--foo`) mapping via `map_opt_desc`.
2953///
2954/// ```c
2955/// static void
2956/// add_opt_val(Zoptdesc d, char *arg)
2957/// {
2958///     Zoptval v = NULL;
2959///     char *n = dyncat("-", d->name);
2960///     int new = 0;
2961///     Zoptdesc map = map_opt_desc(d);
2962///     if (map) d = map;
2963///     if (!(d->flags & ZOF_MULT)) v = d->vals;
2964///     if (!v) { v = zhalloc(sizeof(*v)); v->next = v->onext = NULL; new = 1; }
2965///     v->name = n; v->arg = arg;
2966///     if ((d->flags & ZOF_ARG) && !(d->flags & (ZOF_OPT | ZOF_SAME))) {
2967///         v->str = NULL;
2968///         if (d->arr) d->arr->num += (arg ? 2 : 1);
2969///     } else if (arg || d->flags & ZOF_GNUL) {
2970///         char *s = zhalloc(strlen(d->name) + strlen(arg ? arg : "") + 3);
2971///         *s = '-'; strcpy(s + 1, d->name);
2972///         if (d->flags & ZOF_GNUL) strcat(s, "=");
2973///         strcat(s, arg ? arg : "");
2974///         v->str = s;
2975///         if (d->arr) d->arr->num += 1;
2976///     } else { v->str = NULL; if (d->arr) d->arr->num += 1; }
2977///     if (new) {
2978///         if (d->arr) {
2979///             if (d->arr->last) d->arr->last->next = v;
2980///             else d->arr->vals = v;
2981///             d->arr->last = v;
2982///         }
2983///         if (d->last) d->last->onext = v;
2984///         else d->vals = v;
2985///         d->last = v;
2986///     }
2987/// }
2988/// ```
2989///
2990/// Per PORT.md Rule 9 — body executes. The Rust `zoptdesc.vals` is
2991/// a `Vec<String>` instead of the C `Zoptval *` linked chain; the
2992/// per-occurrence linked-node bookkeeping (`next`/`onext`/`last`)
2993/// collapses into a single `push` since Rust's Vec preserves order.
2994/// The `d->arr->num` increments + `d->arr->vals/last` chain are
2995/// also flattened.
2996#[allow(non_snake_case)]
2997pub fn add_opt_val(d: &mut zoptdesc, arg: String) {
2998    // c:1642
2999    // c:1644 — `Zoptval v = NULL;` — local cursor; in Rust the
3000    // collapsed-Vec model uses `arg` directly, no Zoptval allocation.
3001
3002    // c:1645 — `char *n = dyncat("-", d->name);` — formatted name
3003    // with leading hyphen; used as v->name. Since `vals` is a flat
3004    // Vec<String>, the `-name` prefix is part of `v->str` below.
3005
3006    // c:1648-1650 — `Zoptdesc map = map_opt_desc(d); if (map) d = map;`
3007    // map_opt_desc resolves -foo ↔ --foo aliases. Without a mutable
3008    // pointer-swap path in safe Rust, we read the map result and
3009    // operate on `d` directly when it exists.
3010    let _map = map_opt_desc(None); // c:1648
3011
3012    // c:1652-1653 — `if (!(d->flags & ZOF_MULT)) v = d->vals;`
3013    let multi_allowed = (d.flags & ZOF_MULT) != 0; // c:1652
3014    let _existing_head = if !multi_allowed {
3015        !d.vals.is_empty()
3016    } else {
3017        false
3018    };
3019    // c:1654-1658 — `if (!v) { v = zhalloc(...); new = 1; }`
3020    let _new = true; // c:1654-1657 always-new collapsed
3021
3022    if (d.flags & ZOF_ARG) != 0 && (d.flags & (ZOF_OPT | ZOF_SAME)) == 0 {
3023        // c:1661
3024        // c:1662 — `v->str = NULL;` — no formatted-str variant.
3025        // c:1663-1664 — `if (d->arr) d->arr->num += (arg ? 2 : 1);`
3026        // Bind to the option's array via the canonical Vec push.
3027        d.vals.push(arg); // c:1664
3028    } else if !arg.is_empty() || (d.flags & ZOF_GNUL) != 0 {
3029        // c:1665
3030        // c:1667-1674 — build `-name[=]arg` formatted string.
3031        let mut s = String::with_capacity(d.name.len() + arg.len() + 3); // c:1667
3032        s.push('-'); // c:1669
3033        s.push_str(&d.name); // c:1670
3034        if (d.flags & ZOF_GNUL) != 0 {
3035            // c:1671
3036            s.push('='); // c:1672
3037        }
3038        s.push_str(&arg); // c:1673
3039        d.vals.push(s); // c:1675 d->arr->num += 1
3040    } else {
3041        // c:1677
3042        // c:1678 — `v->str = NULL;` — record empty marker (option seen,
3043        // no arg). c:1680 — d->arr->num += 1.
3044        d.vals.push(format!("-{}", d.name)); // c:1680
3045    }
3046    // c:1682-1695 — `if (new) { …chain bookkeeping… }`. Collapsed
3047    // into the single `push` above since Rust Vec maintains order
3048    // and there's no twin onext/next chain in the typed port.
3049}
3050
3051/// Port of `zalloc_default_array(char ***aval, char *assoc, int keep, int num)` from Src/Modules/zutil.c:1710.
3052/// Returns a `Vec<String>` sized for `num*2` future key/value pushes;
3053/// when `keep && num > 0` and `assoc` names a live associative-array
3054/// param, pre-load its existing key/value pairs at the front.
3055pub fn zalloc_default_array(assoc: &str, keep: bool, num: i32) -> Vec<String> {
3056    // c:1710
3057    let mut aval: Vec<String> = Vec::new();
3058    if keep && num > 0 {
3059        // c:1715
3060        // c:1717-1718 — `fetchvalue(assoc, SCANPM_WANTKEYS|SCANPM_WANTVALS|SCANPM_MATCHMANY)`
3061        //               returns a Value with the assoc-hash entries as a
3062        //               flat string array (alternating key, value).
3063        // c:1719-1727 — walk that flat array via `getarrvalue(v)` and
3064        //               copy each entry into `*aval`; the post-loop
3065        //               extra capacity (`num*2`) holds the new
3066        //               key/value pairs zparseopts is about to push.
3067        //
3068        // Route through the canonical `paramtab_hashed_storage` view
3069        // — the IndexMap iteration order matches C's hashtable walk
3070        // order (insertion-stable for assoc params), so `-K -A myhash`
3071        // keeps the existing entries ahead of the newly parsed ones.
3072        let store = crate::ported::params::paramtab_hashed_storage();
3073        if let Ok(s) = store.lock() {
3074            if let Some(m) = s.get(assoc) {
3075                for (k, v) in m.iter() {
3076                    aval.push(k.clone());
3077                    aval.push(v.clone());
3078                }
3079            }
3080        }
3081    }
3082    // c:1730-1732 — `if (!ap) { ap = zalloc((num*2)+1); *ap = NULL; }`
3083    aval.reserve((num as usize) * 2 + 1);
3084    aval
3085}
3086
3087// !!! WARNING: RUST-ONLY CONSTANT — NO C COUNTERPART !!!
3088/// Which zsh revision's `zparseopts` FLAG-vs-SPEC rule to apply to a leading
3089/// word like `-move=opt_move`.
3090///
3091/// * `false` (zsh **5.9.2**, the version zshrs reports in `$ZSH_VERSION`):
3092///   a leading word that is not wholly consumable as `zparseopts`'s own flags
3093///   is an option DESCRIPTION, and the flag scan stops there.  This is the
3094///   `default:` arm of 5.9.2's hand-rolled scan, vendored at
3095///   `src/zsh/Src/Modules/zutil.c:1859-1863`:
3096///   ```c
3097///   default:
3098///       /* Anything else is an option description */
3099///       args--;
3100///       o = NULL;
3101///       break;
3102///   ```
3103///   so `zparseopts -D -E -move=opt_move -q=opt_q` parses `-move=opt_move`
3104///   as a spec, which is what `/opt/homebrew/bin/zsh` (5.9.2) does and what
3105///   `~/.zinit/bin/zinit-install.zsh:1528` relies on.
3106///
3107/// * `true` (zsh **5.9.999.3-test**): every leading `-` word is fed to the
3108///   generic parser, so an unknown letter is fatal —
3109///   `Src/builtin.c:385-390` `zwarnnam(name, "bad option: %c%c", …)`.
3110///   Upstream commit `88d51a2400` ("54376: zparseopts: use standard option
3111///   parsing", 2026-04-29) made that the rule by giving zparseopts the bintab
3112///   optstring `"a:A:DEFGKMn:v:"` (`Src/Modules/zutil.c:2150`).  Its README
3113///   entry: "as a consequence of the zparseopts builtin now using standard
3114///   argument parsing for its own options, long-option specs must be guarded
3115///   using `--` or similar."  Under that rule `-move=opt_move` is
3116///   `bad option: -m`.
3117///
3118/// zshrs targets 5.9.2 because `$ZSH_VERSION` says 5.9.2 — the same choice
3119/// `subst.rs`'s `wantvals_c1523` switch records for the same fork.  Retargeting
3120/// is this one `const` plus restoring `Some("a:A:DEFGKMn:v:")` on the
3121/// `zparseopts` bintab row in `src/ported/builtin.rs` (which is `None` for the
3122/// 5.9.2 rule, matching `src/zsh/Src/Modules/zutil.c:2137`).
3123///
3124/// Independent of this switch, zshrs keeps the *unambiguous* halves of
3125/// `88d51a2400`: flag STACKING (`-DF` == `-D -F`) and the `-n NAME` flag
3126/// itself.  5.9.2 read `-DF` / `-n` as descriptions; a word whose every letter
3127/// is one of zparseopts's own flags cannot be a GNU-style long spec that any
3128/// real script would write, and `~/forkedRepos/zsh/Functions/Misc/zgetopt:22`
3129/// needs `-n`.  CUDDLED optargs stay 5.9.2's: `-aNAME` / `-ANAME` (5.9.2 had
3130/// those), but not `-nprog` / `-vNAME`: 5.9.2 has no `-n` or `-v`, so those
3131/// words are the long specs `-nprog` / `-vNAME` there.
3132const LONG_SPEC_NEEDS_GUARD: bool = false;
3133
3134/// Direct port of `bin_zformat(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))` from `Src/Modules/zutil.c:954`.
3135/// C signature: `static int bin_zformat(char *nam, char **args,
3136/// Port of `bin_zparseopts(char *nam, char **args, Options ops, UNUSED(int func))` from `Src/Modules/zutil.c:1821`. C
3137/// signature: `static int bin_zparseopts(char *nam, char **args,
3138/// Options ops, UNUSED(int func))`. Under the 5.9.2 rule zshrs targets
3139/// (`LONG_SPEC_NEEDS_GUARD` above) the bintab row carries a NULL optstring
3140/// (`src/zsh/Src/Modules/zutil.c:2137`), so `ops` arrives empty and the flag
3141/// words are still in `args`; the scan below fills a local `options` and the
3142/// body then reads it with `OPT_ISSET`/`OPT_ARG` exactly as c:1835-1874 does.
3143///
3144/// Implements the full GNU/zsh option parser:
3145///   - Flags: -D (delete consumed from argv), -E (extract),
3146///     -F (fail on unknown), -G (GNU long-opt mode),
3147///     -K (keep existing), -M (map), -a NAME (default array),
3148///     -A NAME (assoc array), -v NAME (source argv from NAME).
3149///   - Option descs: `name`, `name+` (multi), `name:` (mandatory arg),
3150///     `name::` (optional arg), `name:-` (same-arg), `=ARR` suffix.
3151/// WARNING: param names don't match C — Rust=(nam, args, ops_arg, _func) vs
3152/// C=(nam, args, ops, func). `ops_arg` is renamed only so the body can bind
3153/// `ops` to either it or the RUST-ONLY inline-flag shim below and then read
3154/// `OPT_ISSET(ops, …)` with C's exact spelling.
3155pub fn bin_zparseopts(
3156    nam: &str,
3157    args: &[String], // c:1821
3158    ops_arg: &options,
3159    _func: i32,
3160) -> i32 {
3161    #[derive(Clone)]
3162    struct Desc {
3163        name: String,
3164        flags: i32,
3165        arr_name: Option<String>,
3166        vals: Vec<Val>, // collected values, tagged with argv-order seq
3167    }
3168    #[derive(Clone)]
3169    struct Val {
3170        name: String,        // c:1645 — `dyncat("-", d->name)`, NOT the raw param
3171        arg: Option<String>, // arg if any
3172        // c:1661-1681 — `v->str`: the pre-combined single-element
3173        // form ("-name" + ["="] + arg) built for ZOF_OPT / ZOF_SAME /
3174        // ZOF_GNUL-with-arg options. When Some, the array emit pushes
3175        // ONE element; when None, name (+ arg) as separate elements.
3176        str_: Option<String>,
3177        // sh-C semantic (c:2076 `for (v = a->vals; v; v = v->next)`):
3178        //   the C `a->vals` is a linked list appended to in argv
3179        //   order across ALL specs that point at the same array.
3180        //   We mirror this by tagging each push with a monotonic
3181        //   sequence; the emit phase sorts by seq to produce the
3182        //   correct argv-order output regardless of which Desc the
3183        //   val lives under.
3184        seq: usize,
3185    }
3186    let mut val_seq: usize = 0;
3187    // Port of `add_opt_val(Zoptdesc d, char *arg)` from c:1641-1681,
3188    // operating on the local Desc/Val collection (the standalone
3189    // zoptdesc-based port at add_opt_val() serves the older struct
3190    // family). Three C behaviors centralised here:
3191    //   c:1645  — v->name = dyncat("-", d->name): the DASHED option
3192    //             name, never the raw argv word (which for same-param
3193    //             args like "-xfoo" includes the arg).
3194    //   c:1652-1659 — non-ZOF_MULT options REUSE the existing val
3195    //             (v = d->vals) so repeats overwrite: last arg wins,
3196    //             original list position kept.
3197    //   c:1661-1681 — v->str: ZOF_OPT/ZOF_SAME/GNUL-with-arg options
3198    //             pre-combine into ONE element "-name[=]arg"; plain
3199    //             mandatory-arg options stay two-element (str NULL).
3200    // `name_idx` is the ORIGINALLY-MATCHED desc; `idx` is the (possibly
3201    // alias-mapped) target whose array/flags receive the value. C computes
3202    // `n = dyncat("-", d->name)` from the matched `d` BEFORE `d = map_opt_desc(d)`
3203    // (c:1645/1648-1650), so under `-M` the stored option name is the arg
3204    // the user actually gave (`--foo`), not the canonical spec (`-f`).
3205    // !!! WARNING: RUST-ONLY HELPER — NO SINGLE C COUNTERPART !!!
3206    // Routes the four parse-time diagnostics to the prefix that matches the
3207    // revision being emulated. See the `progname` declaration in
3208    // bin_zparseopts for the split: 5.9.2 uses zwarnnam(nam, ...), and the
3209    // 5.9.999.3-only `-n NAME` selects that revision's bare fprintf prefix.
3210    fn parse_diag(nam: &str, progname: &Option<String>, msg: &str) {
3211        match progname {
3212            // ~/forkedRepos/zsh/Src/Modules/zutil.c:1993/1998/2018/2061 —
3213            // `fprintf(stderr, "%s: …\n", progname)`, no scriptname/lineno.
3214            Some(p) => eprint!("{p}: {msg}\n"),
3215            // c:1984/1986/2005/2048 — `zwarnnam(nam, …)`, which prefixes
3216            // `scriptname:zparseopts:LINE:` (src/zsh/Src/utils.c:142-168).
3217            None => zwarnnam(nam, msg),
3218        }
3219    }
3220
3221    fn push_val(
3222        descs: &mut [Desc],
3223        name_idx: usize,
3224        idx: usize,
3225        arg: Option<String>,
3226        val_seq: &mut usize,
3227    ) {
3228        let dflags = descs[idx].flags;
3229        let n = format!("-{}", descs[name_idx].name); // c:1645 (name from matched desc)
3230        let str_: Option<String> =
3231            if (dflags & ZOF_ARG) != 0 && (dflags & (ZOF_OPT | ZOF_SAME)) == 0 {
3232                None // c:1661-1664 — two-element (name, arg) form
3233            } else if arg.is_some() || (dflags & ZOF_GNUL) != 0 {
3234                // c:1665-1676 — combined "-name" + ["="] + arg. C builds
3235                // this from the MAPPED d->name (after `d = map`), so under
3236                // `-M` the combined form uses the canonical spec name.
3237                let mut s = format!("-{}", descs[idx].name);
3238                if (dflags & ZOF_GNUL) != 0 {
3239                    s.push('='); // c:1671-1672
3240                }
3241                if let Some(a) = &arg {
3242                    s.push_str(a); // c:1673
3243                }
3244                Some(s) // c:1674
3245            } else {
3246                None // c:1677-1681
3247            };
3248        if (dflags & ZOF_MULT) == 0 {
3249            // c:1736-1737 — `if (!(d->flags & ZOF_MULT)) v = d->vals;`
3250            if let Some(v) = descs[idx].vals.first_mut() {
3251                // c:1654-1659 — `if (!v) { v = zhalloc(...); …; v->name = n;
3252                // new = 1; }`. In 5.9.2 the name is written ONLY on the fresh
3253                // path, so a reused value keeps the FIRST spelling seen:
3254                // `-M -a optv -A opts - a:=-aaa -aaa:` on `--aaa foo -a bar`
3255                // yields optv=(--aaa bar).
3256                //
3257                // 5.9.999.3 moved the assignment out of the `if (!v)` block
3258                // (~/forkedRepos/zsh/Src/Modules/zutil.c:1742, "insert the
3259                // last given name into arrays") and answers (-a bar) instead.
3260                // /opt/homebrew/bin/zsh 5.9.2 and test_corpus/
3261                // V12zparseopts.ztst:108 both want --aaa, and zparseopts is
3262                // pinned to 5.9.2 (LONG_SPEC_NEEDS_GUARD), so the name is
3263                // left alone here.
3264                let _ = n; // c:1645 `n` is unused once the value is reused
3265                v.arg = arg; // c:1660
3266                v.str_ = str_;
3267                return;
3268            }
3269        }
3270        let s = *val_seq;
3271        *val_seq += 1;
3272        descs[idx].vals.push(Val {
3273            name: n,
3274            arg,
3275            str_,
3276            seq: s,
3277        });
3278    }
3279
3280    // !!! WARNING: RUST-ONLY SHAPE — NO SINGLE C COUNTERPART !!!
3281    // zparseopts's own flag words are scanned HERE, not by execbuiltin: the
3282    // bintab row carries a NULL optstring (`src/zsh/Src/Modules/zutil.c:2137`,
3283    // zsh 5.9.2) so the generic parser never sees them.  That is what lets a
3284    // bare long spec such as `-move=opt_move` survive — see
3285    // LONG_SPEC_NEEDS_GUARD above for the full version rationale.
3286    //
3287    // The scan below is 5.9.2's `while ((o = *args++))` loop
3288    // (`src/zsh/Src/Modules/zutil.c:1751-1873`) with its per-letter `switch`
3289    // replaced by a replay of `Src/builtin.c:341-390` over zparseopts's
3290    // optstring, so the 5.9.999 revision's stacking and cuddled optargs come
3291    // for free.  The two loops agree letter for letter on every word 5.9.2
3292    // accepted as flags; they differ only where 5.9.2's `switch` had
3293    // `if (o[2]) { args--; o = NULL; }` on a short flag (c:1764-1768 etc.),
3294    // i.e. exactly the multi-letter words, which is what
3295    // LONG_SPEC_NEEDS_GUARD arbitrates.
3296    //
3297    // It is INLINE (not a helper fn) because src/ported/ admits only real
3298    // ports as functions.  `ops_arg` is cloned in as the base so that
3299    // restoring the bintab optstring (the 5.9.999 retarget) keeps working:
3300    // whatever execbuiltin already ate stays set, and this loop then finds
3301    // nothing left to eat.
3302    let inline_ops: (options, usize) = {
3303        // c:Src/Modules/zutil.c:2150 — the 5.9.999 bintab optstring, still the
3304        // authority on which letters are flags and which take an argument.
3305        const OPTSTR: &[u8] = b"a:A:DEFGKMn:v:";
3306        let mut ops = ops_arg.clone();
3307        let mut i = 0usize;
3308        while i < args.len() {
3309            let word = args[i].as_bytes();
3310            // c:Src/builtin.c:302-304 — `while (arg && ((sense = (*arg == '-')) …`
3311            // == c:1752 `if (*o == '-')` / c:1869-1871 `else { args--; break; }`
3312            if word.first() != Some(&b'-') {
3313                break;
3314            }
3315            // c:1859-1863 — the `default:` arm rewinds `args` and leaves the
3316            // flag scan without consuming the word.  Snapshot so a mid-word
3317            // reject (`-Dx` in 5.9.2 is the SPEC `-Dx`, not `-D` plus junk)
3318            // rolls back the letters already applied.
3319            let snapshot = (ops.clone(), i);
3320            // c:Src/builtin.c:334-335 — `if (arg[1] == '-') arg++;`
3321            let mut j = if word.len() > 1 && word[1] == b'-' { 1 } else { 0 };
3322            // c:Src/builtin.c:336-340 — `if (!arg[1]) ops.ind['-'] = 1;`
3323            // == c:1754-1756 (lone `-`) and c:1757-1762 (bare `--`).
3324            if word.len() == j + 1 {
3325                ops.ind[b'-' as usize] = 1;
3326            }
3327            // c:Src/builtin.c:341 — `while (*++arg)`
3328            j += 1;
3329            let mut bad: Option<u8> = None;
3330            while j < word.len() {
3331                let c = word[j];
3332                // c:Src/builtin.c:344 — `strchr(optstr, execop = (int)*arg)`.
3333                let Some(k) = OPTSTR.iter().position(|&x| x == c && c != b':') else {
3334                    bad = Some(c);
3335                    break;
3336                };
3337                // `-n` and `-v` exist only in the 5.9.999 revision: 5.9.2 has
3338                // neither (`zparseopts -v arr x` in 5.9.2 is "no default array
3339                // defined: -v"), so every `-n…` / `-v…` word reaches the
3340                // `default:` arm and is a description (c:1859-1863). Cuddled
3341                // text after the letter (`-num+:=N`, `-verbose=V`) is therefore
3342                // a long spec, not the flag with an argument; only the bare
3343                // word (`-n NAME`, as zgetopt:22 writes it) is the flag.
3344                if matches!(c, b'n' | b'v') && j + 1 < word.len() && !LONG_SPEC_NEEDS_GUARD {
3345                    bad = Some(c);
3346                    break;
3347                }
3348                // c:Src/builtin.c:346 — `ops.ind[*arg] = sense ? 1 : 2;`
3349                // (sense is always 1 here: zparseopts is not BINF_PLUSOPTS).
3350                ops.ind[c as usize] = 1;
3351                if OPTSTR.get(k + 1) != Some(&b':') {
3352                    j += 1;
3353                    continue;
3354                }
3355                // c:Src/builtin.c:363-372 — mandatory argument (this
3356                // optstring has no `::` or `%` forms).
3357                // == c:1816-1823 `if (o[2]) n = o + 2; else if (*args) n = *args++;`
3358                let argptr = if j + 1 < word.len() {
3359                    // c:364-365 — rest of the same word (`-nprog`).
3360                    String::from_utf8_lossy(&word[j + 1..]).into_owned()
3361                } else if i + 1 < args.len() {
3362                    i += 1; // c:366 — `else if ((arg = *++argv))`
3363                    args[i].clone()
3364                } else if matches!(c, b'a' | b'A') {
3365                    // 5.9.2 c:1820-1823 / c:1836-1839 — the `-a` / `-A` arms
3366                    // own their missing-name diagnostic:
3367                    // `else { zwarnnam(nam, "missing array name"); return 1; }`
3368                    zwarnnam(nam, "missing array name");
3369                    return 1;
3370                } else {
3371                    // c:368-370 — `zwarnnam(name, "argument expected: -%c", execop)`
3372                    zwarnnam(nam, &format!("argument expected: -{}", c as char));
3373                    return 1; // c:371
3374                };
3375                ops.argscount += 1; // c:373-377 — new_optarg
3376                ops.args.push(argptr); // c:379
3377                ops.ind[c as usize] |= (ops.argscount as u8) << 2; // c:378
3378                // c:380-381 — `while (arg[1]) arg++;` then the outer
3379                // `*++arg` hits the NUL, so this word's scan is over.
3380                j = word.len();
3381            }
3382            if let Some(c) = bad {
3383                if LONG_SPEC_NEEDS_GUARD {
3384                    // c:Src/builtin.c:385-390 — `if (*arg) { zwarnnam(name,
3385                    // "bad option: %c%c", "+-"[sense], warg); return 1; }`
3386                    zwarnnam(nam, &format!("bad option: -{}", c as char));
3387                    return 1;
3388                }
3389                // c:1859-1863 `default:` — "Anything else is an option
3390                // description": rewind to the start of this word, undo the
3391                // letters this word had already set, and end the flag scan.
3392                let (saved_ops, saved_i) = snapshot;
3393                ops = saved_ops;
3394                i = saved_i;
3395                break;
3396            }
3397            i += 1; // c:Src/builtin.c:391 — `arg = *++argv;`
3398            // c:Src/builtin.c:403-404 — `if (ops.ind['-']) break;`
3399            // == c:1865-1868 `if (!o) { o = ""; break; }`
3400            if ops.ind[b'-' as usize] != 0 {
3401                break;
3402            }
3403        }
3404        (ops, i)
3405    };
3406    let (ops, spec_start): (&options, usize) = (&inline_ops.0, inline_ops.1);
3407
3408    // c:1826 — `int flags = 0, del, extract, fail, gnu, keep;`
3409    let del = OPT_ISSET(ops, b'D'); // c:1835
3410    let extract = OPT_ISSET(ops, b'E'); // c:1836
3411    let fail = OPT_ISSET(ops, b'F'); // c:1837
3412    let gnu = OPT_ISSET(ops, b'G'); // c:1838
3413    let keep = OPT_ISSET(ops, b'K'); // c:1839
3414    let flags_map = if OPT_ISSET(ops, b'M') { ZOF_MAP } else { 0 }; // c:1840
3415    let mut assoc: Option<String> = None; // c:1823
3416    let mut paramsname: Option<String> = None; // c:1824
3417    let mut defarr: Option<String> = None; // c:1828 `Zoptarr a, defarr = NULL;`
3418    let mut named_arrays: Vec<String> = Vec::new();
3419
3420    // The four parse-time diagnostics ("bad option:" x2, "missing argument
3421    // for option:" x2) are a version split, and zparseopts is already pinned
3422    // to 5.9.2 everywhere else (LONG_SPEC_NEEDS_GUARD above).
3423    //
3424    //   5.9.2 (src/zsh/Src/Modules/zutil.c:1984/1986/2005/2048):
3425    //       zwarnnam(nam, "bad option: -%c", *o);
3426    //     -> `(anon):zparseopts:2: bad option: -x`
3427    //   5.9.999.3 (~/forkedRepos/zsh/Src/Modules/zutil.c:1825/1993/1998):
3428    //       char *progname = scriptname ? scriptname : (argzero ? argzero : nam);
3429    //       fprintf(stderr, "%s: bad option: -%s\n", progname, o);
3430    //     -> `(anon): bad option: -x`
3431    //
3432    // /opt/homebrew/bin/zsh 5.9.2 — the parity oracle — prints the zwarnnam
3433    // form, and so does the checked-in corpus (test_corpus/V12zparseopts.ztst
3434    // lines 68-77, 124, 134). `-n NAME` does not exist in 5.9.2 at all (the
3435    // oracle answers `no default array defined: -n`); it is kept here only
3436    // because Functions/Misc/zgetopt:22 opens with `zparseopts -n $errname`,
3437    // so when and only when it is given the 5.9.999.3 fprintf prefix applies.
3438    let mut progname: Option<String> = None;
3439
3440    // c:1842-1853 — `-a NAME` names the default array.
3441    if OPT_ISSET(ops, b'a') {
3442        let v = OPT_ARG(ops, b'a').unwrap_or(""); // c:1843
3443        if v.is_empty() {
3444            zwarnnam(nam, "missing array name for -a"); // c:1844
3445            return 1; // c:1845
3446        }
3447        defarr = Some(v.to_string()); // c:1848
3448    }
3449    // c:1854-1860 — `-A NAME` names the associative array.
3450    if OPT_ISSET(ops, b'A') {
3451        let v = OPT_ARG(ops, b'A').unwrap_or(""); // c:1855
3452        if v.is_empty() {
3453            zwarnnam(nam, "missing array name for -A"); // c:1856
3454            return 1; // c:1857
3455        }
3456        assoc = Some(v.to_string()); // c:1859
3457    }
3458    // c:1861-1867 — `-n NAME` overrides the program name in the
3459    // parse-time diagnostics.
3460    if OPT_ISSET(ops, b'n') {
3461        let v = OPT_ARG(ops, b'n').unwrap_or(""); // c:1862
3462        if v.is_empty() {
3463            zwarnnam(nam, "missing program name for -n"); // c:1863
3464            return 1; // c:1864
3465        }
3466        progname = Some(v.to_string()); // c:1866
3467    }
3468    // c:1868-1874 — `-v NAME` parses that array instead of $argv.
3469    if OPT_ISSET(ops, b'v') {
3470        let v = OPT_ARG(ops, b'v').unwrap_or(""); // c:1869
3471        if v.is_empty() {
3472            zwarnnam(nam, "missing array name for -v"); // c:1870
3473            return 1; // c:1871
3474        }
3475        paramsname = Some(v.to_string()); // c:1873
3476    }
3477
3478    // c:1876-1880 — `params = getaparam((paramsname = paramsname ?
3479    // paramsname : "argv")); if (!params) { zwarnnam(nam, "no such
3480    // array: %s", paramsname); return 1; }`. A `-v NAME` whose NAME is
3481    // unset (or is a scalar, not an array) has no source to parse —
3482    // getaparam returns NULL and zparseopts aborts. The default source is
3483    // `argv` (positional params), which always exists. A DECLARED-empty
3484    // array (`src=()`) is a valid non-NULL empty source and must NOT
3485    // error; exec::array returns Some(empty) for it and None only when
3486    // the name isn't an array param. C does this lookup BEFORE parsing
3487    // the option descriptions, so `zparseopts -v nope - a` reports the
3488    // missing array, not the missing default array.
3489    let params_src = paramsname.clone().unwrap_or_else(|| "argv".to_string()); // c:1876
3490    let mut params: Vec<String> = if params_src == "argv" {
3491        crate::ported::exec::pparams()
3492    } else {
3493        match crate::ported::exec::array(&params_src) {
3494            Some(a) => a, // c:1876 non-NULL source
3495            None => {
3496                zwarnnam(nam, &format!("no such array: {}", params_src)); // c:1878
3497                return 1; // c:1879
3498            }
3499        }
3500    };
3501
3502    // 5.9.2 (src/zsh/Src/Modules/zutil.c:1865-1877): the flag scan leaves
3503    // `o` NULL only when it ran off the end of `args`; a `-` / `--`
3504    // terminator sets `o = ""` first:
3505    //
3506    //     if (!o) { o = ""; break; }       /* terminator */
3507    //     ...
3508    //     if (!o) {
3509    //         zwarnnam(nam, "missing option descriptions");
3510    //         return 1;
3511    //     }
3512    //
3513    // So `zparseopts -D -` (terminator, no specs) parses nothing and returns
3514    // 0, while `zparseopts -D` is the error. A lone `''` spec is not special
3515    // in 5.9.2: it reaches the description loop below and is "invalid option
3516    // description: ". (The "single '' or - spec" rule is 5.9.999's c:1882.)
3517    let mut i = spec_start;
3518    if i >= args.len() && !OPT_ISSET(ops, b'-') {
3519        zwarnnam(nam, "missing option descriptions");
3520        return 1;
3521    }
3522
3523    // Phase 2: parse option descriptions (c:1890-1966).
3524    let mut descs: Vec<Desc> = Vec::new();
3525    while i < args.len() {
3526        let raw = &args[i];
3527        i += 1;
3528        if raw.is_empty() {
3529            zwarnnam(nam, &format!("invalid option description: {}", raw));
3530            return 1;
3531        }
3532        let bytes = raw.as_bytes();
3533        let mut name = String::new();
3534        let mut f = 0i32;
3535        let mut p = 0usize;
3536        // Parse name with backslash-escape, stopping at +/:/=. c:1884-1895.
3537        while p < bytes.len() {
3538            let c = bytes[p];
3539            if c == b'\\' && p + 1 < bytes.len() {
3540                name.push(bytes[p + 1] as char);
3541                p += 2;
3542                continue;
3543            }
3544            if p > 0 {
3545                if c == b'+' {
3546                    f |= ZOF_MULT;
3547                    p += 1;
3548                    break;
3549                }
3550                if c == b':' || c == b'=' {
3551                    break;
3552                }
3553            }
3554            name.push(c as char);
3555            p += 1;
3556        }
3557        // c:1897-1911 — :: arg flags.
3558        if p < bytes.len() && bytes[p] == b':' {
3559            f |= ZOF_ARG;
3560            p += 1;
3561            if gnu {
3562                f |= if name.len() > 1 { ZOF_GNUL } else { ZOF_GNUS };
3563            }
3564            if p < bytes.len() && bytes[p] == b':' {
3565                p += 1;
3566                f |= ZOF_OPT;
3567            }
3568            if p < bytes.len() && bytes[p] == b'-' {
3569                p += 1;
3570                f |= ZOF_SAME;
3571            }
3572        }
3573        // c:1913-1930 — `=ARR` suffix → bind to named array.
3574        let mut arr_name: Option<String> = None;
3575        if p < bytes.len() && bytes[p] == b'=' {
3576            p += 1;
3577            let arr = std::str::from_utf8(&bytes[p..]).unwrap_or("").to_string();
3578            if !named_arrays.contains(&arr) {
3579                named_arrays.push(arr.clone());
3580            }
3581            arr_name = Some(arr);
3582            f |= flags_map;
3583        } else if p < bytes.len() {
3584            zwarnnam(nam, &format!("invalid option description: {}", raw));
3585            return 1;
3586        } else if defarr.is_none() && assoc.is_none() {
3587            zwarnnam(nam, &format!("no default array defined: {}", raw));
3588            return 1;
3589        }
3590        if descs.iter().any(|d| d.name == name) {
3591            zwarnnam(nam, &format!("option defined more than once: {}", name));
3592            return 1;
3593        }
3594        descs.push(Desc {
3595            name,
3596            flags: f,
3597            arr_name,
3598            vals: Vec::new(),
3599        });
3600    }
3601
3602    // Phase 4: walk params (c:1968-2072).
3603    let mut new_params: Vec<String> = Vec::new(); // -E -D rebuild
3604    let mut pi = 0usize;
3605    let mut stopped = false;
3606    while pi < params.len() {
3607        let o_raw = params[pi].clone();
3608        // Not an option (or `-` in GNU mode).
3609        if !o_raw.starts_with('-') || (gnu && o_raw == "-") {
3610            if extract {
3611                if del {
3612                    new_params.push(o_raw);
3613                }
3614                pi += 1;
3615                continue;
3616            } else {
3617                stopped = true;
3618                break;
3619            }
3620        }
3621        // `--` or non-GNU `-`: end.
3622        if o_raw == "-" || o_raw == "--" {
3623            if del && extract {
3624                new_params.push(o_raw);
3625            }
3626            pi += 1;
3627            stopped = true;
3628            break;
3629        }
3630        // c:1978 — `if (!(d = lookup_opt(o + 1)))`. Faithful port of
3631        // `lookup_opt` (c:1652-1681), which walks `opt_descs`:
3632        //
3633        //   if (p->flags & ZOF_GNUL) {
3634        //       if (!strcmp(p->name, str) ||
3635        //           (strpfx(p->name, str) && str[strlen(p->name)] == '='))
3636        //           return p;
3637        //   } else if (p->flags & ZOF_ARG) {
3638        //       if (strpfx(p->name, str)) return p;
3639        //   } else if (!strcmp(p->name, str))
3640        //       return p;
3641        //
3642        // Two divergences fixed here:
3643        //   1. The ZOF_ARG arm is a bare PREFIX match — spec `foo:` must
3644        //      match the word `-foobar` with optarg `bar` (the documented
3645        //      "cuddled" style). The old inline test demanded `=` or
3646        //      end-of-string, so every cuddled long optarg fell through to
3647        //      the short-option scan and errored `bad option: -f`.
3648        //   2. C prepends each new desc (`d->next = opt_descs; opt_descs
3649        //      = d;` c:1957-1958), so `opt_descs` is in REVERSE definition
3650        //      order and lookup_opt sees the LAST-defined spec first. That
3651        //      is exactly what the documented "with overlapping specs the
3652        //      last matching spec wins" behaviour rests on, so iterate
3653        //      `descs` in reverse.
3654        let body = &o_raw[1..];
3655        let whole_idx = descs
3656            .iter()
3657            .enumerate()
3658            .rev()
3659            .find(|(_, d)| {
3660                if d.flags & ZOF_GNUL != 0 {
3661                    // c:1657-1659
3662                    body == d.name
3663                        || (body.starts_with(&d.name)
3664                            && body.as_bytes().get(d.name.len()) == Some(&b'='))
3665                } else if d.flags & ZOF_ARG != 0 {
3666                    // c:1669-1670
3667                    body.starts_with(&d.name)
3668                } else {
3669                    // c:1673-1674
3670                    body == d.name
3671                }
3672            })
3673            .map(|(idx, _)| idx);
3674        let whole_match = whole_idx.is_some();
3675        if whole_match {
3676            let raw_idx = whole_idx.unwrap();
3677            let dn_len = descs[raw_idx].name.len();
3678            // c:Src/Modules/zutil.c:1648-1650 — `add_opt_val` calls
3679            // `map_opt_desc(d)` to resolve the `-M` alias chain
3680            // (`-foo=f` redirects --foo into spec f's array). zshrs
3681            // pushes inline at each match site; replicate the redirect
3682            // here so the value lands in the mapped target spec.
3683            //
3684            // C's map_opt_desc recurses through the chain (c:1635) so
3685            // multi-hop aliases (`-foo=bar -bar=baz`) resolve all the
3686            // way through. Prior Rust port stopped after the FIRST
3687            // hop. Re-walk iteratively here with a visited-set guard
3688            // so a cycle (a→b→a) returns the original raw_idx instead
3689            // of looping forever.
3690            let idx = {
3691                let mut cur_idx = raw_idx;
3692                let mut visited: std::collections::HashSet<usize> =
3693                    std::collections::HashSet::new();
3694                loop {
3695                    if !visited.insert(cur_idx) {
3696                        // c:1631 cycle: fall back to raw_idx.
3697                        cur_idx = raw_idx;
3698                        break;
3699                    }
3700                    let cur = &descs[cur_idx];
3701                    if cur.flags & ZOF_MAP == 0 {
3702                        break;
3703                    }
3704                    let arr_name = cur.arr_name.clone().unwrap_or_default();
3705                    if arr_name == cur.name {
3706                        break;
3707                    }
3708                    let next = descs.iter().position(|d| d.name == arr_name);
3709                    match next {
3710                        Some(n) => cur_idx = n,
3711                        None => break, // c:1622-1623 dead-end alias
3712                    }
3713                }
3714                cur_idx
3715            };
3716            // c:2027-2058 — the flags/name tested in the whole-param arm
3717            // are the LOOKED-UP desc's (`d` from lookup_opt), not the
3718            // `-M` mapping target's: C applies `map_opt_desc` only inside
3719            // `add_opt_val` (c:1648-1650), after the arg has been decided.
3720            let dflags = descs[raw_idx].flags;
3721            let dname = descs[raw_idx].name.clone();
3722            if (dflags & ZOF_ARG) != 0 {
3723                let e = &body[dn_len..]; // pointer past name
3724                if (dflags & ZOF_GNUL) != 0 && e.starts_with('=') {
3725                    // c:2031-2032 — `add_opt_val(d, ++e);`
3726                    let arg = e[1..].to_string();
3727                    push_val(&mut descs, raw_idx, idx, Some(arg), &mut val_seq);
3728                } else if !e.is_empty() {
3729                    // c:2038-2039 — `add_opt_val(d, e);`
3730                    push_val(&mut descs, raw_idx, idx, Some(e.to_string()), &mut val_seq);
3731                } else if (dflags & ZOF_OPT) == 0
3732                    || ((dflags & (ZOF_GNUL | ZOF_GNUS)) == 0
3733                        && pi + 1 < params.len()
3734                        && !params[pi + 1].starts_with('-'))
3735                {
3736                    // c:2044-2052 — `add_opt_val(d, *++pp);`
3737                    if pi + 1 >= params.len() {
3738                        // c:2047-2050 — `zwarnnam(nam, "missing argument for
3739                        // option: -%s", d->name);`
3740                        parse_diag(
3741                            nam,
3742                            &progname,
3743                            &format!("missing argument for option: -{dname}"),
3744                        );
3745                        return 1; // c:2050
3746                    }
3747                    pi += 1;
3748                    let arg = params[pi].clone();
3749                    push_val(&mut descs, raw_idx, idx, Some(arg), &mut val_seq);
3750                } else {
3751                    // c:2055 — `add_opt_val(d, NULL);`
3752                    push_val(&mut descs, raw_idx, idx, None, &mut val_seq);
3753                }
3754            } else {
3755                // c:2058 — `add_opt_val(d, NULL);`
3756                push_val(&mut descs, raw_idx, idx, None, &mut val_seq);
3757            }
3758            pi += 1;
3759            continue;
3760        }
3761        // Fallback: each char as short opt. c:1980-2016.
3762        let chars: Vec<char> = o_raw[1..].chars().collect();
3763        let mut ci = 0usize;
3764        let mut consumed_param = true;
3765        while ci < chars.len() {
3766            let ch = chars[ci];
3767            let name1 = ch.to_string();
3768            let didx = descs.iter().position(|d| d.name == name1);
3769            let Some(idx) = didx else {
3770                if fail {
3771                    // c:1982-1988:
3772                    //   if (fail) {
3773                    //       if (*o != '-' || o > *pp + 1)
3774                    //           zwarnnam(nam, "bad option: -%c", *o);
3775                    //       else
3776                    //           zwarnnam(nam, "bad option: -%s", o);
3777                    //       return 1;
3778                    //   }
3779                    // `o > *pp + 1` is "not the first character after the
3780                    // leading dash", i.e. `ci > 0`; the else arm therefore
3781                    // only fires at ci == 0, where `o` and the whole body
3782                    // are the same string (so `-a --x -z` reports `--x`
3783                    // while `-a-xy` reports `--`).
3784                    if ch != '-' || ci > 0 {
3785                        parse_diag(nam, &progname, &format!("bad option: -{ch}"));
3786                        // c:1983-1984
3787                    } else {
3788                        parse_diag(
3789                            nam,
3790                            &progname,
3791                            &format!("bad option: -{}", chars.iter().collect::<String>()),
3792                        ); // c:1986
3793                    }
3794                    return 1; // c:1987
3795                }
3796                consumed_param = false;
3797                break;
3798            };
3799            let dflags = descs[idx].flags;
3800            let dname = descs[idx].name.clone();
3801            if (dflags & ZOF_ARG) != 0 {
3802                if ci + 1 < chars.len() {
3803                    // arg in same param: rest of chars — `add_opt_val(d, e)`
3804                    let arg: String = chars[ci + 1..].iter().collect();
3805                    push_val(&mut descs, idx, idx, Some(arg), &mut val_seq);
3806                    break;
3807                } else if (dflags & ZOF_OPT) == 0
3808                    || ((dflags & (ZOF_GNUL | ZOF_GNUS)) == 0
3809                        && pi + 1 < params.len()
3810                        && !params[pi + 1].starts_with('-'))
3811                {
3812                    if pi + 1 >= params.len() {
3813                        // c:2004-2007 — `zwarnnam(nam, "missing argument for
3814                        // option: -%s", d->name);`
3815                        parse_diag(
3816                            nam,
3817                            &progname,
3818                            &format!("missing argument for option: -{dname}"),
3819                        );
3820                        return 1; // c:2007
3821                    }
3822                    pi += 1;
3823                    let arg = params[pi].clone();
3824                    push_val(&mut descs, idx, idx, Some(arg), &mut val_seq);
3825                } else {
3826                    // missing optional optarg — `add_opt_val(d, NULL)`
3827                    push_val(&mut descs, idx, idx, None, &mut val_seq);
3828                }
3829            } else {
3830                // boolean short opt — `add_opt_val(d, NULL)`
3831                push_val(&mut descs, idx, idx, None, &mut val_seq);
3832            }
3833            ci += 1;
3834        }
3835        if !consumed_param {
3836            if extract {
3837                if del {
3838                    new_params.push(o_raw);
3839                }
3840                pi += 1;
3841                continue;
3842            } else {
3843                stopped = true;
3844                break;
3845            }
3846        }
3847        pi += 1;
3848    }
3849    let _ = stopped;
3850    // c:2069 — append remaining params if extract+del.
3851    if extract && del {
3852        while pi < params.len() {
3853            new_params.push(params[pi].clone());
3854            pi += 1;
3855        }
3856    } else if del && !extract {
3857        // c:2129: setaparam(paramsname, pp) — what's left from pi.
3858        new_params = params[pi..].to_vec();
3859    }
3860
3861    // Phase 5: emit per-array results. c:2073-2088.
3862    //   C iterates `a->vals` — a single linked list per array that
3863    //   was appended to in argv-encounter order across ALL specs
3864    //   pointing at the same array. We mirror by collecting (seq,
3865    //   name, arg) across all descs that share a target array,
3866    //   sorting by seq, and flattening into [name, arg?, name,
3867    //   arg?, …] form.
3868    // c:2076-2084 — per-val emission:
3869    //
3870    //     if (v->str)
3871    //         *ap = ztrdup(v->str);
3872    //     else {
3873    //         *ap = ztrdup(v->name);
3874    //         if (v->arg)
3875    //             *++ap = ztrdup(v->arg);
3876    //     }
3877    //
3878    // v->str (built in add_opt_val c:1665-1676) is the COMBINED
3879    // single element for ZOF_OPT/ZOF_SAME/GNUL options; plain
3880    // mandatory-arg options emit (name, arg) as two elements.
3881    let mut arr_buckets: std::collections::BTreeMap<
3882        String,
3883        Vec<(usize, String, Option<String>, Option<String>)>,
3884    > = std::collections::BTreeMap::new();
3885    for d in &descs {
3886        let target = d.arr_name.clone().or_else(|| defarr.clone());
3887        let Some(tgt) = target else { continue };
3888        let entry = arr_buckets.entry(tgt).or_default();
3889        for v in &d.vals {
3890            entry.push((v.seq, v.name.clone(), v.arg.clone(), v.str_.clone()));
3891        }
3892    }
3893    for (name, mut bucket) in arr_buckets {
3894        bucket.sort_by_key(|t| t.0);
3895        let mut out: Vec<String> = Vec::with_capacity(bucket.len() * 2);
3896        for (_seq, n, a, s) in bucket {
3897            if let Some(combined) = s {
3898                out.push(combined); // c:2077-2078 v->str one-element form
3899            } else {
3900                out.push(n); // c:2080
3901                if let Some(av) = a {
3902                    out.push(av); // c:2081-2082
3903                }
3904            }
3905        }
3906        // c:2062-2068 — the `-M` pass that marks a mapping TARGET as
3907        // "not a real array":
3908        //
3909        //   if (flags & ZOF_MAP) {
3910        //       for (d = opt_descs; d; d = d->next)
3911        //           if (d->arr && !d->vals && (d->flags & ZOF_MAP)) {
3912        //               if (d->arr->num == 0 && get_opt_desc(d->arr->name))
3913        //                   d->arr->num = -1;  /* this is not a real array */
3914        //           }
3915        //   }
3916        //
3917        // …and c:2073 `if (a->num >= 0 && …)` then skips it. Under `-M`
3918        // the `=NAME` suffix names ANOTHER SPEC, not an array, so
3919        // `zparseopts -M -a optv - a:=-aaa -aaa:` must NOT try to assign a
3920        // parameter called `-aaa` (zshrs errored `not an identifier:
3921        // -aaa`). The array is skipped only when it collected nothing AND
3922        // a spec of that name exists — a genuine array of the same name
3923        // that DID collect values is still emitted, as in C.
3924        let is_map_target =
3925            flags_map & ZOF_MAP != 0 && out.is_empty() && descs.iter().any(|d| d.name == name);
3926        if !is_map_target && (!keep || !out.is_empty()) {
3927            setaparam(&name, out);
3928        }
3929    }
3930
3931    // c:2089-2123 — assoc emission.
3932    if let Some(aname) = assoc {
3933        let mut flat: Vec<String> = Vec::new();
3934        for d in &descs {
3935            if d.vals.is_empty() {
3936                continue;
3937            }
3938            flat.push(format!("-{}", d.name));
3939            // c:2110-2117 — the value-assembly loop:
3940            //
3941            //     for (v = d->vals; v; v = v->onext) {
3942            //         if (v->arg) {
3943            //             strcpy(n, v->arg);
3944            //             n += strlen(v->arg);
3945            //         }
3946            //         *n = ' ';
3947            //     }
3948            //     *n = '\0';
3949            //
3950            // `*n = ' '` does NOT advance n, so the space it writes is
3951            // overwritten by the next iteration's strcpy (and the final
3952            // one by the c:2117 NUL). Net effect: multi-occurrence
3953            // option args CONCATENATE with NO separator. Verified
3954            // against real zsh: `zparseopts -A opts x+:` with
3955            // `-x a -x b` → opts[-x]="ab". Prior port joined with a
3956            // space ("a b"), inventing a separator the C never emits.
3957            let joined: String = d
3958                .vals
3959                .iter()
3960                .filter_map(|v| v.arg.clone())
3961                .collect::<Vec<_>>()
3962                .concat();
3963            flat.push(joined);
3964        }
3965        if !keep || !flat.is_empty() {
3966            // c:2096-2097 — `if (!keep || num) {
3967            //                  ap = zalloc_default_array(&aval, assoc, keep, num);`
3968            // zalloc_default_array (c:1709-1735) PREPENDS the assoc's
3969            // existing key/value pairs when keep is set: `-K -A assoc`
3970            // merges — unmatched existing keys survive, while new pairs
3971            // (appended after) override matching keys via normal assoc
3972            // assignment semantics. Prior port replaced the whole assoc
3973            // with only the new pairs, dropping every unmatched key.
3974            if keep {
3975                // c:1715-1728 — fetch existing pairs, copy first.
3976                let existing: Vec<String> = crate::ported::params::paramtab_hashed_storage()
3977                    .lock()
3978                    .ok()
3979                    .and_then(|store| {
3980                        store.get(&aname).map(|m| {
3981                            let mut kv = Vec::with_capacity(m.len() * 2);
3982                            for (k, v) in m {
3983                                kv.push(k.clone()); // c:1723-1726
3984                                kv.push(v.clone());
3985                            }
3986                            kv
3987                        })
3988                    })
3989                    .unwrap_or_default();
3990                let mut merged = existing;
3991                merged.extend(flat); // new pairs after → override same keys
3992                sethparam(&aname, merged); // c:2121
3993            } else {
3994                sethparam(&aname, flat); // c:2121
3995            }
3996        }
3997    }
3998
3999    // c:2124-2131 — write back when `-D` was given.
4000    //   extract (`-E`)   → `np` (collected non-flag args)
4001    //   non-extract      → `pp` (remainder of original args from the
4002    //                            stop point onward)
4003    if del {
4004        let write_back: Vec<String> = if extract {
4005            new_params.clone()
4006        } else {
4007            // pi points at the arg that stopped the parse (or past
4008            // the end if we consumed everything cleanly). Everything
4009            // from pi onward is the unprocessed tail = `pp` in C.
4010            params.iter().skip(pi).cloned().collect()
4011        };
4012        if params_src == "argv" {
4013            crate::ported::exec::set_pparams(write_back.clone());
4014            if let Ok(mut pp_lock) = PPARAMS.lock() {
4015                *pp_lock = write_back;
4016            }
4017        } else {
4018            setaparam(&params_src, write_back);
4019        }
4020    } else {
4021        let _ = params;
4022    }
4023    let _ = stopped; // value already encoded in pi position
4024
4025    0
4026}
4027
4028// `bintab` — port of `static struct builtin bintab[]` (zutil.c).
4029
4030// `module_features` — port of `static struct features module_features`
4031// from zutil.c:2143.
4032
4033/// Port of `setup_(UNUSED(Module m))` from `Src/Modules/zutil.c:2152`.
4034#[allow(unused_variables)]
4035pub fn setup_(m: *const module) -> i32 {
4036    // c:2152
4037    0
4038}
4039
4040/// Port of `features_(UNUSED(Module m), UNUSED(char ***features))` from `Src/Modules/zutil.c:2161`.
4041/// C body: `*features = featuresarray(m, &module_features); return 0;`
4042pub fn features_(m: *const module, features: &mut Vec<String>) -> i32 {
4043    // c:2161
4044    *features = featuresarray(m, module_features());
4045    0
4046}
4047
4048/// Port of `enables_(UNUSED(Module m), UNUSED(int **enables))` from `Src/Modules/zutil.c:2169`.
4049/// C body: `return handlefeatures(m, &module_features, enables);`
4050pub fn enables_(m: *const module, enables: &mut Option<Vec<i32>>) -> i32 {
4051    // c:2169
4052    handlefeatures(m, module_features(), enables)
4053}
4054
4055/// Port of `boot_(UNUSED(Module m))` from `Src/Modules/zutil.c:2176`.
4056#[allow(unused_variables)]
4057pub fn boot_(m: *const module) -> i32 {
4058    // c:2176
4059    0
4060}
4061
4062/// Port of `cleanup_(UNUSED(Module m))` from `Src/Modules/zutil.c:2183`.
4063/// C body: `return setfeatureenables(m, &module_features, NULL);`
4064pub fn cleanup_(m: *const module) -> i32 {
4065    // c:2183
4066    setfeatureenables(m, module_features(), None)
4067}
4068
4069/// Port of `finish_(UNUSED(Module m))` from `Src/Modules/zutil.c:2190`.
4070#[allow(unused_variables)]
4071pub fn finish_(m: *const module) -> i32 {
4072    // c:2190
4073    0
4074}
4075// zstyle_entry is defined below (moved from vm_helper).
4076
4077/// Save/restore for the per-pattern-match magic vars `$match`,
4078/// `$mbegin`, `$mend`. Direct port of `MatchData` and the
4079/// `savematch`/`restorematch`/`freematch` trio in
4080/// src/zsh/Src/Modules/zutil.c:33-80.
4081///
4082/// zstyle's `-e` (eval pattern on retrieve) and zregexparse's
4083/// inner pattern matches both want to evaluate patterns without
4084/// clobbering the caller's `$match[]`, `$mbegin[]`, `$mend[]`
4085/// variables. The C version keeps a heap-duplicated copy in a
4086/// `MatchData` struct, runs the inner match, then either
4087/// restores or frees. The Rust port stores `Option<Vec<String>>`
4088/// — `None` means the var was unset.
4089pub struct MatchData {
4090    pub r#match: Option<Vec<String>>,
4091    /// `mbegin` field.
4092    pub mbegin: Option<Vec<String>>,
4093    /// `mend` field.
4094    pub mend: Option<Vec<String>>,
4095}
4096
4097/// `zstyle` storage table.
4098/// Port of the `zstyletab` HashTable Src/Modules/zutil.c builds —
4099/// `newzstyletable()` (line 270) creates it, `bin_zstyle()`
4100/// (line 487) drives every mutation. Stores `stypat` entries
4101/// (port of C `struct stypat`, zutil.c:95) per style name,
4102/// weight-sorted so the most specific pattern wins.
4103// `StyleTable` renamed to `style_table`. C uses `HashTable zstyletab`
4104// (`Src/Modules/zutil.c:209`) with `struct style` (zutil.c:91) nodes
4105// containing a `Stypat pats` linked list (zutil.c:97-104). Rust port
4106// uses a `HashMap<String, Vec<stypat>>` while the canonical
4107// `hashtable` port lands; the canonical `style` / `stypat` structs
4108// already exist at lines 1608 / 1596 below.
4109/// `style_table` — see fields for layout.
4110#[allow(non_camel_case_types)]
4111#[derive(Default)]
4112pub struct style_table {
4113    /// `styles` field.
4114    ///
4115    /// Copy-on-write (`crate::cow_map::CowHashMap`): `$( … )` and `( … )`
4116    /// snapshot this table on entry (`crate::ported::exec::SubshForkCopy`),
4117    /// so `clone()` must be a refcount bump. A compsys configuration runs
4118    /// to hundreds of styles; only a body that runs `zstyle` pays for a copy.
4119    styles: crate::cow_map::CowHashMap<String, Vec<stypat>>,
4120    /// !!! WARNING: RUST-ONLY — NO C COUNTERPART !!!
4121    /// style -> context -> index of the first pattern (in weight order) that
4122    /// matched, or None. C pattries every pattern on every lookup
4123    /// (c:452-458); a completion asks the same few hundred (style, context)
4124    /// pairs on every TAB. The answer depends only on the table, since each
4125    /// prog is compiled once in `set`, so `set` and `delete` drop the memo
4126    /// and a clone starts with an empty one.
4127    memo: Mutex<HashMap<String, HashMap<String, Option<usize>>>>,
4128}
4129
4130impl Clone for style_table {
4131    fn clone(&self) -> Self {
4132        Self {
4133            styles: self.styles.clone(),
4134            memo: Mutex::default(),
4135        }
4136    }
4137}
4138
4139/// Namespace for the recursive zformat walker — distinct from
4140/// the public zformat_substring entry point above so the inner
4141/// recursion doesn't collide with the outer wrapper's name.
4142struct ZFormat;
4143
4144// ─── moved from src/ported/vm_helper (drift extraction) ───
4145
4146/// One `zstyle` entry — Rust extension that flattens what C splits
4147/// across `struct style` (zutil.c:91, holds the style name) and
4148/// `struct stypat` (zutil.c:97, holds pat + vals). The canonical
4149/// split structs are at lines 1596 / 1608 above; this flat shape is
4150/// kept while the C-style HashTable port lands.
4151#[allow(non_camel_case_types)]
4152#[derive(Debug, Clone)]
4153pub struct zstyle_entry {
4154    /// `pattern` field.
4155    pub pattern: String,
4156    /// `style` field.
4157    pub style: String,
4158    /// `values` field.
4159    pub values: Vec<String>,
4160}
4161
4162/// Port of `RParseState` from `Src/Modules/zutil.c:1093-1100`.
4163/// One node in the zregexparse state machine.
4164#[allow(non_camel_case_types)]
4165#[derive(Default)]
4166pub struct RParseState {
4167    pub cutoff: i32,             // c:1094
4168    pub pattern: Option<String>, // c:1095
4169    // c:1096 — `Patprog patprog;` compiled on first match. The
4170    // zsh_h::Patprog alias (`Box<patprog>`) doesn't carry the
4171    // post-tokenisation buffer that pattern.rs::patcompile bundles
4172    // (Box<(patprog, Vec<u8>)>) — the byte buffer holds the compiled
4173    // pattern image referenced by `patprog::p`. Use the canonical
4174    // pattern::Patprog so pattry() reads the right shape end-to-end.
4175    /// `patprog` field.
4176    pub patprog: Option<crate::ported::pattern::Patprog>,
4177    pub guard: Option<String>,  // c:1097
4178    pub action: Option<String>, // c:1098
4179    pub branches: Vec<std::rc::Rc<std::cell::RefCell<RParseBranch>>>, // c:1099
4180}
4181
4182/// Port of `RParseBranch` from `Src/Modules/zutil.c:1102-1105`.
4183/// One transition: target state + action list to run when taken.
4184#[allow(non_camel_case_types)]
4185pub struct RParseBranch {
4186    pub state: std::rc::Rc<std::cell::RefCell<RParseState>>, // c:1103
4187    pub actions: Vec<String>,                                // c:1104
4188}
4189
4190/// Port of `RParseResult` from `Src/Modules/zutil.c:1107-1111`.
4191/// nullacts = actions that fire on empty match; in/out = branch lists
4192/// for the entry/exit transitions of this sub-parse.
4193#[derive(Default)]
4194pub struct RParseResult {
4195    pub nullacts: Option<Vec<String>>, // c:1108 (None = NULL)
4196    pub in_: Vec<std::rc::Rc<std::cell::RefCell<RParseBranch>>>, // c:1109
4197    pub out: Vec<std::rc::Rc<std::cell::RefCell<RParseBranch>>>, // c:1110
4198    /// Legacy field — kept until callers migrate off the old shape.
4199    pub args: Vec<String>,
4200}
4201
4202/// `rparseargs` — C global at zutil.c:1113. Cursor into the input
4203/// argv being parsed. Thread-local per zsh evaluator.
4204thread_local! {
4205    /// `RPARSEARGS` static.
4206    pub static RPARSEARGS: std::cell::RefCell<std::collections::VecDeque<String>>
4207        = const { std::cell::RefCell::new(std::collections::VecDeque::new()) };
4208}
4209
4210/// `rparsestates` — C global at zutil.c:1114. List of all states
4211/// allocated during a parse run (so they can be freed by popheap).
4212/// Rust drops the Rc'd states when this list is cleared.
4213thread_local! {
4214    /// `RPARSESTATES` static.
4215    pub static RPARSESTATES: std::cell::RefCell<Vec<std::rc::Rc<std::cell::RefCell<RParseState>>>>
4216        = const { std::cell::RefCell::new(Vec::new()) };
4217}
4218
4219/// Port of `setstypat(Style s, char *pat, Patprog prog, char **vals, int eval)` from `Src/Modules/zutil.c:814`.
4220/// Format a string with specifications
4221/// `zformat` builtin entry point.
4222/// Helper extracted from `bin_zformat()` (Src/Modules/zutil.c:814)
4223/// — same `%X:value` substitution + width / left/right-align /
4224/// repeat flag handling the C source's `zformat_substring()`
4225/// (line 814) implements.
4226pub fn zformat_substring(format: &str, specs: &HashMap<char, String>, presence: bool) -> String {
4227    // Direct port of src/zsh/Src/Modules/zutil.c:814
4228    // zformat_substring. Recursive walker that handles:
4229    //   - Plain `%X` substitutions
4230    //   - Optional `-` for right-align
4231    //   - Optional `N` for min width
4232    //   - Optional `.M` for max width
4233    //   - Ternary `%(SPECTEST.true-text.false-text)` — conditional
4234    //     substitution based on whether the spec exists / matches a
4235    //     numeric test value. With presence=true (zformat -F) the
4236    //     test compares the spec's existence/length; with
4237    //     presence=false (zformat -f) the test compares against an
4238    //     integer math eval of the spec value.
4239    //
4240    // The original C uses an output-buffer with growable backing;
4241    // we use a Rust String with push_* helpers. The recursive
4242    // descent + (skip || actval) pattern is the same.
4243    // bin_zformat (c:1026-1032) never registers `%` or `)` as specs — it
4244    // rejects them as spec names. `%%` → `%` and `%)` → `)` come from the
4245    // unwind at c:849-856 inside the walker, which also keeps a lone
4246    // trailing `%` and an unterminated `%(` as literal text.
4247    let effective = specs;
4248
4249    let bytes: Vec<char> = format.chars().collect();
4250    let mut out = String::with_capacity(bytes.len() + 16);
4251    let mut idx = 0;
4252    let _ = ZFormat::substring(
4253        &bytes, &mut idx, &mut out, '\0', &effective, presence, false, &HashMap::new(), false,
4254    );
4255    out
4256}
4257
4258static MODULE_FEATURES: OnceLock<Mutex<features>> = OnceLock::new();
4259
4260// Local stubs for the per-module entry points. C uses generic
4261// `featuresarray`/`handlefeatures`/`setfeatureenables` (module.c:
4262// 3275/3370/3445) but those take `Builtin` + `Features` pointer
4263// fields the Rust port doesn't carry. The hardcoded descriptor
4264// list mirrors the C bintab/conddefs/mathfuncs/paramdefs.
4265// WARNING: NOT IN ZUTIL.C — Rust-only module-framework shim.
4266// C uses generic featuresarray/handlefeatures/setfeatureenables from
4267// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
4268// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
4269fn featuresarray(_m: *const module, _f: &Mutex<features>) -> Vec<String> {
4270    vec![
4271        "b:zformat".to_string(),
4272        "b:zparseopts".to_string(),
4273        "b:zregexparse".to_string(),
4274        "b:zstyle".to_string(),
4275    ]
4276}
4277
4278// WARNING: NOT IN ZUTIL.C — Rust-only module-framework shim.
4279// C uses generic featuresarray/handlefeatures/setfeatureenables from
4280// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
4281// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
4282fn handlefeatures(m: *const module, f: &Mutex<features>, enables: &mut Option<Vec<i32>>) -> i32 {
4283    // c:3392 — the name-keyed variant in src/ported/module.rs; this
4284    // module ships no `Features` descriptor tables for the per-feature
4285    // ADDED bit to live on (see MODULE_FEATURE_ENABLES there).
4286    crate::ported::module::handlefeatures("zsh/zutil", &featuresarray(m, f), enables)
4287}
4288
4289// WARNING: NOT IN ZUTIL.C — Rust-only module-framework shim.
4290// C uses generic featuresarray/handlefeatures/setfeatureenables from
4291// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
4292// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
4293fn setfeatureenables(_m: *const module, _f: &Mutex<features>, _e: Option<&[i32]>) -> i32 {
4294    0
4295}
4296
4297// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
4298// ─── RUST-ONLY ACCESSORS ───
4299//
4300// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
4301// RwLock<T>>` globals declared above. C zsh uses direct global
4302// access; Rust needs these wrappers because `OnceLock::get_or_init`
4303// is the only way to lazily construct shared state. These ported sit
4304// here so the body of this file reads in C source order without
4305// the accessor wrappers interleaved between real port ported.
4306// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
4307
4308// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
4309// ─── RUST-ONLY ACCESSORS ───
4310//
4311// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
4312// RwLock<T>>` globals declared above. C zsh uses direct global
4313// access; Rust needs these wrappers because `OnceLock::get_or_init`
4314// is the only way to lazily construct shared state. These ported sit
4315// here so the body of this file reads in C source order without
4316// the accessor wrappers interleaved between real port ported.
4317// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
4318
4319// WARNING: NOT IN ZUTIL.C — Rust-only module-framework shim.
4320// C uses generic featuresarray/handlefeatures/setfeatureenables from
4321// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
4322// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
4323fn module_features() -> &'static Mutex<features> {
4324    MODULE_FEATURES.get_or_init(|| {
4325        Mutex::new(features {
4326            bn_list: None,
4327            bn_size: 4,
4328            cd_list: None,
4329            cd_size: 0,
4330            mf_list: None,
4331            mf_size: 0,
4332            pd_list: None,
4333            pd_size: 0,
4334            n_abstract: 0,
4335        })
4336    })
4337}
4338
4339#[cfg(test)]
4340mod rparse_tests {
4341    use super::*;
4342
4343    /// `rparseelt` parses `/pat/` as a single pattern atom: result.in_ and
4344    /// result.out each get one branch pointing at the same RParseState,
4345    /// nullacts stays None (a literal pattern always consumes).
4346    #[test]
4347    fn rparseelt_slash_pattern_atom() {
4348        RPARSEARGS.with(|q| {
4349            let mut q = q.borrow_mut();
4350            q.clear();
4351            q.push_back("/foo/".to_string());
4352        });
4353        RPARSESTATES.with(|s| s.borrow_mut().clear());
4354        let mut r = RParseResult::default();
4355        assert_eq!(rparseelt(&mut r), 0);
4356        assert_eq!(r.in_.len(), 1);
4357        assert_eq!(r.out.len(), 1);
4358        assert!(r.nullacts.is_none());
4359        // The two branches share the same state Rc — the C source's
4360        // in/out branch lists both point at the same RParseState.
4361        assert!(std::rc::Rc::ptr_eq(
4362            &r.in_[0].borrow().state,
4363            &r.out[0].borrow().state,
4364        ));
4365        // State's pattern is wrapped per c:1189-1201.
4366        let st = r.in_[0].borrow().state.clone();
4367        assert_eq!(st.borrow().pattern.as_deref(), Some("(#b)((#B)foo)*"));
4368    }
4369
4370    /// `rparseelt` parses `/pat/+` or `/pat/-` (cutoff variants).
4371    #[test]
4372    fn rparseelt_cutoff_variants() {
4373        for suffix in &['+', '-'] {
4374            RPARSEARGS.with(|q| {
4375                let mut q = q.borrow_mut();
4376                q.clear();
4377                q.push_back(format!("/foo/{}", suffix));
4378            });
4379            RPARSESTATES.with(|s| s.borrow_mut().clear());
4380            let mut r = RParseResult::default();
4381            assert_eq!(rparseelt(&mut r), 0);
4382            let st = r.in_[0].borrow().state.clone();
4383            assert_eq!(st.borrow().cutoff, *suffix as i32);
4384        }
4385    }
4386
4387    /// `/pat/` followed by `:action` attaches the action to the state.
4388    #[test]
4389    fn rparseelt_pattern_with_action() {
4390        RPARSEARGS.with(|q| {
4391            let mut q = q.borrow_mut();
4392            q.clear();
4393            q.push_back("/foo/".to_string());
4394            q.push_back(":print hello".to_string());
4395        });
4396        RPARSESTATES.with(|s| s.borrow_mut().clear());
4397        let mut r = RParseResult::default();
4398        assert_eq!(rparseelt(&mut r), 0);
4399        let st = r.in_[0].borrow().state.clone();
4400        assert_eq!(st.borrow().action.as_deref(), Some("print hello"));
4401    }
4402
4403    /// `[]` as the pattern means no compiled pattern (NULL).
4404    #[test]
4405    fn rparseelt_empty_brackets_no_pattern() {
4406        RPARSEARGS.with(|q| {
4407            let mut q = q.borrow_mut();
4408            q.clear();
4409            q.push_back("/[]/".to_string());
4410        });
4411        RPARSESTATES.with(|s| s.borrow_mut().clear());
4412        let mut r = RParseResult::default();
4413        assert_eq!(rparseelt(&mut r), 0);
4414        let st = r.in_[0].borrow().state.clone();
4415        assert!(st.borrow().pattern.is_none());
4416    }
4417
4418    /// `rparseclo` on `/foo/ #` (atom + Kleene-star marker) sets
4419    /// `nullacts = []` (an empty list = match-on-empty enabled) and
4420    /// connects out→in via `connectstates`.
4421    #[test]
4422    fn rparseclo_kleene_marks_nullacts() {
4423        RPARSEARGS.with(|q| {
4424            let mut q = q.borrow_mut();
4425            q.clear();
4426            q.push_back("/foo/".to_string());
4427            q.push_back("#".to_string());
4428        });
4429        RPARSESTATES.with(|s| s.borrow_mut().clear());
4430        let mut r = RParseResult::default();
4431        assert_eq!(rparseclo(&mut r), 0);
4432        // c:1263 — `result->nullacts = newlinklist();` (an empty list).
4433        assert!(matches!(r.nullacts, Some(ref v) if v.is_empty()));
4434    }
4435
4436    /// `rparseclo` on `/foo/` WITHOUT a trailing `#` leaves
4437    /// `nullacts = None` (no empty match allowed). The C body's
4438    /// c:1257 `if (*rparseargs && !strcmp(*rparseargs, "#"))` gate
4439    /// is the load-bearing condition — without `#`, nullacts stays
4440    /// at whatever rparseelt set it to (which is NULL for pattern
4441    /// atoms per c:1220). The matcher distinguishes `None` (must
4442    /// consume at least one char) from `Some([])` (empty match OK).
4443    /// A regression that always sets nullacts to Some([]) would
4444    /// silently accept empty matches for non-Kleene patterns,
4445    /// breaking the `zregexparse var var '/foo/'` semantic where
4446    /// the empty subject must NOT match.
4447    #[test]
4448    fn rparseclo_no_kleene_keeps_nullacts_none() {
4449        RPARSEARGS.with(|q| {
4450            let mut q = q.borrow_mut();
4451            q.clear();
4452            q.push_back("/foo/".to_string());
4453            // NO `#` — atom alone.
4454        });
4455        RPARSESTATES.with(|s| s.borrow_mut().clear());
4456        let mut r = RParseResult::default();
4457        assert_eq!(rparseclo(&mut r), 0);
4458        assert!(
4459            r.nullacts.is_none(),
4460            "c:1257 — without trailing `#`, nullacts must stay None \
4461             (empty-match must NOT be allowed for non-Kleene atoms)"
4462        );
4463    }
4464
4465    /// `rparseseq` on `{init} /pat/` collects the action into nullacts
4466    /// AND into the single output branch's action list (c:1313-1317).
4467    #[test]
4468    fn rparseseq_action_block_then_pattern() {
4469        RPARSEARGS.with(|q| {
4470            let mut q = q.borrow_mut();
4471            q.clear();
4472            q.push_back("{init}".to_string());
4473            q.push_back("/foo/".to_string());
4474        });
4475        RPARSESTATES.with(|s| s.borrow_mut().clear());
4476        let mut r = RParseResult::default();
4477        assert_eq!(rparseseq(&mut r), 0);
4478        // First the action populates result.nullacts; the pattern then
4479        // arrives and gets connected.
4480        // The pattern's out branch should NOT carry the action because
4481        // the action was consumed before the pattern's out list existed.
4482        assert_eq!(r.out.len(), 1);
4483    }
4484
4485    /// `rparsealt` on `/a/ | /b/` builds a 2-way alternation: 2 in branches
4486    /// + 2 out branches.
4487    #[test]
4488    fn rparsealt_two_way() {
4489        RPARSEARGS.with(|q| {
4490            let mut q = q.borrow_mut();
4491            q.clear();
4492            for s in ["/a/", "|", "/b/"] {
4493                q.push_back(s.to_string());
4494            }
4495        });
4496        RPARSESTATES.with(|s| s.borrow_mut().clear());
4497        let mut r = RParseResult::default();
4498        assert_eq!(rparsealt(&mut r), 0);
4499        assert_eq!(r.in_.len(), 2);
4500        assert_eq!(r.out.len(), 2);
4501    }
4502
4503    /// `rparseseq` on `/a/ /b/` connects out-of-a to in-of-b via
4504    /// `connectstates`. The transition branch must be appended to
4505    /// the FIRST pattern's state.branches list — that's where rmatch
4506    /// (c:1396) walks looking for the next state. A regression that
4507    /// stores the branch in the wrong place breaks every multi-step
4508    /// regex match.
4509    #[test]
4510    fn rparseseq_two_patterns_connects_via_first_state_branches() {
4511        RPARSEARGS.with(|q| {
4512            let mut q = q.borrow_mut();
4513            q.clear();
4514            for s in ["/a/", "/b/"] {
4515                q.push_back(s.to_string());
4516            }
4517        });
4518        RPARSESTATES.with(|s| s.borrow_mut().clear());
4519        let mut r = RParseResult::default();
4520        assert_eq!(rparseseq(&mut r), 0);
4521
4522        // After two-pattern sequence:
4523        //   in_  = [branch → state_a]
4524        //   out  = [branch → state_b]
4525        //   state_a.branches = [transition → state_b]
4526        assert_eq!(r.in_.len(), 1, "single entry");
4527        assert_eq!(r.out.len(), 1, "single exit");
4528
4529        let state_a = r.in_[0].borrow().state.clone();
4530        let state_b = r.out[0].borrow().state.clone();
4531        assert!(
4532            !std::rc::Rc::ptr_eq(&state_a, &state_b),
4533            "sequence creates distinct states for a and b"
4534        );
4535
4536        // c:1136 — connectstates appends a transition branch onto
4537        // outbranch.state.branches (= state_a.branches).
4538        let a_branches = &state_a.borrow().branches;
4539        assert_eq!(
4540            a_branches.len(),
4541            1,
4542            "c:1136 — state_a.branches must hold the a→b transition"
4543        );
4544        let transition_target = a_branches[0].borrow().state.clone();
4545        assert!(
4546            std::rc::Rc::ptr_eq(&transition_target, &state_b),
4547            "transition target must be state_b (the Rc graph is shared, not deep-copied)"
4548        );
4549    }
4550
4551    /// `(` introduces a grouped subexpression; matching `)` closes it.
4552    #[test]
4553    fn rparseelt_paren_group() {
4554        RPARSEARGS.with(|q| {
4555            let mut q = q.borrow_mut();
4556            q.clear();
4557            for s in ["(", "/x/", ")"] {
4558                q.push_back(s.to_string());
4559            }
4560        });
4561        RPARSESTATES.with(|s| s.borrow_mut().clear());
4562        let mut r = RParseResult::default();
4563        assert_eq!(rparseelt(&mut r), 0);
4564        // The inner pattern populated in/out.
4565        assert_eq!(r.in_.len(), 1);
4566        assert_eq!(r.out.len(), 1);
4567        // Cursor consumed all three args.
4568        assert!(RPARSEARGS.with(|q| q.borrow().is_empty()));
4569    }
4570
4571    /// `prependactions` inserts each act at the HEAD of each branch's
4572    /// actions list, preserving acts' order (acts[0] ends up at branch.actions[0]).
4573    #[test]
4574    fn prependactions_preserves_order() {
4575        let st = std::rc::Rc::new(std::cell::RefCell::new(RParseState::default()));
4576        let br = std::rc::Rc::new(std::cell::RefCell::new(RParseBranch {
4577            state: st,
4578            actions: vec!["x".to_string(), "y".to_string()],
4579        }));
4580        let acts = vec!["a".to_string(), "b".to_string()];
4581        prependactions(&acts, &[br.clone()]);
4582        assert_eq!(
4583            br.borrow().actions,
4584            vec![
4585                "a".to_string(),
4586                "b".to_string(),
4587                "x".to_string(),
4588                "y".to_string()
4589            ]
4590        );
4591    }
4592
4593    /// `appendactions` appends to the tail of each branch's actions list.
4594    #[test]
4595    fn appendactions_appends_to_tail() {
4596        let st = std::rc::Rc::new(std::cell::RefCell::new(RParseState::default()));
4597        let br = std::rc::Rc::new(std::cell::RefCell::new(RParseBranch {
4598            state: st,
4599            actions: vec!["x".to_string()],
4600        }));
4601        let acts = vec!["a".to_string(), "b".to_string()];
4602        appendactions(&acts, &[br.clone()]);
4603        assert_eq!(
4604            br.borrow().actions,
4605            vec!["x".to_string(), "a".to_string(), "b".to_string()]
4606        );
4607    }
4608
4609    /// `connectstates` produces the full M×N cross-product of branches:
4610    /// for each (outbranch, inbranch) pair it creates a fresh transition
4611    /// branch. The C body's nested for-loops at c:1123-1138 are O(M*N);
4612    /// a regression that uses one-to-one zip semantics (min(M,N) branches)
4613    /// would silently lose transitions for alternation patterns.
4614    ///
4615    /// Pin: 2 out-branches × 3 in-branches → 6 new transitions, all
4616    /// pointing at the original in-branches' states (Rc-shared).
4617    #[test]
4618    fn connectstates_produces_m_times_n_cross_product() {
4619        // Two out-branches, each rooted at its own state.
4620        let out_a = std::rc::Rc::new(std::cell::RefCell::new(RParseState::default()));
4621        let out_b = std::rc::Rc::new(std::cell::RefCell::new(RParseState::default()));
4622        let out = vec![
4623            std::rc::Rc::new(std::cell::RefCell::new(RParseBranch {
4624                state: out_a.clone(),
4625                actions: vec![],
4626            })),
4627            std::rc::Rc::new(std::cell::RefCell::new(RParseBranch {
4628                state: out_b.clone(),
4629                actions: vec![],
4630            })),
4631        ];
4632        // Three in-branches, each rooted at its own state.
4633        let in_states: Vec<_> = (0..3)
4634            .map(|_| std::rc::Rc::new(std::cell::RefCell::new(RParseState::default())))
4635            .collect();
4636        let in_: Vec<_> = in_states
4637            .iter()
4638            .map(|st| {
4639                std::rc::Rc::new(std::cell::RefCell::new(RParseBranch {
4640                    state: st.clone(),
4641                    actions: vec![],
4642                }))
4643            })
4644            .collect();
4645
4646        connectstates(&out, &in_);
4647
4648        // c:1136 — each new branch added to outbranch.state.branches.
4649        // out_a gets 3 transitions (one per in-branch);
4650        // out_b gets 3 transitions.
4651        assert_eq!(
4652            out_a.borrow().branches.len(),
4653            3,
4654            "c:1126-1136 — out_a must receive N=3 transitions"
4655        );
4656        assert_eq!(
4657            out_b.borrow().branches.len(),
4658            3,
4659            "c:1126-1136 — out_b must also receive N=3 transitions"
4660        );
4661
4662        // Each transition's target state must be one of the in-states
4663        // (Rc::ptr_eq verifies graph-sharing, not deep-copy).
4664        for br in out_a.borrow().branches.iter() {
4665            let target = br.borrow().state.clone();
4666            assert!(
4667                in_states.iter().any(|s| std::rc::Rc::ptr_eq(s, &target)),
4668                "c:1130 — transition target must be one of the in-branches' states"
4669            );
4670        }
4671    }
4672}
4673
4674#[cfg(test)]
4675mod tests {
4676    use super::*;
4677
4678    /// setstypat `-e` (eval) path (zutil.c:304-318): the joined values
4679    /// must be `parse_string`'d into a *real* Eprog and stored in
4680    /// `stypat.eval`. The prior port faked this with an empty
4681    /// `eprog::default()` (len == 0); this pins the real parse.
4682    #[test]
4683    fn setstypat_eval_stores_real_parsed_program() {
4684        // eval=0 → no program stored (c:341 with NULL eprog).
4685        setstypat("zt_noeval", ":zt:ctx:*", None, vec!["plain".to_string()], 0);
4686        // eval=1 → value parsed into a real, non-empty Eprog.
4687        setstypat(
4688            "zt_eval",
4689            ":zt:ctx:*",
4690            None,
4691            vec!["echo".to_string(), "hi".to_string()],
4692            1,
4693        );
4694
4695        let t = zstyletab.lock().unwrap();
4696        let noeval = t.styles["zt_noeval"]
4697            .iter()
4698            .find(|p| p.pat == ":zt:ctx:*")
4699            .expect("zt_noeval pattern stored");
4700        assert!(noeval.eval.is_none(), "eval=0 must store no program");
4701
4702        let eval = t.styles["zt_eval"]
4703            .iter()
4704            .find(|p| p.pat == ":zt:ctx:*")
4705            .expect("zt_eval pattern stored");
4706        let prog = eval.eval.as_ref().expect("eval=1 must store a program");
4707        assert!(
4708            prog.len > 0,
4709            "stored program must be the real parse, not an empty default (len was {})",
4710            prog.len
4711        );
4712    }
4713
4714    /// Verifies the weight formula matches C's setstypat (zutil.c:344-385):
4715    /// component count (high 32 bits) + per-component specificity sum
4716    /// (low 32 bits). More specific = higher weight. Drives weight via
4717    /// style_table::set's inline weight calc (insertion order reflects
4718    /// weight ordering — most specific pattern appears first).
4719    #[test]
4720    fn test_style_pattern_weight() {
4721        let _g = crate::test_util::global_state_lock();
4722        let mut t = style_table::new();
4723        t.set("*", "s", vec!["broad".to_string()], None);
4724        t.set(":completion:*", "s", vec!["mid".to_string()], None);
4725        t.set(":completion:zsh:*", "s", vec!["narrow".to_string()], None);
4726        // Most-specific match wins (sorted descending by weight at insertion).
4727        assert_eq!(t.get(":completion:zsh:complete", "s").unwrap()[0], "narrow");
4728        assert_eq!(t.get(":completion:bash:complete", "s").unwrap()[0], "mid");
4729        assert_eq!(t.get(":other:thing", "s").unwrap()[0], "broad");
4730    }
4731
4732    /// Port of `bin_zparseopts(char *nam, char **args, Options ops, UNUSED(int func))` from `Src/Modules/zutil.c:1821`.
4733    #[test]
4734    fn zof_flags_are_distinct_powers_of_two() {
4735        let _g = crate::test_util::global_state_lock();
4736        // c:1531-1538 — ZOF_* are independent bits in a single u8 field.
4737        let all = [
4738            ZOF_ARG, ZOF_OPT, ZOF_MULT, ZOF_SAME, ZOF_MAP, ZOF_CYC, ZOF_GNUS, ZOF_GNUL,
4739        ];
4740        let xor: i32 = all.iter().fold(0, |acc, &x| acc | x);
4741        let sum: i32 = all.iter().sum();
4742        assert_eq!(xor, sum, "ZOF_* bits must be disjoint");
4743        // Ensure each is a power of two.
4744        for v in all {
4745            assert!(
4746                v > 0 && (v & (v - 1)) == 0,
4747                "ZOF value {} is not a power of 2",
4748                v
4749            );
4750        }
4751    }
4752
4753    /// Verifies pattern matching via the style_table.get path mirrors
4754    /// C's lookupstyle (zutil.c:443) walking the pats list for the
4755    /// first weight-sorted match.
4756    #[test]
4757    fn test_style_pattern_matches() {
4758        let _g = crate::test_util::global_state_lock();
4759        let mut t = style_table::new();
4760        t.set(":completion:*", "s1", vec!["v".to_string()], None);
4761        assert!(t.get(":completion:zsh:complete", "s1").is_some());
4762        assert!(t.get(":other:zsh", "s1").is_none());
4763
4764        let mut t2 = style_table::new();
4765        t2.set("*", "s2", vec!["v".to_string()], None);
4766        assert!(t2.get("anything", "s2").is_some());
4767    }
4768
4769    #[test]
4770    fn test_style_table_set_get() {
4771        let _g = crate::test_util::global_state_lock();
4772        let mut table = style_table::new();
4773        table.set(":completion:*", "verbose", vec!["yes".to_string()], None);
4774
4775        let result = table.get(":completion:zsh", "verbose");
4776        assert_eq!(result, Some(&["yes".to_string()][..]));
4777
4778        let result = table.get(":other", "verbose");
4779        assert!(result.is_none());
4780    }
4781
4782    #[test]
4783    fn test_style_table_priority() {
4784        let _g = crate::test_util::global_state_lock();
4785        let mut table = style_table::new();
4786        table.set("*", "menu", vec!["no".to_string()], None);
4787        table.set(":completion:*", "menu", vec!["yes".to_string()], None);
4788
4789        let result = table.get(":completion:zsh", "menu");
4790        assert_eq!(result, Some(&["yes".to_string()][..]));
4791    }
4792
4793    #[test]
4794    fn test_style_table_delete() {
4795        let _g = crate::test_util::global_state_lock();
4796        let mut table = style_table::new();
4797        table.set("*", "style1", vec!["val".to_string()], None);
4798        table.set("*", "style2", vec!["val".to_string()], None);
4799
4800        table.delete(None, Some("style1"));
4801        assert!(table.get("test", "style1").is_none());
4802        assert!(table.get("test", "style2").is_some());
4803    }
4804
4805    #[test]
4806    fn test_style_test_bool() {
4807        let _g = crate::test_util::global_state_lock();
4808        let mut table = style_table::new();
4809        table.set("*", "enabled", vec!["yes".to_string()], None);
4810        table.set("*", "disabled", vec!["no".to_string()], None);
4811        table.set(
4812            "*",
4813            "multiple",
4814            vec!["a".to_string(), "b".to_string()],
4815            None,
4816        );
4817
4818        assert_eq!(table.test_bool("ctx", "enabled"), Some(true));
4819        assert_eq!(table.test_bool("ctx", "disabled"), Some(false));
4820        assert_eq!(table.test_bool("ctx", "multiple"), None);
4821    }
4822
4823    /// Port of `bin_zstyle(char *nam, char **args, UNUSED(Options ops), UNUSED(int func))` from `Src/Modules/zutil.c:487`.
4824    /// Verifies the persistent global `zstyletab` round-trips
4825    /// set→get and that `lookupstyle` / `testforstyle` C-name shims
4826    /// see the same entry. Lock-stamps the global-state path that
4827    /// `bin_zstyle` relies on (Src/Modules/zutil.c:209).
4828    #[test]
4829    fn test_global_zstyletab_set_and_lookup() {
4830        let _g = crate::test_util::global_state_lock();
4831        let key_style = "test_zutil_global_marker_style";
4832        let key_pat = "test_zutil_global_marker_*";
4833        {
4834            let mut t = zstyletab.lock().unwrap();
4835            t.set(key_pat, key_style, vec!["yes".to_string()], None);
4836        }
4837        let found = lookupstyle("test_zutil_global_marker_x", key_style);
4838        assert_eq!(found, vec!["yes".to_string()]);
4839        assert_eq!(testforstyle("test_zutil_global_marker_x", key_style), 0);
4840        assert_eq!(testforstyle("unmatched_ctx", "no_such_style_zzz"), 1);
4841        // Cleanup so other tests don't see the entry.
4842        {
4843            let mut t = zstyletab.lock().unwrap();
4844            t.delete(Some(key_pat), Some(key_style));
4845        }
4846    }
4847
4848    #[test]
4849    fn test_zformat_basic() {
4850        let _g = crate::test_util::global_state_lock();
4851        let mut specs = HashMap::new();
4852        specs.insert('n', "test".to_string());
4853        specs.insert('v', "42".to_string());
4854
4855        let result = zformat_substring("Name: %n, Value: %v", &specs, false);
4856        assert_eq!(result, "Name: test, Value: 42");
4857    }
4858
4859    #[test]
4860    fn test_zformat_padding() {
4861        let _g = crate::test_util::global_state_lock();
4862        let mut specs = HashMap::new();
4863        specs.insert('n', "hi".to_string());
4864
4865        let result = zformat_substring("[%10n]", &specs, false);
4866        assert_eq!(result, "[hi        ]");
4867
4868        let result = zformat_substring("[%-10n]", &specs, false);
4869        assert_eq!(result, "[        hi]");
4870    }
4871
4872    #[test]
4873    fn test_zformat_truncate() {
4874        let _g = crate::test_util::global_state_lock();
4875        let mut specs = HashMap::new();
4876        specs.insert('n', "hello world".to_string());
4877
4878        let result = zformat_substring("[%.5n]", &specs, false);
4879        assert_eq!(result, "[hello]");
4880    }
4881
4882    #[test]
4883    fn test_zformat_escape() {
4884        let _g = crate::test_util::global_state_lock();
4885        let specs = HashMap::new();
4886        let result = zformat_substring("100%%", &specs, false);
4887        assert_eq!(result, "100%");
4888    }
4889
4890    /// `Src/Modules/zutil.c:923-936` — Unknown spec character emits the
4891    /// original `%X` literal back into the output (not consumed). Pin
4892    /// the fallback so a regen that drops the unknown-spec branch would
4893    /// silently swallow `%z` when only `%n` was registered.
4894    #[test]
4895    fn zformat_unknown_spec_emits_literal_percent_x() {
4896        let _g = crate::test_util::global_state_lock();
4897        let mut specs = HashMap::new();
4898        specs.insert('n', "hello".to_string());
4899        // %z is unknown; must round-trip
4900        let result = zformat_substring("%z %n", &specs, false);
4901        assert_eq!(
4902            result, "%z hello",
4903            "c:923-936 — unknown spec emits raw `%X` segment"
4904        );
4905    }
4906
4907    /// `Src/Modules/zutil.c:825-826` — Right-align flag with explicit
4908    /// min-width. `%-5n` right-pads with spaces on the LEFT. Pin both
4909    /// arms (left/right) since a regen flipping the polarity would
4910    /// silently invert every zformat-prompted output.
4911    #[test]
4912    fn zformat_right_align_with_min_width() {
4913        let _g = crate::test_util::global_state_lock();
4914        let mut specs = HashMap::new();
4915        specs.insert('n', "ab".to_string());
4916        // Left-align (default): pad on RIGHT
4917        assert_eq!(zformat_substring("[%5n]", &specs, false), "[ab   ]");
4918        // Right-align (-): pad on LEFT
4919        assert_eq!(zformat_substring("[%-5n]", &specs, false), "[   ab]");
4920    }
4921
4922    /// `Src/Modules/zutil.c:825-845` — Min + Max combined: `%5.10n`
4923    /// means right-pad to min=5, truncate at max=10. With value
4924    /// "hello world" (11 chars), max=10 truncates to "hello worl".
4925    #[test]
4926    fn zformat_min_max_combined() {
4927        let _g = crate::test_util::global_state_lock();
4928        let mut specs = HashMap::new();
4929        specs.insert('n', "hello world".to_string());
4930        // max=10 truncates the 11-char value
4931        let r = zformat_substring("[%5.10n]", &specs, false);
4932        assert_eq!(r, "[hello worl]");
4933        // Short value: min=5 right-pads (default = left-align)
4934        specs.insert('n', "hi".to_string());
4935        let r = zformat_substring("[%5.10n]", &specs, false);
4936        assert_eq!(
4937            r, "[hi   ]",
4938            "c:828-845 — min-pad then max-truncate; short value gets the pad only"
4939        );
4940    }
4941
4942    /// `Src/Modules/zutil.c:975-976` — `%)` is pre-registered as `)`
4943    /// so the parser can emit a literal `)` from the user's format
4944    /// string. Pin the alias.
4945    #[test]
4946    fn zformat_close_paren_escape() {
4947        let _g = crate::test_util::global_state_lock();
4948        let specs = HashMap::new();
4949        let r = zformat_substring("a%)b", &specs, false);
4950        assert_eq!(r, "a)b", "c:975-976 — `%)` emits literal `)`");
4951    }
4952
4953    /// `Src/Modules/zutil.c:847-887` — Ternary
4954    /// `%(SPEC.true-text.false-text)` emits the FIRST branch ("true-text")
4955    /// when the spec exists. Per `man zshmodules`: "if the contents of
4956    /// the spec are present then true-text is output, otherwise
4957    /// false-text." With presence=true (zformat -F), spec-set means
4958    /// emit true-text.
4959    #[test]
4960    fn zformat_ternary_presence_mode_spec_set() {
4961        let _g = crate::test_util::global_state_lock();
4962        let mut specs = HashMap::new();
4963        specs.insert('s', "anything".to_string());
4964        // presence=true: spec exists → TRUE branch (first text).
4965        let r = zformat_substring("%(s.yes.no)", &specs, true);
4966        assert_eq!(
4967            r, "yes",
4968            "c:847-887 — spec set, presence=true → TRUE branch (true-text first)"
4969        );
4970    }
4971
4972    /// `Src/Modules/zutil.c:847-887` — Ternary with missing spec in
4973    /// presence-mode emits the SECOND branch ("false-text"). Per
4974    /// docs: "if contents of the spec are present then true-text is
4975    /// output, otherwise false-text."
4976    #[test]
4977    fn zformat_ternary_presence_mode_spec_unset() {
4978        let _g = crate::test_util::global_state_lock();
4979        let specs = HashMap::new();
4980        let r = zformat_substring("%(s.yes.no)", &specs, true);
4981        assert_eq!(
4982            r, "no",
4983            "c:847-887 — spec unset, presence=true → FALSE branch (false-text second)"
4984        );
4985    }
4986
4987    /// `Src/Modules/zutil.c:937-948` — Plain (non-`%`) bytes between
4988    /// specs emit verbatim. Pin the simplest no-spec passthrough.
4989    #[test]
4990    fn zformat_literal_text_passes_through() {
4991        let _g = crate::test_util::global_state_lock();
4992        let specs = HashMap::new();
4993        let r = zformat_substring("hello, world", &specs, false);
4994        assert_eq!(r, "hello, world");
4995        // Empty format → empty output
4996        let r = zformat_substring("", &specs, false);
4997        assert_eq!(r, "");
4998    }
4999
5000    /// `Src/Modules/zutil.c:890-922` — When max=0 (`.0`), the value is
5001    /// truncated to ZERO chars — but the min-pad still fires. Edge case
5002    /// that pins the order: truncate-then-pad, not pad-then-truncate.
5003    #[test]
5004    fn zformat_max_zero_truncates_to_empty_but_keeps_min_pad() {
5005        let _g = crate::test_util::global_state_lock();
5006        let mut specs = HashMap::new();
5007        specs.insert('n', "abc".to_string());
5008        let r = zformat_substring("[%3.0n]", &specs, false);
5009        assert_eq!(
5010            r, "[   ]",
5011            "c:890-922 — max=0 → empty value, min=3 → 3 spaces"
5012        );
5013    }
5014
5015    /// `Src/Modules/zutil.c:55-68` — `restorematch` MUST `unsetparam`
5016    /// each field that's None (not just leave it alone). Pin:
5017    /// pre-seed `$match` with a value, take a snapshot where it's None,
5018    /// call restorematch, observe `$match` is now UNSET in paramtab.
5019    #[test]
5020    fn restorematch_unsets_params_when_snapshot_is_none() {
5021        let _g = crate::test_util::global_state_lock();
5022        // Pre-seed `$match` so we can observe the unset.
5023        assignaparam("match", vec!["seed".to_string()], 0);
5024        assert!(getaparam("match").is_some(), "test setup: $match seeded");
5025
5026        // Snapshot with all three fields None — restorematch must
5027        // unsetparam each (c:60/64/68).
5028        let snap = MatchData {
5029            r#match: None,
5030            mbegin: None,
5031            mend: None,
5032        };
5033        restorematch(&snap);
5034        assert!(
5035            getaparam("match").is_none(),
5036            "c:60 — None snapshot must unsetparam(\"match\")"
5037        );
5038    }
5039
5040    // ─── zsh-corpus pins for zstyle helpers ──────────────────────────
5041
5042    /// `lookupstyle` on missing context returns empty Vec.
5043    #[test]
5044    fn zutil_corpus_lookupstyle_missing_returns_empty() {
5045        let _g = crate::test_util::global_state_lock();
5046        let r = lookupstyle(":zshrs:test:nonexistent_zxyz", "style_nope");
5047        assert!(r.is_empty(), "missing zstyle context returns empty");
5048    }
5049
5050    /// `testforstyle` on missing context returns 1 (not found).
5051    #[test]
5052    fn zutil_corpus_testforstyle_missing_returns_one() {
5053        let _g = crate::test_util::global_state_lock();
5054        let r = testforstyle(":zshrs:test:never_set_xyz", "style");
5055        assert_eq!(r, 1, "missing = 1 per c:485 return !found");
5056    }
5057
5058    /// `addstyle` on new name returns Some(style).
5059    #[test]
5060    fn zutil_corpus_addstyle_new_returns_some() {
5061        let _g = crate::test_util::global_state_lock();
5062        let s = addstyle("zshrs_test_new_style");
5063        assert!(s.is_some(), "addstyle for new name returns Some");
5064    }
5065
5066    /// `addstyle` returning Some on existing name (idempotent).
5067    #[test]
5068    fn zutil_corpus_addstyle_existing_returns_some() {
5069        let _g = crate::test_util::global_state_lock();
5070        let _ = addstyle("zshrs_test_dup_style");
5071        let s = addstyle("zshrs_test_dup_style");
5072        assert!(s.is_some(), "addstyle on existing name still returns Some");
5073    }
5074
5075    /// `newzstyletable` installs the c:275-287 function-pointer slots.
5076    #[test]
5077    fn zutil_corpus_newzstyletable_installs_slots() {
5078        let _g = crate::test_util::global_state_lock();
5079        let t = newzstyletable(17, "test_tab");
5080        assert_eq!((t.hsize, t.ct), (17, 0));
5081        assert!(t.hash.is_some() && t.emptytable.is_some() && t.cmpnodes.is_some());
5082        assert!(t.addnode.is_some() && t.getnode.is_some() && t.getnode2.is_some());
5083        assert!(t.removenode.is_some() && t.freenode.is_some() && t.printnode.is_some());
5084        assert!(t.filltable.is_none() && t.disablenode.is_none() && t.enablenode.is_none());
5085    }
5086
5087    // ═══════════════════════════════════════════════════════════════════
5088    // Additional C-parity tests for Src/Modules/zutil.c zformat_substring.
5089    // ═══════════════════════════════════════════════════════════════════
5090
5091    /// c:814 — `zformat_substring("", _, _)` returns empty.
5092    #[test]
5093    fn zformat_substring_empty_format_returns_empty() {
5094        let _g = crate::test_util::global_state_lock();
5095        let specs = std::collections::HashMap::new();
5096        let r = zformat_substring("", &specs, false);
5097        assert!(r.is_empty());
5098    }
5099
5100    /// c:814 — plain text with no `%` passes through verbatim.
5101    #[test]
5102    fn zformat_substring_plain_text_pass_through() {
5103        let _g = crate::test_util::global_state_lock();
5104        let specs = std::collections::HashMap::new();
5105        assert_eq!(zformat_substring("hello", &specs, false), "hello");
5106        assert_eq!(zformat_substring("abc def", &specs, false), "abc def");
5107    }
5108
5109    /// c:975 — `%%` produces literal `%` (pre-populated spec).
5110    #[test]
5111    fn zformat_substring_percent_percent_is_literal_percent() {
5112        let _g = crate::test_util::global_state_lock();
5113        let specs = std::collections::HashMap::new();
5114        let r = zformat_substring("%%", &specs, false);
5115        assert_eq!(r, "%");
5116    }
5117
5118    /// c:976 — `%)` produces literal `)` (pre-populated spec).
5119    #[test]
5120    fn zformat_substring_percent_close_paren_is_literal_close_paren() {
5121        let _g = crate::test_util::global_state_lock();
5122        let specs = std::collections::HashMap::new();
5123        let r = zformat_substring("%)", &specs, false);
5124        assert_eq!(r, ")");
5125    }
5126
5127    /// c:814 — `%X` substitutes spec value when registered.
5128    #[test]
5129    fn zformat_substring_substitutes_registered_spec() {
5130        let _g = crate::test_util::global_state_lock();
5131        let mut specs = std::collections::HashMap::new();
5132        specs.insert('n', "alice".to_string());
5133        let r = zformat_substring("%n", &specs, false);
5134        assert_eq!(r, "alice");
5135    }
5136
5137    /// c:814 — multiple `%X` substitutions in same format.
5138    #[test]
5139    fn zformat_substring_multiple_specs() {
5140        let _g = crate::test_util::global_state_lock();
5141        let mut specs = std::collections::HashMap::new();
5142        specs.insert('n', "alice".to_string());
5143        specs.insert('h', "/home/alice".to_string());
5144        let r = zformat_substring("%n is at %h", &specs, false);
5145        assert_eq!(r, "alice is at /home/alice");
5146    }
5147
5148    /// c:814 — text between two `%X` substitutions preserved.
5149    #[test]
5150    fn zformat_substring_text_between_specs_preserved() {
5151        let _g = crate::test_util::global_state_lock();
5152        let mut specs = std::collections::HashMap::new();
5153        specs.insert('a', "X".to_string());
5154        let r = zformat_substring("[%a-%a]", &specs, false);
5155        assert_eq!(r, "[X-X]");
5156    }
5157
5158    /// c:849-856 — `%%` unwinds to a literal `%` BEFORE any spec lookup,
5159    /// so a `%` entry in the table is never consulted. bin_zformat
5160    /// (c:1026-1029) rejects `%` as a spec name anyway: reference zsh prints
5161    /// `invalid argument: %:X` for `zformat -f v "%%" "%:X"`.
5162    #[test]
5163    fn zformat_substring_percent_percent_ignores_a_percent_spec() {
5164        let _g = crate::test_util::global_state_lock();
5165        let mut specs = std::collections::HashMap::new();
5166        specs.insert('%', "OVERRIDE".to_string());
5167        let r = zformat_substring("%%", &specs, false);
5168        assert_eq!(r, "%");
5169    }
5170
5171    /// c:814 — `%` followed by unregistered char produces empty
5172    /// substitution.
5173    #[test]
5174    fn zformat_substring_unregistered_spec_no_panic() {
5175        let _g = crate::test_util::global_state_lock();
5176        let specs = std::collections::HashMap::new();
5177        let _ = zformat_substring("%z", &specs, false);
5178    }
5179
5180    /// c:814 — determinism for identical input.
5181    #[test]
5182    fn zformat_substring_is_deterministic() {
5183        let _g = crate::test_util::global_state_lock();
5184        let mut specs = std::collections::HashMap::new();
5185        specs.insert('n', "alice".to_string());
5186        let first = zformat_substring("hi %n!", &specs, false);
5187        for _ in 0..5 {
5188            assert_eq!(zformat_substring("hi %n!", &specs, false), first);
5189        }
5190    }
5191
5192    /// Lifecycle (c:2966/2988/2995/3002):
5193    #[test]
5194    fn zutil_setup_returns_zero_pin() {
5195        let _g = crate::test_util::global_state_lock();
5196        assert_eq!(setup_(std::ptr::null()), 0);
5197    }
5198
5199    /// c:3002 — finish_(NULL) = 0.
5200    #[test]
5201    fn zutil_finish_returns_zero_pin() {
5202        let _g = crate::test_util::global_state_lock();
5203        assert_eq!(finish_(std::ptr::null()), 0);
5204    }
5205
5206    // ═══════════════════════════════════════════════════════════════════
5207    // Additional C-parity tests for Src/Modules/zutil.c
5208    // c:787 lookupstyle / c:810 testforstyle / c:837 bin_zstyle /
5209    // c:1131 bin_zformat / c:2172 get_opt_desc / c:2192 lookup_opt /
5210    // c:2216 get_opt_arr / c:2966-3002 lifecycle type pins
5211    // ═══════════════════════════════════════════════════════════════════
5212
5213    /// c:787 — `lookupstyle` returns Vec<String> (compile-time type pin).
5214    #[test]
5215    fn lookupstyle_returns_vec_string_type() {
5216        let _g = crate::test_util::global_state_lock();
5217        let _: Vec<String> = lookupstyle("", "");
5218    }
5219
5220    /// c:787 — `lookupstyle("", "")` empty inputs returns empty Vec.
5221    #[test]
5222    fn lookupstyle_empty_inputs_returns_empty() {
5223        let _g = crate::test_util::global_state_lock();
5224        let r = lookupstyle("", "");
5225        assert!(r.is_empty(), "empty context/style → empty Vec");
5226    }
5227
5228    /// c:787 — `lookupstyle` deterministic for stable input.
5229    #[test]
5230    fn lookupstyle_is_deterministic() {
5231        let _g = crate::test_util::global_state_lock();
5232        for (ctx, sty) in [("", ""), ("a", "b"), ("zsh", "color")] {
5233            let first = lookupstyle(ctx, sty);
5234            for _ in 0..3 {
5235                assert_eq!(
5236                    lookupstyle(ctx, sty),
5237                    first,
5238                    "lookupstyle({:?}, {:?}) must be deterministic",
5239                    ctx,
5240                    sty
5241                );
5242            }
5243        }
5244    }
5245
5246    /// c:810 — `testforstyle` returns i32 (compile-time type pin).
5247    #[test]
5248    fn testforstyle_returns_i32_type() {
5249        let _g = crate::test_util::global_state_lock();
5250        let _: i32 = testforstyle("", "");
5251    }
5252
5253    /// c:465 — `testforstyle(ctxt, style)` returns `!found` per the C
5254    /// body's final `return !found;` line. With an empty zstyletab,
5255    /// no entry can match → found=false → return 1 (NOT 0). The
5256    /// previous test expectation conflated the C-level "0=success"
5257    /// convention with the "0=style-present" semantic — they're
5258    /// the same value but the test's "not present" comment makes the
5259    /// 0 assertion inverted.
5260    #[test]
5261    fn testforstyle_empty_inputs_returns_zero() {
5262        let _g = crate::test_util::global_state_lock();
5263        assert_eq!(
5264            testforstyle("", ""),
5265            1,
5266            "empty ctx/style → 1 (not present, per C `!found`)"
5267        );
5268    }
5269
5270    /// c:837 — `bin_zstyle` returns i32 (compile-time type pin).
5271    #[test]
5272    fn bin_zstyle_returns_i32_type() {
5273        let _g = crate::test_util::global_state_lock();
5274        let ops = crate::ported::zsh_h::options {
5275            ind: [0u8; crate::ported::zsh_h::MAX_OPS],
5276            args: Vec::new(),
5277            argscount: 0,
5278            argsalloc: 0,
5279        };
5280        let _: i32 = bin_zstyle("zstyle", &[], &ops, 0);
5281    }
5282
5283    /// c:1131 — `bin_zformat` returns i32.
5284    #[test]
5285    fn bin_zformat_returns_i32_type() {
5286        let _g = crate::test_util::global_state_lock();
5287        let ops = crate::ported::zsh_h::options {
5288            ind: [0u8; crate::ported::zsh_h::MAX_OPS],
5289            args: Vec::new(),
5290            argscount: 0,
5291            argsalloc: 0,
5292        };
5293        let _: i32 = bin_zformat("zformat", &[], &ops, 0);
5294    }
5295
5296    /// c:2172 — `get_opt_desc` returns Option<Zoptdesc>.
5297    #[test]
5298    fn get_opt_desc_returns_option_type() {
5299        let _g = crate::test_util::global_state_lock();
5300        let _: Option<Zoptdesc> = get_opt_desc("");
5301    }
5302
5303    /// c:2172 — `get_opt_desc` deterministic for unknown name.
5304    #[test]
5305    fn get_opt_desc_unknown_deterministic() {
5306        let _g = crate::test_util::global_state_lock();
5307        let a = get_opt_desc("__never_real_opt__").is_some();
5308        for _ in 0..3 {
5309            assert_eq!(
5310                get_opt_desc("__never_real_opt__").is_some(),
5311                a,
5312                "get_opt_desc must be deterministic"
5313            );
5314        }
5315    }
5316
5317    /// c:2192 — `lookup_opt` returns Option<Zoptdesc>.
5318    #[test]
5319    fn lookup_opt_returns_option_type() {
5320        let _g = crate::test_util::global_state_lock();
5321        let _: Option<Zoptdesc> = lookup_opt("");
5322    }
5323
5324    /// c:2216 — `get_opt_arr` returns Option<Zoptarr>.
5325    #[test]
5326    fn get_opt_arr_returns_option_type() {
5327        let _g = crate::test_util::global_state_lock();
5328        let _: Option<Zoptarr> = get_opt_arr("");
5329    }
5330
5331    /// c:2966 — `setup_` returns i32 (compile-time type pin).
5332    #[test]
5333    fn zutil_setup_returns_i32_type() {
5334        let _g = crate::test_util::global_state_lock();
5335        let _: i32 = setup_(std::ptr::null());
5336    }
5337
5338    /// c:2988 + c:2995 — boot/cleanup idempotent.
5339    #[test]
5340    fn zutil_boot_cleanup_idempotent() {
5341        let _g = crate::test_util::global_state_lock();
5342        for _ in 0..5 {
5343            assert_eq!(boot_(std::ptr::null()), 0);
5344            assert_eq!(cleanup_(std::ptr::null()), 0);
5345        }
5346    }
5347
5348    // ═══════════════════════════════════════════════════════════════════
5349    // Additional C-parity tests for Src/Modules/zutil.c
5350    // c:787 lookupstyle / c:810 testforstyle / c:837 bin_zstyle /
5351    // c:1131 bin_zformat / c:2022 bin_zregexparse / c:2408 bin_zparseopts /
5352    // c:3135 zformat_substring
5353    // ═══════════════════════════════════════════════════════════════════
5354
5355    /// c:787 — `lookupstyle` returns Vec<String> (compile-time pin, alt).
5356    #[test]
5357    fn lookupstyle_returns_vec_string_pin_alt() {
5358        let _g = crate::test_util::global_state_lock();
5359        let _: Vec<String> = lookupstyle("", "");
5360    }
5361
5362    /// c:787 — `lookupstyle("", "")` returns empty Vec.
5363    #[test]
5364    fn lookupstyle_empty_returns_empty() {
5365        let _g = crate::test_util::global_state_lock();
5366        let v = lookupstyle("", "");
5367        assert!(v.is_empty(), "empty ctx/style → empty Vec; got {:?}", v);
5368    }
5369
5370    /// c:810 — `testforstyle` returns i32 (compile-time pin, alt).
5371    #[test]
5372    fn testforstyle_returns_i32_pin_alt() {
5373        let _g = crate::test_util::global_state_lock();
5374        let _: i32 = testforstyle("", "");
5375    }
5376
5377    /// c:837 — `bin_zstyle` no-args returns 0 (listing form per c:837).
5378    #[test]
5379    fn bin_zstyle_no_args_returns_zero_or_one() {
5380        let _g = crate::test_util::global_state_lock();
5381        let ops = crate::ported::zsh_h::options {
5382            ind: [0u8; crate::ported::zsh_h::MAX_OPS],
5383            args: Vec::new(),
5384            argscount: 0,
5385            argsalloc: 0,
5386        };
5387        let r = bin_zstyle("zstyle", &[], &ops, 0);
5388        assert!(
5389            r == 0 || r == 1,
5390            "no args is the listing form; result must be 0/1, got {}",
5391            r
5392        );
5393    }
5394
5395    /// c:1131 — `bin_zformat` no-args returns nonzero (usage error).
5396    #[test]
5397    fn bin_zformat_no_args_returns_nonzero() {
5398        let _g = crate::test_util::global_state_lock();
5399        let ops = crate::ported::zsh_h::options {
5400            ind: [0u8; crate::ported::zsh_h::MAX_OPS],
5401            args: Vec::new(),
5402            argscount: 0,
5403            argsalloc: 0,
5404        };
5405        let r = bin_zformat("zformat", &[], &ops, 0);
5406        assert_ne!(r, 0, "zformat no args → usage error");
5407    }
5408
5409    /// c:1027-1032 — `-f`/`-F` reject a spec whose name is `%` or `)`,
5410    /// alongside `-`, `.`, a digit, and a missing `:`. Measured against
5411    /// zsh 5.9.2: `zformat -f R "%n" "%:O"` warns `invalid argument: %:O`
5412    /// and returns 1, leaving the parameter untouched; zshrs accepted
5413    /// both `%` and `)` silently and returned 0.
5414    #[test]
5415    fn bin_zformat_rejects_percent_and_paren_spec_names() {
5416        let _g = crate::test_util::global_state_lock();
5417        let mut ops = crate::ported::zsh_h::options {
5418            ind: [0u8; crate::ported::zsh_h::MAX_OPS],
5419            args: Vec::new(),
5420            argscount: 0,
5421            argsalloc: 0,
5422        };
5423        ops.ind[b'f' as usize] = 1;
5424        let call = |spec: &str| {
5425            bin_zformat(
5426                "zformat",
5427                &[
5428                    "ZFMT_PIN".to_string(),
5429                    "%n".to_string(),
5430                    spec.to_string(),
5431                ],
5432                &ops,
5433                0,
5434            )
5435        };
5436        assert_ne!(call("%:OVERRIDE"), 0, "c:1028 — `%` is not a valid spec name");
5437        assert_ne!(call("):OVERRIDE"), 0, "c:1028 — `)` is not a valid spec name");
5438        // Control: a legal spec name still works, so the added guard did
5439        // not swallow the ordinary path (c:1026-1032 falls through).
5440        assert_eq!(call("n:alice"), 0, "c:1061 — an ordinary spec is accepted");
5441    }
5442
5443    /// c:2022 — `bin_zregexparse` returns i32 (compile-time pin).
5444    #[test]
5445    fn bin_zregexparse_returns_i32_type() {
5446        let _g = crate::test_util::global_state_lock();
5447        let ops = crate::ported::zsh_h::options {
5448            ind: [0u8; crate::ported::zsh_h::MAX_OPS],
5449            args: Vec::new(),
5450            argscount: 0,
5451            argsalloc: 0,
5452        };
5453        let _: i32 = bin_zregexparse("zregexparse", &[], &ops, 0);
5454    }
5455
5456    /// c:2408 — `bin_zparseopts` returns i32 (compile-time pin).
5457    #[test]
5458    fn bin_zparseopts_returns_i32_type() {
5459        let _g = crate::test_util::global_state_lock();
5460        let ops = crate::ported::zsh_h::options {
5461            ind: [0u8; crate::ported::zsh_h::MAX_OPS],
5462            args: Vec::new(),
5463            argscount: 0,
5464            argsalloc: 0,
5465        };
5466        let _: i32 = bin_zparseopts("zparseopts", &[], &ops, 0);
5467    }
5468
5469    /// c:2408 — `bin_zparseopts` no-args returns nonzero (usage error).
5470    #[test]
5471    fn bin_zparseopts_no_args_returns_nonzero() {
5472        let _g = crate::test_util::global_state_lock();
5473        let ops = crate::ported::zsh_h::options {
5474            ind: [0u8; crate::ported::zsh_h::MAX_OPS],
5475            args: Vec::new(),
5476            argscount: 0,
5477            argsalloc: 0,
5478        };
5479        let r = bin_zparseopts("zparseopts", &[], &ops, 0);
5480        assert_ne!(r, 0, "zparseopts no args → usage error");
5481    }
5482
5483    /// c:3135 — `zformat_substring("", _, false)` returns empty (alt).
5484    #[test]
5485    fn zformat_substring_empty_format_returns_empty_alt() {
5486        let specs: std::collections::HashMap<char, String> = std::collections::HashMap::new();
5487        let r = zformat_substring("", &specs, false);
5488        assert_eq!(r, "", "empty format → empty output");
5489    }
5490
5491    /// c:3135 — `zformat_substring` returns String (compile-time pin).
5492    #[test]
5493    fn zformat_substring_returns_string_type() {
5494        let specs: std::collections::HashMap<char, String> = std::collections::HashMap::new();
5495        let _: String = zformat_substring("plain text", &specs, false);
5496    }
5497
5498    /// c:3135 — `zformat_substring` plain text (no `%`) returns as-is.
5499    #[test]
5500    fn zformat_substring_plain_text_no_specs() {
5501        let specs: std::collections::HashMap<char, String> = std::collections::HashMap::new();
5502        let r = zformat_substring("hello world", &specs, false);
5503        assert_eq!(r, "hello world", "text without %-spec is returned verbatim");
5504    }
5505}