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