Skip to main content

zsh/ported/zle/
computil.rs

1//! Completion utility functions for ZLE
2//!
3//! Port from zsh/Src/Zle/computil.c (5,180 lines)
4//!
5//! Help for `_describe'.                                                    // c:34
6//! Help for `_arguments'.                                                   // c:897
7//!
8//! The full utility library is in compsys/computil.rs (674 lines).
9//! This module provides _describe, _values, _alternative, _combination,
10//! and the compdescribe/comparguments/compvalues builtins.
11//!
12//! Key C functions and their Rust locations:
13//! - bin_compdescribe  → crate::compsys::describe::describe()
14//! - bin_comparguments → crate::compsys::arguments (full _arguments)
15//! - bin_compvalues    → crate::compsys::computil::compvalues()
16//! - bin_comptags      → crate::compsys::state::comptags()
17//! - bin_comptry       → crate::compsys::state::comptry()
18
19use std::os::unix::fs::MetadataExt;
20use std::sync::atomic::Ordering;
21
22use crate::ported::glob::{hasbraces, remnulargs, tokenize, xpandbraces};
23use crate::ported::lex::untokenize;
24use crate::ported::mem::ztrdup;
25use crate::ported::params::{
26    getstrvalue, getvalue, getvaluearr, locallevel, paramtab, setaparam, setarrvalue, sethparam,
27    setiparam, setsparam, setstrvalue,
28};
29use crate::ported::pattern::{haswilds, patcompile, pattry, Patprog};
30use crate::ported::string::tricat;
31use crate::ported::utils::{
32    adjustcolumns, inittyptab, niceztrlen, quotestring, set_noerrs, strpfx, ztrlen, zwarnnam,
33};
34use crate::ported::zle::comp_h::{
35    Cmatcher, Cpattern, CGF_NOSORT, CGF_UNIQALL, CGF_UNIQCON, CMF_LEFT, CMF_RIGHT, CPAT_ANY,
36    CPAT_CCLASS, CPAT_CHAR, CPAT_EQUIV, CPAT_NCLASS,
37};
38use crate::ported::zle::compcore::{begcmgroup, comppatmatch, endcmgroup, get_user_var, rembslash};
39use crate::ported::zle::complete::{
40    ignore_prefix, ignore_suffix, parse_cmatcher, restrict_range, COMPCURRENT, COMPPREFIX,
41    COMPQSTACK, COMPSUFFIX, COMPWORDS, INCOMPFUNC,
42};
43use crate::ported::zle::compmatch::{pattern_match, pattern_match1, pattern_match_equivalence};
44use crate::ported::zle::compresult::ztat;
45use crate::ported::zsh_h::{
46    isset, options, unset, value, Comma, Inbrace, Outbrace, GLOBDOTS, KSHARRAYS, MAX_OPS,
47    OPT_ISSET, PM_ARRAY, PM_TYPE, PP_LOWER, PP_RANGE, PP_UPPER, QT_BACKSLASH, QT_BACKSLASH_PATTERN,
48};
49use crate::ported::ztype_h::{iblank, idigit, imeta, inblank};
50
51// =====================================================================
52// CRT_* — `_describe` row-type discriminator from `computil.c:79-83`.
53// Drives the `cdescr` table-builder switch.
54// =====================================================================
55
56#[allow(unused_imports)]
57use crate::ported::zle::{
58    deltochar::*, textobjects::*, zle_hist::*, zle_main::*, zle_misc::*, zle_move::*,
59    zle_params::*, zle_refresh::*, zle_tricky::*, zle_utils::*, zle_vi::*, zle_word::*,
60};
61/// Port of `CRT_SIMPLE` from `Src/Zle/computil.c:79`. Plain match row.
62
63// --- AUTO: cross-zle hoisted-fn use glob ---
64#[allow(unused_imports)]
65#[allow(unused_imports)]
66// =====================================================================
67// `_describe`-completion types — direct ports of the C structs at
68// Src/Zle/computil.c:40-91 (the cdset/cdstr/cdrun/cdstate chain
69// the `_describe` completion path builds + processes).
70// =====================================================================
71
72// CRT_* constants already declared above (file scope).
73
74/// Port of `typedef struct cdset *Cdset` from `Src/Zle/computil.c:36`.
75pub type Cdset = Box<cdset>; // c:36
76
77/// Direct port of `struct cdset` from `Src/Zle/computil.c:85-91`.
78/// One set of matches (one `compadd` invocation worth) with its
79/// compadd options + the cdstr chain.
80#[derive(Debug, Default)]
81#[allow(non_camel_case_types)]
82pub struct cdset {
83    // c:85
84    pub next: Option<Box<cdset>>,  // c:86 Cdset next
85    pub opts: Option<Vec<String>>, // c:87 char **opts
86    pub strs: Option<Box<cdstr>>,  // c:88 Cdstr strs
87    pub count: i32,                // c:89 int count
88    pub desc: i32,                 // c:90 int desc
89}
90/// Port of `typedef struct cdstr *Cdstr` from `computil.c:37`.
91pub type Cdstr = Box<cdstr>; // c:37
92
93/// Direct port of `struct cdstr` from `Src/Zle/computil.c:58-70`.
94/// One match string inside a `_describe` group, with optional
95/// description and the same-description chain.
96#[derive(Debug, Default, Clone)]
97#[allow(non_camel_case_types)]
98pub struct cdstr {
99    // c:58
100    pub next: Option<Box<cdstr>>,  // c:59 Cdstr next
101    pub str: Option<String>,       // c:60 char *str
102    pub desc: Option<String>,      // c:61 char *desc
103    pub r#match: Option<String>,   // c:62 char *match
104    pub sortstr: Option<String>,   // c:63 char *sortstr
105    pub len: i32,                  // c:64 int len
106    pub width: i32,                // c:65 int width
107    pub other: Option<Box<cdstr>>, // c:66 Cdstr other
108    pub kind: i32,                 // c:67 int kind (0/1/2)
109    pub set: usize,                // c:68 Cdset set (raw ptr index)
110    pub run: Option<Box<cdstr>>,   // c:69 Cdstr run
111}
112/// Port of `typedef struct cdrun *Cdrun` from `computil.c:38`.
113pub type Cdrun = Box<cdrun>; // c:38
114
115/// Direct port of `struct cdrun` from `Src/Zle/computil.c:72-77`.
116/// One contiguous "run" of cdstr entries the shell code should
117/// emit as a block.
118#[derive(Debug, Default)]
119#[allow(non_camel_case_types)]
120pub struct cdrun {
121    // c:72
122    pub next: Option<Box<cdrun>>, // c:73 Cdrun next
123    pub r#type: i32,              // c:74 int type (CRT_*)
124    pub strs: Option<Box<cdstr>>, // c:75 Cdstr strs
125    pub count: i32,               // c:76 int count
126}
127
128/// Direct port of `struct cdstate` from `Src/Zle/computil.c:40-56`.
129/// File-static state for the `_describe` engine — holds the active
130/// sets/runs/dimensions during a single `_describe` invocation.
131#[derive(Debug, Default)]
132#[allow(non_camel_case_types)]
133pub struct cdstate {
134    // c:40
135    pub showd: i32,               // c:41
136    pub sep: Option<String>,      // c:42 char *sep
137    pub slen: i32,                // c:43
138    pub swidth: i32,              // c:44
139    pub maxmlen: i32,             // c:45
140    pub sets: Option<Box<cdset>>, // c:46 Cdset sets
141    pub pre: i32,                 // c:47
142    pub premaxw: i32,             // c:48
143    pub suf: i32,                 // c:49
144    pub maxg: i32,                // c:50
145    pub maxglen: i32,             // c:51
146    pub groups: i32,              // c:52
147    pub descs: i32,               // c:53
148    pub gprew: i32,               // c:54
149    pub runs: Option<Box<cdrun>>, // c:55 Cdrun runs
150}
151/// `CRT_SIMPLE` constant.
152pub const CRT_SIMPLE: i32 = 0; // c:79
153/// Port of `CRT_DESC` from `computil.c:80`. Match with description.
154pub const CRT_DESC: i32 = 1; // c:80
155/// Port of `CRT_SPEC` from `computil.c:81`. Special separator row.
156pub const CRT_SPEC: i32 = 2; // c:81
157/// Port of `CRT_DUMMY` from `computil.c:82`. Placeholder row.
158pub const CRT_DUMMY: i32 = 3; // c:82
159/// Port of `CRT_EXPL` from `computil.c:83`. Explanation header row.
160pub const CRT_EXPL: i32 = 4; // c:83
161
162/// Port of `static int cd_parsed` from `Src/Zle/computil.c:188`. Flag
163/// signalling whether `cd_state` holds a parsed-but-unconsumed
164/// description set.
165pub static cd_parsed: std::sync::atomic::AtomicI32 = // c:94
166    std::sync::atomic::AtomicI32::new(0);
167
168/// Direct port of `static void freecdsets(Cdset p)` from
169/// `Src/Zle/computil.c:97`. Walks the cdset `next` chain
170/// freeing each set's opts/strs sub-chains and the cd_state runs
171/// list at the end.
172pub fn freecdsets(mut p: Option<Box<cdset>>) {
173    // c:97
174    while let Some(mut set) = p {
175        // c:97 for (; p; ...)
176        p = set.next.take(); // c:104 n = p->next
177                             // c:105-106 — `if (p->opts) freearray(p->opts)`.
178        set.opts = None;
179        // c:107-115 — for each cdstr: free sortstr/str/desc/match.
180        let mut s = set.strs.take();
181        while let Some(mut node) = s {
182            s = node.next.take();
183            node.sortstr = None; // c:109
184            node.str = None; // c:110
185            node.desc = None; // c:111
186                              // c:112-113 — `if (s->match != s->str) zsfree(s->match)`.
187                              // Rust's Option<String> drop is unconditional; the C
188                              // pointer-equality guard collapses out.
189            node.r#match = None;
190            drop(node); // c:114
191        }
192        // c:116-119 — drain cd_state.runs.
193        if let Ok(mut st) = cd_state.lock() {
194            let mut r = st.runs.take();
195            while let Some(mut run) = r {
196                r = run.next.take();
197                drop(run); // c:118
198            }
199        }
200        drop(set); // c:120
201    }
202}
203
204/// Direct port of `static void cd_group(int maxg)` from
205/// `Src/Zle/computil.c:127-182`. Walks `cd_state.sets` looking for
206/// matches sharing the same description; links them via the `other`
207/// chain on the first cdstr of each group. Sets `cd_state.groups`,
208/// `descs`, `maxg`, `maxglen` accordingly.
209pub fn cd_group(maxg: i32) {
210    // c:127
211    let mut st = cd_state.lock().unwrap();
212    st.groups = 0; // c:133
213    st.descs = 0;
214    st.maxglen = 0;
215    st.maxg = 0;
216
217    // c:136-140 — reset kind/other on every cdstr.
218    // Rust port: walk via raw pointers since we need mutable access
219    // through nested chains while holding the set borrow.
220    let st_ptr: *mut cdstate = &mut *st;
221    unsafe {
222        let mut set = (*st_ptr).sets.as_deref_mut();
223        while let Some(s) = set {
224            let mut sp = s.strs.as_deref_mut();
225            while let Some(sn) = sp {
226                sn.kind = 0;
227                sn.other = None;
228                sp = sn.next.as_deref_mut();
229            }
230            set = s.next.as_deref_mut();
231        }
232
233        // c:142-180 — find matching desc, build "other" chain.
234        let mut set1 = (*st_ptr).sets.as_deref_mut();
235        while let Some(s1) = set1 {
236            let s1_ptr: *mut cdset = s1;
237            let mut str1 = (*s1_ptr).strs.as_deref_mut();
238            while let Some(t1) = str1 {
239                if t1.desc.is_none() || t1.kind != 0 {
240                    // c:144
241                    str1 = t1.next.as_deref_mut();
242                    continue;
243                }
244                let mut num = 1i32; // c:147
245                let mut width = t1.width + (*st_ptr).swidth; // c:148
246                if width > (*st_ptr).maxglen {
247                    (*st_ptr).maxglen = width;
248                }
249                // Iterate set2 from set1 onwards; str2 starts at str1.next
250                // when same set, else strs head.
251                let t1_desc = t1.desc.clone().unwrap_or_default();
252                let mut other_tail: *mut Option<Box<cdstr>> = &mut t1.other;
253                let mut hit_break = false;
254                let mut set2 = Some(&mut *s1_ptr);
255                let mut first_iter = true;
256                while let Some(s2) = set2 {
257                    let s2_ptr: *mut cdset = s2;
258                    let mut str2 = if first_iter {
259                        t1.next.as_deref_mut()
260                    } else {
261                        (*s2_ptr).strs.as_deref_mut()
262                    };
263                    first_iter = false;
264                    while let Some(t2) = str2 {
265                        if t2.desc.as_deref() == Some(t1_desc.as_str()) {
266                            width += CM_SPACE + t2.width; // c:157
267                            if width > (*st_ptr).maxmlen || num == maxg {
268                                // c:158
269                                hit_break = true;
270                                break;
271                            }
272                            if width > (*st_ptr).maxglen {
273                                // c:160
274                                (*st_ptr).maxglen = width;
275                            }
276                            t1.kind = 1; // c:162
277                            t2.kind = 2;
278                            num += 1;
279                            // Clone t2 into the other chain (Rust ownership).
280                            let clone = Box::new(cdstr {
281                                next: None,
282                                str: t2.str.clone(),
283                                desc: t2.desc.clone(),
284                                r#match: t2.r#match.clone(),
285                                sortstr: t2.sortstr.clone(),
286                                len: t2.len,
287                                width: t2.width,
288                                other: None,
289                                kind: t2.kind,
290                                set: t2.set,
291                                run: None,
292                            });
293                            *other_tail = Some(clone);
294                            let nxt = &mut (*other_tail).as_mut().unwrap().other;
295                            other_tail = nxt as *mut _;
296                        }
297                        str2 = t2.next.as_deref_mut();
298                    }
299                    if hit_break {
300                        break;
301                    }
302                    set2 = (*s2_ptr).next.as_deref_mut();
303                }
304                if num > 1 {
305                    // c:173
306                    (*st_ptr).groups += 1;
307                } else {
308                    (*st_ptr).descs += 1; // c:176
309                }
310                if num > (*st_ptr).maxg {
311                    // c:178
312                    (*st_ptr).maxg = num;
313                }
314                str1 = t1.next.as_deref_mut();
315            }
316            set1 = s1.next.as_deref_mut();
317        }
318    }
319}
320
321/// Direct port of `static void cd_calc(void)` from
322/// `Src/Zle/computil.c:188-211`. Walks `cd_state.sets`, computing
323/// each set's `count`/`desc` and updating
324/// `cd_state.pre`/`premaxw`/`suf` (the global max widths) for the
325/// `_describe` column layout.
326pub fn cd_calc() {
327    // c:188
328    let mut st = cd_state.lock().unwrap();
329    st.pre = 0; // c:194
330    st.suf = 0;
331    let mut max_pre = 0i32;
332    let mut max_premaxw = st.premaxw;
333    let mut max_suf = 0i32;
334
335    let mut set = st.sets.as_deref_mut();
336    while let Some(s) = set {
337        s.count = 0; // c:197
338        s.desc = 0;
339        let mut str_iter = s.strs.as_deref();
340        while let Some(st_node) = str_iter {
341            s.count += 1; // c:199
342            let str_s = st_node.str.as_deref().unwrap_or("");
343            let l = str_s.len() as i32;
344            if l > max_pre {
345                max_pre = l;
346            } // c:200
347              // c:202 — ZMB_nicewidth(str). Rust niceztrlen returns usize.
348            let nw = niceztrlen(str_s) as i32;
349            if nw > max_premaxw {
350                max_premaxw = nw;
351            }
352            if let Some(d) = st_node.desc.as_deref() {
353                // c:204
354                s.desc += 1;
355                let dl = d.len() as i32;
356                if dl > max_suf {
357                    max_suf = dl;
358                } // c:206
359            }
360            str_iter = st_node.next.as_deref();
361        }
362        set = s.next.as_deref_mut();
363    }
364    st.pre = max_pre;
365    st.premaxw = max_premaxw;
366    st.suf = max_suf;
367}
368
369/// Direct port of `static int cd_groups_want_sorting(void)` from
370/// `Src/Zle/computil.c:215-230`. Returns 0 if any set's opts contain
371/// `-V` (preserve order), 1 if any contain `-J` (sort), 1 default.
372pub fn cd_groups_want_sorting() -> i32 {
373    // c:215
374    let st = cd_state.lock().unwrap();
375    let mut set = st.sets.as_deref();
376    while let Some(s) = set {
377        if let Some(opts) = s.opts.as_deref() {
378            for o in opts {
379                if o.starts_with("-V") {
380                    return 0;
381                } // c:222
382                if o.starts_with("-J") {
383                    return 1;
384                } // c:224
385            }
386        }
387        set = s.next.as_deref();
388    }
389    1 // c:229
390}
391
392/// Direct port of `static int cd_sort(const void *a, const void *b)`
393/// from `Src/Zle/computil.c:233-236`. qsort comparator over Cdstr
394/// pointers — compares the `sortstr` fields via `zstrcmp` (case-
395/// sensitive by default).
396pub fn cd_sort(a: &cdstr, b: &cdstr) -> std::cmp::Ordering {
397    // c:233
398    crate::ported::sort::zstrcmp(
399        a.sortstr.as_deref().unwrap_or(""),
400        b.sortstr.as_deref().unwrap_or(""),
401        0,
402    ) // c:235
403}
404
405/// Direct port of `static int cd_prep(void)` from
406/// `Src/Zle/computil.c:239-439`. Builds the `cd_state.runs` chain
407/// from the parsed `cd_state.sets`.
408///
409/// Three branches:
410///   - groups (cd_state.groups > 0): build CRT_EXPL + CRT_SPEC/
411///     CRT_DUMMY interleaved + CRT_SIMPLE per set. The most complex
412///     path; depends on width tracking via cd_state.gprew. **This
413///     branch returns 1 when the laid-out group width exceeds the
414///     terminal — the caller (cd_init at c:582-586) loops with a
415///     shrunken maxg until prep succeeds.**
416///   - showd (cd_state.showd != 0): emit CRT_DESC for entries with
417///     descriptions and CRT_SIMPLE for plain matches per set.
418///   - default: one CRT_SIMPLE run per set.
419pub fn cd_prep() -> i32 {
420    // c:239
421    // CRT_SIMPLE/DESC/SPEC/DUMMY/EXPL declared at the top of this file
422
423    // Build the new runs list as a Vec; link into cd_state.runs at the end.
424    let mut new_runs: Vec<Box<cdrun>> = Vec::new();
425    let mut st = cd_state.lock().unwrap();
426    st.runs = None;
427
428    if st.groups != 0 {
429        // c:247-394 — groups path. Full algorithm: collect leaders
430        // (kind==1 from cd_group OR kind==0+desc standalone) into a
431        // `prep_lines` Vec; sort by width inside each leader's .other
432        // chain; track per-column widths; bail-with-1 on overflow;
433        // sort by sortstr; dedup-adjacent so same-desc entries cluster;
434        // emit CRT_EXPL header + CRT_SPEC per leader column 0; for each
435        // additional column emit CRT_DUMMY/CRT_SPEC interleave; finally
436        // emit CRT_SIMPLE per set for un-described entries.
437
438        let maxg = st.maxg.max(1) as usize;
439        let maxmlen = st.maxmlen;
440        let maxglen = st.maxglen;
441        let swidth = st.swidth;
442
443        // c:256 — wids[0..maxg] tracks max width per column.
444        let mut wids: Vec<i32> = vec![0; maxg];
445
446        // c:257-287 — collect leaders into prep_lines (Vec of owned
447        // cdstr clones with their .other chains).
448        let mut prep_lines: Vec<Box<cdstr>> = Vec::new();
449        let mut set = st.sets.as_deref();
450        while let Some(s) = set {
451            let mut str_iter = s.strs.as_deref();
452            while let Some(node) = str_iter {
453                if node.kind != 1 {
454                    if node.kind == 0 && node.desc.is_some() {
455                        // c:262
456                        if node.width > wids[0] {
457                            // c:263
458                            wids[0] = node.width;
459                        }
460                        let mut clone = Box::new({
461                            let n = node;
462                            cdstr {
463                                next: None,
464                                str: n.str.clone(),
465                                desc: n.desc.clone(),
466                                r#match: n.r#match.clone(),
467                                sortstr: n.sortstr.clone(),
468                                len: n.len,
469                                width: n.width,
470                                other: None,
471                                kind: n.kind,
472                                set: n.set,
473                                run: None,
474                            }
475                        });
476                        clone.other = None; // c:265
477                        prep_lines.push(clone);
478                    }
479                    str_iter = node.next.as_deref();
480                    continue;
481                }
482                // c:270 — kind==1 leader: collect, sort its .other by
483                // width descending, update wids[i] per column.
484                let mut gs = Box::new({
485                    let n = node;
486                    cdstr {
487                        next: None,
488                        str: n.str.clone(),
489                        desc: n.desc.clone(),
490                        r#match: n.r#match.clone(),
491                        sortstr: n.sortstr.clone(),
492                        len: n.len,
493                        width: n.width,
494                        other: None,
495                        kind: n.kind,
496                        set: n.set,
497                        run: None,
498                    }
499                });
500                gs.kind = 2; // c:271
501                gs.other = None;
502
503                // Walk node.other; build a sorted insert into gs.other
504                // by descending width (matches c:274-281).
505                let mut gp = node.other.as_deref();
506                while let Some(g_node) = gp {
507                    let new_clone = Box::new({
508                        let n = g_node;
509                        cdstr {
510                            next: None,
511                            str: n.str.clone(),
512                            desc: n.desc.clone(),
513                            r#match: n.r#match.clone(),
514                            sortstr: n.sortstr.clone(),
515                            len: n.len,
516                            width: n.width,
517                            other: None,
518                            kind: n.kind,
519                            set: n.set,
520                            run: None,
521                        }
522                    });
523                    // Sorted-insert by width descending.
524                    // Drain gs's .other chain into a flat Vec, sort-insert
525                    // new_clone, then rebuild the chain.
526                    let mut chain: Vec<Box<cdstr>> = Vec::new();
527                    // First entry: a clone of gs itself (without other).
528                    chain.push(Box::new(cdstr {
529                        next: None,
530                        str: gs.str.clone(),
531                        desc: gs.desc.clone(),
532                        r#match: gs.r#match.clone(),
533                        sortstr: gs.sortstr.clone(),
534                        len: gs.len,
535                        width: gs.width,
536                        other: None,
537                        kind: gs.kind,
538                        set: gs.set,
539                        run: None,
540                    }));
541                    let mut rest = gs.other.take();
542                    while let Some(mut n) = rest {
543                        rest = n.other.take();
544                        chain.push(n);
545                    }
546                    // Find insert index where existing.width <= new_clone.width.
547                    let mut ins = chain.len();
548                    for (i, c) in chain.iter().enumerate() {
549                        if c.width <= new_clone.width {
550                            ins = i;
551                            break;
552                        }
553                    }
554                    chain.insert(ins, new_clone);
555                    // Rebuild gs from chain[0]; link tail via .other.
556                    let mut new_head = chain.remove(0);
557                    let mut tail_ptr: *mut Option<Box<cdstr>> = &mut new_head.other;
558                    for entry in chain {
559                        unsafe {
560                            *tail_ptr = Some(entry);
561                            let nxt = &mut (*tail_ptr).as_mut().unwrap().other;
562                            tail_ptr = nxt as *mut _;
563                        }
564                    }
565                    gs = new_head;
566                    gp = g_node.other.as_deref();
567                }
568
569                // c:282-284 — update wids per column.
570                let mut col = 0usize;
571                let mut walker = Some(gs.as_ref());
572                while let Some(g) = walker {
573                    if col < wids.len() && g.width > wids[col] {
574                        wids[col] = g.width;
575                    }
576                    col += 1;
577                    walker = g.other.as_deref();
578                }
579
580                prep_lines.push(gs);
581                str_iter = node.next.as_deref();
582            }
583            set = s.next.as_deref();
584        }
585
586        // c:289-292 — gprew = sum(wids[i] + CM_SPACE).
587        let mut gprew = 0i32;
588        for w in &wids {
589            gprew += w + CM_SPACE;
590        }
591        st.gprew = gprew;
592
593        // c:294 — bail with retry if too wide.
594        if gprew > maxmlen && maxglen > 1 {
595            let _ = swidth;
596            return 1;
597        }
598
599        // c:297-303 — set sortstr from unmetafy(str) for each line.
600        for line in prep_lines.iter_mut() {
601            let s = line.str.clone().unwrap_or_default();
602            line.sortstr = Some(crate::ported::utils::unmeta(&s));
603        }
604
605        // c:305 — sort if requested.
606        // We have to drop the lock briefly because cd_groups_want_sorting
607        // re-acquires it.
608        let want_sort = {
609            drop(st);
610            let r = cd_groups_want_sorting();
611            st = cd_state.lock().unwrap();
612            r
613        };
614        if want_sort != 0 {
615            // c:305
616            prep_lines.sort_by(|a, b| cd_sort(a, b)); // c:306
617        }
618
619        // c:308-322 — dedup-adjacent: shuffle same-desc entries together.
620        let mut i = 0usize;
621        while i + 1 < prep_lines.len() {
622            let strp_desc = prep_lines[i].desc.clone().unwrap_or_default();
623            let next_desc = prep_lines[i + 1].desc.clone().unwrap_or_default();
624            if strp_desc == next_desc {
625                i += 1;
626                continue;
627            }
628            // Find a later entry with matching desc; bubble it to i+1.
629            let mut found: Option<usize> = None;
630            for j in i + 2..prep_lines.len() {
631                if prep_lines[j].desc.clone().unwrap_or_default() == strp_desc {
632                    found = Some(j);
633                    break;
634                }
635            }
636            if let Some(j) = found {
637                let entry = prep_lines.remove(j);
638                prep_lines.insert(i + 1, entry);
639            }
640            i += 1;
641        }
642
643        let preplines = prep_lines.len();
644
645        // c:323-326 — CRT_EXPL header: link all preplines via .run.
646        // Build a chain of header cdstrs (desc + str only).
647        if preplines > 0 {
648            let mut expl_head: Option<Box<cdstr>> = None;
649            let mut tail_ptr: *mut Option<Box<cdstr>> = &mut expl_head;
650            for line in &prep_lines {
651                let header = Box::new(cdstr {
652                    next: None,
653                    str: line.str.clone(),
654                    desc: line.desc.clone(),
655                    r#match: line.r#match.clone(),
656                    sortstr: line.sortstr.clone(),
657                    len: line.len,
658                    width: line.width,
659                    other: None,
660                    kind: line.kind,
661                    set: line.set,
662                    run: None,
663                });
664                unsafe {
665                    *tail_ptr = Some(header);
666                    let nxt = &mut (*tail_ptr).as_mut().unwrap().run;
667                    tail_ptr = nxt as *mut _;
668                }
669            }
670            // c:323-326 — emit CRT_EXPL run with the header chain.
671            let expl_run = Box::new(cdrun {
672                next: None,
673                r#type: CRT_EXPL,
674                strs: expl_head,
675                count: preplines as i32,
676            });
677            // Store at the END (matches c:373 `*runp = expl; runp = &(expl->next)`).
678            // We'll insert after column-emit runs below.
679
680            // c:328-340 — emit CRT_SPEC for each column-0 leader.
681            // Each line has a .other chain; we consume it column-by-column.
682            let mut grps: Vec<Option<Box<cdstr>>> = prep_lines.into_iter().map(Some).collect();
683
684            for line_opt in grps.iter_mut() {
685                if let Some(line) = line_opt.take() {
686                    let mut owned = *line;
687                    let next_col = owned.other.take();
688                    owned.run = None;
689                    let spec_run = Box::new(cdrun {
690                        next: None,
691                        r#type: CRT_SPEC,
692                        strs: Some(Box::new(owned)),
693                        count: 1,
694                    });
695                    new_runs.push(spec_run);
696                    *line_opt = next_col;
697                }
698            }
699
700            // c:343-372 — for columns 1..maxg, emit CRT_DUMMY/CRT_SPEC.
701            for _col in 1..maxg {
702                let mut dummy_count = 0i32;
703                for line_opt in grps.iter_mut() {
704                    if let Some(line) = line_opt.take() {
705                        // Flush pending dummies first.
706                        if dummy_count > 0 {
707                            new_runs.push(Box::new(cdrun {
708                                next: None,
709                                r#type: CRT_DUMMY,
710                                strs: None,
711                                count: dummy_count,
712                            }));
713                            dummy_count = 0;
714                        }
715                        let mut owned = *line;
716                        let next_col = owned.other.take();
717                        owned.run = None;
718                        new_runs.push(Box::new(cdrun {
719                            next: None,
720                            r#type: CRT_SPEC,
721                            strs: Some(Box::new(owned)),
722                            count: 1,
723                        }));
724                        *line_opt = next_col;
725                    } else {
726                        dummy_count += 1;
727                    }
728                }
729                if dummy_count > 0 {
730                    // c:365
731                    new_runs.push(Box::new(cdrun {
732                        next: None,
733                        r#type: CRT_DUMMY,
734                        strs: None,
735                        count: dummy_count,
736                    }));
737                }
738            }
739
740            // c:373 — append the expl run at the end of the column emits.
741            new_runs.push(expl_run);
742        }
743
744        // c:376-394 — emit CRT_SIMPLE per set for entries without
745        // kind and without desc (the un-described ones).
746        let mut set = st.sets.as_deref();
747        while let Some(s) = set {
748            let mut head: Option<Box<cdstr>> = None;
749            let mut tail_ptr: *mut Option<Box<cdstr>> = &mut head;
750            let mut count = 0i32;
751            let mut str_iter = s.strs.as_deref();
752            while let Some(node) = str_iter {
753                if node.kind == 0 && node.desc.is_none() {
754                    let clone = Box::new(cdstr {
755                        next: None,
756                        str: node.str.clone(),
757                        desc: None,
758                        r#match: node.r#match.clone(),
759                        sortstr: node.sortstr.clone(),
760                        len: node.len,
761                        width: node.width,
762                        other: None,
763                        kind: 0,
764                        set: node.set,
765                        run: None,
766                    });
767                    unsafe {
768                        *tail_ptr = Some(clone);
769                        let nxt = &mut (*tail_ptr).as_mut().unwrap().run;
770                        tail_ptr = nxt as *mut _;
771                    }
772                    count += 1;
773                }
774                str_iter = node.next.as_deref();
775            }
776            if count > 0 {
777                new_runs.push(Box::new(cdrun {
778                    next: None,
779                    r#type: CRT_SIMPLE,
780                    strs: head,
781                    count,
782                }));
783            }
784            set = s.next.as_deref();
785        }
786    } else if st.showd != 0 {
787        // c:395-423 — showd: emit CRT_DESC (described entries) then
788        // CRT_SIMPLE (undescribed) per set.
789        let mut set = st.sets.as_deref();
790        while let Some(s) = set {
791            if s.desc > 0 {
792                // c:397-409 — CRT_DESC for entries with descriptions.
793                let mut head: Option<Box<cdstr>> = None;
794                let mut tail: *mut Option<Box<cdstr>> = &mut head;
795                let mut str_iter = s.strs.as_deref();
796                while let Some(st_node) = str_iter {
797                    if st_node.desc.is_some() {
798                        let clone = Box::new(cdstr {
799                            next: None,
800                            str: st_node.str.clone(),
801                            desc: st_node.desc.clone(),
802                            r#match: st_node.r#match.clone(),
803                            sortstr: st_node.sortstr.clone(),
804                            len: st_node.len,
805                            width: st_node.width,
806                            other: None,
807                            kind: st_node.kind,
808                            set: st_node.set,
809                            run: None,
810                        });
811                        unsafe {
812                            *tail = Some(clone);
813                            let nxt = &mut (*tail).as_mut().unwrap().run;
814                            tail = nxt as *mut _;
815                        }
816                    }
817                    str_iter = st_node.next.as_deref();
818                }
819                new_runs.push(Box::new(cdrun {
820                    next: None,
821                    r#type: CRT_DESC,
822                    strs: head,
823                    count: s.desc,
824                }));
825            }
826            if s.desc != s.count {
827                // c:410-422 — CRT_SIMPLE for undescribed entries.
828                let mut head: Option<Box<cdstr>> = None;
829                let mut tail: *mut Option<Box<cdstr>> = &mut head;
830                let mut str_iter = s.strs.as_deref();
831                while let Some(st_node) = str_iter {
832                    if st_node.desc.is_none() {
833                        let clone = Box::new(cdstr {
834                            next: None,
835                            str: st_node.str.clone(),
836                            desc: st_node.desc.clone(),
837                            r#match: st_node.r#match.clone(),
838                            sortstr: st_node.sortstr.clone(),
839                            len: st_node.len,
840                            width: st_node.width,
841                            other: None,
842                            kind: st_node.kind,
843                            set: st_node.set,
844                            run: None,
845                        });
846                        unsafe {
847                            *tail = Some(clone);
848                            let nxt = &mut (*tail).as_mut().unwrap().run;
849                            tail = nxt as *mut _;
850                        }
851                    }
852                    str_iter = st_node.next.as_deref();
853                }
854                new_runs.push(Box::new(cdrun {
855                    next: None,
856                    r#type: CRT_SIMPLE,
857                    strs: head,
858                    count: s.count - s.desc,
859                }));
860            }
861            set = s.next.as_deref();
862        }
863    } else {
864        // c:424-435 — default: one CRT_SIMPLE per non-empty set.
865        let mut set = st.sets.as_deref();
866        while let Some(s) = set {
867            if s.count != 0 {
868                // c:431 — link str.run = str.next for each entry.
869                let mut head: Option<Box<cdstr>> = None;
870                let mut tail: *mut Option<Box<cdstr>> = &mut head;
871                let mut str_iter = s.strs.as_deref();
872                while let Some(st_node) = str_iter {
873                    let clone = Box::new(cdstr {
874                        next: None,
875                        str: st_node.str.clone(),
876                        desc: st_node.desc.clone(),
877                        r#match: st_node.r#match.clone(),
878                        sortstr: st_node.sortstr.clone(),
879                        len: st_node.len,
880                        width: st_node.width,
881                        other: None,
882                        kind: st_node.kind,
883                        set: st_node.set,
884                        run: None,
885                    });
886                    unsafe {
887                        *tail = Some(clone);
888                        let nxt = &mut (*tail).as_mut().unwrap().run;
889                        tail = nxt as *mut _;
890                    }
891                    str_iter = st_node.next.as_deref();
892                }
893                new_runs.push(Box::new(cdrun {
894                    next: None,
895                    r#type: CRT_SIMPLE,
896                    strs: head,
897                    count: s.count,
898                }));
899            }
900            set = s.next.as_deref();
901        }
902    }
903
904    // Link new_runs as a chain into cd_state.runs.
905    let mut head: Option<Box<cdrun>> = None;
906    for run in new_runs.into_iter().rev() {
907        let mut run = run;
908        run.next = head;
909        head = Some(run);
910    }
911    st.runs = head;
912    0 // c:438
913}
914
915/// Port of `static char **cd_arrcat(char **a, char **b)` from
916/// `Src/Zle/computil.c:444`. Concatenates string arrays `a` + `b`
917/// into a fresh heap-allocated NULL-terminated array.
918/// ```c
919/// static char **
920/// cd_arrcat(char **a, char **b)
921/// {
922///     if (!b) return zarrdup(a);
923///     else {
924///         char **r = zalloc((arrlen(a) + arrlen(b) + 1) * sizeof(char *));
925///         char **p = r;
926///         for (; *a; a++) *p++ = ztrdup(*a);
927///         for (; *b; b++) *p++ = ztrdup(*b);
928///         *p = NULL;
929///         return r;
930///     }
931/// }
932/// ```
933pub fn cd_arrcat(a: &[String], b: &[String]) -> Vec<String> {
934    // c:444
935    // c:446-447 — `if (!b) return zarrdup(a);` collapses to the
936    // generic path since `&[String]` is never null in Rust; an
937    // empty slice yields the same result as zarrdup(a).
938    let mut r: Vec<String> = Vec::with_capacity(a.len() + b.len()); // c:449
939    for s in a {
940        // c:453 for (; *a; a++)
941        r.push(ztrdup(s)); // c:454 *p++ = ztrdup(*a)
942    }
943    for s in b {
944        // c:455 for (; *b; b++)
945        r.push(ztrdup(s)); // c:456 *p++ = ztrdup(*b)
946    }
947    // c:458 — `*p = NULL;` — Rust Vec doesn't need a sentinel
948    r // c:460
949}
950
951/// Direct port of `static int cd_init(char *nam, char *hide, char *mlen,
952///                                       char *sep, char **opts, char **args,
953///                                       int disp)`
954/// from `Src/Zle/computil.c:477-594`. Parses the `_describe` input
955/// (match arrays + optional display arrays) into the `cd_state.sets`
956/// chain, then runs `cd_calc` + `cd_prep` to build the run chain.
957///
958/// `args` is the consolidated arg list — match-array param name,
959/// optional disp-array name, optional `--`-separated per-set opts.
960/// `-g` prefix on `args` enables group detection (cd_group loop).
961pub fn cd_init(
962    nam: &str,
963    hide: &str,
964    mlen: &str,
965    sep: &str, // c:477
966    opts: &[String],
967    args: &[String],
968    disp: i32,
969) -> i32 {
970    // c:485 — discard prior parsed state.
971    if cd_parsed.load(Ordering::Relaxed) != 0 {
972        let mut st = cd_state.lock().unwrap();
973        st.sep = None;
974        freecdsets(st.sets.take());
975        cd_parsed.store(0, Ordering::Relaxed);
976    }
977
978    // c:491 — seed cd_state.
979    {
980        let mut st = cd_state.lock().unwrap();
981        st.sep = Some(sep.to_string());
982        st.slen = sep.len() as i32;
983        st.swidth = niceztrlen(sep) as i32;
984        st.sets = None;
985        st.showd = disp;
986        st.maxg = 0;
987        st.groups = 0;
988        st.descs = 0;
989        st.maxmlen = mlen.parse::<i32>().unwrap_or(0);
990        st.premaxw = 0;
991        let cols = adjustcolumns() as i32;
992        let itmp = cols - st.swidth - 4; // c:499
993        if st.maxmlen > itmp {
994            st.maxmlen = itmp;
995        }
996        if st.maxmlen < 4 {
997            st.maxmlen = 4;
998        }
999    }
1000
1001    // c:504 — strip leading `-g` for group detection.
1002    let mut idx = 0usize;
1003    let grp = if args.first().map(|s| s.as_str()) == Some("-g") {
1004        idx = 1;
1005        true
1006    } else {
1007        false
1008    };
1009
1010    // c:508 — walk arg pairs (match-array [disp-array] [-- opts]).
1011    let mut sets_collected: Vec<Box<cdset>> = Vec::new();
1012    while idx < args.len() {
1013        let arg = &args[idx];
1014        let Some(mat_arr) = get_user_var(Some(arg.as_str())) else {
1015            // c:515
1016            zwarnnam(nam, &format!("invalid argument: {}", arg));
1017            let mut st = cd_state.lock().unwrap();
1018            st.sep = None;
1019            freecdsets(st.sets.take());
1020            return 1;
1021        };
1022        idx += 1;
1023
1024        // c:521-543 — parse `match:desc` entries into cdstr chain.
1025        let mut strs_vec: Vec<Box<cdstr>> = Vec::new();
1026        for entry in &mat_arr {
1027            let bytes = entry.as_bytes();
1028            let mut p = 0usize;
1029            while p < bytes.len() && bytes[p] != b':' {
1030                // c:530
1031                if bytes[p] == b'\\' && p + 1 < bytes.len() {
1032                    p += 1;
1033                }
1034                p += 1;
1035            }
1036            let (match_part, desc_part) = if p < bytes.len() {
1037                let m = std::str::from_utf8(&bytes[..p]).unwrap_or("");
1038                let d = std::str::from_utf8(&bytes[p + 1..]).unwrap_or("");
1039                (rembslash(m), Some(rembslash(d)))
1040            } else {
1041                (rembslash(entry), None)
1042            };
1043            let str_s = match_part.clone();
1044            let mut new_str = Box::new(cdstr::default());
1045            new_str.str = Some(str_s.clone());
1046            new_str.r#match = Some(str_s.clone());
1047            new_str.desc = desc_part;
1048            new_str.len = str_s.len() as i32;
1049            new_str.width = niceztrlen(&str_s) as i32;
1050            new_str.kind = 0;
1051            strs_vec.push(new_str);
1052        }
1053
1054        // c:547-557 — optional separate match array.
1055        if idx < args.len() && !args[idx].starts_with('-') {
1056            let Some(match_arr) = get_user_var(Some(args[idx].as_str())) else {
1057                zwarnnam(nam, &format!("invalid argument: {}", args[idx]));
1058                let mut st = cd_state.lock().unwrap();
1059                st.sep = None;
1060                freecdsets(st.sets.take());
1061                return 1;
1062            };
1063            for (i, m) in match_arr.iter().enumerate() {
1064                if i < strs_vec.len() {
1065                    strs_vec[i].r#match = Some(m.clone());
1066                }
1067            }
1068            idx += 1;
1069        }
1070
1071        // c:559 — apply hide (strip leading `-`/`--` from str).
1072        if !hide.is_empty() {
1073            let hb = hide.as_bytes();
1074            let double = hb.len() > 1;
1075            for s in strs_vec.iter_mut() {
1076                if let Some(cur) = s.str.clone() {
1077                    let mut bytes = cur.into_bytes();
1078                    if double && bytes.len() >= 2 && bytes[0] == b'-' && bytes[1] == b'-' {
1079                        bytes.drain(0..2); // c:564
1080                    } else if !bytes.is_empty() && (bytes[0] == b'-' || bytes[0] == b'+') {
1081                        bytes.drain(0..1); // c:566
1082                    }
1083                    s.str = String::from_utf8(bytes).ok();
1084                }
1085            }
1086        }
1087
1088        // c:569-577 — gather per-set opts up to `--`.
1089        let opt_start = idx;
1090        while idx < args.len()
1091            && !(args[idx].as_bytes().len() == 2
1092                && args[idx].as_bytes()[0] == b'-'
1093                && args[idx].as_bytes()[1] == b'-')
1094        {
1095            idx += 1;
1096        }
1097        let per_set: &[String] = &args[opt_start..idx];
1098        let combined = cd_arrcat(per_set, opts);
1099        if idx < args.len() {
1100            idx += 1;
1101        } // c:577 skip `--`
1102
1103        // Link strs_vec as a chain into a new cdset.
1104        let mut strs_head: Option<Box<cdstr>> = None;
1105        for s in strs_vec.into_iter().rev() {
1106            let mut s = s;
1107            s.next = strs_head;
1108            strs_head = Some(s);
1109        }
1110        let mut set = Box::new(cdset::default());
1111        set.opts = Some(combined);
1112        set.strs = strs_head;
1113        sets_collected.push(set);
1114    }
1115
1116    // Link sets_collected as a chain into cd_state.sets.
1117    {
1118        let mut head: Option<Box<cdset>> = None;
1119        for s in sets_collected.into_iter().rev() {
1120            let mut s = s;
1121            s.next = head;
1122            head = Some(s);
1123        }
1124        cd_state.lock().unwrap().sets = head;
1125    }
1126
1127    // c:579 — group-aware vs simple prep.
1128    if disp != 0 && grp {
1129        let cols = adjustcolumns() as i32;
1130        let mut mg = cols;
1131        // c:582-586 — retry cd_prep with shrinking maxg.
1132        loop {
1133            cd_group(mg);
1134            mg = {
1135                let st = cd_state.lock().unwrap();
1136                st.maxg - 1
1137            };
1138            cd_calc();
1139            if cd_prep() == 0 || mg <= 0 {
1140                break;
1141            }
1142        }
1143    } else {
1144        cd_calc();
1145        cd_prep();
1146    }
1147    cd_parsed.store(1, Ordering::Relaxed); // c:592
1148    0
1149}
1150
1151/// Direct port of `static char **cd_arrdup(char **a)` from
1152/// `Src/Zle/computil.c:somewhere`. Duplicate a string array.
1153pub fn cd_arrdup(a: &[String]) -> Vec<String> {
1154    // c:cd_arrdup
1155    a.to_vec()
1156}
1157
1158/// Direct port of `static int cd_get(char **params)` from
1159/// `Src/Zle/computil.c:614-841`. Pops the next `cdrun` off
1160/// `cd_state.runs` and emits its match/display arrays + per-run
1161/// compadd options into the four named params:
1162///   `params[0]` = csl ("" or "packed")
1163///   `params[1]` = opts (compadd flags)
1164///   `params[2]` = mats (match strings)
1165///   `params[3]` = dpys (display strings)
1166/// Returns 1 when no runs remain, 0 otherwise.
1167pub fn cd_get(params: &[String]) -> i32 {
1168    // c:614
1169
1170    // c:618 — pop the head run.
1171    let run_opt = {
1172        let mut st = cd_state.lock().unwrap();
1173        st.runs.take().map(|mut r| {
1174            let next = r.next.take();
1175            st.runs = next;
1176            r
1177        })
1178    };
1179    let Some(run) = run_opt else {
1180        return 1;
1181    };
1182
1183    let mut mats: Vec<String> = Vec::new();
1184    let mut dpys: Vec<String> = Vec::new();
1185    let mut opts: Vec<String> = Vec::new();
1186    let mut csl: String = String::new();
1187
1188    let rtype = run.r#type;
1189
1190    // Helper: walk a cdstr chain via .run, applying f.
1191    let mut walk_run = |head: &Option<Box<cdstr>>, mut f: Box<dyn FnMut(&cdstr)>| {
1192        let mut cur = head.as_deref();
1193        while let Some(s) = cur {
1194            f(s);
1195            cur = s.run.as_deref();
1196        }
1197    };
1198
1199    if rtype == CRT_SIMPLE {
1200        // c:625
1201        let head_opts = run
1202            .strs
1203            .as_deref()
1204            .map(|s| {
1205                let st = cd_state.lock().unwrap();
1206                // c:634 — zarrdup(run->strs->set->opts). Set is an index;
1207                // we walk cd_state.sets to find the matching index.
1208                let mut set_iter = st.sets.as_deref();
1209                let mut found: Option<Vec<String>> = None;
1210                let mut idx_count = 0usize;
1211                while let Some(set) = set_iter {
1212                    if idx_count == s.set {
1213                        found = set.opts.clone();
1214                        break;
1215                    }
1216                    idx_count += 1;
1217                    set_iter = set.next.as_deref();
1218                }
1219                found.unwrap_or_default()
1220            })
1221            .unwrap_or_default();
1222        walk_run(
1223            &run.strs,
1224            Box::new(|s| {
1225                // c:629
1226                mats.push(s.r#match.clone().unwrap_or_default());
1227                dpys.push(
1228                    s.str
1229                        .clone()
1230                        .or_else(|| s.r#match.clone())
1231                        .unwrap_or_default(),
1232                );
1233            }),
1234        );
1235        let groups_flag = cd_state.lock().unwrap().groups;
1236        opts = if groups_flag != 0 {
1237            // c:635
1238            // c:641 — strip `-X` options.
1239            let mut filtered: Vec<String> = Vec::new();
1240            let mut skip_next = false;
1241            for o in head_opts.iter() {
1242                if skip_next {
1243                    skip_next = false;
1244                    continue;
1245                }
1246                if o.starts_with("-X") {
1247                    // c:642
1248                    if o.len() == 2 {
1249                        skip_next = true;
1250                    } // c:643
1251                    continue;
1252                }
1253                filtered.push(o.clone());
1254            }
1255            filtered
1256        } else {
1257            head_opts
1258        };
1259    } else if rtype == CRT_DESC {
1260        // c:652
1261        let st_snapshot = {
1262            let st = cd_state.lock().unwrap();
1263            (
1264                st.pre,
1265                st.suf,
1266                st.premaxw,
1267                st.slen,
1268                st.swidth,
1269                st.sep.clone(),
1270                adjustcolumns() as i32,
1271            )
1272        };
1273        let (cd_pre, _cd_suf, cd_premaxw, _cd_slen, cd_swidth, cd_sep, cols) = st_snapshot;
1274        let sep_str = cd_sep.unwrap_or_default();
1275        walk_run(
1276            &run.strs,
1277            Box::new(|s| {
1278                // c:669
1279                let str_s = s.str.clone().unwrap_or_default();
1280                let desc_s = s.desc.clone().unwrap_or_default();
1281                let mut buf =
1282                    String::with_capacity((cd_pre + cd_premaxw + cd_swidth + 16) as usize);
1283                // c:674 — write str.
1284                buf.push_str(&str_s);
1285                // c:676 — pad to premaxw + CM_SPACE.
1286                let pad = (cd_premaxw - s.width + CM_SPACE).max(0) as usize;
1287                for _ in 0..pad {
1288                    buf.push(' ');
1289                }
1290
1291                // c:679-715 — append separator + truncated desc to fit terminal.
1292                let mut remw = cols - cd_premaxw - cd_swidth - 3;
1293                while remw < 0 && cols > 0 {
1294                    remw += cols;
1295                }
1296                if (sep_str.len() as i32) < remw {
1297                    // c:685
1298                    buf.push_str(&sep_str);
1299                    remw -= sep_str.len() as i32;
1300                    let dw = niceztrlen(&desc_s) as i32;
1301                    if dw <= remw {
1302                        buf.push_str(&desc_s);
1303                    } else {
1304                        // c:701
1305                        // Truncate desc to fit. Use char boundaries.
1306                        let mut w_used = 0i32;
1307                        for ch in desc_s.chars() {
1308                            let cw = niceztrlen(&ch.to_string()) as i32;
1309                            if w_used + cw > remw {
1310                                break;
1311                            }
1312                            buf.push(ch);
1313                            w_used += cw;
1314                        }
1315                    }
1316                }
1317                mats.push(s.r#match.clone().unwrap_or_default()); // c:673
1318                dpys.push(buf);
1319            }),
1320        );
1321        // c:721 — opts = cd_arrdup + opts[0] = "-l".
1322        let head_opts = run
1323            .strs
1324            .as_deref()
1325            .map(|s| {
1326                let st = cd_state.lock().unwrap();
1327                let mut set_iter = st.sets.as_deref();
1328                let mut found: Option<Vec<String>> = None;
1329                let mut idx_count = 0usize;
1330                while let Some(set) = set_iter {
1331                    if idx_count == s.set {
1332                        found = set.opts.clone();
1333                        break;
1334                    }
1335                    idx_count += 1;
1336                    set_iter = set.next.as_deref();
1337                }
1338                found.unwrap_or_default()
1339            })
1340            .unwrap_or_default();
1341        opts = std::iter::once("-l".to_string()).chain(head_opts).collect();
1342    } else if rtype == CRT_SPEC {
1343        // c:726
1344        let s = run.strs.as_deref();
1345        if let Some(s) = s {
1346            mats.push(s.r#match.clone().unwrap_or_default());
1347            dpys.push(s.str.clone().unwrap_or_default());
1348        }
1349        // c:732 — opts = cd_arrdup + flip -J/-V to -2V or insert -2V-default-.
1350        let head_opts = s
1351            .map(|s| {
1352                let st = cd_state.lock().unwrap();
1353                let mut set_iter = st.sets.as_deref();
1354                let mut found: Option<Vec<String>> = None;
1355                let mut idx_count = 0usize;
1356                while let Some(set) = set_iter {
1357                    if idx_count == s.set {
1358                        found = set.opts.clone();
1359                        break;
1360                    }
1361                    idx_count += 1;
1362                    set_iter = set.next.as_deref();
1363                }
1364                found.unwrap_or_default()
1365            })
1366            .unwrap_or_default();
1367        let mut new_opts: Vec<String> = head_opts.clone();
1368        let mut found_jv = false;
1369        // c:736 — `for (dp = opts + 1; *dp; dp++)`. Skip slot 0 (the
1370        // existing first element which we'll overwrite below) and look
1371        // for the first -J/-V flag.
1372        for i in 1..new_opts.len() {
1373            if new_opts[i].starts_with("-J") || new_opts[i].starts_with("-V") {
1374                let rest = new_opts[i][2..].to_string();
1375                new_opts[i] = format!("-2V{}", rest);
1376                found_jv = true;
1377                break;
1378            }
1379        }
1380        if !found_jv {
1381            new_opts.insert(0, "-2V-default-".to_string()); // c:750
1382        }
1383        opts = new_opts;
1384        csl = "packed".to_string();
1385    } else if rtype == CRT_DUMMY {
1386        // c:754
1387        // c:758 — opts[0] = "-E<count>".
1388        let head_opts = run
1389            .strs
1390            .as_deref()
1391            .map(|s| {
1392                let st = cd_state.lock().unwrap();
1393                let mut set_iter = st.sets.as_deref();
1394                let mut found: Option<Vec<String>> = None;
1395                let mut idx_count = 0usize;
1396                while let Some(set) = set_iter {
1397                    if idx_count == s.set {
1398                        found = set.opts.clone();
1399                        break;
1400                    }
1401                    idx_count += 1;
1402                    set_iter = set.next.as_deref();
1403                }
1404                found.unwrap_or_default()
1405            })
1406            .unwrap_or_default();
1407        opts = std::iter::once(format!("-E{}", run.count))
1408            .chain(head_opts)
1409            .collect();
1410        csl = "packed".to_string();
1411    } else if rtype == CRT_EXPL {
1412        // c:772
1413        let st_snapshot = {
1414            let st = cd_state.lock().unwrap();
1415            (
1416                st.suf,
1417                st.slen,
1418                st.swidth,
1419                st.gprew,
1420                st.sep.clone(),
1421                adjustcolumns() as i32,
1422            )
1423        };
1424        let (_cd_suf, _cd_slen, cd_swidth, cd_gprew, cd_sep, cols) = st_snapshot;
1425        let sep_str = cd_sep.unwrap_or_default();
1426        let count = run.count;
1427
1428        walk_run(
1429            &run.strs,
1430            Box::new(|s| {
1431                // c:785
1432                // c:786 — if run sibling has same desc, emit empty.
1433                let next_desc = s.run.as_deref().and_then(|n| n.desc.clone());
1434                if next_desc.is_some() && next_desc == s.desc {
1435                    dpys.push(String::new());
1436                    return;
1437                }
1438                let mut buf = String::new();
1439                buf.push_str(&sep_str);
1440                let mut remw = cols - cd_gprew - cd_swidth - CM_SPACE;
1441                let desc_s = s.desc.clone().unwrap_or_default();
1442                let dw = niceztrlen(&desc_s) as i32;
1443                if dw <= remw {
1444                    // c:797
1445                    buf.push_str(&desc_s);
1446                    remw -= dw;
1447                } else {
1448                    for ch in desc_s.chars() {
1449                        let cw = niceztrlen(&ch.to_string()) as i32;
1450                        if cw > remw {
1451                            break;
1452                        }
1453                        buf.push(ch);
1454                        remw -= cw;
1455                    }
1456                }
1457                while remw > 0 {
1458                    // c:817
1459                    buf.push(' ');
1460                    remw -= 1;
1461                }
1462                dpys.push(buf);
1463            }),
1464        );
1465        // c:825 — opts[0] = "-E<count>".
1466        let head_opts = run
1467            .strs
1468            .as_deref()
1469            .map(|s| {
1470                let st = cd_state.lock().unwrap();
1471                let mut set_iter = st.sets.as_deref();
1472                let mut found: Option<Vec<String>> = None;
1473                let mut idx_count = 0usize;
1474                while let Some(set) = set_iter {
1475                    if idx_count == s.set {
1476                        found = set.opts.clone();
1477                        break;
1478                    }
1479                    idx_count += 1;
1480                    set_iter = set.next.as_deref();
1481                }
1482                found.unwrap_or_default()
1483            })
1484            .unwrap_or_default();
1485        opts = std::iter::once(format!("-E{}", count))
1486            .chain(head_opts)
1487            .collect();
1488        csl = "packed".to_string();
1489    }
1490
1491    // c:832 — emit the four params.
1492    if params.len() >= 4 {
1493        setsparam(&params[0], &csl);
1494        setaparam(&params[1], opts);
1495        setaparam(&params[2], mats);
1496        setaparam(&params[3], dpys);
1497    }
1498    0 // c:839
1499}
1500
1501/// Direct port of `static int bin_compdescribe(char *nam, char **args,
1502///                                                UNUSED(Options ops),
1503///                                                UNUSED(int func))`
1504/// from `Src/Zle/computil.c:846-895`. Subcommand dispatch for
1505/// `compdescribe -i/-I/-g`:
1506///   - `-i hide mlen ARGS...` → cd_init with empty opts and disp=0
1507///   - `-I hide mlen sep optsParam ARGS...` → cd_init with disp=1
1508///   - `-g param csl mats dpys` → cd_get with the 4 output params
1509pub fn bin_compdescribe(
1510    nam: &str,
1511    args: &[String], // c:846
1512    _ops: &options,
1513    _func: i32,
1514) -> i32 {
1515    if INCOMPFUNC.load(Ordering::Relaxed) != 1 {
1516        // c:850
1517        zwarnnam(nam, "can only be called from completion function");
1518        return 1;
1519    }
1520    if args.is_empty() {
1521        return 1;
1522    }
1523    let a0 = args[0].as_bytes();
1524    // c:854 — `args[0]` must be exactly 2 chars starting with `-`.
1525    if a0.len() != 2 || a0[0] != b'-' {
1526        zwarnnam(nam, &format!("invalid argument: {}", args[0]));
1527        return 1;
1528    }
1529    let n = args.len() as i32;
1530
1531    match a0[1] {
1532        b'i' => {
1533            // c:859
1534            if n < 3 {
1535                zwarnnam(nam, "not enough arguments");
1536                return 1;
1537            }
1538            cd_init(nam, &args[1], &args[2], "", &[], &args[3..], 0) // c:865
1539        }
1540        b'I' => {
1541            // c:866
1542            if n < 6 {
1543                zwarnnam(nam, "not enough arguments");
1544                return 1;
1545            }
1546            // c:874 — getaparam(args[4]).
1547            let opts_arr: Vec<String> = paramtab()
1548                .read()
1549                .ok()
1550                .and_then(|tab| tab.get(&args[4]).and_then(|pm| pm.u_arr.clone()))
1551                .unwrap_or_default();
1552            if opts_arr.is_empty()
1553                && paramtab()
1554                    .read()
1555                    .ok()
1556                    .map_or(true, |tab| tab.get(&args[4]).is_none())
1557            {
1558                zwarnnam(nam, &format!("unknown parameter: {}", args[4]));
1559                return 1;
1560            }
1561            cd_init(
1562                nam,
1563                &args[1],
1564                &args[2],
1565                &args[3],
1566                &opts_arr, // c:878
1567                &args[5..],
1568                1,
1569            )
1570        }
1571        b'g' => {
1572            // c:880
1573            if cd_parsed.load(Ordering::Relaxed) == 0 {
1574                // c:881
1575                zwarnnam(nam, "no parsed state"); // c:889
1576                return 1;
1577            }
1578            if n != 5 {
1579                zwarnnam(
1580                    nam,
1581                    if n < 5 {
1582                        "not enough arguments"
1583                    } else {
1584                        "too many arguments"
1585                    },
1586                );
1587                return 1;
1588            }
1589            cd_get(&args[1..]) // c:887
1590        }
1591        _ => {
1592            zwarnnam(nam, &format!("invalid option: {}", args[0]));
1593            1
1594        }
1595    }
1596}
1597
1598// =====================================================================
1599// `_arguments`-cache types — direct ports of the C structs at
1600// Src/Zle/computil.c:899-968. CAO_* / CAA_* / CDF_SEP /
1601// MAX_CACACHE constants already declared above (file scope).
1602// =====================================================================
1603
1604/// Port of `typedef struct cadef *Cadef` from `Src/Zle/computil.c:899`.
1605pub type Cadef = Box<cadef>; // c:899
1606
1607/// Direct port of `struct cadef` from `Src/Zle/computil.c:905-922`.
1608/// Cache entry for a set of `_arguments` definitions.
1609#[derive(Debug, Default, Clone)]
1610#[allow(non_camel_case_types)]
1611pub struct cadef {
1612    // c:905
1613    pub next: Option<Box<cadef>>,                // c:906 Cadef next
1614    pub snext: Option<Box<cadef>>,               // c:907 Cadef snext
1615    pub opts: Option<Box<caopt>>,                // c:908 Caopt opts
1616    pub nopts: i32,                              // c:909
1617    pub ndopts: i32,                             // c:909
1618    pub nodopts: i32,                            // c:909
1619    pub args: Option<Box<caarg>>,                // c:910 Caarg args
1620    pub rest: Option<Box<caarg>>,                // c:911 Caarg rest
1621    pub defs: Option<Vec<String>>,               // c:912 char **defs
1622    pub ndefs: i32,                              // c:913
1623    pub lastt: i64,                              // c:914 time_t lastt
1624    pub single: Option<Vec<Option<Box<caopt>>>>, // c:915 Caopt *single (188-slot)
1625    pub r#match: Option<String>,                 // c:916 char *match
1626    pub argsactive: i32,                         // c:917
1627    pub set: Option<String>,                     // c:919 char *set
1628    pub flags: i32,                              // c:920 int flags (CDF_*)
1629    pub nonarg: Option<String>,                  // c:921 char *nonarg
1630}
1631/// Port of `typedef struct caopt *Caopt` from `Src/Zle/computil.c:900`.
1632pub type Caopt = Box<caopt>; // c:900
1633
1634/// Direct port of `struct caopt` from `Src/Zle/computil.c:928-939`.
1635/// Description for one `_arguments` option spec.
1636#[derive(Debug, Default, Clone)]
1637#[allow(non_camel_case_types)]
1638pub struct caopt {
1639    // c:928
1640    pub next: Option<Box<caopt>>, // c:929 Caopt next
1641    pub name: Option<String>,     // c:930 char *name
1642    pub descr: Option<String>,    // c:931 char *descr
1643    pub xor: Option<Vec<String>>, // c:932 char **xor
1644    pub r#type: i32,              // c:933 int type (CAO_*)
1645    pub args: Option<Box<caarg>>, // c:934 Caarg args
1646    pub active: i32,              // c:935 int active
1647    pub num: i32,                 // c:936 int num
1648    pub gsname: Option<String>,   // c:937 char *gsname
1649    pub not: i32,                 // c:938 int not
1650}
1651/// Port of `typedef struct caarg *Caarg` from `Src/Zle/computil.c:901`.
1652pub type Caarg = Box<caarg>; // c:901
1653
1654/// Direct port of `struct caarg` from `Src/Zle/computil.c:949-962`.
1655/// Description for one `_arguments` argument spec.
1656#[derive(Debug, Default, Clone)]
1657#[allow(non_camel_case_types)]
1658pub struct caarg {
1659    // c:949
1660    pub next: Option<Box<caarg>>, // c:950 Caarg next
1661    pub descr: Option<String>,    // c:951 char *descr
1662    pub xor: Option<Vec<String>>, // c:952 char **xor
1663    pub action: Option<String>,   // c:953 char *action
1664    pub r#type: i32,              // c:954 int type (CAA_*)
1665    pub end: Option<String>,      // c:955 char *end
1666    pub opt: Option<String>,      // c:956 char *opt
1667    pub num: i32,                 // c:957 int num
1668    pub min: i32,                 // c:958 int min
1669    pub direct: i32,              // c:959 int direct
1670    pub active: i32,              // c:960 int active
1671    pub gsname: Option<String>,   // c:961 char *gsname
1672}
1673
1674/// Port of `CDF_SEP` from `Src/Zle/computil.c:924`. `-S` flag — `--`
1675/// terminates options.
1676pub const CDF_SEP: i32 = 1; // c:924
1677
1678// =====================================================================
1679// CAO_* — Cadef option-argument attachment style — `computil.c:941-945`.
1680// =====================================================================
1681
1682/// Port of `CAO_NEXT` from `computil.c:941`. Argument in next argv slot.
1683pub const CAO_NEXT: i32 = 1; // c:941
1684/// Port of `CAO_DIRECT` from `computil.c:942`. Argument directly attached
1685/// to option (`-opt:value`).
1686pub const CAO_DIRECT: i32 = 2; // c:942
1687/// Port of `CAO_ODIRECT` from `computil.c:943`. Optional direct attach.
1688pub const CAO_ODIRECT: i32 = 3; // c:943
1689/// Port of `CAO_EQUAL` from `computil.c:944`. Argument after `=`.
1690pub const CAO_EQUAL: i32 = 4; // c:944
1691/// Port of `CAO_OEQUAL` from `computil.c:945`. Optional `=` argument.
1692pub const CAO_OEQUAL: i32 = 5; // c:945
1693
1694// =====================================================================
1695// CAA_* — Cadef positional-argument kinds — `computil.c:964-968`.
1696// =====================================================================
1697
1698/// Port of `CAA_NORMAL` from `computil.c:964`. Plain positional arg.
1699pub const CAA_NORMAL: i32 = 1; // c:964
1700/// Port of `CAA_OPT` from `computil.c:965`. Optional positional arg.
1701pub const CAA_OPT: i32 = 2; // c:965
1702/// Port of `CAA_REST` from `computil.c:966`. Mandatory rest of args.
1703pub const CAA_REST: i32 = 3; // c:966
1704/// Port of `CAA_RARGS` from `computil.c:967`. Repeated args sequence.
1705pub const CAA_RARGS: i32 = 4; // c:967
1706/// Port of `CAA_RREST` from `computil.c:968`. Repeated rest of args.
1707pub const CAA_RREST: i32 = 5; // c:968
1708
1709/// Port of `MAX_CACACHE` from `computil.c:972`. Cadef LRU cache size.
1710pub const MAX_CACACHE: usize = 8; // c:972
1711
1712/// Port of `static Cadef cadef_cache[MAX_CACACHE]` from
1713/// `Src/Zle/computil.c:973`. The LRU cache holds parsed
1714/// `_arguments` defs keyed by the raw arg vector — `get_cadef`
1715/// scans linearly, returns on first match (arr-compare on `defs`),
1716/// and on miss evicts the entry with the oldest `lastt` slot before
1717/// inserting the freshly parsed result.
1718pub static cadef_cache: std::sync::Mutex<[Option<Box<cadef>>; MAX_CACACHE]> = // c:973
1719    std::sync::Mutex::new([const { None }; MAX_CACACHE]);
1720
1721/// Direct port of `static int arrcmp(char **a, char **b)` from
1722/// `Src/Zle/computil.c:978`. Element-wise string-equality test on
1723/// two `char**` arrays — returns 1 if both are null or both contain
1724/// the same sequence of strings, 0 otherwise.
1725pub fn arrcmp(a: Option<&[String]>, b: Option<&[String]>) -> i32 {
1726    // c:978
1727    match (a, b) {
1728        (None, None) => 1,          // c:980
1729        (None, _) | (_, None) => 0, // c:982
1730        (Some(a), Some(b)) => {
1731            // c:984
1732            // c:985-988 — walk in lockstep, bail on inequality.
1733            let len = a.len().min(b.len());
1734            for i in 0..len {
1735                if a[i] != b[i] {
1736                    return 0;
1737                } // c:986
1738            }
1739            // c:989 — equal iff both reached end together.
1740            if a.len() == b.len() {
1741                1
1742            } else {
1743                0
1744            }
1745        }
1746    }
1747}
1748
1749/// Direct port of `static void freecaargs(Caarg a)` from
1750/// `Src/Zle/computil.c:996`. Walks the `next` chain and frees
1751/// each entry. In Rust this is `Box` ownership — dropping the head
1752/// recursively drops the chain, but we mirror the C body for ABI
1753/// parity with callers that want explicit teardown.
1754pub fn freecaargs(mut a: Option<Box<caarg>>) {
1755    // c:996
1756    while let Some(mut node) = a {
1757        // c:996 for (; a; ...)
1758        a = node.next.take(); // c:1001 n = a->next
1759                              // c:1002-1007 — zsfree on descr/xor/action/end/opt is implicit
1760                              //               via Drop on the String / Vec<String> fields.
1761        node.descr = None; // c:1013
1762        node.xor = None; // c:1013-1004
1763        node.action = None; // c:1013
1764        node.end = None; // c:1013
1765        node.opt = None; // c:1013
1766        drop(node); // c:1013 zfree(a, sizeof(*a))
1767    }
1768}
1769
1770/// Direct port of `static void freecadef(Cadef d)` from
1771/// `Src/Zle/computil.c:1013`. Walks the `snext` chain freeing
1772/// each cadef plus its opts/args/rest sub-chains.
1773pub fn freecadef(mut d: Option<Box<cadef>>) {
1774    // c:1013
1775    while let Some(mut node) = d {
1776        // c:1013 while (d)
1777        d = node.snext.take(); // c:1019 s = d->snext
1778                               // c:1020-1023 — zsfree match/set, freearray(defs).
1779        node.r#match = None;
1780        node.set = None;
1781        node.defs = None;
1782
1783        // c:1025-1033 — for each opt: zsfree name/descr, freearray xor,
1784        // freecaargs(opt->args), zfree opt.
1785        let mut p = node.opts.take();
1786        while let Some(mut popt) = p {
1787            p = popt.next.take();
1788            popt.name = None;
1789            popt.descr = None;
1790            popt.xor = None;
1791            freecaargs(popt.args.take()); // c:1031
1792            drop(popt); // c:1032
1793        }
1794        freecaargs(node.args.take()); // c:1034
1795        freecaargs(node.rest.take()); // c:1035
1796        node.nonarg = None; // c:1036
1797        node.single = None; // c:1037-1038
1798        drop(node); // c:1039 zfree(d, sizeof(*d))
1799    }
1800}
1801
1802/// Port of `rembslashcolon(char *s)` from `Src/Zle/computil.c:1046`.
1803/// ```c
1804/// static char *
1805/// rembslashcolon(char *s)
1806/// {
1807///     char *p, *r;
1808///     r = p = s = dupstring(s);
1809///     while (*s) {
1810///         if (s[0] != '\\' || s[1] != ':')
1811///             *p++ = *s;
1812///         s++;
1813///     }
1814///     *p = '\0';
1815///     return r;
1816/// }
1817/// ```
1818/// Strip every `\:` two-byte sequence to nothing (the `\` is dropped,
1819/// the `:` follows on the next iteration). Used to unescape colon-
1820/// bearing description strings produced by `_arguments`.
1821pub fn rembslashcolon(s: &str) -> String {
1822    // c:1047
1823    let bytes = s.as_bytes(); // c:1047 dupstring(s)
1824    let mut out = Vec::<u8>::with_capacity(bytes.len());
1825    let mut i = 0;
1826    while i < bytes.len() {
1827        // c:1053 while (*s)
1828        // c:1054 — `if (s[0] != '\\' || s[1] != ':') *p++ = *s`.
1829        let drop = bytes[i] == b'\\' && i + 1 < bytes.len() && bytes[i + 1] == b':';
1830        if !drop {
1831            out.push(bytes[i]); // c:1055 *p++ = *s
1832        }
1833        i += 1; // c:1056 s++
1834    }
1835    // c:1058 — `*p = '\0'`. Rust strings are length-tracked.
1836    String::from_utf8(out).unwrap_or_default() // c:1060 return r
1837}
1838
1839/// Port of `bslashcolon(char *s)` from `Src/Zle/computil.c:1065`.
1840/// ```c
1841/// static char *
1842/// bslashcolon(char *s)
1843/// {
1844///     char *p, *r;
1845///     r = p = zhalloc((2 * strlen(s)) + 1);
1846///     while (*s) {
1847///         if (*s == ':')
1848///             *p++ = '\\';
1849///         *p++ = *s++;
1850///     }
1851///     *p = '\0';
1852///     return r;
1853/// }
1854/// ```
1855/// Insert a backslash before every `:`, doubling the worst-case
1856/// length. Inverse of `rembslashcolon` for description-string
1857/// emission.
1858pub fn bslashcolon(s: &str) -> String {
1859    // c:1066
1860    let bytes = s.as_bytes(); // c:1066 zhalloc(2*strlen(s)+1)
1861    let mut out = Vec::<u8>::with_capacity(2 * bytes.len() + 1);
1862    for &b in bytes {
1863        // c:1072 while (*s)
1864        if b == b':' {
1865            // c:1073
1866            out.push(b'\\'); // c:1074 *p++ = '\\'
1867        }
1868        out.push(b); // c:1075 *p++ = *s++
1869    }
1870    // c:1077 — `*p = '\0'`.
1871    String::from_utf8(out).unwrap_or_default() // c:1079 return r
1872}
1873
1874/// Port of `single_index(char pre, char opt)` from `Src/Zle/computil.c:1088`.
1875/// ```c
1876/// static int
1877/// single_index(char pre, char opt)
1878/// {
1879///     if (opt <= 0x20 || opt > 0x7e)
1880///         return -1;
1881///     return opt + (pre == '-' ? -0x21 : 94 - 0x21);
1882/// }
1883/// ```
1884/// Map a `(prefix, option-letter)` pair into the flat 188-slot array
1885/// that `cadef` keeps for single-letter option lookup. Returns -1
1886/// when `opt` is outside the printable-ASCII range.
1887///
1888/// `pre` is `-` for the negative-prefix slot and anything else
1889/// (typically `+`) for the positive-prefix slot.
1890pub fn single_index(pre: u8, opt: u8) -> i32 {
1891    // c:1089
1892    if opt <= 0x20 || opt > 0x7e {
1893        // c:1089
1894        return -1; // c:1092
1895    }
1896    // c:1094 — `return opt + (pre == '-' ? -0x21 : 94 - 0x21)`.
1897    let off: i32 = if pre == b'-' { -0x21 } else { 94 - 0x21 };
1898    (opt as i32) + off
1899}
1900
1901/// Direct port of `static Caarg parse_caarg(int mult, int type, int num,
1902///                                          int opt, char *oname, char **def,
1903///                                          char *set)` from
1904/// `Src/Zle/computil.c:1099-1144`. Parses one `:descr[:action]`
1905/// fragment of an `_arguments` spec into a freshly-allocated caarg.
1906/// On return, `*idx` points at the first byte of `bytes` not consumed
1907/// (either the separator `:` for `mult=1` or `bytes.len()` for
1908/// `mult=0` rest specs).
1909pub fn parse_caarg(
1910    mult: i32,
1911    atype: i32,
1912    num: i32,
1913    opt: i32, // c:1099
1914    oname: Option<&str>,
1915    bytes: &[u8],
1916    idx: &mut usize,
1917    set: Option<&str>,
1918) -> Box<caarg> {
1919    let mut ret = Box::new(caarg::default());
1920    ret.num = num; // c:1109
1921    ret.min = num - opt; // c:1110
1922    ret.r#type = atype; // c:1111
1923    ret.opt = oname.map(|s| s.to_string()); // c:1112
1924    ret.direct = 0; // c:1113
1925    ret.gsname = set.map(|s| s.to_string()); // c:1114
1926
1927    let n = bytes.len();
1928
1929    // c:1118-1120 — scan description up to the next `:` (escaped `\:` skipped).
1930    let d_start = *idx;
1931    while *idx < n && bytes[*idx] != b':' {
1932        if bytes[*idx] == b'\\' && *idx + 1 < n {
1933            *idx += 1;
1934        }
1935        *idx += 1;
1936    }
1937    let has_sav = *idx < n;
1938    let descr_slice = &bytes[d_start..*idx];
1939    let descr_str = std::str::from_utf8(descr_slice).unwrap_or("");
1940    ret.descr = Some(rembslashcolon(descr_str)); // c:1123
1941
1942    if has_sav {
1943        // c:1127
1944        if mult != 0 {
1945            // c:1128
1946            // c:1129-1136 — `*p == ':'` start, scan to next `:` or NUL.
1947            *idx += 1;
1948            let a_start = *idx;
1949            while *idx < n && bytes[*idx] != b':' {
1950                if bytes[*idx] == b'\\' && *idx + 1 < n {
1951                    *idx += 1;
1952                }
1953                *idx += 1;
1954            }
1955            let action_slice = &bytes[a_start..*idx];
1956            let action_str = std::str::from_utf8(action_slice).unwrap_or("");
1957            ret.action = Some(rembslashcolon(action_str)); // c:1134
1958        } else {
1959            // c:1137
1960            // c:1138 — `ret->action = ztrdup(rembslashcolon(p + 1))`.
1961            let action_slice = &bytes[*idx + 1..];
1962            let action_str = std::str::from_utf8(action_slice).unwrap_or("");
1963            ret.action = Some(rembslashcolon(action_str));
1964            *idx = n;
1965        }
1966    } else {
1967        // c:1139
1968        ret.action = Some(String::new()); // c:1140
1969    }
1970    // c:1141 — `*def = p`. Caller reads `bytes[*idx]` to decide whether to
1971    // continue scanning more `:` fragments.
1972
1973    ret
1974}
1975
1976// ===========================================================
1977// Methods moved verbatim from src/ported/vm_helper because their
1978// C counterpart's source file maps 1:1 to this Rust module.
1979// Rust permits multiple inherent impl blocks for the same
1980// type within a crate, so call sites in vm_helper are unchanged.
1981// ===========================================================
1982
1983// BEGIN moved-from-exec-rs
1984// (impl ShellExecutor block moved to src/exec_shims.rs — see file marker)
1985
1986// END moved-from-exec-rs
1987
1988// ─── moved from src/ported/vm_helper (drift extraction) ───
1989
1990// CompSpec / CompMatch / CompGroup / CompState moved out of this
1991// port file to `src/extensions/bash_complete.rs` — they are
1992// Rust-original types backing the bash-style `complete` builtin
1993// extension, not zsh C ports. The ported zle/ tree should stay a
1994// faithful C-source mirror; Rust-only types live in extensions/.
1995//
1996// Callers that used `crate::ported::zle::computil::Comp*` should
1997// switch to `crate::bash_complete::Comp*` (the path lib.rs
1998// exports). vm_helper's re-export updated to point to the new home.
1999
2000/// Direct port of `static Cadef alloc_cadef(char **args, int single,
2001/// char *match, char *nonarg, int flags)` from `Src/Zle/computil.c:1147-1177`.
2002///
2003/// Builds a fresh `cadef` with the option/single-letter/match/nonarg
2004/// fields initialized. `args` (if present) is captured into `defs`
2005/// for later cache-key compare in `get_cadef` (c:1681). `single` set
2006/// allocates the 188-slot single-letter index array. `match` is the
2007/// match-spec carried through to the option/arg matchers.
2008pub fn alloc_cadef(
2009    args: Option<&[String]>,
2010    single: i32,
2011    matchstr: &str, // c:1147
2012    nonarg: Option<&str>,
2013    flags: i32,
2014) -> Box<cadef> {
2015    Box::new(cadef {
2016        next: None,                                // c:1152
2017        snext: None,                               // c:1152
2018        opts: None,                                // c:1153
2019        args: None,                                // c:1154
2020        rest: None,                                // c:1154
2021        nonarg: nonarg.map(|s| s.to_string()),     // c:1155 ztrdup(nonarg)
2022        defs: args.map(|a| a.to_vec()),            // c:1157 zarrdup(args)
2023        ndefs: args.map_or(0, |a| a.len() as i32), // c:1158 arrlen(args)
2024        nopts: 0,                                  // c:1163
2025        ndopts: 0,                                 // c:1164
2026        nodopts: 0,                                // c:1165
2027        lastt: {
2028            // c:1166 time(0)
2029            use std::time::{SystemTime, UNIX_EPOCH};
2030            SystemTime::now()
2031                .duration_since(UNIX_EPOCH)
2032                .map(|d| d.as_secs() as i64)
2033                .unwrap_or(0)
2034        },
2035        set: None, // c:1167
2036        // c:1168-1172 — 188-slot single-letter Caopt index. Capacity
2037        // 188 matches C exactly (range of single-letter option names).
2038        single: if single != 0 {
2039            Some((0..188).map(|_| None).collect())
2040        } else {
2041            None
2042        },
2043        r#match: Some(matchstr.to_string()), // c:1173 ztrdup(match)
2044        argsactive: 0,
2045        flags, // c:1174
2046    })
2047}
2048
2049/// Direct port of `static void set_cadef_opts(Cadef def)` from
2050/// `Src/Zle/computil.c:1180-1191`. After a set-of-arg-definitions has
2051/// been parsed into the cadef, walk the args linked list and update
2052/// each non-direct argp's `min` field to the cumulative number of
2053/// CAA_OPT entries that precede it. The optionality count compounds
2054/// down the chain, which determines minimum-argument-count semantics
2055/// during completion.
2056pub fn set_cadef_opts(def: &mut cadef) {
2057    // c:1180
2058    let mut xnum: i32 = 0;
2059    let mut argp = def.args.as_deref_mut(); // c:1185 argp = def->args
2060    while let Some(node) = argp {
2061        // c:1185
2062        if node.direct == 0 {
2063            // c:1186 !argp->direct
2064            node.min = node.num - xnum; // c:1187
2065        }
2066        if node.r#type == CAA_OPT {
2067            // c:1188
2068            xnum += 1; // c:1189
2069        }
2070        argp = node.next.as_deref_mut(); // c:1185 argp = argp->next
2071    }
2072}
2073
2074/// Direct port of `static Cadef parse_cadef(char *nam, char **args)` from
2075/// `Src/Zle/computil.c:1196-1666`. Parses the leading auto-description
2076/// (first arg up to `%d`), the `-s/-A/-S/-M` flag block, then the
2077/// main spec-list loop that fills opts/args/rest from each remaining
2078/// `_arguments` spec entry.
2079pub fn parse_cadef(nam: &str, args: &[String]) -> Option<Box<cadef>> {
2080    // c:1196
2081
2082    if args.is_empty() {
2083        return None; // c:1262 `!*args`
2084    }
2085
2086    let orig_args = args;
2087    let mut idx = 0usize;
2088    let mut single: i32 = 0;
2089    let mut flags: i32 = 0;
2090    let mut match_spec: String = "r:|[_-]=* r:|=*".to_string(); // c:1200
2091    let mut nonarg: Option<String> = None;
2092
2093    // c:1208-1216 — split args[0] on `%d` into (adpre, adsuf). Used at
2094    // c:1543-1554 to auto-derive option descriptions.
2095    let (adpre, adsuf): (Option<String>, Option<String>) = {
2096        let first = args[0].as_bytes();
2097        let mut split_at: Option<usize> = None;
2098        let mut i = 0usize;
2099        while i + 1 < first.len() {
2100            if first[i] == b'%' && first[i + 1] == b'd' {
2101                split_at = Some(i);
2102                break;
2103            }
2104            i += 1;
2105        }
2106        if let Some(at) = split_at {
2107            let pre = String::from_utf8_lossy(&first[..at]).into_owned();
2108            let suf = String::from_utf8_lossy(&first[at + 2..]).into_owned();
2109            (Some(pre), Some(suf))
2110        } else {
2111            (None, None)
2112        }
2113    };
2114
2115    idx += 1; // c:1220 args++
2116
2117    // c:1221-1259 — `-s/-A/-S/-M[arg]` flag block.
2118    while idx < args.len() {
2119        let p = &args[idx];
2120        let bytes = p.as_bytes();
2121        if bytes.len() < 2 || bytes[0] != b'-' {
2122            // c:1221
2123            break;
2124        }
2125        let cluster = &bytes[1..];
2126        let mut ok = true;
2127        for (i, &c) in cluster.iter().enumerate() {
2128            match c {
2129                b's' => single = 1,       // c:1233
2130                b'S' => flags |= CDF_SEP, // c:1235
2131                b'A' => {
2132                    // c:1237
2133                    if i + 1 < cluster.len() {
2134                        // c:1238
2135                        nonarg = Some(String::from_utf8_lossy(&cluster[i + 1..]).into_owned());
2136                    } else if idx + 1 < args.len() {
2137                        // c:1241
2138                        nonarg = Some(args[idx + 1].clone());
2139                        idx += 1;
2140                    } else {
2141                        ok = false;
2142                    }
2143                    break;
2144                }
2145                b'M' => {
2146                    // c:1245
2147                    if i + 1 < cluster.len() {
2148                        // c:1246
2149                        match_spec = String::from_utf8_lossy(&cluster[i + 1..]).into_owned();
2150                    } else if idx + 1 < args.len() {
2151                        // c:1249
2152                        match_spec = args[idx + 1].clone();
2153                        idx += 1;
2154                    } else {
2155                        ok = false;
2156                    }
2157                    break;
2158                }
2159                _ => {
2160                    ok = false;
2161                    break;
2162                }
2163            }
2164        }
2165        if !ok {
2166            break; // c:1230
2167        }
2168        idx += 1; // c:1258
2169    }
2170
2171    if idx < args.len() && args[idx] == ":" {
2172        // c:1260
2173        idx += 1;
2174    }
2175    if idx >= args.len() {
2176        // c:1262
2177        return None;
2178    }
2179
2180    // c:1266 — `tokenize(nonarg = dupstring(nonarg))`. The Rust matcher
2181    // path lazily tokenizes on use; the stored bytes are the spec text.
2182
2183    // c:1269 — `all = ret = alloc_cadef(orig_args, single, match, nonarg, flags)`.
2184    let first_def = alloc_cadef(
2185        Some(orig_args),
2186        single,
2187        &match_spec,
2188        nonarg.as_deref(),
2189        flags,
2190    );
2191
2192    // ---- spec-list loop state (c:1271-1273) ----
2193    // `sets` accumulates each Cadef in `snext` order; per-set opts/args/rest
2194    // are collected in parallel Vecs and linked into the cadef at the end.
2195    let mut sets: Vec<Box<cadef>> = vec![first_def];
2196    let mut opts_per_set: Vec<Vec<Box<caopt>>> = vec![Vec::new()];
2197    let mut args_per_set: Vec<Vec<Box<caarg>>> = vec![Vec::new()];
2198    let mut rest_per_set: Vec<Option<Box<caarg>>> = vec![None];
2199
2200    let sargs = idx; // c:1271 saved set-start
2201    let mut anum: i32 = 1; // c:1203
2202    let mut doset: Option<String> = None;
2203    let mut axor: Option<String> = None;
2204    let mut curset: Option<usize> = None; // c:1201
2205    let mut pendset: Option<usize> = None;
2206    let mut foreignset = false;
2207
2208    // c:1275 — `for (; *args || pendset; args++)`.
2209    'outer: loop {
2210        // c:1276 — `if (!*args)` start a fresh set (restart from sargs).
2211        if idx >= args.len() {
2212            if pendset.is_none() {
2213                break 'outer;
2214            }
2215            // c:1278-1286 — set_cadef_opts on current; alloc new cadef as snext.
2216            {
2217                let cur = sets.last_mut().unwrap();
2218                let cur_args = args_per_set.last_mut().unwrap();
2219                // Link the args list into cur so set_cadef_opts can walk it.
2220                let mut head: Option<Box<caarg>> = None;
2221                for arg_box in cur_args.drain(..).rev() {
2222                    let mut a = arg_box;
2223                    a.next = head;
2224                    head = Some(a);
2225                }
2226                cur.args = head;
2227                set_cadef_opts(cur); // c:1280
2228                                     // Stash args back as a Vec for the rest of the loop. We need
2229                                     // both forms; the linked list will be rebuilt at the end.
2230                let mut walk = cur.args.take();
2231                while let Some(mut node) = walk {
2232                    walk = node.next.take();
2233                    cur_args.push(node);
2234                }
2235            }
2236            idx = sargs; // c:1278
2237            doset = None; // c:1279
2238            sets.push(alloc_cadef(
2239                None,
2240                single,
2241                &match_spec, // c:1281
2242                nonarg.as_deref(),
2243                flags,
2244            ));
2245            opts_per_set.push(Vec::new());
2246            args_per_set.push(Vec::new());
2247            rest_per_set.push(None);
2248            anum = 1; // c:1283
2249            foreignset = false; // c:1284
2250            curset = pendset; // c:1285
2251            pendset = None; // c:1286
2252        }
2253
2254        let arg = &args[idx];
2255        let arg_bytes = arg.as_bytes();
2256
2257        // c:1288 — `args[0][0] == '-' && !args[0][1] && args[1]` — set marker.
2258        if arg_bytes == b"-" && idx + 1 < args.len() {
2259            if curset.is_some() && curset != Some(idx) {
2260                // c:1289
2261                foreignset = true;
2262                if pendset.is_none() && Some(idx) > curset {
2263                    // c:1290
2264                    pendset = Some(idx);
2265                }
2266                idx += 1; // c:1292 ++args
2267            } else {
2268                // c:1293
2269                foreignset = false;
2270                idx += 1;
2271                let p_str = &args[idx]; // c:1295 char *p = *++args
2272                let pb = p_str.as_bytes();
2273                let l = pb.len().saturating_sub(1);
2274                // c:1298 — `if (*p == '(' && p[l] == ')')` strip parens for axor.
2275                let (set_name, ax) = if !pb.is_empty() && pb[0] == b'(' && pb[l] == b')' {
2276                    let inner = String::from_utf8_lossy(&pb[1..l]).into_owned();
2277                    (inner.clone(), Some(inner))
2278                } else {
2279                    (p_str.clone(), None)
2280                };
2281                axor = ax;
2282                if set_name.is_empty() {
2283                    // c:1302
2284                    zwarnnam(nam, "empty set name");
2285                    return None;
2286                }
2287                let new_set = tricat(&set_name, "-", ""); // c:1307
2288                doset = Some(new_set.clone());
2289                {
2290                    let cur = sets.last_mut().unwrap();
2291                    cur.set = Some(new_set);
2292                }
2293                curset = Some(idx); // c:1308
2294            }
2295            idx += 1;
2296            continue; // c:1310
2297        }
2298
2299        // c:1311 — `args[0][0] == '+' && !args[0][1] && args[1]` — group marker.
2300        if arg_bytes == b"+" && idx + 1 < args.len() {
2301            foreignset = false; // c:1315
2302            idx += 1;
2303            let p_str = &args[idx]; // c:1316
2304            let pb = p_str.as_bytes();
2305            let l = pb.len().saturating_sub(1);
2306            let (group_name, ax) = if !pb.is_empty() && pb[0] == b'(' && pb[l] == b')' {
2307                let inner = String::from_utf8_lossy(&pb[1..l]).into_owned();
2308                (inner.clone(), Some(inner))
2309            } else {
2310                (p_str.clone(), None)
2311            };
2312            axor = ax;
2313            if group_name.is_empty() {
2314                // c:1322
2315                zwarnnam(nam, "empty group name");
2316                return None;
2317            }
2318            doset = Some(tricat(&group_name, "-", "")); // c:1327
2319            idx += 1;
2320            continue; // c:1328
2321        }
2322
2323        // c:1329 — `if (foreignset) continue` — skip specs for other sets.
2324        if foreignset {
2325            idx += 1;
2326            continue;
2327        }
2328
2329        // c:1331 — parse one spec entry.
2330        let bytes = arg_bytes;
2331        let mut p = 0usize;
2332        let mut xnum: i32 = 0; // c:1332
2333        let mut not_flag = false;
2334        if p < bytes.len() && bytes[p] == b'!' {
2335            // c:1333
2336            not_flag = true;
2337            p += 1;
2338        }
2339
2340        let mut xor: Option<Vec<String>> = None;
2341        if p < bytes.len() && bytes[p] == b'(' {
2342            // c:1335 xor list
2343            let mut list: Vec<String> = Vec::new();
2344            // c:1342-1354 — collect words inside parens.
2345            let mut bad = false;
2346            'paren: loop {
2347                if p >= bytes.len() || bytes[p] == b')' {
2348                    break;
2349                }
2350                p += 1; // c:1343 p++
2351                while p < bytes.len() && inblank(bytes[p]) {
2352                    p += 1;
2353                } // c:1343 inblank skip
2354                if p >= bytes.len() {
2355                    bad = true;
2356                    break 'paren;
2357                }
2358                if bytes[p] == b')' {
2359                    break 'paren;
2360                }
2361                let q = p;
2362                p += 1;
2363                while p < bytes.len() && bytes[p] != b')' && !inblank(bytes[p]) {
2364                    p += 1;
2365                }
2366                if p >= bytes.len() {
2367                    bad = true;
2368                    break 'paren;
2369                } // c:1349
2370                let word = String::from_utf8_lossy(&bytes[q..p]).into_owned();
2371                list.push(word);
2372                xnum += 1; // c:1353
2373            }
2374            if bad || p >= bytes.len() || bytes[p] != b')' {
2375                // c:1356
2376                zwarnnam(nam, &format!("invalid argument: {}", arg));
2377                return None;
2378            }
2379            if doset.is_some() && axor.is_some() {
2380                // c:1361
2381                xnum += 1;
2382                list.push(axor.clone().unwrap()); // c:1366-1367
2383            }
2384            xor = Some(list);
2385            p += 1; // c:1370
2386        } else if doset.is_some() && axor.is_some() {
2387            // c:1371
2388            xnum = 1;
2389            xor = Some(vec![axor.clone().unwrap()]);
2390        }
2391
2392        // c:1379 — option spec OR rest-arg OR normal-arg.
2393        let is_opt = p < bytes.len()
2394            && (bytes[p] == b'-'
2395                || bytes[p] == b'+'
2396                || (bytes[p] == b'*'
2397                    && p + 1 < bytes.len()
2398                    && (bytes[p + 1] == b'-' || bytes[p + 1] == b'+')));
2399
2400        if is_opt {
2401            // ---- c:1381-1580 option spec branch ----
2402            // The `rec:` goto loop handles `-+`/`+-` duplication by
2403            // parsing the same spec twice with name[0] flipped between
2404            // `-` and `+`.
2405            let mut again_iter = 0i32; // c:1384
2406            let mut againp_start: Option<usize> = None;
2407            let mut p_state = p;
2408            let mut xor_state = xor;
2409            let mut xnum_state = xnum;
2410
2411            'rec: loop {
2412                let mut multi = false; // c:1390
2413                if p_state < bytes.len() && bytes[p_state] == b'*' {
2414                    multi = true;
2415                    p_state += 1;
2416                }
2417
2418                let mut name_start: usize;
2419                let mut name_buf: Vec<u8>;
2420                let need_flip = p_state + 2 < bytes.len()
2421                    && ((bytes[p_state] == b'-' && bytes[p_state + 1] == b'+')
2422                        || (bytes[p_state] == b'+' && bytes[p_state + 1] == b'-'))
2423                    && bytes[p_state + 2] != b':'
2424                    && bytes[p_state + 2] != b'['
2425                    && bytes[p_state + 2] != b'='
2426                    && bytes[p_state + 2] != b'-'
2427                    && bytes[p_state + 2] != b'+';
2428
2429                if need_flip {
2430                    // c:1393
2431                    if again_iter == 0 {
2432                        againp_start = Some(p_state);
2433                    }
2434                    name_start = p_state + 1;
2435                    name_buf = bytes[name_start..].to_vec();
2436                    if !name_buf.is_empty() {
2437                        name_buf[0] = if again_iter != 0 { b'-' } else { b'+' };
2438                    }
2439                    again_iter += 1;
2440                    p_state = name_start;
2441                } else {
2442                    // c:1404
2443                    name_start = p_state;
2444                    name_buf = bytes[name_start..].to_vec();
2445                    if p_state + 1 < bytes.len()
2446                        && bytes[p_state] == b'-'
2447                        && bytes[p_state + 1] == b'-'
2448                    {
2449                        p_state += 1; // c:1407 skip 2nd '-'
2450                    }
2451                }
2452
2453                if p_state + 1 >= bytes.len() {
2454                    // c:1409
2455                    zwarnnam(nam, &format!("invalid argument: {}", arg));
2456                    return None;
2457                }
2458
2459                // c:1416-1422 — skip option name body up to type byte.
2460                let mut np = p_state - name_start + 1;
2461                let nlen = name_buf.len();
2462                while np < nlen
2463                    && name_buf[np] != b':'
2464                    && name_buf[np] != b'['
2465                    && !((name_buf[np] == b'-' || name_buf[np] == b'+')
2466                        && np + 1 < nlen
2467                        && (name_buf[np + 1] == b':' || name_buf[np + 1] == b'['))
2468                    && !(name_buf[np] == b'='
2469                        && np + 1 < nlen
2470                        && (name_buf[np + 1] == b':'
2471                            || name_buf[np + 1] == b'['
2472                            || name_buf[np + 1] == b'-'))
2473                {
2474                    if name_buf[np] == b'\\' && np + 1 < nlen {
2475                        np += 1;
2476                    }
2477                    np += 1;
2478                }
2479
2480                let mut c_byte = if np < nlen { name_buf[np] } else { 0 };
2481                let opt_name_slice = &name_buf[..np];
2482                let opt_name = String::from_utf8_lossy(opt_name_slice).into_owned();
2483
2484                let mut otype = CAO_NEXT; // c:1384
2485                if c_byte == b'-' {
2486                    // c:1427
2487                    otype = CAO_DIRECT;
2488                    np += 1;
2489                    c_byte = if np < nlen { name_buf[np] } else { 0 };
2490                } else if c_byte == b'+' {
2491                    // c:1430
2492                    otype = CAO_ODIRECT;
2493                    np += 1;
2494                    c_byte = if np < nlen { name_buf[np] } else { 0 };
2495                } else if c_byte == b'=' {
2496                    // c:1433
2497                    otype = CAO_OEQUAL;
2498                    np += 1;
2499                    c_byte = if np < nlen { name_buf[np] } else { 0 };
2500                    if c_byte == b'-' {
2501                        otype = CAO_EQUAL; // c:1436
2502                        np += 1;
2503                        c_byte = if np < nlen { name_buf[np] } else { 0 };
2504                    }
2505                }
2506
2507                // c:1441 — optional `[descr]`.
2508                let mut descr_str: Option<String> = None;
2509                if c_byte == b'[' {
2510                    // c:1441
2511                    np += 1;
2512                    let d_start = np;
2513                    while np < nlen && name_buf[np] != b']' {
2514                        if name_buf[np] == b'\\' && np + 1 < nlen {
2515                            np += 1;
2516                        }
2517                        np += 1;
2518                    }
2519                    if np >= nlen {
2520                        // c:1446
2521                        zwarnnam(nam, &format!("invalid option definition: {}", arg));
2522                        return None;
2523                    }
2524                    let d_slice = &name_buf[d_start..np];
2525                    descr_str = Some(String::from_utf8_lossy(d_slice).into_owned());
2526                    np += 1;
2527                    c_byte = if np < nlen { name_buf[np] } else { 0 };
2528                }
2529
2530                if c_byte != 0 && c_byte != b':' {
2531                    // c:1456
2532                    zwarnnam(nam, &format!("invalid option definition: {}", arg));
2533                    return None;
2534                }
2535
2536                // c:1461 — add option name to xor list if not `*-...`.
2537                let clean_name = rembslashcolon(&opt_name);
2538                if !multi {
2539                    let xv = xor_state.get_or_insert_with(Vec::new);
2540                    if xv.len() <= xnum_state as usize {
2541                        xv.resize(xnum_state as usize + 1, String::new());
2542                    }
2543                    xv[xnum_state as usize] = clean_name.clone();
2544                }
2545
2546                // c:1470-1531 — argument loop for `:descr:action[:...]`.
2547                let mut oargs: Vec<Box<caarg>> = Vec::new();
2548                if c_byte == b':' {
2549                    let mut oanum: i32 = 1; // c:1473
2550                    let mut onum: i32 = 0;
2551                    while c_byte == b':' {
2552                        // c:1479
2553                        let mut rest = 0;
2554                        let mut end_str: Option<String> = None;
2555                        np += 1; // c:1484 *++p
2556                        let atype: i32;
2557                        c_byte = if np < nlen { name_buf[np] } else { 0 };
2558                        if c_byte == b':' {
2559                            // c:1485
2560                            atype = CAA_OPT;
2561                            np += 1;
2562                        } else if c_byte == b'*' {
2563                            // c:1487
2564                            np += 1;
2565                            if np < nlen && name_buf[np] != b':' {
2566                                // c:1488
2567                                let end_start = np;
2568                                while np < nlen && name_buf[np] != b':' {
2569                                    if name_buf[np] == b'\\' && np + 1 < nlen {
2570                                        np += 1;
2571                                    }
2572                                    np += 1;
2573                                }
2574                                let e_slice = &name_buf[end_start..np];
2575                                end_str = Some(String::from_utf8_lossy(e_slice).into_owned());
2576                            }
2577                            if np >= nlen || name_buf[np] != b':' {
2578                                // c:1500
2579                                zwarnnam(nam, &format!("invalid option definition: {}", arg));
2580                                return None;
2581                            }
2582                            np += 1; // c:1507 *++p
2583                            if np < nlen && name_buf[np] == b':' {
2584                                // c:1508
2585                                np += 1;
2586                                if np < nlen && name_buf[np] == b':' {
2587                                    // c:1509
2588                                    atype = CAA_RREST;
2589                                    np += 1;
2590                                } else {
2591                                    atype = CAA_RARGS;
2592                                }
2593                            } else {
2594                                atype = CAA_REST;
2595                            }
2596                            rest = 1;
2597                        } else {
2598                            atype = CAA_NORMAL;
2599                        }
2600
2601                        // c:1521 — parse_caarg.
2602                        let mut oarg = parse_caarg(
2603                            if rest != 0 { 0 } else { 1 },
2604                            atype,
2605                            oanum,
2606                            onum,
2607                            Some(&clean_name),
2608                            &name_buf,
2609                            &mut np,
2610                            doset.as_deref(),
2611                        );
2612                        oanum += 1;
2613                        if atype == CAA_OPT {
2614                            onum += 1;
2615                        } // c:1524
2616                        if let Some(end) = end_str {
2617                            oarg.end = Some(end); // c:1526
2618                        }
2619                        oargs.push(oarg);
2620
2621                        if rest != 0 {
2622                            break;
2623                        } // c:1528
2624                        c_byte = if np < nlen { name_buf[np] } else { 0 }; // c:1530
2625                    }
2626                }
2627
2628                // c:1534 — build the caopt.
2629                let mut opt_box = Box::new(caopt::default());
2630                opt_box.gsname = doset.clone(); // c:1539
2631                opt_box.name = Some(clean_name.clone()); // c:1540
2632                opt_box.descr = if let Some(d) = descr_str.clone() {
2633                    // c:1542
2634                    Some(d)
2635                } else if adpre.is_some() && oargs.len() == 1 {
2636                    // c:1543
2637                    let first_arg = &oargs[0];
2638                    let d_field = first_arg.descr.as_deref().unwrap_or("");
2639                    let has_visible = d_field.bytes().any(|b| !iblank(b));
2640                    if has_visible {
2641                        // c:1550
2642                        Some(tricat(
2643                            adpre.as_deref().unwrap_or(""),
2644                            d_field,
2645                            adsuf.as_deref().unwrap_or(""),
2646                        ))
2647                    } else {
2648                        None // c:1553
2649                    }
2650                } else {
2651                    None
2652                };
2653                let xor_clone = if again_iter == 1 {
2654                    // c:1556
2655                    xor_state.clone()
2656                } else {
2657                    xor_state.take()
2658                };
2659                opt_box.xor = xor_clone;
2660                opt_box.r#type = otype; // c:1557
2661                opt_box.not = if not_flag { 1 } else { 0 }; // c:1560
2662
2663                // Link in the arg list.
2664                let mut head: Option<Box<caarg>> = None;
2665                for a in oargs.into_iter().rev() {
2666                    let mut a = a;
2667                    a.next = head;
2668                    head = Some(a);
2669                }
2670                opt_box.args = head;
2671
2672                {
2673                    let cur = sets.last_mut().unwrap();
2674                    opt_box.num = cur.nopts;
2675                    cur.nopts += 1; // c:1559
2676                    if otype == CAO_DIRECT || otype == CAO_EQUAL {
2677                        // c:1562
2678                        cur.ndopts += 1;
2679                    } else if otype == CAO_ODIRECT || otype == CAO_OEQUAL {
2680                        // c:1564
2681                        cur.nodopts += 1;
2682                    }
2683                    // c:1571 — single-letter lookup table.
2684                    if single != 0 {
2685                        let nb = clean_name.as_bytes();
2686                        if nb.len() == 2 && nb[1] != b'-' {
2687                            let sidx = single_index(nb[0], nb[1]);
2688                            if sidx >= 0 {
2689                                if let Some(ref mut s) = cur.single {
2690                                    if (sidx as usize) < s.len() {
2691                                        s[sidx as usize] = Some(Box::new(caopt {
2692                                            next: None,
2693                                            name: opt_box.name.clone(),
2694                                            descr: opt_box.descr.clone(),
2695                                            xor: opt_box.xor.clone(),
2696                                            r#type: opt_box.r#type,
2697                                            args: None,
2698                                            active: 0,
2699                                            num: opt_box.num,
2700                                            gsname: opt_box.gsname.clone(),
2701                                            not: opt_box.not,
2702                                        }));
2703                                    }
2704                                }
2705                            }
2706                        }
2707                    }
2708                }
2709
2710                opts_per_set.last_mut().unwrap().push(opt_box);
2711
2712                if again_iter == 1 {
2713                    // c:1576
2714                    if let Some(start) = againp_start {
2715                        p_state = start;
2716                        xnum_state = xnum; // restore
2717                        xor_state = xor_state.clone();
2718                        continue 'rec;
2719                    }
2720                }
2721                break 'rec;
2722            }
2723        } else if p < bytes.len() && bytes[p] == b'*' {
2724            // ---- c:1581-1607 rest-arg branch ----
2725            if not_flag {
2726                // c:1586
2727                idx += 1;
2728                continue;
2729            }
2730            p += 1; // c:1589 *++p
2731            if p >= bytes.len() || bytes[p] != b':' {
2732                zwarnnam(nam, &format!("invalid rest argument definition: {}", arg));
2733                return None;
2734            }
2735            if rest_per_set.last().unwrap().is_some() {
2736                // c:1594
2737                zwarnnam(nam, &format!("doubled rest argument definition: {}", arg));
2738                return None;
2739            }
2740            let mut atype = CAA_REST; // c:1584
2741            p += 1; // c:1599 *++p
2742            if p < bytes.len() && bytes[p] == b':' {
2743                // c:1599
2744                p += 1;
2745                if p < bytes.len() && bytes[p] == b':' {
2746                    // c:1600
2747                    atype = CAA_RREST;
2748                    p += 1;
2749                } else {
2750                    atype = CAA_RARGS;
2751                }
2752            }
2753            let mut rarg = parse_caarg(0, atype, -1, 0, None, bytes, &mut p, doset.as_deref()); // c:1606
2754            rarg.xor = xor; // c:1607
2755            *rest_per_set.last_mut().unwrap() = Some(rarg);
2756        } else {
2757            // ---- c:1608-1661 normal-arg branch ----
2758            if not_flag {
2759                // c:1614
2760                idx += 1;
2761                continue;
2762            }
2763            let mut direct = 0; // c:1611
2764            if p < bytes.len() && idigit(bytes[p]) {
2765                // c:1617
2766                direct = 1;
2767                let mut num: i32 = 0;
2768                while p < bytes.len() && idigit(bytes[p]) {
2769                    num = num * 10 + (bytes[p] - b'0') as i32;
2770                    p += 1;
2771                }
2772                anum = num + 1; // c:1624
2773            } else {
2774                anum += 1; // c:1627
2775            }
2776            if p >= bytes.len() || bytes[p] != b':' {
2777                // c:1629
2778                zwarnnam(nam, &format!("invalid argument: {}", arg));
2779                return None;
2780            }
2781            let mut atype = CAA_NORMAL;
2782            p += 1; // c:1636 *++p
2783            if p < bytes.len() && bytes[p] == b':' {
2784                // c:1636
2785                atype = CAA_OPT;
2786                p += 1;
2787            }
2788            let mut narg =
2789                parse_caarg(0, atype, anum - 1, 0, None, bytes, &mut p, doset.as_deref()); // c:1641
2790            narg.xor = xor; // c:1642
2791            narg.direct = direct; // c:1643
2792
2793            // c:1647-1661 — sorted insert by num.
2794            let target = anum - 1;
2795            let cur_args = args_per_set.last_mut().unwrap();
2796            let mut insert_at = cur_args.len();
2797            for (i, existing) in cur_args.iter().enumerate() {
2798                if existing.num >= target {
2799                    insert_at = i;
2800                    break;
2801                }
2802            }
2803            if insert_at < cur_args.len() && cur_args[insert_at].num == target {
2804                zwarnnam(nam, &format!("doubled argument definition: {}", arg));
2805                return None;
2806            }
2807            cur_args.insert(insert_at, narg);
2808        }
2809
2810        idx += 1;
2811    }
2812
2813    // c:1664 — final set_cadef_opts on the last set.
2814    {
2815        let last_idx = sets.len() - 1;
2816        let cur = &mut sets[last_idx];
2817        let cur_args = &mut args_per_set[last_idx];
2818        let mut head: Option<Box<caarg>> = None;
2819        for a in cur_args.drain(..).rev() {
2820            let mut a = a;
2821            a.next = head;
2822            head = Some(a);
2823        }
2824        cur.args = head;
2825        set_cadef_opts(cur);
2826    }
2827
2828    // ---- finalize: link opts/args/rest per set, then snext-chain ----
2829    let n_sets = sets.len();
2830    for i in 0..n_sets {
2831        // opts — append order.
2832        let mut head: Option<Box<caopt>> = None;
2833        for o in opts_per_set[i].drain(..).rev() {
2834            let mut o = o;
2835            o.next = head;
2836            head = Some(o);
2837        }
2838        sets[i].opts = head;
2839        // args was already linked in the per-set finalize step above for
2840        // every set except possibly the last (which is now done). Walk
2841        // any still-present Vec entries into the linked list for safety.
2842        if !args_per_set[i].is_empty() {
2843            let mut head: Option<Box<caarg>> = None;
2844            for a in args_per_set[i].drain(..).rev() {
2845                let mut a = a;
2846                a.next = head;
2847                head = Some(a);
2848            }
2849            sets[i].args = head;
2850        }
2851        sets[i].rest = rest_per_set[i].take();
2852    }
2853
2854    // c:1281 — snext chain links each subsequent set off the head.
2855    while sets.len() > 1 {
2856        let tail = sets.pop().unwrap();
2857        let prev = sets.last_mut().unwrap();
2858        // Walk to the end of the snext chain on prev and attach tail.
2859        let mut cursor: &mut Option<Box<cadef>> = &mut prev.snext;
2860        while cursor.is_some() {
2861            cursor = &mut cursor.as_mut().unwrap().snext;
2862        }
2863        *cursor = Some(tail);
2864    }
2865
2866    Some(sets.pop().unwrap())
2867}
2868
2869// `freecastate` / `freectags` / `freectset` / `freecvdef` real ports
2870// landed above with the castate / ctags / ctset / cvdef structs.
2871
2872/// Direct port of `static Cadef get_cadef(char *nam, char **args)`
2873/// from `Src/Zle/computil.c:1673-1694`. Walks `cadef_cache` looking
2874/// for an entry whose `defs` array matches the requested `args`
2875/// (same length + position-for-position string equality). On hit,
2876/// bumps that entry's `lastt` and returns it. On miss, parses via
2877/// `parse_cadef` and evicts the entry with the oldest `lastt`
2878/// (or the first empty slot) to make room for the new one.
2879///
2880/// Returns `1` on hit, `0` on miss-and-cache-insert. The previous
2881/// return-`i32` shape is preserved for callers; the parsed cadef
2882/// itself lives in `cadef_cache` and is looked up by separate
2883/// per-name accessors (`ca_get_opt`, `ca_get_arg`, etc.).
2884pub fn get_cadef(nam: &str, args: &[String]) -> i32 {
2885    // c:1673
2886    let na = args.len() as i32;
2887    let now = {
2888        // c:1681 time(0)
2889        use std::time::{SystemTime, UNIX_EPOCH};
2890        SystemTime::now()
2891            .duration_since(UNIX_EPOCH)
2892            .map(|d| d.as_secs() as i64)
2893            .unwrap_or(0)
2894    };
2895
2896    if let Ok(mut cache) = cadef_cache.lock() {
2897        // c:1678 — `for (i = MAX_CACACHE, p = cadef_cache, min = NULL;
2898        //                  i && *p; p++, i--)`. Linear scan; track LRU
2899        //          candidate for eviction in `min_idx`.
2900        let mut min_idx: Option<usize> = None;
2901        let mut min_lastt: i64 = i64::MAX;
2902        let mut hit_idx: Option<usize> = None;
2903        for (i, slot) in cache.iter().enumerate() {
2904            match slot {
2905                Some(entry) => {
2906                    // c:1679 — `if (*p && na == (*p)->ndefs && arrcmp(args, (*p)->defs))`.
2907                    if entry.ndefs == na
2908                        && entry.defs.as_deref().map_or(false, |d| {
2909                            d.len() == args.len() && d.iter().zip(args.iter()).all(|(a, b)| a == b)
2910                        })
2911                    {
2912                        hit_idx = Some(i);
2913                        break; // c:1682 break on match
2914                    }
2915                    // c:1684 — track entry with smallest lastt as eviction target.
2916                    if entry.lastt < min_lastt {
2917                        min_lastt = entry.lastt;
2918                        min_idx = Some(i);
2919                    }
2920                }
2921                None => {
2922                    // c:1684 — empty slot wins as eviction target.
2923                    min_idx = Some(i);
2924                    break;
2925                }
2926            }
2927        }
2928        if let Some(i) = hit_idx {
2929            if let Some(entry) = cache[i].as_mut() {
2930                entry.lastt = now; // c:1681
2931            }
2932            return 1; // c:1683 hit
2933        }
2934        // c:1688 — parse_cadef; on success replace the chosen slot.
2935        if let Some(new) = parse_cadef(nam, args) {
2936            let idx = min_idx.unwrap_or(0);
2937            cache[idx] = Some(new);
2938        }
2939    }
2940    0 // c:1693 miss
2941}
2942
2943/// Direct port of `static Caopt ca_get_opt(Cadef d, char *line, int full,
2944///                                          char **end)` from
2945/// `Src/Zle/computil.c:1706-1742`. Looks up an option-spec by name
2946/// against `line`. With `full=0`, also accepts a prefix-of-`line`
2947/// match where the option name is a prefix and the rest of `line` is
2948/// the option's argument (handles `=` / `--name=value` shapes per
2949/// `CAO_OEQUAL` / `CAO_EQUAL`). Sets `*end` to the byte offset past
2950/// the option text (and past the `=` separator when applicable).
2951/// Returns a cloned shallow copy of the matched `caopt` (without its
2952/// `next` chain) — Rust ownership artifact, equivalent to C returning
2953/// the aliased `Caopt` pointer.
2954pub fn ca_get_opt(
2955    d: &cadef,
2956    line: &str,
2957    full: i32, // c:1706
2958    end: &mut usize,
2959) -> Option<Box<caopt>> {
2960    let line_bytes = line.as_bytes();
2961
2962    // c:1712-1718 — exact match against an active option name.
2963    let mut cur = d.opts.as_deref();
2964    while let Some(p) = cur {
2965        // c:1712
2966        if p.active != 0 {
2967            // c:1713
2968            if let Some(name) = p.name.as_deref() {
2969                if name == line {
2970                    *end = line_bytes.len(); // c:1715
2971                    return Some(Box::new(caopt {
2972                        // c:1717
2973                        next: None,
2974                        name: p.name.clone(),
2975                        descr: p.descr.clone(),
2976                        xor: p.xor.clone(),
2977                        r#type: p.r#type,
2978                        args: None,
2979                        active: p.active,
2980                        num: p.num,
2981                        gsname: p.gsname.clone(),
2982                        not: p.not,
2983                    }));
2984                }
2985            }
2986        }
2987        cur = p.next.as_deref();
2988    }
2989
2990    if full == 0 {
2991        // c:1720
2992        // c:1722-1739 — prefix-match path for `name=value` / `nameSPC value`.
2993        let mut cur = d.opts.as_deref();
2994        while let Some(p) = cur {
2995            if p.active != 0 {
2996                // c:1723
2997                if let Some(name) = p.name.as_deref() {
2998                    // c:1723-1724 — short args/NEXT → exact match, else strpfx.
2999                    let is_match = if p.args.is_none() || p.r#type == CAO_NEXT {
3000                        name == line
3001                    } else {
3002                        strpfx(name, line)
3003                    };
3004                    if is_match {
3005                        let l = name.len();
3006                        // c:1726-1728 — for OEQUAL/EQUAL, the char at name's
3007                        // end must be `=` or absent; otherwise skip.
3008                        if (p.r#type == CAO_OEQUAL || p.r#type == CAO_EQUAL)
3009                            && l < line_bytes.len()
3010                            && line_bytes[l] != b'='
3011                        {
3012                            cur = p.next.as_deref();
3013                            continue; // c:1728
3014                        }
3015                        // c:1731-1736 — set end past the option (+= 1 for `=`).
3016                        let mut at = l;
3017                        if (p.r#type == CAO_OEQUAL || p.r#type == CAO_EQUAL)
3018                            && l < line_bytes.len()
3019                            && line_bytes[l] == b'='
3020                        {
3021                            at += 1; // c:1734
3022                        }
3023                        *end = at; // c:1736
3024                        return Some(Box::new(caopt {
3025                            // c:1738
3026                            next: None,
3027                            name: p.name.clone(),
3028                            descr: p.descr.clone(),
3029                            xor: p.xor.clone(),
3030                            r#type: p.r#type,
3031                            args: None,
3032                            active: p.active,
3033                            num: p.num,
3034                            gsname: p.gsname.clone(),
3035                            not: p.not,
3036                        }));
3037                    }
3038                }
3039            }
3040            cur = p.next.as_deref();
3041        }
3042    }
3043    None // c:1741
3044}
3045
3046/// Direct port of `static Caopt ca_get_sopt(Cadef d, char *line,
3047///                                           char **end, LinkList *lp)`
3048/// from `Src/Zle/computil.c:1747-1781`. Single-letter option lookup
3049/// for clumped flags like `-abc`. Walks `line[1..]` consulting
3050/// `d->single[]` for each char; CAO_NEXT matches accumulate in `lp`,
3051/// the first non-NEXT match terminates and sets `*end` past it.
3052/// Returns the terminating Caopt (cloned, no chain) or None.
3053pub fn ca_get_sopt(
3054    d: &cadef,
3055    line: &str, // c:1747
3056    end: &mut usize,
3057    lp: &mut Option<Vec<Box<caopt>>>,
3058) -> Option<Box<caopt>> {
3059    let line_bytes = line.as_bytes();
3060    if line_bytes.is_empty() {
3061        *lp = None;
3062        return None;
3063    }
3064    let pre = line_bytes[0]; // c:1750
3065    let mut idx: usize = 1;
3066    *lp = None; // c:1754
3067
3068    let single = match d.single.as_ref() {
3069        // c:1757
3070        Some(s) => s,
3071        None => return None,
3072    };
3073
3074    let mut p_cur: Option<&caopt> = None; // c:1755 p = NULL
3075    let mut pp_cur: Option<&caopt> = None;
3076    let mut list_acc: Option<Vec<Box<caopt>>> = None;
3077
3078    while idx < line_bytes.len() {
3079        // c:1755 for (;*line;line++)
3080        let ch = line_bytes[idx];
3081        let sidx = single_index(pre, ch); // c:1756
3082
3083        // c:1756 — d->single[sidx] lookup (assigns to p if valid).
3084        let lookup: Option<&caopt> = if sidx >= 0 && (sidx as usize) < single.len() {
3085            single[sidx as usize].as_deref()
3086        } else {
3087            None
3088        };
3089        if lookup.is_some() {
3090            p_cur = lookup;
3091        }
3092        let active_with_args = lookup.filter(|p| p.active != 0 && p.args.is_some());
3093
3094        if let Some(p) = active_with_args {
3095            // c:1757
3096            if p.r#type == CAO_NEXT {
3097                // c:1758
3098                let list = list_acc.get_or_insert_with(Vec::new);
3099                list.push(Box::new(caopt {
3100                    // c:1761 addlinknode
3101                    next: None,
3102                    name: p.name.clone(),
3103                    descr: p.descr.clone(),
3104                    xor: p.xor.clone(),
3105                    r#type: p.r#type,
3106                    args: None,
3107                    active: p.active,
3108                    num: p.num,
3109                    gsname: p.gsname.clone(),
3110                    not: p.not,
3111                }));
3112            } else {
3113                // c:1762
3114                idx += 1; // c:1764 line++
3115                if (p.r#type == CAO_OEQUAL || p.r#type == CAO_EQUAL)         // c:1765
3116                    && idx < line_bytes.len() && line_bytes[idx] == b'='
3117                {
3118                    idx += 1; // c:1767
3119                }
3120                *end = idx; // c:1768
3121                pp_cur = Some(p); // c:1770
3122                break; // c:1771
3123            }
3124        } else if p_cur.is_none() || p_cur.map_or(true, |p| p.active == 0) {
3125            // c:1773
3126            return None; // c:1774
3127        }
3128
3129        // c:1775 — pp = (p->name[0] == pre ? p : NULL); p = NULL.
3130        pp_cur = p_cur.filter(|p| {
3131            p.name
3132                .as_deref()
3133                .and_then(|n| n.as_bytes().first().copied())
3134                .map_or(false, |b| b == pre)
3135        });
3136        p_cur = None;
3137        idx += 1; // c:1755 line++
3138    }
3139
3140    // c:1778 — pp && end: *end = line.
3141    if pp_cur.is_some() {
3142        *end = idx;
3143    }
3144
3145    *lp = list_acc;
3146
3147    pp_cur.map(|p| {
3148        Box::new(caopt {
3149            // c:1780
3150            next: None,
3151            name: p.name.clone(),
3152            descr: p.descr.clone(),
3153            xor: p.xor.clone(),
3154            r#type: p.r#type,
3155            args: None,
3156            active: p.active,
3157            num: p.num,
3158            gsname: p.gsname.clone(),
3159            not: p.not,
3160        })
3161    })
3162}
3163
3164/// Direct port of `static int ca_foreign_opt(Cadef curset, Cadef all,
3165///                                            char *option)` from
3166/// `Src/Zle/computil.c:1786-1802`. Walks the `snext` chain of `all`
3167/// skipping `curset` and reports whether any other set defines an
3168/// option with the requested name. Returns 1 on match, 0 otherwise.
3169pub fn ca_foreign_opt(curset: &cadef, all: &cadef, option: &str) -> i32 {
3170    // c:1787
3171    let curset_ptr = curset as *const cadef;
3172    let mut d_opt = Some(all);
3173    while let Some(d) = d_opt {
3174        // c:1792
3175        if std::ptr::addr_eq(d as *const cadef, curset_ptr) {
3176            // c:1793
3177            d_opt = d.snext.as_deref();
3178            continue;
3179        }
3180        let mut p = d.opts.as_deref();
3181        while let Some(opt) = p {
3182            // c:1796
3183            if opt.name.as_deref() == Some(option) {
3184                // c:1797
3185                return 1; // c:1798
3186            }
3187            p = opt.next.as_deref();
3188        }
3189        d_opt = d.snext.as_deref();
3190    }
3191    0 // c:1801
3192}
3193
3194/// Direct port of `static Caarg ca_get_arg(Cadef d, int n)` from
3195/// `Src/Zle/computil.c:1807-1823`. Walks `d->args` looking for the
3196/// arg whose `[min, num]` range contains `n`. Falls back to `d->rest`
3197/// when no positional matches. Returns a shallow clone (no `next`)
3198/// of the matched arg.
3199pub fn ca_get_arg(d: &cadef, mut n: i32) -> Option<Box<caarg>> {
3200    // c:1807
3201    if d.argsactive == 0 {
3202        // c:1809
3203        return None; // c:1822
3204    }
3205
3206    // c:1810-1816 — skip inactive entries (advance `n` to compensate for
3207    // each skipped one, mirroring the C `n++` inside the loop).
3208    let mut a = d.args.as_deref();
3209    while let Some(node) = a {
3210        // c:1812
3211        let in_range = node.active != 0 && n >= node.min && n <= node.num;
3212        if in_range {
3213            break;
3214        } // c:1812 inverted
3215        if node.active == 0 {
3216            // c:1813
3217            n += 1; // c:1814
3218        }
3219        a = node.next.as_deref(); // c:1815
3220    }
3221
3222    if let Some(node) = a {
3223        // c:1817
3224        if node.active != 0 && node.min <= n && node.num >= n {
3225            return Some(Box::new(caarg {
3226                // c:1818
3227                next: None,
3228                descr: node.descr.clone(),
3229                xor: node.xor.clone(),
3230                action: node.action.clone(),
3231                r#type: node.r#type,
3232                end: node.end.clone(),
3233                opt: node.opt.clone(),
3234                num: node.num,
3235                min: node.min,
3236                direct: node.direct,
3237                active: node.active,
3238                gsname: node.gsname.clone(),
3239            }));
3240        }
3241    }
3242
3243    // c:1820 — rest fallback.
3244    if let Some(r) = d.rest.as_deref() {
3245        if r.active != 0 {
3246            return Some(Box::new(caarg {
3247                next: None,
3248                descr: r.descr.clone(),
3249                xor: r.xor.clone(),
3250                action: r.action.clone(),
3251                r#type: r.r#type,
3252                end: r.end.clone(),
3253                opt: r.opt.clone(),
3254                num: r.num,
3255                min: r.min,
3256                direct: r.direct,
3257                active: r.active,
3258                gsname: r.gsname.clone(),
3259            }));
3260        }
3261    }
3262    None // c:1820
3263}
3264
3265/// Direct port of `static void ca_inactive(Cadef d, char **xor, int cur,
3266///                                          int opts)` from
3267/// `Src/Zle/computil.c:1832-1918`. Marks options/args inactive based
3268/// on a xor-list (or, when `opts=1`, deactivates all options except
3269/// rest-of-line args). Each xor entry can be:
3270///   - bare name → exact-match opt or numeric positional or `:`/`-`/`*`/group
3271///   - `group-name-:` / `group-name--` → group-scoped exclusion
3272///   - excludeall path (just a set/group name) → kills the whole set
3273pub fn ca_inactive(d: &mut cadef, xor: &[String], cur: i32, opts: i32) {
3274    // c:1832
3275
3276    if (xor.is_empty() && opts == 0)                                         // c:1834
3277        || cur > COMPCURRENT.load(Ordering::Relaxed)
3278    {
3279        return;
3280    }
3281
3282    // c:1839 — single-letter exclusions only when at compcurrent (option
3283    // clumping safety: a prefix-of-longer-opt at cursor mustn't kill the
3284    // multi-letter form prematurely).
3285    let single = opts == 0 && cur == COMPCURRENT.load(Ordering::Relaxed);
3286
3287    // c:1841 — iterate xor entries. When opts=1 we synthesize a "-" pass.
3288    let iter_xor: Vec<String> = if opts != 0 {
3289        vec!["-".to_string()]
3290    } else {
3291        xor.to_vec()
3292    };
3293
3294    for x_orig in iter_xor.iter() {
3295        let mut x = x_orig.as_str();
3296        let mut excludeall = 0; // c:1842
3297        let mut grp: Option<&str> = None;
3298        let mut grplen: usize = 0;
3299
3300        // c:1845-1858 — split off optional `group-name-` prefix.
3301        let xb = x.as_bytes();
3302        let mut sep_byte = if xb.is_empty() { 0u8 } else { xb[0] };
3303        let mut sep_pos = 0usize;
3304        loop {
3305            if sep_pos >= xb.len() {
3306                break;
3307            }
3308            sep_byte = xb[sep_pos];
3309            if sep_byte == b'+'
3310                || sep_byte == b'-'
3311                || sep_byte == b':'
3312                || sep_byte == b'*'
3313                || idigit(sep_byte)
3314            {
3315                break;
3316            }
3317            // Find next '-'.
3318            let after = &xb[sep_pos..];
3319            let dash_off = after.iter().position(|&b| b == b'-');
3320            match dash_off {
3321                None => {
3322                    excludeall = 1; // c:1850
3323                    sep_pos = xb.len();
3324                    break;
3325                }
3326                Some(d) => {
3327                    let next = sep_pos + d + 1;
3328                    if next >= xb.len() {
3329                        // c:1848
3330                        excludeall = 1;
3331                        sep_pos = xb.len();
3332                        break;
3333                    }
3334                    sep_pos = next;
3335                }
3336            }
3337        }
3338        if sep_pos > 0 && sep_pos < xb.len() {
3339            // c:1859
3340            grp = Some(&x[..sep_pos]);
3341            grplen = sep_pos;
3342            x = &x[sep_pos..];
3343        } else if sep_pos > 0 && excludeall != 0 && sep_pos == xb.len() {
3344            // c:1850 path — the whole string was a group name.
3345            grp = Some(x);
3346            grplen = sep_pos;
3347            x = "";
3348        }
3349        let xb = x.as_bytes();
3350
3351        // c:1865 — excludeall or `:` alone.
3352        if excludeall != 0 || (xb.len() == 1 && xb[0] == b':') {
3353            if let Some(g) = grp {
3354                // c:1866
3355                let mut cur_arg = d.args.as_deref_mut();
3356                while let Some(a) = cur_arg {
3357                    let matches = a.gsname.as_deref().map_or(false, |gn| {
3358                        let gnb = gn.as_bytes();
3359                        gnb.len() == grplen + (excludeall as usize) && gn.starts_with(g)
3360                    });
3361                    if matches {
3362                        a.active = 0; // c:1872
3363                    }
3364                    cur_arg = a.next.as_deref_mut();
3365                }
3366                if let Some(r) = d.rest.as_deref_mut() {
3367                    let matches = r.gsname.as_deref().map_or(false, |gn| {
3368                        let gnb = gn.as_bytes();
3369                        gnb.len() == grplen + (excludeall as usize) && gn.starts_with(g)
3370                    });
3371                    if matches {
3372                        r.active = 0; // c:1876
3373                    }
3374                }
3375            } else {
3376                d.argsactive = 0; // c:1878
3377            }
3378        }
3379
3380        // c:1881 — excludeall or `-` alone: kill options.
3381        if excludeall != 0 || (xb.len() == 1 && xb[0] == b'-') {
3382            let mut cur_opt = d.opts.as_deref_mut();
3383            while let Some(p) = cur_opt {
3384                let grp_ok = grp.map_or(true, |g| {
3385                    p.gsname.as_deref().map_or(false, |gn| {
3386                        gn.len() == grplen + (excludeall as usize) && gn.starts_with(g)
3387                    })
3388                });
3389                let single_skip = single
3390                    && p.name.as_deref().map_or(false, |n| {
3391                        let nb = n.as_bytes();
3392                        nb.len() >= 3 && nb[0] != 0
3393                    });
3394                if grp_ok && !single_skip {
3395                    p.active = 0; // c:1888
3396                }
3397                cur_opt = p.next.as_deref_mut();
3398            }
3399        }
3400
3401        // c:1891 — excludeall or `*` alone: kill rest.
3402        if excludeall != 0 || (xb.len() == 1 && xb[0] == b'*') {
3403            if let Some(r) = d.rest.as_deref_mut() {
3404                let grp_ok = grp.map_or(true, |g| {
3405                    r.gsname.as_deref().map_or(false, |gn| {
3406                        gn.len() == grplen + (excludeall as usize) && gn.starts_with(g)
3407                    })
3408                });
3409                if grp_ok {
3410                    r.active = 0; // c:1895
3411                }
3412            }
3413        }
3414
3415        if excludeall == 0 {
3416            // c:1898
3417            if !xb.is_empty() && idigit(xb[0]) {
3418                // c:1899
3419                let n: i32 = x
3420                    .bytes()
3421                    .take_while(|b| idigit(*b))
3422                    .fold(0i32, |acc, b| acc * 10 + (b - b'0') as i32);
3423                let mut cur_arg = d.args.as_deref_mut();
3424                let mut hit: Option<&mut caarg> = None;
3425                while let Some(a) = cur_arg {
3426                    if a.num >= n {
3427                        hit = Some(a);
3428                        break;
3429                    }
3430                    cur_arg = a.next.as_deref_mut();
3431                }
3432                if let Some(a) = hit {
3433                    if a.num == n {
3434                        let grp_ok = grp.map_or(true, |g| {
3435                            a.gsname.as_deref().map_or(false, |gn| gn.starts_with(g))
3436                        });
3437                        if grp_ok {
3438                            a.active = 0; // c:1908
3439                        }
3440                    }
3441                }
3442            } else {
3443                // c:1909 — ca_get_opt for full match.
3444                let mut end_unused = 0usize;
3445                if let Some(matched) = ca_get_opt(d, x, 1, &mut end_unused) {
3446                    let grp_ok = grp.map_or(true, |g| {
3447                        matched
3448                            .gsname
3449                            .as_deref()
3450                            .map_or(false, |gn| gn.starts_with(g))
3451                    });
3452                    let single_skip = single
3453                        && matched.name.as_deref().map_or(false, |n| {
3454                            let nb = n.as_bytes();
3455                            nb.len() >= 3 && nb[0] != 0
3456                        });
3457                    if grp_ok && !single_skip {
3458                        // Walk d.opts to find the actual node and clear its active.
3459                        let target_name = matched.name.clone();
3460                        let mut cur_opt = d.opts.as_deref_mut();
3461                        while let Some(p) = cur_opt {
3462                            if p.name == target_name {
3463                                p.active = 0; // c:1912
3464                                break;
3465                            }
3466                            cur_opt = p.next.as_deref_mut();
3467                        }
3468                    }
3469                }
3470            }
3471            if opts != 0 {
3472                break; // c:1914
3473            }
3474        }
3475        let _ = sep_byte;
3476    }
3477}
3478
3479// =====================================================================
3480// `castate` — command-line parse state for `_arguments`.
3481// Src/Zle/computil.c:1920-1957.
3482// =====================================================================
3483
3484/// Port of `typedef struct castate *Castate` from
3485/// `Src/Zle/computil.c:1922`.
3486pub type Castate = Box<castate>; // c:1922
3487
3488/// Direct port of `struct castate` from `Src/Zle/computil.c:1928-1953`.
3489/// Encapsulates the parsed-command-line state for one `_arguments`
3490/// set — used as a linked list (`snext`) with one state per set.
3491#[derive(Debug, Default, Clone)]
3492#[allow(non_camel_case_types)]
3493pub struct castate {
3494    // c:1928
3495    pub snext: Option<Box<castate>>, // c:1929 Castate snext
3496    pub d: Option<Box<cadef>>,       // c:1930 Cadef d
3497    pub nopts: i32,                  // c:1931
3498    pub def: Option<Box<caarg>>,     // c:1932 Caarg def
3499    pub ddef: Option<Box<caarg>>,    // c:1933 Caarg ddef
3500    pub curopt: Option<Box<caopt>>,  // c:1934 Caopt curopt
3501    pub dopt: Option<Box<caopt>>,    // c:1935 Caopt dopt
3502    pub opt: i32,                    // c:1936
3503    pub arg: i32,                    // c:1937
3504    pub argbeg: i32,                 // c:1938
3505    pub optbeg: i32,                 // c:1939
3506    pub nargbeg: i32,                // c:1941
3507    pub restbeg: i32,                // c:1942
3508    pub curpos: i32,                 // c:1943
3509    pub argend: i32,                 // c:1944
3510    pub inopt: i32,                  // c:1945
3511    pub inarg: i32,                  // c:1946
3512    pub nth: i32,                    // c:1947
3513    pub singles: i32,                // c:1948
3514    pub oopt: i32,                   // c:1949
3515    pub actopts: i32,                // c:1950
3516    pub args: Option<Vec<String>>,   // c:1951 LinkList args
3517    pub oargs: Option<Vec<Option<Vec<String>>>>, // c:1952 LinkList *oargs
3518}
3519
3520/// Port of `static int ca_parsed` from `Src/Zle/computil.c:1956`.
3521pub static ca_parsed: std::sync::atomic::AtomicI32 = // c:1956
3522    std::sync::atomic::AtomicI32::new(0);
3523
3524/// Port of `static int ca_doff` from `Src/Zle/computil.c:1960`. Count
3525/// of chars of ignored prefix (for clumped options or arg to an
3526/// option).
3527pub static ca_doff: std::sync::atomic::AtomicI32 = // c:1960
3528    std::sync::atomic::AtomicI32::new(0);
3529
3530/// Direct port of `static void freecastate(Castate s)` from
3531/// `Src/Zle/computil.c:1960`. Frees the args/oargs lists.
3532pub fn freecastate(s: &mut castate) {
3533    // c:1960
3534    s.args = None; // c:1960 freelinklist(s->args)
3535    s.oargs = None; // c:1966-1969 freelinklist per slot
3536}
3537
3538/// Port of `ca_opt_arg(Caopt opt, char *line)` from Src/Zle/computil.c:1976.
3539/// WARNING: param names don't match C — Rust=(opt_name, line, equal_kind) vs C=(opt, line)
3540pub fn ca_opt_arg(opt_name: &str, line: &str, equal_kind: bool) -> String {
3541    // c:1976
3542    // C body c:1978-1996: walks `o = opt->name` and `line` byte-by-byte,
3543    //                     skipping `\\` escapes; if any quote (`\\` `'` `"`)
3544    //                     in line, advance line; once they diverge, return
3545    //                     dup of remaining line minus optional `=` if
3546    //                     opt is CAO_EQUAL/CAO_OEQUAL.
3547    let o_bytes = opt_name.as_bytes();
3548    let l_bytes = line.as_bytes();
3549    let mut oi = 0usize;
3550    let mut li = 0usize;
3551    loop {
3552        // c:1980
3553        if oi >= o_bytes.len() || li >= l_bytes.len() {
3554            break;
3555        }
3556        let mut oc = o_bytes[oi];
3557        if oc == b'\\' {
3558            // c:1981
3559            oi += 1;
3560            if oi >= o_bytes.len() {
3561                break;
3562            }
3563            oc = o_bytes[oi];
3564        }
3565        let mut lc = l_bytes[li];
3566        if matches!(lc, b'\\' | b'\'' | b'"') {
3567            // c:1983
3568            li += 1;
3569            if li >= l_bytes.len() {
3570                break;
3571            }
3572            lc = l_bytes[li];
3573        }
3574        if oc != lc {
3575            // c:1985
3576            break;
3577        }
3578        oi += 1;
3579        li += 1;
3580    }
3581    let rest = &l_bytes[li..];
3582    let mut s = String::from_utf8_lossy(rest).into_owned();
3583    if equal_kind && s.starts_with('\\') {
3584        // c:2004
3585        s.remove(0);
3586    }
3587    if equal_kind {
3588        s = s.strip_prefix('=').map(|t| t.to_string()).unwrap_or(s); // c:2004
3589    }
3590    s
3591}
3592
3593/// Direct port of `static int ca_parse_line(Cadef d, Cadef all, int multi,
3594///                                            int first)` from
3595/// `Src/Zle/computil.c:2004-2403`. Walks `compwords[1..]` matching
3596/// each word against the cadef's option/arg specs, updating active
3597/// flags via `ca_inactive` and populating `ca_laststate` with the
3598/// completion-point info downstream subcommands need. Returns 1 when
3599/// the set should be skipped (foreign option spotted in multi-set
3600/// mode), 0 otherwise.
3601///
3602/// Substrate notes:
3603/// - `endpat` (CAA_RREST/RARGS end-pattern matching) is compiled via
3604///   `patcompile`; the resulting `Patprog` is held in the local
3605///   `endpat` slot exactly like the C code.
3606/// - `napat` (the `-A` "non-arg" pattern) is also compiled via
3607///   `patcompile`.
3608/// - The `sopts` (clumped single-letter remainders) LinkList is
3609///   represented as a `Vec<Box<caopt>>` queue.
3610pub fn ca_parse_line(d: &mut cadef, all: &cadef, multi: i32, first: i32) -> i32 {
3611    // c:2004
3612
3613    let compcur = COMPCURRENT.load(Ordering::Relaxed);
3614
3615    // c:2019 — free old state if this is the first set.
3616    if first != 0 && ca_alloced.load(Ordering::Relaxed) != 0 {
3617        if let Ok(mut ls) = ca_laststate.lock() {
3618            freecastate(&mut ls);
3619            ls.snext = None;
3620        }
3621    }
3622
3623    // c:2030-2036 — mark everything active.
3624    let mut p = d.opts.as_deref_mut();
3625    while let Some(o) = p {
3626        o.active = 1;
3627        p = o.next.as_deref_mut();
3628    }
3629    d.argsactive = 1;
3630    if let Some(r) = d.rest.as_deref_mut() {
3631        r.active = 1;
3632    }
3633    let mut a = d.args.as_deref_mut();
3634    while let Some(ar) = a {
3635        ar.active = 1;
3636        a = ar.next.as_deref_mut();
3637    }
3638
3639    // c:2040-2056 — build the initial castate.
3640    let compwords: Vec<String> = COMPWORDS
3641        .get()
3642        .and_then(|m| m.lock().ok().map(|w| w.clone()))
3643        .unwrap_or_default();
3644    let argend_init = (compwords.len() as i32) - 1;
3645
3646    // Set up the working state. Note d is stored as None in the
3647    // working `state`; we re-populate ca_laststate.d from a clone at
3648    // the end (we can't move d into state since the caller still owns
3649    // it).
3650    let mut state = castate {
3651        snext: None,
3652        d: None,
3653        nopts: d.nopts,
3654        def: None,
3655        ddef: None,
3656        curopt: None,
3657        dopt: None,
3658        opt: 1,
3659        arg: 1,
3660        argbeg: 1,
3661        optbeg: 1,
3662        nargbeg: 1,
3663        restbeg: 1,
3664        actopts: 1,
3665        nth: 1,
3666        inarg: 1,
3667        inopt: 0,
3668        singles: 0,
3669        oopt: 0,
3670        argend: argend_init,
3671        curpos: compcur,
3672        args: Some(Vec::new()),
3673        oargs: Some((0..d.nopts as usize).map(|_| None).collect()),
3674    };
3675    ca_alloced.store(1, Ordering::Relaxed);
3676
3677    // Snapshot state → ca_laststate (the "early return on empty"
3678    // path uses this).
3679    if let Ok(mut ls) = ca_laststate.lock() {
3680        *ls = clone_castate(&state, d);
3681    }
3682
3683    if compwords.len() < 2 {
3684        // c:2058
3685        if let Ok(mut ls) = ca_laststate.lock() {
3686            ls.opt = 0;
3687            ls.arg = 0;
3688        }
3689        // c:2061 goto end — fall through to actopts count.
3690    } else {
3691        // c:2063-2064 — compile -A nonarg pattern.
3692        let napat = d
3693            .nonarg
3694            .as_deref()
3695            .and_then(|s| patcompile(&{ let mut __pat_tok = (s).to_string(); crate::ported::glob::tokenize(&mut __pat_tok); __pat_tok }, 0, None::<&mut String>));
3696        let mut endpat: Option<Patprog> = None;
3697
3698        // c:2068 — walk words.
3699        let mut cur = 2i32;
3700        let mut argxor: Option<Vec<String>> = None;
3701        let mut sopts: Vec<Box<caopt>> = Vec::new();
3702        let mut wasopt_idx: Option<usize> = None;
3703        let mut doff: i32 = 0;
3704        let mut adef: Option<Box<caarg>> = None;
3705        let mut ddef: Option<Box<caarg>> = None;
3706        let mut dopt: Option<Box<caopt>> = None;
3707        state.curopt = None;
3708        state.def = None;
3709
3710        loop {
3711            let line_idx = (cur - 1) as usize;
3712            if line_idx >= compwords.len() {
3713                break;
3714            }
3715            let oline = compwords[line_idx].clone();
3716            let mut line = oline.clone();
3717            ddef = None;
3718            adef = None;
3719            dopt = None;
3720            state.singles = 0;
3721            let mut arglast = 0;
3722
3723            remnulargs(&mut line);
3724            line = untokenize(&line);
3725
3726            // c:2095 — apply pending arg-xor.
3727            if let Some(xor) = argxor.take() {
3728                ca_inactive(d, &xor, cur - 1, 0);
3729            }
3730
3731            // c:2099 — CDF_SEP `--` separator turns off option parsing.
3732            if (d.flags & CDF_SEP) != 0 && cur != compcur && state.actopts != 0 && line == "--" {
3733                ca_inactive(d, &[], cur, 1);
3734                state.actopts = 0;
3735                cur += 1;
3736                continue;
3737            }
3738
3739            // c:2108 — already have a def from previous opt, collect args.
3740            if state.def.is_some() {
3741                state.arg = 0;
3742                if let Some(co) = state.curopt.as_deref() {
3743                    let cn = co.num as usize;
3744                    if let Some(oargs) = state.oargs.as_mut() {
3745                        if cn < oargs.len() {
3746                            oargs[cn].get_or_insert_with(Vec::new).push(oline.clone());
3747                        }
3748                    }
3749                }
3750                let def_type = state.def.as_deref().map_or(0, |d| d.r#type);
3751                let def_is_opt = def_type == CAA_OPT;
3752                state.opt = if def_is_opt { 1 } else { 0 };
3753                if def_is_opt {
3754                    if state.def.as_deref().map_or(false, |d| d.opt.is_some()) {
3755                        state.oopt += 1;
3756                    }
3757                }
3758
3759                if def_type == CAA_REST || def_type == CAA_RARGS || def_type == CAA_RREST {
3760                    // c:2118 — end-pattern check.
3761                    let matched_end = state
3762                        .def
3763                        .as_deref()
3764                        .and_then(|d| d.end.as_deref())
3765                        .map_or(false, |_| {
3766                            endpat.as_ref().map_or(false, |ep| pattry(ep, &line))
3767                        });
3768                    if matched_end {
3769                        state.def = None;
3770                        state.curopt = None;
3771                        state.opt = 1;
3772                        state.arg = 1;
3773                        state.argend = cur - 1;
3774                        if let Ok(mut ls) = ca_laststate.lock() {
3775                            ls.argend = cur - 1;
3776                        }
3777                        // c:2124 goto cont.
3778                    }
3779                } else {
3780                    // c:2125 — advance to next arg slot.
3781                    let next = state.def.as_deref().and_then(|d| d.next.clone());
3782                    if next.is_some() {
3783                        state.def = next;
3784                        state.argbeg = cur;
3785                        state.argend = argend_init;
3786                    } else if let Some(s) = sopts.first().cloned() {
3787                        // c:2128 — pop a queued single-letter opt arg.
3788                        sopts.remove(0);
3789                        state.curopt = Some(s);
3790                        state.def = state.curopt.as_deref().and_then(|c| c.args.clone());
3791                        state.opt = 0;
3792                        state.argbeg = cur;
3793                        state.optbeg = cur;
3794                        state.inopt = cur;
3795                        state.argend = argend_init;
3796                        doff = 0;
3797                        state.singles = 1;
3798                        if let Some(co) = state.curopt.as_deref() {
3799                            let cn = co.num as usize;
3800                            if let Some(oargs) = state.oargs.as_mut() {
3801                                if cn < oargs.len() && oargs[cn].is_none() {
3802                                    oargs[cn] = Some(Vec::new());
3803                                }
3804                            }
3805                        }
3806                        // c:2138 goto cont.
3807                    } else {
3808                        state.curopt = None;
3809                        state.opt = 1;
3810                    }
3811                }
3812            } else {
3813                state.opt = 1;
3814                state.arg = 1;
3815                state.curopt = None;
3816            }
3817            if state.opt != 0 {
3818                let lb = line.as_bytes();
3819                state.opt = if lb.is_empty() {
3820                    0
3821                } else if lb.len() == 1 {
3822                    1
3823                } else {
3824                    2
3825                };
3826            }
3827
3828            let mut pe_off: i32 = 0;
3829            wasopt_idx = None;
3830
3831            // c:2156 — option lookup.
3832            let opt_match = if state.opt == 2 {
3833                let lb = line.as_bytes();
3834                if !lb.is_empty() && (lb[0] == b'-' || lb[0] == b'+') {
3835                    let mut end = 0usize;
3836                    if let Some(found) = ca_get_opt(d, &line, 0, &mut end) {
3837                        pe_off = end as i32;
3838                        // c:2158 — for OEQUAL/EQUAL check `=` boundary.
3839                        let pe_ok = match found.r#type {
3840                            t if t == CAO_OEQUAL => {
3841                                (line_idx + 1 < compwords.len())
3842                                    || (pe_off > 0 && lb.get(pe_off as usize - 1) == Some(&b'='))
3843                            }
3844                            t if t == CAO_EQUAL => {
3845                                pe_off > 0
3846                                    && (lb.get(pe_off as usize - 1) == Some(&b'=')
3847                                        || pe_off as usize >= lb.len())
3848                            }
3849                            _ => true,
3850                        };
3851                        if pe_ok {
3852                            Some(found)
3853                        } else {
3854                            None
3855                        }
3856                    } else {
3857                        None
3858                    }
3859                } else {
3860                    None
3861                }
3862            } else {
3863                None
3864            };
3865
3866            if let Some(co) = opt_match {
3867                // Bind found opt details for later.
3868                let co_name = co.name.clone().unwrap_or_default();
3869                let co_type = co.r#type;
3870                let co_num = co.num;
3871                let co_xor = co.xor.clone().unwrap_or_default();
3872                let co_args = co.args.clone();
3873                state.curopt = Some(co);
3874                let pe_at_eq =
3875                    pe_off > 0 && line.as_bytes().get(pe_off as usize - 1) == Some(&b'=');
3876                let pe_tail_present = (pe_off as usize) < line.as_bytes().len();
3877
3878                let take_args = co_type != CAO_EQUAL || pe_at_eq;
3879                state.def = if take_args { co_args.clone() } else { None };
3880                if state.def.is_some() {
3881                    ddef = state.def.clone();
3882                    dopt = state.curopt.clone();
3883                }
3884
3885                doff = pe_off;
3886                state.optbeg = cur;
3887                state.argbeg = cur;
3888                state.inopt = cur;
3889                state.argend = argend_init;
3890                let single_ok = d.single.is_some()
3891                    && !pe_tail_present
3892                    && co_name.as_bytes().len() >= 2
3893                    && co_name.as_bytes()[1] != b'-'
3894                    && co_name.as_bytes().get(2).is_none();
3895                state.singles = if single_ok { 1 } else { 0 };
3896
3897                let cn = co_num as usize;
3898                if let Some(oargs) = state.oargs.as_mut() {
3899                    if cn < oargs.len() && oargs[cn].is_none() {
3900                        oargs[cn] = Some(Vec::new());
3901                    }
3902                }
3903                ca_inactive(d, &co_xor, cur, 0); // c:2179
3904
3905                let collect_arg = state.def.is_some()
3906                    && (co_type == CAO_DIRECT
3907                        || co_type == CAO_EQUAL
3908                        || (co_type == CAO_ODIRECT && pe_tail_present)
3909                        || (co_type == CAO_OEQUAL && (pe_tail_present || pe_at_eq)));
3910                if collect_arg {
3911                    let dtype = state.def.as_deref().map_or(0, |d| d.r#type);
3912                    if dtype != CAA_REST && dtype != CAA_RARGS && dtype != CAA_RREST {
3913                        let next = state.def.as_deref().and_then(|d| d.next.clone());
3914                        state.def = next;
3915                    }
3916                    let arg_str = ca_opt_arg(&co_name, &oline, false);
3917                    if let Some(oargs) = state.oargs.as_mut() {
3918                        if cn < oargs.len() {
3919                            oargs[cn].get_or_insert_with(Vec::new).push(arg_str);
3920                        }
3921                    }
3922                }
3923                if state.def.is_some() {
3924                    state.opt = 0;
3925                } else {
3926                    if d.single.is_none()
3927                        || (co_name.as_bytes().len() >= 3 && co_name.as_bytes()[1] != 0)
3928                    {
3929                        wasopt_idx = Some(co_num as usize); // c:2201
3930                    }
3931                    state.curopt = None;
3932                }
3933            } else if state.opt == 2
3934                && d.single.is_some()
3935                && line
3936                    .as_bytes()
3937                    .first()
3938                    .copied()
3939                    .map_or(false, |b| b == b'-' || b == b'+')
3940            {
3941                // c:2204 — single-letter clump.
3942                let mut end = 0usize;
3943                let mut tmp_sopts: Option<Vec<Box<caopt>>> = None;
3944                let s_match = ca_get_sopt(d, &line, &mut end, &mut tmp_sopts);
3945                if let Some(queued) = tmp_sopts {
3946                    sopts.extend(queued);
3947                }
3948                let active_sopt = s_match.or_else(|| {
3949                    if cur != compcur && !sopts.is_empty() {
3950                        Some(sopts.remove(0))
3951                    } else {
3952                        None
3953                    }
3954                });
3955                if let Some(co) = active_sopt {
3956                    let co_name = co.name.clone().unwrap_or_default();
3957                    let co_type = co.r#type;
3958                    let co_num = co.num;
3959                    let co_xor = co.xor.clone().unwrap_or_default();
3960                    let co_args = co.args.clone();
3961                    state.curopt = Some(co);
3962
3963                    let cn = co_num as usize;
3964                    if let Some(oargs) = state.oargs.as_mut() {
3965                        if cn < oargs.len() && oargs[cn].is_none() {
3966                            oargs[cn] = Some(Vec::new());
3967                        }
3968                    }
3969                    state.def = co_args.clone();
3970                    if co_type == CAO_NEXT && cur == compcur {
3971                        ddef = None;
3972                    } else {
3973                        ddef = state.def.clone();
3974                    }
3975                    dopt = state.curopt.clone();
3976                    doff = end as i32;
3977                    state.optbeg = cur;
3978                    state.argbeg = cur;
3979                    state.inopt = cur;
3980                    state.argend = argend_init;
3981                    state.singles = if end >= line.as_bytes().len() { 1 } else { 0 };
3982
3983                    let lb = line.as_bytes();
3984                    let pre = lb.first().copied().unwrap_or(0);
3985                    let mut p_idx = 1usize;
3986                    while p_idx < end.min(lb.len()) {
3987                        let sidx = single_index(pre, lb[p_idx]);
3988                        if sidx >= 0 && (sidx as usize) < d.single.as_ref().map_or(0, |s| s.len()) {
3989                            let tmp_xor = d
3990                                .single
3991                                .as_ref()
3992                                .and_then(|s| s.get(sidx as usize))
3993                                .and_then(|so| so.as_ref())
3994                                .and_then(|so| so.xor.clone());
3995                            let tmp_num = d
3996                                .single
3997                                .as_ref()
3998                                .and_then(|s| s.get(sidx as usize))
3999                                .and_then(|so| so.as_ref())
4000                                .map_or(-1, |so| so.num);
4001                            let tn = tmp_num as usize;
4002                            if tmp_num >= 0 {
4003                                if let Some(oargs) = state.oargs.as_mut() {
4004                                    if tn < oargs.len() && oargs[tn].is_none() {
4005                                        oargs[tn] = Some(Vec::new());
4006                                    }
4007                                }
4008                            }
4009                            if let Some(xor) = tmp_xor {
4010                                ca_inactive(d, &xor, cur, 0);
4011                            }
4012                        }
4013                        p_idx += 1;
4014                    }
4015
4016                    let pe_tail_present = end < line.as_bytes().len();
4017                    let pe_at_eq = end > 0 && line.as_bytes().get(end - 1) == Some(&b'=');
4018                    let collect_arg = state.def.is_some()
4019                        && (co_type == CAO_DIRECT
4020                            || co_type == CAO_EQUAL
4021                            || (co_type == CAO_ODIRECT && pe_tail_present)
4022                            || (co_type == CAO_OEQUAL && (pe_tail_present || pe_at_eq)));
4023                    if collect_arg {
4024                        let dtype = state.def.as_deref().map_or(0, |d| d.r#type);
4025                        if dtype != CAA_REST && dtype != CAA_RARGS && dtype != CAA_RREST {
4026                            let next = state.def.as_deref().and_then(|d| d.next.clone());
4027                            state.def = next;
4028                        }
4029                        let arg_str = ca_opt_arg(&co_name, &line, false);
4030                        if let Some(oargs) = state.oargs.as_mut() {
4031                            if cn < oargs.len() {
4032                                oargs[cn].get_or_insert_with(Vec::new).push(arg_str);
4033                            }
4034                        }
4035                    }
4036                    if state.def.is_some() {
4037                        state.opt = 0;
4038                    } else {
4039                        state.curopt = None;
4040                    }
4041                }
4042            } else if multi != 0
4043                && line
4044                    .as_bytes()
4045                    .first()
4046                    .copied()
4047                    .map_or(false, |b| b == b'-' || b == b'+')
4048                && cur != compcur
4049                && ca_foreign_opt(d, all, &line) != 0
4050            {
4051                return 1; // c:2258
4052            } else if state.arg != 0 && cur <= compcur {
4053                // c:2259
4054                // c:2264 — napat -A pattern.
4055                if let Some(np) = napat.as_ref() {
4056                    if cur < compcur && state.actopts != 0 {
4057                        if pattry(np, &line) {
4058                            cur += 1;
4059                            continue;
4060                        }
4061                        ca_inactive(d, &[], cur + 1, 1);
4062                        state.actopts = 0;
4063                    }
4064                }
4065
4066                arglast = 1;
4067                if state.inopt != 0 {
4068                    // c:2274
4069                    state.inopt = 0;
4070                    state.nargbeg = cur - 1;
4071                    state.argend = argend_init;
4072                }
4073                // c:2279 — no args/rest + non-empty non-flag line → skip set.
4074                let lb = line.as_bytes();
4075                let non_flag = !lb.is_empty() && lb[0] != b'-' && lb[0] != b'+';
4076                if d.args.is_none() && d.rest.is_none() && non_flag {
4077                    if multi == 0 && cur > compcur {
4078                        break;
4079                    }
4080                    return 1;
4081                }
4082
4083                adef = ca_get_arg(d, state.nth);
4084                state.def = adef.clone();
4085                let dtype = state.def.as_deref().map_or(0, |d| d.r#type);
4086                if state.def.is_some() && (dtype == CAA_RREST || dtype == CAA_RARGS) {
4087                    if ca_laststate
4088                        .lock()
4089                        .map(|ls| ls.def.is_some())
4090                        .unwrap_or(false)
4091                    {
4092                        break;
4093                    }
4094                    state.opt = if cur == state.nargbeg + 1
4095                        && (multi == 0 || line.is_empty() || lb[0] == b'-' || lb[0] == b'+')
4096                    {
4097                        1
4098                    } else {
4099                        0
4100                    };
4101                    state.optbeg = state.nargbeg;
4102                    state.argbeg = cur - 1;
4103                    state.argend = argend_init;
4104                    // c:2311 — gather remaining words into state.args.
4105                    while line_idx < compwords.len() {
4106                        state
4107                            .args
4108                            .get_or_insert_with(Vec::new)
4109                            .push(compwords[line_idx].clone());
4110                        cur += 1;
4111                        if (cur - 1) as usize >= compwords.len() {
4112                            break;
4113                        }
4114                    }
4115                    if let Ok(mut ls) = ca_laststate.lock() {
4116                        *ls = clone_castate(&state, d);
4117                        ls.ddef = None;
4118                        ls.dopt = None;
4119                    }
4120                    break;
4121                }
4122                state.args.get_or_insert_with(Vec::new).push(line.clone());
4123                if let Some(a) = adef.as_deref() {
4124                    state.oopt = a.num - state.nth;
4125                }
4126
4127                if state.def.is_some() && cur != compcur {
4128                    // c:2323
4129                    argxor = state.def.as_deref().and_then(|d| d.xor.clone());
4130                }
4131                let dtype2 = state.def.as_deref().map_or(0, |d| d.r#type);
4132                if state.def.is_some()
4133                    && dtype2 != CAA_NORMAL
4134                    && dtype2 != CAA_OPT
4135                    && state.inarg != 0
4136                {
4137                    state.restbeg = cur;
4138                    state.inarg = 0;
4139                } else if state.def.is_none() || dtype2 == CAA_NORMAL || dtype2 == CAA_OPT {
4140                    state.inarg = 1;
4141                }
4142                state.nth += 1;
4143                state.def = None;
4144            }
4145
4146            // c:2338 — end-pattern compile for rest-args.
4147            if state.def.is_some() && state.curopt.is_some() {
4148                let dt = state.def.as_deref().map_or(0, |d| d.r#type);
4149                if dt == CAA_RREST || dt == CAA_RARGS {
4150                    let end_pat_str = state.def.as_deref().and_then(|d| d.end.clone());
4151                    if let Some(eps) = end_pat_str {
4152                        endpat = patcompile(&{ let mut __pat_tok = (&eps).to_string(); crate::ported::glob::tokenize(&mut __pat_tok); __pat_tok }, 0, None::<&mut String>);
4153                    } else {
4154                        // c:2342-2353 — no end-pattern: gather rest into oargs.
4155                        if cur < compcur {
4156                            if let Ok(mut ls) = ca_laststate.lock() {
4157                                *ls = clone_castate(&state, d);
4158                            }
4159                        }
4160                        let cn = state.curopt.as_deref().map(|c| c.num as usize);
4161                        if let Some(cn) = cn {
4162                            if let Some(oargs) = state.oargs.as_mut() {
4163                                if cn < oargs.len() {
4164                                    let bucket = oargs[cn].get_or_insert_with(Vec::new);
4165                                    let mut k = line_idx;
4166                                    while k < compwords.len() {
4167                                        bucket.push(compwords[k].clone());
4168                                        k += 1;
4169                                    }
4170                                }
4171                            }
4172                        }
4173                        if let Ok(mut ls) = ca_laststate.lock() {
4174                            ls.ddef = None;
4175                            ls.dopt = None;
4176                        }
4177                        break;
4178                    }
4179                }
4180            } else if state.def.is_some() {
4181                let eps = state.def.as_deref().and_then(|d| d.end.clone());
4182                if let Some(eps) = eps {
4183                    endpat = patcompile(&{ let mut __pat_tok = (&eps).to_string(); crate::ported::glob::tokenize(&mut __pat_tok); __pat_tok }, 0, None::<&mut String>);
4184                }
4185            }
4186
4187            // c:2360 cont: — checkpoint to ca_laststate.
4188            if cur + 1 == compcur {
4189                if let Ok(mut ls) = ca_laststate.lock() {
4190                    *ls = clone_castate(&state, d);
4191                    ls.ddef = None;
4192                    ls.dopt = None;
4193                }
4194            } else if cur == compcur {
4195                let mut ls = ca_laststate.lock().unwrap();
4196                if ls.def.is_none() {
4197                    if let Some(ddef_v) = ddef.clone() {
4198                        ls.def = Some(ddef_v);
4199                        ls.singles = state.singles;
4200                        if state
4201                            .curopt
4202                            .as_deref()
4203                            .map_or(false, |c| c.r#type == CAO_NEXT)
4204                        {
4205                            ls.ddef = ddef.clone();
4206                            ls.dopt = dopt.clone();
4207                            ls.def = None;
4208                            ls.opt = 1;
4209                            // Mark curopt active again in d.
4210                            if let Some(co) = state.curopt.as_deref() {
4211                                let target_name = co.name.clone();
4212                                let mut p = d.opts.as_deref_mut();
4213                                while let Some(op) = p {
4214                                    if op.name == target_name {
4215                                        op.active = 1;
4216                                        break;
4217                                    }
4218                                    p = op.next.as_deref_mut();
4219                                }
4220                            }
4221                        } else {
4222                            ca_doff.store(doff, Ordering::Relaxed);
4223                            ls.opt = 0;
4224                        }
4225                    } else {
4226                        ls.def = adef.clone();
4227                        ls.opt = if arglast == 0
4228                            || multi == 0
4229                            || line.is_empty()
4230                            || line.as_bytes()[0] == b'-'
4231                            || line.as_bytes()[0] == b'+'
4232                        {
4233                            1
4234                        } else {
4235                            0
4236                        };
4237                        ls.ddef = None;
4238                        ls.dopt = None;
4239                        ls.optbeg = state.nargbeg;
4240                        ls.argbeg = state.restbeg;
4241                        ls.argend = state.argend;
4242                        ls.singles = state.singles;
4243                        ls.oopt = state.oopt;
4244                        if let Some(wi) = wasopt_idx {
4245                            let mut p = d.opts.as_deref_mut();
4246                            while let Some(op) = p {
4247                                if op.num as usize == wi {
4248                                    op.active = 1;
4249                                    break;
4250                                }
4251                                p = op.next.as_deref_mut();
4252                            }
4253                        }
4254                    }
4255                }
4256            }
4257            cur += 1;
4258        }
4259        let _ = (endpat, ddef, dopt, adef);
4260    }
4261
4262    // c:2397-2400 — count active opts.
4263    let mut actopts = 0i32;
4264    let mut p = d.opts.as_deref();
4265    while let Some(o) = p {
4266        if o.active != 0 {
4267            actopts += 1;
4268        }
4269        p = o.next.as_deref();
4270    }
4271    if let Ok(mut ls) = ca_laststate.lock() {
4272        ls.actopts = actopts;
4273        // Make sure ls.d reflects the (now-mutated) d.
4274        ls.d = Some(Box::new(clone_cadef_shallow(d)));
4275    }
4276    0
4277}
4278
4279/// Port of `ca_nullist(LinkList l)` from Src/Zle/computil.c:2411.
4280pub fn ca_nullist(l: &[String]) -> Vec<u8> {
4281    // c:2411
4282    // C body c:2413-2419 — `if (l) { array = zlinklist2array(l, 0);
4283    //                              ret = zjoin(array, '\\0', 0); free(array);
4284    //                              return ret; } else return ztrdup("")`.
4285    //                      Returns NUL-joined byte buffer.
4286    if l.is_empty() {
4287        return Vec::new(); // c:2419
4288    }
4289    let mut out = Vec::new();
4290    for (i, item) in l.iter().enumerate() {
4291        if i > 0 {
4292            out.push(0);
4293        }
4294        out.extend_from_slice(item.as_bytes());
4295    }
4296    out
4297}
4298
4299/// Port of `ca_colonlist(LinkList l)` from Src/Zle/computil.c:2428.
4300pub fn ca_colonlist(l: &[String]) -> String {
4301    // c:2428
4302    // C body c:2430-2459 — joins l with `:`, escapes `:` and `\`
4303    //                      with `\` per item.
4304    if l.is_empty() {
4305        return String::new(); // c:2459
4306    }
4307    let mut out = String::new();
4308    for (i, item) in l.iter().enumerate() {
4309        // c:2444
4310        if i > 0 {
4311            out.push(':'); // c:2452
4312        }
4313        for ch in item.chars() {
4314            if ch == ':' || ch == '\\' {
4315                // c:2447
4316                out.push('\\');
4317            }
4318            out.push(ch);
4319        }
4320    }
4321    out
4322}
4323
4324/// Direct port of `static void ca_set_data(LinkList descr, LinkList act,
4325///                                           LinkList subc, char *opt,
4326///                                           Caarg arg, Caopt optdef,
4327///                                           int single)` from
4328/// `Src/Zle/computil.c:2472-2582`. Appends to descr/act/subc the
4329/// description/action/subcontext for each `arg` whose `[min,num]`
4330/// range covers `ca_laststate.nth`. When `opt` is non-None, all
4331/// args are treated as option args; otherwise positional. Recurses
4332/// via the goto-rec C path to retry after the first loop when more
4333/// state remains.
4334pub fn ca_set_data(
4335    descr: &mut Vec<String>, // c:2472
4336    act: &mut Vec<String>,
4337    subc: &mut Vec<String>,
4338    opt: Option<&str>,
4339    start_arg: Option<Box<caarg>>,
4340    optdef: Option<&caopt>,
4341    single: i32,
4342) {
4343    let mut arg: Option<Box<caarg>> = start_arg;
4344    let mut opt = opt.map(|s| s.to_string());
4345    let mut restr = 0;
4346    let mut miss = 0;
4347    let mut oopt = 1i32;
4348    let mut lopt;
4349
4350    'rec: loop {
4351        // c:2481 — addopt = (opt ? 0 : ca_laststate.oopt).
4352        let addopt = if opt.is_some() {
4353            0
4354        } else {
4355            ca_laststate.lock().map(|s| s.oopt).unwrap_or(0)
4356        };
4357
4358        // c:2483 — main arg walk.
4359        while let Some(a) = arg.as_ref() {
4360            let cont = {
4361                let nth = ca_laststate.lock().map(|s| s.nth).unwrap_or(0);
4362                opt.is_some() || a.num < 0 || (a.min <= nth + addopt && a.num >= nth)
4363            };
4364            if !cont {
4365                break;
4366            }
4367
4368            lopt = a.r#type == CAA_OPT; // c:2486
4369            if opt.is_none() && !lopt && oopt > 0 {
4370                // c:2487
4371                oopt = 0;
4372            }
4373
4374            // c:2490 — dedup: skip if (descr, act) pair already present.
4375            let mut dup = false;
4376            let descr_str = a.descr.clone().unwrap_or_default();
4377            let act_str = a.action.clone().unwrap_or_default();
4378            for (d, ac) in descr.iter().zip(act.iter()) {
4379                if d == &descr_str && ac == &act_str {
4380                    dup = true;
4381                    break;
4382                }
4383            }
4384
4385            // c:2497 — with ignored prefix, no normal args.
4386            if single != 0 && a.opt.is_none() {
4387                return;
4388            }
4389
4390            if !dup {
4391                // c:2500
4392                descr.push(descr_str.clone()); // c:2501
4393                act.push(act_str.clone());
4394
4395                if restr == 0 {
4396                    // c:2504
4397                    let nrestr = if a.r#type == CAA_RARGS {
4398                        // c:2506
4399                        let (optbeg, argend) = ca_laststate
4400                            .lock()
4401                            .map(|s| (s.optbeg, s.argend))
4402                            .unwrap_or((0, 0));
4403                        restrict_range(optbeg, argend);
4404                        1
4405                    } else if a.r#type == CAA_RREST {
4406                        // c:2508
4407                        let (argbeg, argend) = ca_laststate
4408                            .lock()
4409                            .map(|s| (s.argbeg, s.argend))
4410                            .unwrap_or((0, 0));
4411                        restrict_range(argbeg, argend);
4412                        1
4413                    } else {
4414                        0
4415                    };
4416                    restr = nrestr;
4417                }
4418
4419                // c:2511 — build subcontext string.
4420                let buf = if let Some(o) = a.opt.as_deref() {
4421                    // c:2511
4422                    let gs = a.gsname.as_deref().unwrap_or("");
4423                    if a.num > 0 && a.r#type < CAA_REST {
4424                        // c:2514
4425                        format!("{}option{}-{}", gs, o, a.num)
4426                    } else {
4427                        // c:2518
4428                        format!("{}option{}-rest", gs, o)
4429                    }
4430                } else if a.num > 0 {
4431                    // c:2520
4432                    if let Some(gs) = a.gsname.as_deref() {
4433                        format!("{}argument-{}", gs, a.num)
4434                    } else {
4435                        format!("argument-{}", a.num)
4436                    }
4437                } else {
4438                    // c:2523
4439                    if let Some(gs) = a.gsname.as_deref() {
4440                        format!("{}argument-rest", gs)
4441                    } else {
4442                        "argument-rest".to_string()
4443                    }
4444                };
4445                subc.push(buf); // c:2527
4446            }
4447
4448            // c:2539 — guard: NORMAL inside an opt where opt requires its
4449            // argument as a separate word — return so we don't keep trying
4450            // to match positionals.
4451            if a.r#type == CAA_NORMAL && opt.is_some() {
4452                if let Some(od) = optdef {
4453                    if od.r#type == CAO_NEXT || od.r#type == CAO_ODIRECT || od.r#type == CAO_OEQUAL
4454                    {
4455                        return;
4456                    }
4457                }
4458            }
4459
4460            if single != 0 {
4461                break;
4462            } // c:2545
4463
4464            // c:2548-2568 — advance to the next arg.
4465            if opt.is_none() {
4466                // c:2548
4467                let next_is_none_and_miss = a.num >= 0 && a.next.is_none() && miss != 0;
4468                if next_is_none_and_miss {
4469                    // c:2549
4470                    let rest = ca_laststate
4471                        .lock()
4472                        .ok()
4473                        .and_then(|s| s.d.as_ref().and_then(|d| d.rest.clone()));
4474                    arg = rest.filter(|r| r.active != 0); // c:2550
4475                } else {
4476                    let onum = a.num; // c:2553
4477                    let nth = ca_laststate.lock().map(|s| s.nth).unwrap_or(0);
4478                    let rest_flag = onum != a.min && onum == nth; // c:2554
4479                    let next = a.next.clone();
4480                    if let Some(n) = next {
4481                        // c:2555
4482                        if n.num != onum + 1 {
4483                            miss = 1;
4484                        } // c:2556
4485                        arg = Some(n);
4486                    } else if rest_flag || (oopt > 0 && opt.is_none()) {
4487                        // c:2558
4488                        let rest = ca_laststate
4489                            .lock()
4490                            .ok()
4491                            .and_then(|s| s.d.as_ref().and_then(|d| d.rest.clone()));
4492                        arg = rest.filter(|r| r.active != 0);
4493                        oopt = -1;
4494                    } else {
4495                        arg = None;
4496                    }
4497                }
4498            } else {
4499                // c:2564
4500                if !lopt {
4501                    break;
4502                } // c:2565
4503                arg = a.next.clone(); // c:2567
4504            }
4505        }
4506
4507        // c:2570 — retry as positional after the option args path.
4508        let laststate_oopt = ca_laststate.lock().map(|s| s.oopt).unwrap_or(0);
4509        let cur_lopt = arg.as_ref().map_or(false, |a| a.r#type == CAA_OPT);
4510        if single == 0 && opt.is_some() && (cur_lopt || laststate_oopt != 0) {
4511            opt = None;
4512            let nth = ca_laststate.lock().map(|s| s.nth).unwrap_or(0);
4513            // c:2572 — arg = ca_get_arg(ca_laststate.d, ca_laststate.nth).
4514            arg = ca_laststate
4515                .lock()
4516                .ok()
4517                .and_then(|s| s.d.as_ref().and_then(|d| ca_get_arg(d, nth)));
4518            continue 'rec;
4519        }
4520        // c:2575 — retry as rest after positional path.
4521        if opt.is_none() && oopt > 0 {
4522            oopt = -1;
4523            let rest = ca_laststate
4524                .lock()
4525                .ok()
4526                .and_then(|s| s.d.as_ref().and_then(|d| d.rest.clone()));
4527            arg = rest.filter(|r| r.active != 0);
4528            continue 'rec;
4529        }
4530        break 'rec;
4531    }
4532}
4533
4534/// Direct port of `static int bin_comparguments(char *nam, char **args,
4535///                                                 UNUSED(Options ops),
4536///                                                 UNUSED(int func))`
4537/// from `Src/Zle/computil.c:2585-2914`. Full subcommand dispatch for
4538/// `comparguments -i/-D/-O/-L/-s/-M/-a/-W/-n`. Each branch consumes
4539/// the parsed `ca_laststate` from `ca_parse_line`.
4540pub fn bin_comparguments(
4541    nam: &str,
4542    args: &[String], // c:2585
4543    _ops: &options,
4544    _func: i32,
4545) -> i32 {
4546    if INCOMPFUNC.load(Ordering::Relaxed) != 1 {
4547        // c:2590
4548        zwarnnam(nam, "can only be called from completion function");
4549        return 1;
4550    }
4551    if args.is_empty() {
4552        return 1;
4553    }
4554    let a0 = args[0].as_bytes();
4555    // c:2594 — must be `-X` exactly (2 chars).
4556    if a0.len() != 2 || a0[0] != b'-' {
4557        zwarnnam(nam, &format!("invalid argument: {}", args[0]));
4558        return 1;
4559    }
4560    let sub = a0[1];
4561
4562    // c:2598 — non-init subcommands require ca_parsed.
4563    if sub != b'i' && sub != b'I' && ca_parsed.load(Ordering::Relaxed) == 0 {
4564        zwarnnam(nam, "no parsed state");
4565        return 1;
4566    }
4567
4568    // c:2602 — per-subcommand arg-count bounds.
4569    let (min, max): (i32, i32) = match sub {
4570        b'i' => (2, -1),
4571        b'D' => (3, 3),
4572        b'O' => (4, 4),
4573        b'L' => (3, 4),
4574        b's' => (1, 1),
4575        b'M' => (1, 1),
4576        b'a' => (0, 0),
4577        b'W' => (3, 3),
4578        b'n' => (1, 1),
4579        _ => {
4580            zwarnnam(nam, &format!("invalid option: {}", args[0]));
4581            return 1;
4582        }
4583    };
4584    let n = (args.len() as i32) - 1;
4585    if n < min {
4586        zwarnnam(nam, "not enough arguments");
4587        return 1;
4588    }
4589    if max >= 0 && n > max {
4590        zwarnnam(nam, "too many arguments");
4591        return 1;
4592    }
4593
4594    match sub {
4595        b'i' => {
4596            // c:2625
4597            // c:2629 — compcurrent > 1 && compwords[0].
4598            let compcur = COMPCURRENT.load(Ordering::Relaxed);
4599            let compwords_nonempty = COMPWORDS
4600                .get()
4601                .and_then(|m| m.lock().ok().map(|w| !w.is_empty() && !w[0].is_empty()))
4602                .unwrap_or(false);
4603            if compcur <= 1 || !compwords_nonempty {
4604                return 1; // c:2670
4605            }
4606            // c:2636 — get_cadef(nam, args[1..]).
4607            // get_cadef returns 1 on cache hit. Look up the cached cadef
4608            // from the cadef_cache by argv match and run parse_line.
4609            let spec = &args[1..];
4610            let _ = get_cadef(nam, spec); // c:2636
4611                                          // Now find the cadef in the cache.
4612            let cached: Option<Box<cadef>> = {
4613                let cache = cadef_cache.lock().ok();
4614                cache.and_then(|c| {
4615                    c.iter().find_map(|slot| {
4616                        slot.as_ref()
4617                            .filter(|e| {
4618                                e.ndefs == spec.len() as i32
4619                                    && e.defs.as_deref().map_or(false, |d| {
4620                                        d.len() == spec.len()
4621                                            && d.iter().zip(spec.iter()).all(|(a, b)| a == b)
4622                                    })
4623                            })
4624                            .cloned()
4625                    })
4626                })
4627            };
4628            let Some(mut def_head) = cached else {
4629                return 1;
4630            };
4631            ca_parsed.store(0, Ordering::Relaxed); // c:2634
4632            ca_doff.store(0, Ordering::Relaxed);
4633            let all_clone = Box::new(clone_cadef_shallow(&def_head));
4634
4635            // c:2643-2664 — for each set walk: track which parses
4636            // succeeded ("use"). When a set succeeds AND more sets
4637            // remain, snapshot ca_laststate into a fallback chain.
4638            // When the final set rejects, restore the most-recent
4639            // saved state (or fail with ret=1 if no fallback).
4640            let mut first = 1;
4641            let mut def_opt: Option<Box<cadef>> = Some(def_head);
4642            let mut multi = 0;
4643            // Look ahead once to see if we have more than one set;
4644            // matches C's `multi = !!def->snext` at c:2639.
4645            if let Some(ref d) = def_opt {
4646                if d.snext.is_some() {
4647                    multi = 1;
4648                }
4649            }
4650            let mut states: Vec<castate> = Vec::new(); // c:2632
4651            let mut ret = 0i32;
4652
4653            while let Some(mut current) = def_opt {
4654                let next = current.snext.take();
4655                let parse_ret = ca_parse_line(&mut current, &all_clone, multi, first);
4656                let use_state = parse_ret == 0; // c:2644
4657                let has_next = next.is_some();
4658                if use_state && has_next {
4659                    // c:2646
4660                    // c:2648 — snapshot ca_laststate, push onto fallback.
4661                    if let Ok(ls) = ca_laststate.lock() {
4662                        states.push(clone_castate_full(&ls));
4663                    }
4664                } else if !use_state && !has_next {
4665                    // c:2652
4666                    // c:2654 — restore most-recent saved state (if any).
4667                    if let Some(saved) = states.pop() {
4668                        if let Ok(mut ls) = ca_laststate.lock() {
4669                            freecastate(&mut ls);
4670                            *ls = saved;
4671                        }
4672                    } else {
4673                        ret = 1; // c:2661
4674                    }
4675                }
4676                first = 0; // c:2663
4677                def_opt = next;
4678            }
4679            ca_parsed.store(1, Ordering::Relaxed); // c:2665
4680
4681            // c:2666 — thread fallback chain into ca_laststate.snext.
4682            if !states.is_empty() {
4683                if let Ok(mut ls) = ca_laststate.lock() {
4684                    // Build a linked snext chain from oldest → newest.
4685                    let mut head: Option<Box<castate>> = None;
4686                    for s in states.into_iter().rev() {
4687                        let mut s = s;
4688                        s.snext = head;
4689                        head = Some(Box::new(s));
4690                    }
4691                    ls.snext = head;
4692                }
4693            }
4694            ret // c:2668
4695        }
4696
4697        b'D' => {
4698            // c:2672
4699            let mut descr: Vec<String> = Vec::new();
4700            let mut act: Vec<String> = Vec::new();
4701            let mut subc: Vec<String> = Vec::new();
4702            let mut ret = 1i32;
4703            ignore_prefix(ca_doff.load(Ordering::Relaxed));
4704
4705            // Walk lstate (ca_laststate + its snext chain).
4706            let mut state_clone = ca_laststate.lock().map(|s| clone_castate_full(&s)).ok();
4707            while let Some(s) = state_clone {
4708                let arg = s.def.clone();
4709                if let Some(a) = arg {
4710                    ret = 0;
4711                    let opt_str = a.opt.clone();
4712                    let optdef = s.curopt.clone();
4713                    ca_set_data(
4714                        &mut descr,
4715                        &mut act,
4716                        &mut subc,
4717                        opt_str.as_deref(),
4718                        Some(a),
4719                        optdef.as_deref(),
4720                        if ca_doff.load(Ordering::Relaxed) > 0 {
4721                            1
4722                        } else {
4723                            0
4724                        },
4725                    );
4726                }
4727                state_clone = s.snext.map(|b| *b);
4728            }
4729            if ret == 0 {
4730                // c:2698
4731                setaparam(&args[1], descr);
4732                setaparam(&args[2], act);
4733                setaparam(&args[3], subc);
4734            }
4735            ret
4736        }
4737
4738        b'M' => {
4739            // c:2827
4740            let m = ca_laststate
4741                .lock()
4742                .ok()
4743                .and_then(|s| s.d.as_ref().and_then(|d| d.r#match.clone()))
4744                .unwrap_or_default();
4745            setsparam(&args[1], &m);
4746            0
4747        }
4748
4749        b'a' => {
4750            // c:2833
4751            let mut state_clone = ca_laststate.lock().map(|s| clone_castate_full(&s)).ok();
4752            while let Some(s) = state_clone {
4753                if s.d
4754                    .as_ref()
4755                    .map_or(false, |d| d.args.is_some() || d.rest.is_some())
4756                {
4757                    return 0;
4758                }
4759                state_clone = s.snext.map(|b| *b);
4760            }
4761            1 // c:2840
4762        }
4763
4764        b'n' => {
4765            // c:2899
4766            let optbeg = ca_laststate.lock().map(|s| s.optbeg).unwrap_or(0);
4767            let kshoffset = if isset(KSHARRAYS) { 0 } else { 1 };
4768            setiparam(&args[1], (optbeg + kshoffset) as i64);
4769            0
4770        }
4771
4772        b'O' => {
4773            // c:2705
4774            // Build the four lists; for each non-`not` active opt assign it
4775            // to one of {next, direct, odirect, equal}.
4776            let mut next_l: Vec<String> = Vec::new();
4777            let mut direct_l: Vec<String> = Vec::new();
4778            let mut odirect_l: Vec<String> = Vec::new();
4779            let mut equal_l: Vec<String> = Vec::new();
4780            let mut ret = 1i32;
4781
4782            let mut state_clone = ca_laststate.lock().map(|s| clone_castate_full(&s)).ok();
4783            while let Some(s) = state_clone {
4784                // c:2721 — gate on actopts + position.
4785                let actopts_ok = s.actopts != 0
4786                    && (s.opt != 0
4787                        || (ca_doff.load(Ordering::Relaxed) != 0 && s.def.is_some())
4788                        || (s.def.is_some()
4789                            && s.def.as_deref().map_or(false, |d| {
4790                                d.opt.is_some()
4791                                    && (d.r#type == CAA_OPT || (d.r#type >= CAA_RARGS && d.num < 0))
4792                            })));
4793                let pos_ok = s.def.is_none()
4794                    || s.def.as_deref().map_or(true, |d| d.r#type < CAA_RARGS)
4795                    || (s.def.as_deref().map_or(false, |d| d.r#type == CAA_RARGS)
4796                        && s.curpos == s.argbeg + 1)
4797                    || COMPCURRENT.load(Ordering::Relaxed) == 1;
4798                if actopts_ok && pos_ok {
4799                    ret = 0;
4800                    if let Some(d) = s.d.as_ref() {
4801                        let mut p = d.opts.as_deref();
4802                        while let Some(opt) = p {
4803                            if opt.active != 0 && opt.not == 0 {
4804                                let bucket: &mut Vec<String> = match opt.r#type {
4805                                    t if t == CAO_NEXT => &mut next_l,
4806                                    t if t == CAO_DIRECT => &mut direct_l,
4807                                    t if t == CAO_ODIRECT => &mut odirect_l,
4808                                    _ => &mut equal_l,
4809                                };
4810                                let name_esc = bslashcolon(opt.name.as_deref().unwrap_or(""));
4811                                let str_val = if let Some(desc) = opt.descr.as_deref() {
4812                                    format!("{}:{}", name_esc, desc)
4813                                } else {
4814                                    name_esc
4815                                };
4816                                if !bucket.iter().any(|s| s == &str_val) {
4817                                    bucket.push(str_val);
4818                                }
4819                            }
4820                            p = opt.next.as_deref();
4821                        }
4822                    }
4823                }
4824                state_clone = s.snext.map(|b| *b);
4825            }
4826
4827            if ret == 0 {
4828                setaparam(&args[1], next_l);
4829                setaparam(&args[2], direct_l);
4830                setaparam(&args[3], odirect_l);
4831                setaparam(&args[4], equal_l);
4832                0
4833            } else {
4834                let singles = ca_laststate.lock().map(|s| s.singles).unwrap_or(0);
4835                if singles != 0 {
4836                    2
4837                } else {
4838                    1
4839                } // c:2769
4840            }
4841        }
4842
4843        b'L' => {
4844            // c:2771
4845            // c:2787 — for each state, ca_get_opt(d, args[1], 1, NULL).
4846            let mut descr: Vec<String> = Vec::new();
4847            let mut act: Vec<String> = Vec::new();
4848            let mut subc: Vec<String> = Vec::new();
4849            let mut ret = 1i32;
4850            let mut state_clone = ca_laststate.lock().map(|s| clone_castate_full(&s)).ok();
4851            while let Some(s) = state_clone {
4852                if let Some(d) = s.d.as_ref() {
4853                    let mut end = 0usize;
4854                    if let Some(opt) = ca_get_opt(d, &args[1], 1, &mut end) {
4855                        if opt.args.is_some() {
4856                            ret = 0;
4857                            let opt_name = opt.name.clone();
4858                            let opt_args = opt.args.clone();
4859                            ca_set_data(
4860                                &mut descr,
4861                                &mut act,
4862                                &mut subc,
4863                                opt_name.as_deref(),
4864                                opt_args,
4865                                Some(&opt),
4866                                1,
4867                            );
4868                        }
4869                    }
4870                }
4871                state_clone = s.snext.map(|b| *b);
4872            }
4873            if ret == 0 {
4874                setaparam(&args[2], descr);
4875                setaparam(&args[3], act);
4876                setaparam(&args[4], subc);
4877            }
4878            ret
4879        }
4880
4881        b's' => {
4882            // c:2803
4883            let mut state_clone = ca_laststate.lock().map(|s| clone_castate_full(&s)).ok();
4884            while let Some(s) = state_clone {
4885                let single_active = s.d.as_ref().map_or(false, |d| d.single.is_some())
4886                    && s.singles != 0
4887                    && s.actopts != 0;
4888                if single_active {
4889                    let kind = if let (Some(_), Some(dopt)) = (&s.ddef, &s.dopt) {
4890                        match dopt.r#type {
4891                            t if t == CAO_DIRECT => "direct",
4892                            t if t == CAO_OEQUAL || t == CAO_EQUAL => "equal",
4893                            _ => "next",
4894                        }
4895                    } else {
4896                        ""
4897                    };
4898                    setsparam(&args[1], kind);
4899                    return 0;
4900                }
4901                state_clone = s.snext.map(|b| *b);
4902            }
4903            1
4904        }
4905
4906        b'W' => {
4907            // c:2841
4908            // Build state.args concat and oargs concat for $opt_args.
4909            let mut all_args: Vec<String> = Vec::new();
4910            let opt_args_use_nul = !args[3].starts_with('0');
4911            let mut state_clone = ca_laststate.lock().map(|s| clone_castate_full(&s)).ok();
4912            // Pass 1: state.args.
4913            let mut snapshot = state_clone.clone();
4914            while let Some(s) = snapshot {
4915                if let Some(a) = s.args.as_ref() {
4916                    all_args.extend(a.iter().cloned());
4917                }
4918                snapshot = s.snext.map(|b| *b);
4919            }
4920            setaparam(&args[1], all_args);
4921
4922            // Pass 2: oargs into a hash.
4923            let mut hash_vec: Vec<String> = Vec::new();
4924            while let Some(s) = state_clone {
4925                if let Some(d) = s.d.as_ref() {
4926                    let mut o = d.opts.as_deref();
4927                    let mut a_idx = 0usize;
4928                    let oargs_ref = s.oargs.as_deref();
4929                    while let Some(op) = o {
4930                        if let Some(oa) = oargs_ref
4931                            .and_then(|v| v.get(a_idx))
4932                            .and_then(|x| x.as_ref())
4933                        {
4934                            let key = match (op.gsname.as_deref(), op.name.as_deref()) {
4935                                (Some(gs), Some(n)) => format!("{}{}", gs, n),
4936                                (None, Some(n)) => n.to_string(),
4937                                _ => String::new(),
4938                            };
4939                            hash_vec.push(key);
4940                            let joined = if opt_args_use_nul {
4941                                String::from_utf8_lossy(&ca_nullist(oa)).into_owned()
4942                            } else {
4943                                ca_colonlist(oa)
4944                            };
4945                            hash_vec.push(joined);
4946                        }
4947                        a_idx += 1;
4948                        o = op.next.as_deref();
4949                    }
4950                }
4951                state_clone = s.snext.map(|b| *b);
4952            }
4953            sethparam(&args[2], hash_vec);
4954            0
4955        }
4956
4957        _ => 0,
4958    }
4959}
4960
4961// =====================================================================
4962// `cvdef` / `cvval` — `_values` completion cache types.
4963// Src/Zle/computil.c:2919-2956. CVV_* and MAX_CVCACHE consts
4964// already declared above (file scope).
4965// =====================================================================
4966
4967/// Port of `typedef struct cvdef *Cvdef` from `Src/Zle/computil.c:2919`.
4968pub type Cvdef = Box<cvdef>; // c:2919
4969
4970/// Direct port of `struct cvdef` from `Src/Zle/computil.c:2924-2935`.
4971/// One parsed `_values` definition entry, cached for reuse.
4972#[derive(Debug, Default, Clone)]
4973#[allow(non_camel_case_types)]
4974pub struct cvdef {
4975    // c:2924
4976    pub descr: Option<String>,     // c:2925 char *descr
4977    pub hassep: i32,               // c:2926
4978    pub sep: i32,                  // c:2927 char sep
4979    pub argsep: i32,               // c:2928 char argsep
4980    pub next: Option<Box<cvdef>>,  // c:2929 Cvdef next
4981    pub vals: Option<Box<cvval>>,  // c:2930 Cvval vals
4982    pub defs: Option<Vec<String>>, // c:2931 char **defs
4983    pub ndefs: i32,                // c:2932
4984    pub lastt: i64,                // c:2933 time_t lastt
4985    pub words: i32,                // c:2934
4986}
4987/// Port of `typedef struct cvval *Cvval` from `computil.c:2920`.
4988pub type Cvval = Box<cvval>; // c:2920
4989
4990/// Direct port of `struct cvval` from `Src/Zle/computil.c:2939-2947`.
4991/// One value definition inside a cvdef.
4992#[derive(Debug, Default, Clone)]
4993#[allow(non_camel_case_types)]
4994pub struct cvval {
4995    // c:2939
4996    pub next: Option<Box<cvval>>, // c:2940 Cvval next
4997    pub name: Option<String>,     // c:2961 char *name
4998    pub descr: Option<String>,    // c:2961 char *descr
4999    pub xor: Option<Vec<String>>, // c:2961 char **xor
5000    pub r#type: i32,              // c:2961 int type (CVV_*)
5001    pub arg: Option<Box<caarg>>,  // c:2961 Caarg arg
5002    pub active: i32,              // c:2961
5003}
5004
5005// =====================================================================
5006// CVV_* — Cvval value-kind — `computil.c:2949-2951`.
5007// =====================================================================
5008
5009/// Port of `CVV_NOARG` from `computil.c:2949`. Value without argument.
5010pub const CVV_NOARG: i32 = 0; // c:2949
5011/// Port of `CVV_ARG` from `computil.c:2950`. Value requires argument.
5012pub const CVV_ARG: i32 = 1; // c:2950
5013/// Port of `CVV_OPT` from `computil.c:2951`. Argument optional.
5014pub const CVV_OPT: i32 = 2; // c:2951
5015
5016/// Port of `MAX_CVCACHE` from `computil.c:2955`. Cvdef LRU cache size.
5017pub const MAX_CVCACHE: usize = 8; // c:2955
5018
5019/// Port of `static Cvdef cvdef_cache[MAX_CVCACHE]` from
5020/// `Src/Zle/computil.c:2956`. Same LRU layout as cadef_cache;
5021/// `get_cvdef` scans for a defs-match hit, evicts the oldest slot
5022/// on miss.
5023pub static cvdef_cache: std::sync::Mutex<[Option<Box<cvdef>>; MAX_CVCACHE]> = // c:2956
5024    std::sync::Mutex::new([const { None }; MAX_CVCACHE]);
5025
5026/// Direct port of `static void freecvdef(Cvdef d)` from
5027/// `Src/Zle/computil.c:2961`. Walks the vals chain freeing
5028/// each cvval (which frees its caarg via freecaargs).
5029pub fn freecvdef(d: Option<Box<cvdef>>) {
5030    // c:2961
5031    let Some(mut node) = d else {
5032        return;
5033    }; // c:2961 if (d)
5034    node.descr = None; // c:2966 zsfree(d->descr)
5035    node.defs = None; // c:2967-2968 freearray(d->defs)
5036    let mut p = node.vals.take();
5037    while let Some(mut v) = p {
5038        // c:2970 for (p = d->vals; ...)
5039        p = v.next.take(); // c:2971 n = p->next
5040        v.name = None; // c:2972
5041        v.descr = None; // c:2973
5042        v.xor = None; // c:2974-2975
5043        freecaargs(v.arg.take()); // c:2976
5044        drop(v); // c:2977
5045    }
5046    drop(node); // c:2979
5047}
5048
5049// `freecaargs(Caarg)` + `freecadef(Cadef)` ported above with the
5050// caarg/caopt/cadef struct ports (c:996 / c:1013).
5051
5052#[cfg(test)]
5053mod cao_caa_tests {
5054    use super::*;
5055
5056    #[test]
5057    fn cao_values_match_c_source() {
5058        let _g = crate::test_util::global_state_lock();
5059        let _g = zle_test_setup();
5060        // c:941-945 — sequential 1..=5.
5061        assert_eq!(CAO_NEXT, 1);
5062        assert_eq!(CAO_DIRECT, 2);
5063        assert_eq!(CAO_ODIRECT, 3);
5064        assert_eq!(CAO_EQUAL, 4);
5065        assert_eq!(CAO_OEQUAL, 5);
5066    }
5067
5068    #[test]
5069    fn caa_values_match_c_source() {
5070        let _g = crate::test_util::global_state_lock();
5071        let _g = zle_test_setup();
5072        // c:964-968 — sequential 1..=5.
5073        assert_eq!(CAA_NORMAL, 1);
5074        assert_eq!(CAA_OPT, 2);
5075        assert_eq!(CAA_REST, 3);
5076        assert_eq!(CAA_RARGS, 4);
5077        assert_eq!(CAA_RREST, 5);
5078    }
5079
5080    #[test]
5081    fn crt_values_match_c_source() {
5082        let _g = crate::test_util::global_state_lock();
5083        let _g = zle_test_setup();
5084        // c:79-83 — sequential 0..=4.
5085        assert_eq!(CRT_SIMPLE, 0);
5086        assert_eq!(CRT_DESC, 1);
5087        assert_eq!(CRT_SPEC, 2);
5088        assert_eq!(CRT_DUMMY, 3);
5089        assert_eq!(CRT_EXPL, 4);
5090    }
5091
5092    #[test]
5093    fn cvv_values_match_c_source() {
5094        let _g = crate::test_util::global_state_lock();
5095        let _g = zle_test_setup();
5096        // c:2949-2951 — sequential 0..=2.
5097        assert_eq!(CVV_NOARG, 0);
5098        assert_eq!(CVV_ARG, 1);
5099        assert_eq!(CVV_OPT, 2);
5100    }
5101
5102    #[test]
5103    fn cache_sizes_are_8() {
5104        let _g = crate::test_util::global_state_lock();
5105        let _g = zle_test_setup();
5106        // c:972 + c:2955 — both LRU caches are 8 entries.
5107        assert_eq!(MAX_CACACHE, 8);
5108        assert_eq!(MAX_CVCACHE, 8);
5109    }
5110
5111    #[test]
5112    fn max_tags_is_256() {
5113        let _g = crate::test_util::global_state_lock();
5114        let _g = zle_test_setup();
5115        assert_eq!(MAX_TAGS, 256);
5116    }
5117
5118    #[test]
5119    fn path_max2_is_8192() {
5120        let _g = crate::test_util::global_state_lock();
5121        let _g = zle_test_setup();
5122        assert_eq!(PATH_MAX2, 8192);
5123    }
5124}
5125
5126/// Direct port of `static Cvdef parse_cvdef(char *nam, char **args)`
5127/// from `Src/Zle/computil.c:2986-3148`. Parses the leading
5128/// `-s SEP / -S SEP / -w` flag block, then the description, then
5129/// each value spec into a cvval chain.
5130pub fn parse_cvdef(nam: &str, args: &[String]) -> Option<Box<cvdef>> {
5131    // c:2986
5132
5133    let orig_args = args;
5134    let mut idx = 0usize;
5135
5136    let mut sep: i32 = 0; // c:2991 char sep = '\0'
5137    let mut asep: i32 = b'=' as i32; // c:2991 char asep = '='
5138    let mut hassep: i32 = 0; // c:2992
5139    let mut words: i32 = 0; // c:2992
5140
5141    // c:2994-3010 — leading flag block (-s SEP, -S SEP, -w).
5142    while idx + 1 < args.len()
5143        && args[idx].len() == 2
5144        && args[idx].starts_with('-')
5145        && (args[idx].as_bytes()[1] == b's'
5146            || args[idx].as_bytes()[1] == b'S'
5147            || args[idx].as_bytes()[1] == b'w')
5148    {
5149        let flag = args[idx].as_bytes()[1];
5150        if flag == b's' {
5151            // c:2999
5152            hassep = 1;
5153            sep = args[idx + 1].as_bytes().first().copied().unwrap_or(0) as i32;
5154            idx += 2;
5155        } else if flag == b'S' {
5156            // c:3003
5157            asep = args[idx + 1].as_bytes().first().copied().unwrap_or(0) as i32;
5158            idx += 2;
5159        } else {
5160            // c:3006 -w
5161            words = 1;
5162            idx += 1;
5163        }
5164    }
5165
5166    if idx + 1 >= args.len() {
5167        // c:3011
5168        zwarnnam(nam, "not enough arguments");
5169        return None;
5170    }
5171    let descr = args[idx].clone(); // c:3015 descr = *args++
5172    idx += 1;
5173
5174    let mut ret = Box::new(cvdef {
5175        descr: Some(descr),             // c:3018
5176        hassep,                         // c:3019
5177        sep,                            // c:3020
5178        argsep: asep,                   // c:3021
5179        next: None,                     // c:3022
5180        vals: None,                     // c:3023
5181        defs: Some(orig_args.to_vec()), // c:3024
5182        ndefs: orig_args.len() as i32,  // c:3025
5183        lastt: {
5184            // c:3026
5185            use std::time::{SystemTime, UNIX_EPOCH};
5186            SystemTime::now()
5187                .duration_since(UNIX_EPOCH)
5188                .map(|d| d.as_secs() as i64)
5189                .unwrap_or(0)
5190        },
5191        words, // c:3027
5192    });
5193
5194    // c:3029-3147 — for each remaining arg, parse one value spec.
5195    let mut vals_collected: Vec<Box<cvval>> = Vec::new();
5196
5197    while idx < args.len() {
5198        let spec = &args[idx];
5199        let bytes = spec.as_bytes();
5200        let mut p: usize = 0;
5201        let mut xnum: i32 = 0; // c:3032
5202        let mut bs = 0; // c:3030
5203        let mut xor: Option<Vec<String>> = None;
5204
5205        // c:3035-3068 — `(opt1 opt2)` xor list.
5206        if p < bytes.len() && bytes[p] == b'(' {
5207            // c:3035
5208            let mut list: Vec<String> = Vec::new();
5209            let mut bad = false;
5210            'paren: loop {
5211                if p >= bytes.len() || bytes[p] == b')' {
5212                    break;
5213                }
5214                p += 1; // c:3041 p++
5215                while p < bytes.len() && inblank(bytes[p]) {
5216                    p += 1;
5217                }
5218                if p >= bytes.len() {
5219                    bad = true;
5220                    break 'paren;
5221                }
5222                if bytes[p] == b')' {
5223                    break 'paren;
5224                }
5225                let q = p;
5226                p += 1;
5227                while p < bytes.len() && bytes[p] != b')' && !inblank(bytes[p]) {
5228                    p += 1;
5229                }
5230                if p >= bytes.len() {
5231                    bad = true;
5232                    break 'paren;
5233                }
5234                let word = String::from_utf8_lossy(&bytes[q..p]).into_owned();
5235                list.push(word);
5236                xnum += 1;
5237            }
5238            if bad || p >= bytes.len() || bytes[p] != b')' {
5239                // c:3056
5240                zwarnnam(nam, &format!("invalid argument: {}", spec));
5241                return None;
5242            }
5243            xor = Some(list);
5244            p += 1; // c:3066
5245        }
5246
5247        // c:3071 — `*` (multi).
5248        let multi = p < bytes.len() && bytes[p] == b'*';
5249        if multi {
5250            p += 1;
5251        }
5252
5253        // c:3076 — scan option name up to `:` or `[`.
5254        let name_start = p;
5255        while p < bytes.len() && bytes[p] != b':' && bytes[p] != b'[' {
5256            // c:3076
5257            if bytes[p] == b'\\' && p + 1 < bytes.len() {
5258                p += 1;
5259                bs = 1; // c:3078
5260            }
5261            p += 1;
5262        }
5263
5264        // c:3080-3085 — multi-letter check against empty separator.
5265        if hassep != 0 && sep == 0 && name_start + (bs as usize) + 1 < p {
5266            // c:3080
5267            zwarnnam(nam, "no multi-letter values with empty separator allowed");
5268            return None;
5269        }
5270
5271        let name_bytes = &bytes[name_start..p];
5272        let name = String::from_utf8_lossy(name_bytes).into_owned();
5273
5274        // c:3087 — optional [descr].
5275        let mut value_descr: Option<String> = None;
5276        let mut c_byte = if p < bytes.len() { bytes[p] } else { 0 };
5277        if c_byte == b'[' {
5278            // c:3088
5279            p += 1;
5280            let d_start = p;
5281            while p < bytes.len() && bytes[p] != b']' {
5282                // c:3090
5283                if bytes[p] == b'\\' && p + 1 < bytes.len() {
5284                    p += 1;
5285                }
5286                p += 1;
5287            }
5288            if p >= bytes.len() {
5289                // c:3094
5290                zwarnnam(nam, &format!("invalid value definition: {}", spec));
5291                return None;
5292            }
5293            value_descr = Some(String::from_utf8_lossy(&bytes[d_start..p]).into_owned());
5294            p += 1; // c:3100
5295            c_byte = if p < bytes.len() { bytes[p] } else { 0 };
5296        }
5297
5298        if c_byte != 0 && c_byte != b':' {
5299            // c:3106
5300            zwarnnam(nam, &format!("invalid value definition: {}", spec));
5301            return None;
5302        }
5303
5304        // c:3114 — :arg or ::optarg.
5305        let mut vtype = CVV_NOARG;
5306        let mut arg: Option<Box<caarg>> = None;
5307        if c_byte == b':' {
5308            // c:3114
5309            if hassep != 0 && sep == 0 {
5310                // c:3115
5311                zwarnnam(nam, "no value with argument with empty separator allowed");
5312                return None;
5313            }
5314            p += 1; // c:3121 *++p
5315            if p < bytes.len() && bytes[p] == b':' {
5316                // c:3121
5317                p += 1;
5318                vtype = CVV_OPT; // c:3123
5319            } else {
5320                vtype = CVV_ARG; // c:3125
5321            }
5322            arg = Some(parse_caarg(0, 0, 0, 0, Some(&name), bytes, &mut p, None));
5323            // c:3126
5324        }
5325
5326        // c:3131-3137 — add own name to xor list when not multi.
5327        if !multi {
5328            // c:3131
5329            let xv = xor.get_or_insert_with(Vec::new);
5330            if xv.len() <= xnum as usize {
5331                xv.resize(xnum as usize + 1, String::new());
5332            }
5333            xv[xnum as usize] = name.clone(); // c:3136
5334        }
5335
5336        let v = Box::new(cvval {
5337            // c:3138
5338            next: None,
5339            name: Some(name),   // c:3142
5340            descr: value_descr, // c:3143
5341            xor,                // c:3144
5342            r#type: vtype,      // c:3145
5343            arg,                // c:3146
5344            active: 0,
5345        });
5346        vals_collected.push(v);
5347
5348        idx += 1;
5349    }
5350
5351    // Link vals_collected as a chain.
5352    let mut head: Option<Box<cvval>> = None;
5353    for v in vals_collected.into_iter().rev() {
5354        let mut v = v;
5355        v.next = head;
5356        head = Some(v);
5357    }
5358    ret.vals = head;
5359
5360    Some(ret)
5361}
5362
5363/// Direct port of `static Cvdef get_cvdef(char *nam, char **args)` from
5364/// `Src/Zle/computil.c:3154-3173`. LRU lookup over `cvdef_cache`
5365/// keyed by the raw argv. On hit bumps `lastt` and returns 1. On
5366/// miss parses via `parse_cvdef` and evicts the entry with the
5367/// oldest `lastt` (or the first empty slot) for insertion.
5368pub fn get_cvdef(nam: &str, args: &[String]) -> i32 {
5369    // c:3154
5370    let na = args.len() as i32;
5371    let now = {
5372        // c:3161 time(0)
5373        use std::time::{SystemTime, UNIX_EPOCH};
5374        SystemTime::now()
5375            .duration_since(UNIX_EPOCH)
5376            .map(|d| d.as_secs() as i64)
5377            .unwrap_or(0)
5378    };
5379
5380    if let Ok(mut cache) = cvdef_cache.lock() {
5381        let mut min_idx: Option<usize> = None;
5382        let mut min_lastt: i64 = i64::MAX;
5383        let mut hit_idx: Option<usize> = None;
5384        for (i, slot) in cache.iter().enumerate() {
5385            // c:3159
5386            match slot {
5387                Some(entry) => {
5388                    if entry.ndefs == na                                     // c:3160
5389                        && entry.defs.as_deref()
5390                            .map_or(false, |d| d.len() == args.len()
5391                                && d.iter().zip(args.iter()).all(|(a, b)| a == b))
5392                    {
5393                        hit_idx = Some(i);
5394                        break;
5395                    }
5396                    if entry.lastt < min_lastt {
5397                        // c:3164
5398                        min_lastt = entry.lastt;
5399                        min_idx = Some(i);
5400                    }
5401                }
5402                None => {
5403                    // c:3164 empty slot
5404                    min_idx = Some(i);
5405                    break;
5406                }
5407            }
5408        }
5409        if let Some(i) = hit_idx {
5410            // c:3160
5411            if let Some(entry) = cache[i].as_mut() {
5412                entry.lastt = now; // c:3161
5413            }
5414            return 1; // c:3163 hit
5415        }
5416        // c:3168 — parse_cvdef; on success replace the chosen slot.
5417        if let Some(new) = parse_cvdef(nam, args) {
5418            let idx = min_idx.unwrap_or(0);
5419            cache[idx] = Some(new); // c:3170
5420        }
5421    }
5422    0 // c:3172 miss
5423}
5424
5425/// Direct port of `static Cvval cv_get_val(Cvdef d, char *name)` from
5426/// `Src/Zle/computil.c:3178-3187`. Linear scan over `d->vals` for a
5427/// name match. Returns a shallow clone of the matched cvval.
5428pub fn cv_get_val(d: &cvdef, name: &str) -> Option<Box<cvval>> {
5429    // c:3178
5430    let mut p = d.vals.as_deref();
5431    while let Some(v) = p {
5432        // c:3182
5433        if v.name.as_deref() == Some(name) {
5434            // c:3183
5435            return Some(Box::new(cvval {
5436                next: None,
5437                name: v.name.clone(),
5438                descr: v.descr.clone(),
5439                xor: v.xor.clone(),
5440                r#type: v.r#type,
5441                arg: v.arg.clone(),
5442                active: v.active,
5443            }));
5444        }
5445        p = v.next.as_deref();
5446    }
5447    None // c:3186
5448}
5449
5450/// Direct port of `static Cvval cv_quote_get_val(Cvdef d, char *name)`
5451/// from `Src/Zle/computil.c:3190-3204`. Unquotes `name` via the full
5452/// C chain: `parse_subst_string` (with noerrs=2 to suppress errors),
5453/// `remnulargs`, then `untokenize`; result fed to `cv_get_val`.
5454pub fn cv_quote_get_val(d: &cvdef, name: &str) -> Option<Box<cvval>> {
5455    // c:3190
5456    // c:3195 — `name = dupstring(name)` (Rust: own a mutable copy).
5457    let mut s = name.to_string();
5458    // c:3196-3199 — `ne = noerrs; noerrs = 2; parse_subst_string(name);
5459    //                noerrs = ne`. The parse_subst_string port (lex.rs:3797)
5460    // returns Result; we discard errors so noerrs=2/restore is a no-op.
5461    set_noerrs(2);
5462    let parsed = crate::ported::lex::parse_subst_string(&s).ok();
5463    set_noerrs(0);
5464    if let Some(p) = parsed {
5465        s = p;
5466    }
5467    // c:3200 — `remnulargs(name)`.
5468    remnulargs(&mut s);
5469    // c:3201 — `untokenize(name)`.
5470    let s = untokenize(&s);
5471    // c:3203 — `return cv_get_val(d, name)`.
5472    cv_get_val(d, &s)
5473}
5474
5475/// Direct port of `static void cv_inactive(Cvdef d, char **xor)` from
5476/// `Src/Zle/computil.c:3209-3218`. Clears `active` on each cvval named
5477/// in the xor list.
5478pub fn cv_inactive(d: &mut cvdef, xor: &[String]) {
5479    // c:3209
5480    for name in xor {
5481        // c:3214
5482        let mut p = d.vals.as_deref_mut();
5483        while let Some(v) = p {
5484            if v.name.as_deref() == Some(name.as_str()) {
5485                v.active = 0; // c:3216
5486            }
5487            p = v.next.as_deref_mut();
5488        }
5489    }
5490}
5491
5492// =====================================================================
5493// `cvstate` — `_values` parse state.
5494// Src/Zle/computil.c:3220-3231.
5495// =====================================================================
5496
5497/// Direct port of `struct cvstate` from `Src/Zle/computil.c:3222-3227`.
5498#[derive(Debug, Default)]
5499#[allow(non_camel_case_types)]
5500pub struct cvstate {
5501    // c:3222
5502    pub d: Option<Box<cvdef>>,     // c:3223 Cvdef d
5503    pub def: Option<Box<caarg>>,   // c:3224 Caarg def
5504    pub val: Option<Box<cvval>>,   // c:3225 Cvval val
5505    pub vals: Option<Vec<String>>, // c:3226 LinkList vals
5506}
5507
5508/// Port of `static int cv_parsed` from `Src/Zle/computil.c:3230`.
5509pub static cv_parsed: std::sync::atomic::AtomicI32 = // c:3230
5510    std::sync::atomic::AtomicI32::new(0);
5511
5512/// Direct port of `static Cvval cv_next(Cvdef d, char **sp, char **ap)`
5513/// from `Src/Zle/computil.c:3240-3331`. Splits the next value out of
5514/// `*sp` using `d->sep` / `d->argsep`, returns its matched Cvval.
5515/// On success, `*sp` advances past the consumed prefix; `*ap` is set
5516/// to the value's argument (if any) or `None`.
5517pub fn cv_next(
5518    d: &cvdef,
5519    sp: &mut Option<String>, // c:3240
5520    ap: &mut Option<String>,
5521) -> Option<Box<cvval>> {
5522    let s_in = sp.take().unwrap_or_default();
5523    if s_in.is_empty() {
5524        // c:3245
5525        *sp = None;
5526        *ap = None;
5527        return None;
5528    }
5529    let bytes = s_in.as_bytes();
5530
5531    // Branch 1: hassep && !sep, or !argsep — greedy match (longest prefix).
5532    if (d.hassep != 0 && d.sep == 0) || d.argsep == 0 {
5533        // c:3250
5534        let ec_byte: u8 = if d.hassep != 0 && d.sep != 0 {
5535            d.sep as u8
5536        } else {
5537            d.argsep as u8
5538        };
5539        let mut s_idx: usize = 0;
5540        let mut r: Option<Box<cvval>> = None;
5541        // c:3255 — extend until cv_quote_get_val matches or hit ec.
5542        loop {
5543            s_idx += 1;
5544            if s_idx > bytes.len() {
5545                break;
5546            }
5547            let candidate = std::str::from_utf8(&bytes[..s_idx]).unwrap_or("");
5548            r = cv_quote_get_val(d, candidate);
5549            if r.is_some() {
5550                break;
5551            }
5552            if s_idx >= bytes.len() || bytes[s_idx] == ec_byte {
5553                break;
5554            }
5555        }
5556        let os = s_idx;
5557        // c:3268 — advance *sp.
5558        if d.hassep != 0 && d.sep != 0 {
5559            let sep_byte = d.sep as u8;
5560            if let Some(off) = bytes[s_idx.min(bytes.len())..]
5561                .iter()
5562                .position(|&b| b == sep_byte)
5563            {
5564                let after = s_idx + off + 1;
5565                *sp = Some(String::from_utf8_lossy(&bytes[after..]).into_owned());
5566            } else {
5567                *sp = None;
5568            }
5569        } else {
5570            *sp = if s_idx < bytes.len() {
5571                Some(String::from_utf8_lossy(&bytes[s_idx..]).into_owned())
5572            } else {
5573                None
5574            };
5575        }
5576        // c:3275 — set *ap.
5577        let argsep_b = d.argsep as u8;
5578        if d.argsep != 0 && os < bytes.len() && bytes[os] == argsep_b {
5579            *ap = Some(String::from_utf8_lossy(&bytes[os + 1..]).into_owned());
5580            *sp = None;
5581        } else if r.as_deref().map_or(false, |v| v.r#type != CVV_NOARG) {
5582            *ap = if os < bytes.len() {
5583                Some(String::from_utf8_lossy(&bytes[os..]).into_owned())
5584            } else {
5585                None
5586            };
5587        } else {
5588            *ap = None;
5589        }
5590        return r;
5591    }
5592
5593    // Branch 2: hassep set (with both sep and argsep).
5594    if d.hassep != 0 {
5595        // c:3285
5596        let sep_b = d.sep as u8;
5597        let argsep_b = d.argsep as u8;
5598        let ns = bytes.iter().position(|&b| b == sep_b);
5599        let mut as_off: Option<usize> = None;
5600        let mut skip = false;
5601        if d.argsep != 0 {
5602            if let Some(a_pos) = bytes.iter().position(|&b| b == argsep_b) {
5603                if ns.map_or(true, |n| a_pos <= n) {
5604                    // c:3289
5605                    as_off = Some(a_pos);
5606                    *ap = Some(String::from_utf8_lossy(&bytes[a_pos + 1..]).into_owned());
5607                    skip = true;
5608                }
5609            }
5610        }
5611        let sap = as_off.or(ns);
5612        let head = match sap {
5613            Some(p) => std::str::from_utf8(&bytes[..p]).unwrap_or(""),
5614            None => std::str::from_utf8(&bytes).unwrap_or(""),
5615        };
5616        let r = cv_quote_get_val(d, head);
5617        // c:3302 — if NOARG/no match and skip, fall back to as.
5618        let ns_eff = if (r.as_deref().map_or(true, |v| v.r#type == CVV_NOARG)) && skip {
5619            as_off
5620        } else if skip {
5621            // skip is set ⇒ as_off was the chosen sep; ns might be after.
5622            ns.filter(|&n| as_off.map_or(true, |a| n > a))
5623        } else {
5624            ns
5625        };
5626        let next_off = match ns_eff {
5627            None => None,
5628            Some(n)
5629                if Some(n) == as_off && r.as_deref().map_or(false, |v| v.r#type != CVV_NOARG) =>
5630            {
5631                None
5632            }
5633            Some(n) => Some(n + 1),
5634        };
5635        *sp = next_off.map(|o| String::from_utf8_lossy(&bytes[o..]).into_owned());
5636        if !skip {
5637            *ap = None;
5638        }
5639        return r;
5640    }
5641
5642    // Branch 3: no hassep, argsep set.
5643    *sp = None; // c:3314
5644    let argsep_b = d.argsep as u8;
5645    let as_pos = bytes.iter().position(|&b| b == argsep_b);
5646    let head = match as_pos {
5647        Some(p) => {
5648            *ap = Some(String::from_utf8_lossy(&bytes[p + 1..]).into_owned());
5649            std::str::from_utf8(&bytes[..p]).unwrap_or("")
5650        }
5651        None => {
5652            *ap = None;
5653            &s_in
5654        }
5655    };
5656    cv_quote_get_val(d, head) // c:3324
5657}
5658
5659/// Direct port of `static void cv_parse_word(Cvdef d)` from
5660/// `Src/Zle/computil.c:3336-3472`. Walks `compwords[1..]` (when
5661/// `d->words` set) and `compprefix` calling `cv_next` repeatedly,
5662/// accumulating recognized values into `cv_laststate.vals`.
5663pub fn cv_parse_word(d: &mut cvdef) {
5664    // c:3336
5665
5666    // c:3343 — free old vals.
5667    if cv_alloced.load(Ordering::Relaxed) != 0 {
5668        if let Ok(mut ls) = cv_laststate.lock() {
5669            ls.vals = None;
5670        }
5671    }
5672    // c:3346 — mark all vals active.
5673    let mut v = d.vals.as_deref_mut();
5674    while let Some(vv) = v {
5675        vv.active = 1;
5676        v = vv.next.as_deref_mut();
5677    }
5678
5679    let mut state_vals: Vec<String> = Vec::new();
5680    let mut state_def: Option<Box<caarg>> = None;
5681    let mut state_val: Option<Box<cvval>> = None;
5682    cv_alloced.store(1, Ordering::Relaxed);
5683
5684    let compcur = COMPCURRENT.load(Ordering::Relaxed);
5685    let compwords: Vec<String> = COMPWORDS
5686        .get()
5687        .and_then(|m| m.lock().ok().map(|w| w.clone()))
5688        .unwrap_or_default();
5689    let compprefix: String = COMPPREFIX
5690        .get()
5691        .and_then(|m| m.lock().ok().map(|s| s.clone()))
5692        .unwrap_or_default();
5693    let compsuffix: String = COMPSUFFIX
5694        .get()
5695        .and_then(|m| m.lock().ok().map(|s| s.clone()))
5696        .unwrap_or_default();
5697    let mut pign = compprefix.clone(); // c:3340
5698    let mut nosfx = false;
5699
5700    // c:3356 — scan compwords[1..] if d.words is set.
5701    if d.words != 0 && !compwords.is_empty() && !compwords[0].is_empty() {
5702        for i in 1..compwords.len() {
5703            if (i as i32) == compcur - 1 {
5704                continue;
5705            }
5706            let mut str_opt: Option<String> = Some(compwords[i].clone());
5707            while str_opt.as_deref().map_or(false, |s| !s.is_empty()) {
5708                let mut ap: Option<String> = None;
5709                let val = cv_next(d, &mut str_opt, &mut ap);
5710                if let Some(v) = val {
5711                    state_vals.push(v.name.clone().unwrap_or_default());
5712                    state_vals.push(ap.unwrap_or_default());
5713                    if (i as i32) + 1 < compcur {
5714                        let xor = v.xor.clone().unwrap_or_default();
5715                        cv_inactive(d, &xor);
5716                    }
5717                } else {
5718                    break;
5719                }
5720            }
5721        }
5722    }
5723
5724    // c:3385 — scan compprefix.
5725    let mut str_opt: Option<String> = Some(compprefix.clone());
5726    let mut last_arg: Option<String> = None;
5727    while str_opt.as_deref().map_or(false, |s| !s.is_empty()) {
5728        let mut ap: Option<String> = None;
5729        let val = cv_next(d, &mut str_opt, &mut ap);
5730        if let Some(v) = val {
5731            state_vals.push(v.name.clone().unwrap_or_default());
5732            match ap.as_deref() {
5733                Some(arg_v) => {
5734                    if str_opt.is_some() {
5735                        state_vals.push(arg_v.to_string());
5736                    } else {
5737                        let joined = format!("{}{}", arg_v, compsuffix);
5738                        state_vals.push(joined);
5739                        nosfx = true;
5740                    }
5741                    last_arg = ap.clone();
5742                }
5743                None => state_vals.push(String::new()),
5744            }
5745            let xor = v.xor.clone().unwrap_or_default();
5746            cv_inactive(d, &xor);
5747            if let Some(s) = str_opt.as_deref() {
5748                pign = s.to_string();
5749            } else {
5750                // c:3407 — re-activate v in the cvdef.
5751                let target_name = v.name.clone();
5752                let mut p = d.vals.as_deref_mut();
5753                while let Some(vv) = p {
5754                    if vv.name == target_name {
5755                        vv.active = 1;
5756                        break;
5757                    }
5758                    p = vv.next.as_deref_mut();
5759                }
5760            }
5761            state_val = Some(v);
5762        } else {
5763            break;
5764        }
5765    }
5766    if state_val.is_some() && last_arg.is_some() && str_opt.is_none() {
5767        // c:3411
5768        state_def = state_val.as_ref().and_then(|v| v.arg.clone());
5769    }
5770
5771    // c:3414 — separator handling for compsuffix.
5772    if !nosfx && d.hassep != 0 {
5773        let pign_len = pign.len();
5774        let cp_len = compprefix.len();
5775        ignore_prefix(cp_len as i32 - pign_len as i32); // c:3418
5776
5777        let mut ign = 0usize;
5778        let mut more: Option<String> = None;
5779        if d.sep == 0
5780            && (state_val.is_none() || state_val.as_deref().map_or(true, |v| v.r#type == CVV_NOARG))
5781        {
5782            ign = compsuffix.len();
5783            more = Some(compsuffix.clone());
5784        } else if d.sep != 0 {
5785            let sep_b = d.sep as u8;
5786            let ns_pos = compsuffix.as_bytes().iter().position(|&b| b == sep_b);
5787            let as_pos = if d.argsep != 0 {
5788                compsuffix
5789                    .as_bytes()
5790                    .iter()
5791                    .position(|&b| b == d.argsep as u8)
5792            } else {
5793                None
5794            };
5795            if let Some(a) = as_pos {
5796                if ns_pos.map_or(true, |n| a <= n) {
5797                    ign = compsuffix.len() - a;
5798                } else {
5799                    ign = ns_pos.map_or(0, |n| compsuffix.len() - n);
5800                }
5801            } else {
5802                ign = ns_pos.map_or(0, |n| compsuffix.len() - n);
5803            }
5804            more = ns_pos.map(|n| compsuffix[n + 1..].to_string());
5805        } else if d.argsep != 0 {
5806            let as_pos = compsuffix
5807                .as_bytes()
5808                .iter()
5809                .position(|&b| b == d.argsep as u8);
5810            if let Some(a) = as_pos {
5811                ign = compsuffix.len() - a;
5812            }
5813        }
5814
5815        if ign > 0 {
5816            ignore_suffix(ign as i32); // c:3444
5817        }
5818
5819        let mut more_opt = more;
5820        while more_opt.as_deref().map_or(false, |s| !s.is_empty()) {
5821            // c:3446
5822            let mut ap: Option<String> = None;
5823            let val = cv_next(d, &mut more_opt, &mut ap);
5824            if let Some(v) = val {
5825                state_vals.push(v.name.clone().unwrap_or_default());
5826                match ap.as_deref() {
5827                    Some(arg_v) => {
5828                        if more_opt.is_some() {
5829                            state_vals.push(arg_v.to_string());
5830                        } else {
5831                            state_vals.push(format!("{}{}", arg_v, compsuffix));
5832                        }
5833                    }
5834                    None => state_vals.push(String::new()),
5835                }
5836                let xor = v.xor.clone().unwrap_or_default();
5837                cv_inactive(d, &xor);
5838            } else {
5839                break;
5840            }
5841        }
5842    } else if last_arg.is_some() {
5843        let cp_len = compprefix.len();
5844        let arg_off = compprefix
5845            .find(last_arg.as_deref().unwrap_or(""))
5846            .map(|i| i as i32)
5847            .unwrap_or(cp_len as i32);
5848        ignore_prefix(arg_off); // c:3467
5849    } else {
5850        let cp_len = compprefix.len();
5851        ignore_prefix(cp_len as i32 - pign.len() as i32); // c:3469
5852    }
5853
5854    // c:3471 — commit state.
5855    if let Ok(mut ls) = cv_laststate.lock() {
5856        *ls = cvstate {
5857            d: Some(Box::new(d.clone())),
5858            def: state_def,
5859            val: state_val,
5860            vals: if state_vals.is_empty() {
5861                None
5862            } else {
5863                Some(state_vals)
5864            },
5865        };
5866    }
5867}
5868
5869/// Direct port of `static int bin_compvalues(char *nam, char **args,
5870///                                              UNUSED(Options ops),
5871///                                              UNUSED(int func))` from
5872/// `Src/Zle/computil.c:3475-3658`. Full subcommand dispatch for
5873/// `compvalues -i/-D/-C/-V/-s/-S/-d/-L/-v`. Each branch consumes
5874/// `cv_laststate` populated by `cv_parse_word`.
5875pub fn bin_compvalues(
5876    nam: &str,
5877    args: &[String], // c:3475
5878    _ops: &options,
5879    _func: i32,
5880) -> i32 {
5881    if INCOMPFUNC.load(Ordering::Relaxed) != 1 {
5882        // c:3479
5883        zwarnnam(nam, "can only be called from completion function");
5884        return 1;
5885    }
5886    if args.is_empty() {
5887        return 1;
5888    }
5889    let a0 = args[0].as_bytes();
5890    if a0.len() != 2 || a0[0] != b'-' {
5891        // c:3483
5892        zwarnnam(nam, &format!("invalid argument: {}", args[0]));
5893        return 1;
5894    }
5895    let sub = a0[1];
5896
5897    if sub != b'i' && cv_parsed.load(Ordering::Relaxed) == 0 {
5898        // c:3487
5899        zwarnnam(nam, "no parsed state");
5900        return 1;
5901    }
5902
5903    let (min, max): (i32, i32) = match sub {
5904        // c:3491
5905        b'i' => (2, -1),
5906        b'D' => (2, 2),
5907        b'C' => (1, 1),
5908        b'V' => (3, 3),
5909        b's' => (1, 1),
5910        b'S' => (1, 1),
5911        b'd' => (1, 1),
5912        b'L' => (3, 4),
5913        b'v' => (1, 1),
5914        _ => {
5915            zwarnnam(nam, &format!("invalid option: {}", args[0]));
5916            return 1;
5917        }
5918    };
5919    let n = (args.len() as i32) - 1;
5920    if n < min {
5921        zwarnnam(nam, "not enough arguments");
5922        return 1;
5923    }
5924    if max >= 0 && n > max {
5925        zwarnnam(nam, "too many arguments");
5926        return 1;
5927    }
5928
5929    match sub {
5930        b'i' => {
5931            // c:3514
5932            let spec = &args[1..];
5933            let _ = get_cvdef(nam, spec);
5934            let mut cached: Option<Box<cvdef>> = {
5935                let cache = cvdef_cache.lock().ok();
5936                cache.and_then(|c| {
5937                    c.iter().find_map(|slot| {
5938                        slot.as_ref()
5939                            .filter(|e| {
5940                                e.ndefs == spec.len() as i32
5941                                    && e.defs.as_deref().map_or(false, |d| {
5942                                        d.len() == spec.len()
5943                                            && d.iter().zip(spec.iter()).all(|(a, b)| a == b)
5944                                    })
5945                            })
5946                            .cloned()
5947                    })
5948                })
5949            };
5950            let Some(ref mut def) = cached else {
5951                return 1;
5952            };
5953            cv_parsed.store(0, Ordering::Relaxed); // c:3521
5954            cv_parse_word(def); // c:3527
5955            cv_parsed.store(1, Ordering::Relaxed); // c:3528
5956            0
5957        }
5958
5959        b'D' => {
5960            // c:3533
5961            let arg = cv_laststate.lock().ok().and_then(|s| s.def.clone());
5962            if let Some(a) = arg {
5963                setsparam(&args[1], a.descr.as_deref().unwrap_or("")); // c:3541
5964                setsparam(&args[2], a.action.as_deref().unwrap_or("")); // c:3542
5965                0
5966            } else {
5967                1 // c:3546
5968            }
5969        }
5970
5971        b'C' => {
5972            // c:3548
5973            let arg = cv_laststate.lock().ok().and_then(|s| s.def.clone());
5974            if let Some(a) = arg {
5975                setsparam(&args[1], a.opt.as_deref().unwrap_or("")); // c:3555
5976                0
5977            } else {
5978                1 // c:3559
5979            }
5980        }
5981
5982        b'V' => {
5983            // c:3561
5984            let mut noarg: Vec<String> = Vec::new();
5985            let mut arg_l: Vec<String> = Vec::new();
5986            let mut opt_l: Vec<String> = Vec::new();
5987            if let Ok(ls) = cv_laststate.lock() {
5988                if let Some(d) = ls.d.as_ref() {
5989                    let mut p = d.vals.as_deref();
5990                    while let Some(v) = p {
5991                        if v.active != 0 {
5992                            // c:3574
5993                            let bucket: &mut Vec<String> = match v.r#type {
5994                                t if t == CVV_NOARG => &mut noarg,
5995                                t if t == CVV_ARG => &mut arg_l,
5996                                _ => &mut opt_l,
5997                            };
5998                            let name = v.name.as_deref().unwrap_or("");
5999                            let str_val = if let Some(d) = v.descr.as_deref() {
6000                                format!("{}:{}", name, d)
6001                            } else {
6002                                name.to_string()
6003                            };
6004                            bucket.push(str_val); // c:3589
6005                        }
6006                        p = v.next.as_deref();
6007                    }
6008                }
6009            }
6010            setaparam(&args[1], noarg);
6011            setaparam(&args[2], arg_l);
6012            setaparam(&args[3], opt_l);
6013            0 // c:3596
6014        }
6015
6016        b's' => {
6017            // c:3598
6018            let (hassep, sep) = cv_laststate
6019                .lock()
6020                .ok()
6021                .and_then(|ls| ls.d.as_ref().map(|d| (d.hassep, d.sep)))
6022                .unwrap_or((0, 0));
6023            if hassep != 0 {
6024                let tmp = (sep as u8 as char).to_string();
6025                setsparam(&args[1], &tmp);
6026                0 // c:3608
6027            } else {
6028                1 // c:3610
6029            }
6030        }
6031
6032        b'S' => {
6033            // c:3611
6034            let argsep = cv_laststate
6035                .lock()
6036                .ok()
6037                .and_then(|ls| ls.d.as_ref().map(|d| d.argsep))
6038                .unwrap_or(0);
6039            let tmp = (argsep as u8 as char).to_string();
6040            setsparam(&args[1], &tmp);
6041            0 // c:3620
6042        }
6043
6044        b'd' => {
6045            // c:3621
6046            let descr = cv_laststate
6047                .lock()
6048                .ok()
6049                .and_then(|ls| ls.d.as_ref().and_then(|d| d.descr.clone()))
6050                .unwrap_or_default();
6051            setsparam(&args[1], &descr);
6052            0
6053        }
6054
6055        b'L' => {
6056            // c:3626
6057            let val = cv_laststate
6058                .lock()
6059                .ok()
6060                .and_then(|ls| ls.d.as_ref().and_then(|d| cv_get_val(d, &args[1])));
6061            if let Some(v) = val {
6062                if let Some(a) = v.arg.as_deref() {
6063                    // c:3634
6064                    setsparam(&args[2], a.descr.as_deref().unwrap_or(""));
6065                    setsparam(&args[3], a.action.as_deref().unwrap_or(""));
6066                    if args.len() > 4 {
6067                        // c:3638
6068                        setsparam(&args[4], v.name.as_deref().unwrap_or(""));
6069                    }
6070                    return 0;
6071                }
6072            }
6073            1 // c:3643
6074        }
6075
6076        b'v' => {
6077            // c:3645
6078            let vals = cv_laststate.lock().ok().and_then(|ls| ls.vals.clone());
6079            if let Some(v) = vals {
6080                sethparam(&args[1], v);
6081                0
6082            } else {
6083                1 // c:3656
6084            }
6085        }
6086
6087        _ => 1, // c:3658
6088    }
6089}
6090
6091/// Port of `comp_quote(char *str, int prefix)` from Src/Zle/computil.c:3662.
6092pub fn comp_quote(str: &str, prefix: i32) -> String {
6093    // c:3662
6094    // c:3667 — `x = (prefix && *str == '=')`.
6095    let (s_eff, x) = if prefix != 0 && str.starts_with('=') {
6096        // c:3667
6097        ("x".to_string() + &str[1..], true) // c:3668
6098    } else {
6099        (str.to_string(), false)
6100    };
6101    // c:3670 — `ret = quotestring(str, *compqstack)`.
6102    //          *compqstack is the first byte of the qstack string.
6103    let qhead = COMPQSTACK
6104        .get()
6105        .and_then(|m| m.lock().ok().and_then(|str| str.bytes().next()))
6106        .unwrap_or(0);
6107    let mut ret = quotename(&s_eff, qhead as i32);
6108    // c:3672-3673 — restore `=` prefix on both ret and original.
6109    if x {
6110        if !ret.is_empty() {
6111            ret.replace_range(0..1, "=");
6112        }
6113    }
6114    ret
6115}
6116
6117// `setup_` is ported above with the cadef_cache/cvdef_cache/comptags
6118// reset body cited at Src/Zle/computil.c:5124. This duplicate shim
6119// was retired when the real port landed.
6120
6121// =====================================================================
6122// bin_compquote / bin_comptags / bin_comptry / bin_compvalues —
6123// Src/Zle/computil.c. Each is a real port matching the C signature
6124// exactly; state mutations go through the canonical
6125// getvalue/setstrvalue/setarrvalue ops in params.rs, the comptags
6126// state machine in the cs_* helpers below, and the compvalues table
6127// via cv_parse_word.
6128// =====================================================================
6129
6130/// Direct port of `bin_compquote(char *nam, char **args, Options ops, UNUSED(int func))` from `Src/Zle/computil.c:3679`.
6131/// C body (c:3683-3725):
6132/// ```c
6133/// if (incompfunc != 1) { error; return 1; }
6134/// if (!compqstack || !*compqstack) return 0;
6135/// while ((name = *args++)) {
6136///     if ((v = getvalue(...))) {
6137///         switch (PM_TYPE(v->pm->node.flags)) {
6138///         case PM_SCALAR/NAMEREF:
6139///             setstrvalue(v, comp_quote(getstrvalue(v), -p));
6140///         case PM_ARRAY:
6141///             foreach val in array: comp_quote each
6142///         default: zwarnnam("invalid parameter type");
6143///         }
6144///     }
6145/// }
6146/// ```
6147/// Quoting routes through `comp_quote()` per param type (PM_SCALAR
6148/// / PM_ARRAY); the entry validates `incompfunc` + `compqstack`
6149/// guards before dispatch.
6150/// WARNING: param names don't match C — Rust=(nam, args, _func) vs C=(nam, args, ops, func)
6151pub fn bin_compquote(
6152    nam: &str,
6153    args: &[String], // c:3679
6154    ops: &options,
6155    _func: i32,
6156) -> i32 {
6157    if INCOMPFUNC.load(Ordering::Relaxed) != 1 {
6158        // c:3685
6159        zwarnnam(nam, "can only be called from completion function");
6160        return 1;
6161    }
6162    // c:3691 — `if (!compqstack || !*compqstack) return 0;`.
6163    let qstack_empty = COMPQSTACK
6164        .get()
6165        .map(|m| m.lock().map(|s| s.is_empty()).unwrap_or(true))
6166        .unwrap_or(true);
6167    if qstack_empty {
6168        return 0;
6169    }
6170    let p_flag = OPT_ISSET(ops, b'p'); // c:3704
6171
6172    // c:3696 — `while ((name = *args++))`. Walk param names.
6173    for name in args {
6174        // c:3696
6175        let mut vbuf = value {
6176            pm: None,
6177            arr: Vec::new(),
6178            scanflags: 0,
6179            valflags: 0,
6180            start: 0,
6181            end: 0,
6182        };
6183        let mut nameref: &str = name.as_str();
6184        let v = getvalue(Some(&mut vbuf), &mut nameref, 0); // c:3699
6185        if v.is_none() {
6186            // c:3724
6187            zwarnnam(nam, &format!("unknown parameter: {}", name));
6188            continue;
6189        }
6190        let v = v.unwrap();
6191        let flags = v.pm.as_ref().map(|pm| pm.node.flags).unwrap_or(0);
6192        let pm_type = PM_TYPE(flags as u32);
6193        // c:3700-3705 — PM_SCALAR / PM_NAMEREF path.
6194        if pm_type == 0 || (flags as u32 & crate::ported::zsh_h::PM_NAMEREF) != 0 {
6195            let s = getstrvalue(Some(v));
6196            let q = comp_quote(&s, p_flag as i32);
6197            let mut nameref_re: &str = name.as_str();
6198            setstrvalue(getvalue(Some(&mut vbuf), &mut nameref_re, 0), &q);
6199        } else if pm_type == PM_ARRAY {
6200            // c:3706
6201            let arr = getvaluearr(Some(v));
6202            let new_arr: Vec<String> = arr
6203                .into_iter()
6204                .map(|elem| comp_quote(&elem, p_flag as i32))
6205                .collect();
6206            // Re-fetch a fresh value for the setarrvalue call (getvalue
6207            // consumed the prior borrow).
6208            let mut vbuf2 = value {
6209                pm: None,
6210                arr: Vec::new(),
6211                scanflags: 0,
6212                valflags: 0,
6213                start: 0,
6214                end: 0,
6215            };
6216            let mut nameref2: &str = name.as_str();
6217            if let Some(v2) = getvalue(Some(&mut vbuf2), &mut nameref2, 0) {
6218                setarrvalue(v2, new_arr);
6219            }
6220        } else {
6221            // c:3720
6222            zwarnnam(nam, &format!("invalid parameter type: {}", name));
6223        }
6224    }
6225    0 // c:3725
6226}
6227
6228// =====================================================================
6229// `ctags` / `ctset` — `comptags` cache.
6230// Src/Zle/computil.c:3732-3760. MAX_TAGS already declared above.
6231// =====================================================================
6232
6233/// Port of `typedef struct ctags *Ctags` from `Src/Zle/computil.c:3732`.
6234pub type Ctags = Box<ctags>; // c:3732
6235
6236/// Direct port of `struct ctags` from `Src/Zle/computil.c:3737-3742`.
6237/// A bunch of tag sets keyed by locallevel.
6238#[derive(Debug, Default)]
6239#[allow(non_camel_case_types)]
6240pub struct ctags {
6241    // c:3737
6242    pub all: Option<Vec<String>>, // c:3738 char **all
6243    pub context: Option<String>,  // c:3739 char *context
6244    pub init: i32,                // c:3740
6245    pub sets: Option<Box<ctset>>, // c:3741 Ctset sets
6246}
6247/// Port of `typedef struct ctset *Ctset` from `computil.c:3733`.
6248pub type Ctset = Box<ctset>; // c:3733
6249
6250/// Direct port of `struct ctset` from `Src/Zle/computil.c:3763`.
6251#[derive(Debug, Default)]
6252#[allow(non_camel_case_types)]
6253pub struct ctset {
6254    // c:3763
6255    pub next: Option<Box<ctset>>,  // c:3763 Ctset next
6256    pub tags: Option<Vec<String>>, // c:3763 char **tags
6257    pub tag: Option<String>,       // c:3763 char *tag
6258    pub ptr: i32,                  // c:3763 char **ptr (index)
6259}
6260
6261/// Port of `MAX_TAGS` from `computil.c:3755`. Maximum nested completion
6262/// tags depth.
6263pub const MAX_TAGS: usize = 256; // c:3755
6264
6265/// Port of `static Ctags comptags[MAX_TAGS]` from
6266/// `Src/Zle/computil.c:3756`. One ctags entry per `locallevel`;
6267/// indexed by completion level.
6268pub static comptags: std::sync::Mutex<[Option<Box<ctags>>; MAX_TAGS]> = // c:3756
6269    std::sync::Mutex::new([const { None }; MAX_TAGS]);
6270
6271/// Port of `static int lasttaglevel` from `Src/Zle/computil.c:3760`.
6272/// "locallevel at last comptags -i".
6273pub static lasttaglevel: std::sync::atomic::AtomicI32 = // c:3760
6274    std::sync::atomic::AtomicI32::new(0);
6275
6276/// Direct port of `static void freectset(Ctset s)` from
6277/// `Src/Zle/computil.c:3780`.
6278pub fn freectset(mut s: Option<Box<ctset>>) {
6279    // c:3763
6280    while let Some(mut node) = s {
6281        // c:3780 while (s)
6282        s = node.next.take(); // c:3780 n = s->next
6283        node.tags = None; // c:3780-3771
6284        node.tag = None; // c:3780
6285        drop(node); // c:3780
6286    }
6287}
6288
6289/// Direct port of `static void freectags(Ctags t)` from
6290/// `Src/Zle/computil.c:3780`.
6291pub fn freectags(t: Option<Box<ctags>>) {
6292    // c:3780
6293    let Some(mut node) = t else {
6294        return;
6295    }; // c:3780 if (t)
6296    node.all = None; // c:3783-3784
6297    node.context = None; // c:3785
6298    freectset(node.sets.take()); // c:3786
6299    drop(node); // c:3787
6300}
6301
6302/// Direct port of `static void settags(int level, char **tags)` from
6303/// `Src/Zle/computil.c:3794`. Replaces `comptags[level]` with a fresh
6304/// ctags carrying `tags[0]` as context and `tags[1..]` as the full
6305/// tag-list. Used at the start of every completion level transition
6306/// (`comptags -i`).
6307pub fn settags(level: i32, tags: &[String]) {
6308    // c:3794
6309    let idx = level as usize;
6310    if idx >= MAX_TAGS {
6311        return;
6312    } // c:3756 bounds
6313
6314    if let Ok(mut tab) = comptags.lock() {
6315        if tab[idx].is_some() {
6316            // c:3798
6317            freectags(tab[idx].take()); // c:3799
6318        }
6319        let context = tags.first().cloned(); // c:3804 *tags
6320        let all: Vec<String> = tags.iter().skip(1).cloned().collect(); // c:3803 tags+1
6321        tab[idx] = Some(Box::new(ctags {
6322            // c:3801 zalloc
6323            all: Some(all), // c:3803
6324            context,        // c:3804
6325            init: 1,        // c:3806
6326            sets: None,     // c:3805
6327        }));
6328    }
6329}
6330
6331/// Port of `arrcontains(char **a, char *s, int colon)` from Src/Zle/computil.c:3813.
6332pub fn arrcontains(a: &[String], s: &str, colon: bool) -> i32 {
6333    // c:3813
6334    // C body c:3817-3826: linear scan; if colon, compare up to first
6335    //                    `:` in either side; else strcmp.
6336    for entry in a {
6337        if colon {
6338            let p = s.split(':').next().unwrap_or(s);
6339            let q = entry.split(':').next().unwrap_or(entry);
6340            if p == q {
6341                return 1; // c:3823
6342            }
6343        } else if entry == s {
6344            return 1; // c:3825
6345        }
6346    }
6347    0 // c:3827
6348}
6349
6350/// Direct port of `static int bin_comptags(char *nam, char **args,
6351///                                          UNUSED(Options ops),
6352///                                          UNUSED(int func))` from
6353/// `Src/Zle/computil.c:3831-3958`. Full subcommand dispatch for
6354/// `comptags -i/-C/-T/-N/-R/-S/-A`. Reads `locallevel` to index
6355/// `comptags[]`; `--` suffix decrements the level by one.
6356pub fn bin_comptags(
6357    nam: &str,
6358    args: &[String], // c:3831
6359    _ops: &options,
6360    _func: i32,
6361) -> i32 {
6362    if INCOMPFUNC.load(Ordering::Relaxed) != 1 {
6363        // c:3835
6364        zwarnnam(nam, "can only be called from completion function");
6365        return 1;
6366    }
6367    if args.is_empty() {
6368        return 1;
6369    }
6370    let a0 = args[0].as_bytes();
6371    // c:3839 — validate `-X` or `-X--` shape.
6372    if a0.len() < 2 || a0[0] != b'-' || (a0.len() > 2 && (a0[2] != b'-' || a0.len() > 3)) {
6373        zwarnnam(nam, &format!("invalid argument: {}", args[0]));
6374        return 1;
6375    }
6376
6377    let level: i32 = locallevel.load(Ordering::Relaxed)                      // c:3844
6378        - if a0.len() > 2 { 1 } else { 0 };
6379    if level < 0 || (level as usize) >= MAX_TAGS {
6380        // c:3845
6381        zwarnnam(nam, "nesting level too deep");
6382        return 1;
6383    }
6384    let lvl_idx = level as usize;
6385
6386    let sub = a0[1];
6387
6388    // c:3849 — non-init subcommands require a registered comptags[level].
6389    if sub != b'i' && sub != b'I' {
6390        let registered = {
6391            let tab = comptags.lock().unwrap();
6392            tab[lvl_idx].is_some()
6393        };
6394        if !registered {
6395            zwarnnam(nam, "no tags registered");
6396            return 1;
6397        }
6398    }
6399
6400    // c:3854-3864 — per-subcommand arg-count bounds.
6401    let (min, max): (i32, i32) = match sub {
6402        b'i' => (2, -1),
6403        b'C' => (1, 1),
6404        b'T' => (0, 0),
6405        b'N' => (0, 0),
6406        b'R' => (1, 1),
6407        b'S' => (1, 1),
6408        b'A' => (2, 3),
6409        _ => {
6410            zwarnnam(nam, &format!("invalid option: {}", args[0]));
6411            return 1;
6412        }
6413    };
6414    let n = (args.len() as i32) - 1;
6415    if n < min {
6416        zwarnnam(nam, "not enough arguments");
6417        return 1;
6418    }
6419    if max >= 0 && n > max {
6420        zwarnnam(nam, "too many arguments");
6421        return 1;
6422    }
6423
6424    match sub {
6425        b'i' => {
6426            // c:3874
6427            settags(level, &args[1..]);
6428            lasttaglevel.store(level, Ordering::Relaxed); // c:3876
6429            0
6430        }
6431        b'C' => {
6432            // c:3878
6433            let ctx = {
6434                let tab = comptags.lock().unwrap();
6435                tab[lvl_idx]
6436                    .as_ref()
6437                    .and_then(|t| t.context.clone())
6438                    .unwrap_or_default()
6439            };
6440            setsparam(&args[1], &ctx); // c:3879
6441            0
6442        }
6443        b'T' => {
6444            // c:3881
6445            let empty = {
6446                let tab = comptags.lock().unwrap();
6447                tab[lvl_idx].as_ref().map_or(true, |t| t.sets.is_none())
6448            };
6449            if empty {
6450                1
6451            } else {
6452                0
6453            } // c:3882
6454        }
6455        b'N' => {
6456            // c:3883
6457            let mut tab = comptags.lock().unwrap();
6458            if let Some(t) = tab[lvl_idx].as_mut() {
6459                if t.init != 0 {
6460                    // c:3887
6461                    t.init = 0;
6462                } else if let Some(mut s) = t.sets.take() {
6463                    // c:3889
6464                    t.sets = s.next.take(); // c:3890
6465                    freectset(Some(s)); // c:3892
6466                }
6467                if t.sets.is_some() {
6468                    0
6469                } else {
6470                    1
6471                } // c:3894
6472            } else {
6473                1
6474            }
6475        }
6476        b'R' => {
6477            // c:3896
6478            let tab = comptags.lock().unwrap();
6479            let hit = tab[lvl_idx]
6480                .as_ref()
6481                .and_then(|t| t.sets.as_ref())
6482                .map(|s| {
6483                    s.tags
6484                        .as_deref()
6485                        .map_or(false, |tgs| arrcontains(tgs, &args[1], true) != 0)
6486                })
6487                .unwrap_or(false);
6488            if hit {
6489                0
6490            } else {
6491                1
6492            } // c:3900
6493        }
6494        b'A' => {
6495            // c:3903
6496            let mut tab = comptags.lock().unwrap();
6497            let Some(t) = tab[lvl_idx].as_mut() else {
6498                return 1;
6499            };
6500            let Some(s) = t.sets.as_mut() else {
6501                return 1;
6502            };
6503            // c:3911 — refresh ptr if tag changed.
6504            if s.tag.as_deref() != Some(args[1].as_str()) {
6505                s.tag = Some(args[1].clone()); // c:3913
6506                s.ptr = 0; // c:3914
6507            }
6508            let tags_vec = s.tags.clone().unwrap_or_default();
6509            // c:3916-3925 — walk tags from ptr looking for a name match.
6510            let mut found: Option<(usize, String, String)> = None;
6511            for (i, q) in tags_vec.iter().enumerate().skip(s.ptr as usize) {
6512                if strpfx(&args[1], q) {
6513                    // c:3917
6514                    let l = args[1].len();
6515                    let qb = q.as_bytes();
6516                    if qb.len() == l {
6517                        // c:3918
6518                        found = Some((i, q.clone(), q.clone()));
6519                        break;
6520                    } else if qb.len() > l && qb[l] == b':' {
6521                        // c:3921
6522                        let v = String::from_utf8_lossy(&qb[l + 1..]).into_owned();
6523                        found = Some((i, q.clone(), v));
6524                        break;
6525                    }
6526                }
6527            }
6528            let (q_idx, q_full, v) = match found {
6529                None => {
6530                    // c:3927
6531                    s.tag = None;
6532                    return 1;
6533                }
6534                Some(t) => t,
6535            };
6536            s.ptr = (q_idx + 1) as i32; // c:3932
6537                                        // c:3933 — `setsparam(args[2], v == '-' ? dyncat(args[1], v) : v)`.
6538            let value = if v.starts_with('-') {
6539                crate::ported::string::dyncat(&args[1], &v)
6540            } else {
6541                v.clone()
6542            };
6543            setsparam(&args[2], &value);
6544            // c:3934 — optional 3rd arg gets the "name-up-to-`:`" of q.
6545            if args.len() > 3 {
6546                let pre_colon: String = q_full.splitn(2, ':').next().unwrap_or("").to_string();
6547                setsparam(&args[3], &pre_colon);
6548            }
6549            0 // c:3942
6550        }
6551        b'S' => {
6552            // c:3946
6553            let tab = comptags.lock().unwrap();
6554            if let Some(tags) = tab[lvl_idx]
6555                .as_ref()
6556                .and_then(|t| t.sets.as_ref())
6557                .and_then(|s| s.tags.clone())
6558            {
6559                setaparam(&args[1], tags); // c:3951
6560                0
6561            } else {
6562                1 // c:3952
6563            }
6564        }
6565        _ => 0,
6566    }
6567}
6568
6569/// Direct port of `static int bin_comptry(char *nam, char **args,
6570///                                          UNUSED(Options ops),
6571///                                          UNUSED(int func))` from
6572/// `Src/Zle/computil.c:3961-4138`. Builds a new tag-set under the
6573/// active comptags[lasttaglevel] entry. Two forms:
6574///   - `comptry -m "pat1 pat2" [...]` — for each space-separated
6575///     tag pattern, glob-expand via braces/wildcards, match against
6576///     the registered `all` array, filter out tags already in any
6577///     existing set, then append the deduplicated matches as a new
6578///     ctset.
6579///   - `comptry [-s] tag1 tag2 ...` — filter args to keep only
6580///     registered tags not in any existing set; with `-s`, build one
6581///     ctset per arg, else one ctset for all of them.
6582pub fn bin_comptry(
6583    nam: &str,
6584    args: &[String], // c:3961
6585    _ops: &options,
6586    _func: i32,
6587) -> i32 {
6588    if INCOMPFUNC.load(Ordering::Relaxed) != 1 {
6589        // c:3963
6590        zwarnnam(nam, "can only be called from completion function");
6591        return 1;
6592    }
6593
6594    let lvl = lasttaglevel.load(Ordering::Relaxed);
6595    if lvl <= 0 {
6596        // c:3967 — !lasttaglevel
6597        zwarnnam(nam, "no tags registered");
6598        return 1;
6599    }
6600    let lvl_idx = lvl as usize;
6601    let registered = comptags
6602        .lock()
6603        .ok()
6604        .map(|t| lvl_idx < t.len() && t[lvl_idx].is_some())
6605        .unwrap_or(false);
6606    if !registered {
6607        zwarnnam(nam, "no tags registered");
6608        return 1;
6609    }
6610
6611    if args.is_empty() {
6612        return 0;
6613    } // c:3971
6614
6615    // Helper: append a new ctset to comptags[lvl_idx].sets.
6616    let append_set = |tags: Vec<String>| {
6617        if let Ok(mut tab) = comptags.lock() {
6618            if let Some(t) = tab[lvl_idx].as_mut() {
6619                let new_set = Box::new(ctset {
6620                    next: None,
6621                    tags: Some(tags),
6622                    tag: None,
6623                    ptr: 0,
6624                });
6625                // c:4082 — walk to tail of existing sets.
6626                if let Some(head) = t.sets.as_mut() {
6627                    let mut cur = head.as_mut();
6628                    while cur.next.is_some() {
6629                        cur = cur.next.as_mut().unwrap();
6630                    }
6631                    cur.next = Some(new_set);
6632                } else {
6633                    t.sets = Some(new_set);
6634                }
6635            }
6636        }
6637    };
6638
6639    if args[0] == "-m" {
6640        // c:3972
6641        // c:3973-4090 — pattern-match mode.
6642        for arg in &args[1..] {
6643            // c:3978
6644            let mut s = arg.as_bytes().to_vec();
6645            let mut list: Vec<String> = Vec::new();
6646            let mut num = 0i32;
6647            let mut i = 0usize;
6648
6649            while i < s.len() {
6650                // c:3980 — skip leading blanks.
6651                while i < s.len() && iblank(s[i]) {
6652                    i += 1;
6653                }
6654                if i >= s.len() {
6655                    break;
6656                }
6657                // c:3982 — accumulate the token, watching for `\X` escape
6658                // and tracking the first unescaped ':' separator.
6659                let p_start = i;
6660                let mut p_pos = i;
6661                let mut colon_at: Option<usize> = None;
6662                while i < s.len() && !inblank(s[i]) {
6663                    if colon_at.is_none() && s[i] == b':' {
6664                        colon_at = Some(p_pos);
6665                    }
6666                    if s[i] == b'\\' && i + 1 < s.len() {
6667                        i += 1;
6668                    }
6669                    s[p_pos] = s[i];
6670                    p_pos += 1;
6671                    i += 1;
6672                }
6673                // Skip the trailing blank.
6674                if i < s.len() {
6675                    i += 1;
6676                }
6677
6678                let token_full = String::from_utf8_lossy(&s[p_start..p_pos]).into_owned();
6679                if token_full.is_empty() {
6680                    continue;
6681                }
6682
6683                // c:3997 — split at colon: q = head, c = trailing.
6684                let (q, c_opt): (String, Option<String>) = match colon_at {
6685                    Some(c_idx) => {
6686                        let head = String::from_utf8_lossy(&s[p_start..c_idx]).into_owned();
6687                        let tail = String::from_utf8_lossy(&s[c_idx + 1..p_pos]).into_owned();
6688                        (head, Some(tail))
6689                    }
6690                    None => (token_full.clone(), None),
6691                };
6692                if q.is_empty() {
6693                    continue;
6694                }
6695
6696                // c:4001-4012 — convert `{` / `}` / `,` to Inbrace / Outbrace
6697                // / Comma tokens for glob processing.
6698                let mut qq: String = q
6699                    .chars()
6700                    .map(|ch| match ch {
6701                        '\\' => ch, // keep; handled below
6702                        '{' => Inbrace,
6703                        '}' => Outbrace,
6704                        ',' => Comma,
6705                        other => other,
6706                    })
6707                    .collect();
6708                // Handle `\X` — keep both bytes literal by re-walking. The
6709                // C does this inline in the same loop; we just leave them.
6710                tokenize(&mut qq);
6711
6712                // c:4013 — if hasbraces/haswilds, glob-expand.
6713                let has_meta = hasbraces(&qq, false) || haswilds(&qq);
6714                let all_arr: Vec<String> = comptags
6715                    .lock()
6716                    .ok()
6717                    .and_then(|t| t[lvl_idx].as_ref().and_then(|c| c.all.clone()))
6718                    .unwrap_or_default();
6719                let sets_clone: Vec<Vec<String>> = comptags
6720                    .lock()
6721                    .ok()
6722                    .map(|t| {
6723                        let mut out = Vec::new();
6724                        if let Some(c) = t[lvl_idx].as_ref() {
6725                            let mut p = c.sets.as_deref();
6726                            while let Some(set) = p {
6727                                if let Some(ts) = set.tags.as_ref() {
6728                                    out.push(ts.clone());
6729                                }
6730                                p = set.next.as_deref();
6731                            }
6732                        }
6733                        out
6734                    })
6735                    .unwrap_or_default();
6736
6737                if has_meta {
6738                    // c:4015-4022 — expand braces, then compile each as a Patprog.
6739                    let mut blist: Vec<String> = vec![qq.clone()];
6740                    let mut bi = 0usize;
6741                    while bi < blist.len() {
6742                        if hasbraces(&blist[bi], false) {
6743                            let expanded = xpandbraces(&blist[bi], false);
6744                            blist.remove(bi);
6745                            for e in expanded {
6746                                blist.insert(bi, e);
6747                                bi += 1;
6748                            }
6749                        } else {
6750                            bi += 1;
6751                        }
6752                    }
6753                    for bb in &blist {
6754                        // c:4023
6755                        if let Some(prog) = patcompile(bb, 0, None::<&mut String>) {
6756                            for a in &all_arr {
6757                                // c:4029
6758                                // Skip if `a:c` (or just a) already in list.
6759                                let already = list.iter().any(|item| {
6760                                    let item_head = item.split(':').next().unwrap_or(item);
6761                                    item_head == a.as_str()
6762                                });
6763                                if already {
6764                                    continue;
6765                                }
6766                                if pattry(&prog, a) {
6767                                    // c:4043
6768                                    let entry = match &c_opt {
6769                                        Some(c) => format!("{}:{}", a, c),
6770                                        None => a.clone(),
6771                                    };
6772                                    list.push(entry);
6773                                    num += 1;
6774                                }
6775                            }
6776                        }
6777                    }
6778                } else if arrcontains(&all_arr, &q, false) != 0 {
6779                    // c:4056
6780                    // c:4057-4064 — literal token: include if not in any set.
6781                    let in_set = sets_clone.iter().any(|s| arrcontains(s, &q, false) != 0);
6782                    if !in_set {
6783                        list.push(q.clone());
6784                        num += 1;
6785                    }
6786                }
6787            }
6788
6789            if num > 0 {
6790                // c:4072
6791                append_set(list);
6792            }
6793        }
6794    } else {
6795        // c:4091 — plain mode
6796        let mut idx = 0usize;
6797        let sep = args[idx] == "-s"; // c:4095
6798        if sep {
6799            idx += 1;
6800        }
6801        let all_arr: Vec<String> = comptags
6802            .lock()
6803            .ok()
6804            .and_then(|t| t[lvl_idx].as_ref().and_then(|c| c.all.clone()))
6805            .unwrap_or_default();
6806        let sets_clone: Vec<Vec<String>> = comptags
6807            .lock()
6808            .ok()
6809            .map(|t| {
6810                let mut out = Vec::new();
6811                if let Some(c) = t[lvl_idx].as_ref() {
6812                    let mut p = c.sets.as_deref();
6813                    while let Some(set) = p {
6814                        if let Some(ts) = set.tags.as_ref() {
6815                            out.push(ts.clone());
6816                        }
6817                        p = set.next.as_deref();
6818                    }
6819                }
6820                out
6821            })
6822            .unwrap_or_default();
6823
6824        // c:4098-4108 — filter args, keep only registered tags not in any set.
6825        let filtered: Vec<String> = args[idx..]
6826            .iter()
6827            .filter(|p| {
6828                arrcontains(&all_arr, p, true) != 0
6829                    && !sets_clone.iter().any(|s| arrcontains(s, p, false) != 0)
6830            })
6831            .cloned()
6832            .collect();
6833
6834        if filtered.is_empty() {
6835            return 0;
6836        }
6837
6838        // c:4114-4134 — push as one set, or split (one per arg) with -s.
6839        if sep {
6840            for t in &filtered {
6841                append_set(vec![t.clone()]);
6842            }
6843        } else {
6844            append_set(filtered);
6845        }
6846    }
6847    0 // c:4138
6848}
6849
6850/// Port of `PATH_MAX2` from `computil.c:4141`. `PATH_MAX * 2` — buffer
6851/// budget for path-completion staging strings.
6852pub const PATH_MAX2: usize = 8192; // c:4141 (PATH_MAX*2, 4096*2)
6853
6854/// Direct port of `static LinkList cfp_test_exact(LinkList names,
6855///                                                  char **accept,
6856///                                                  char *skipped)` from
6857/// `Src/Zle/computil.c:4160-4290`. Returns the subset of `names` whose
6858/// `name + skipped + compprefix + compsuffix` resolves to an existing
6859/// file. When `accept` is non-boolean, the resolved path must also
6860/// match at least one of the compiled accept-patterns. Returns None
6861/// when nothing matched.
6862pub fn cfp_test_exact(
6863    names: &[String],
6864    accept: &[String], // c:4160
6865    skipped: &str,
6866) -> Option<Vec<String>> {
6867    let compprefix = COMPPREFIX
6868        .get()
6869        .and_then(|m| m.lock().ok().map(|s| s.clone()))
6870        .unwrap_or_default();
6871    let compsuffix = COMPSUFFIX
6872        .get()
6873        .and_then(|m| m.lock().ok().map(|s| s.clone()))
6874        .unwrap_or_default();
6875
6876    // c:4175 — bail when both prefix and suffix are empty.
6877    if compprefix.is_empty() && compsuffix.is_empty() {
6878        return None;
6879    }
6880
6881    // c:4181 — accept-exact off?
6882    let accept_off = accept.is_empty()
6883        || (accept.len() == 1 && matches!(accept[0].as_str(), "false" | "no" | "off" | "0"));
6884    if accept_off {
6885        // c:4188
6886        return None;
6887    }
6888
6889    // c:4199-4214 — build compiled Patprog list from non-boolean accept.
6890    let mut alist: Option<Vec<Patprog>> = None;
6891    let is_boolean_true =
6892        accept.len() == 1 && matches!(accept[0].as_str(), "true" | "yes" | "on" | "1");
6893    if !is_boolean_true {
6894        let mut list: Vec<Patprog> = Vec::new();
6895        let mut all_star = false;
6896        for p in accept {
6897            if p == "*" {
6898                // c:4207 wildcard short-circuit
6899                all_star = true;
6900                break;
6901            }
6902            let mut p_copy = p.clone();
6903            tokenize(&mut p_copy);
6904            if let Some(prog) = patcompile(&p_copy, 0, None::<&mut String>) {
6905                list.push(prog);
6906            }
6907        }
6908        if !all_star {
6909            alist = Some(list);
6910        }
6911    }
6912
6913    // c:4220-4227 — assemble `suf = skipped + rembslash(prefix + suffix)`.
6914    let sl = skipped.len() + compprefix.len() + compsuffix.len();
6915    if sl > PATH_MAX2 {
6916        // c:4223
6917        return None;
6918    }
6919    let suf = format!(
6920        "{}{}",
6921        skipped,
6922        rembslash(&format!("{}{}", compprefix, compsuffix))
6923    );
6924
6925    let mut ret: Vec<String> = Vec::new();
6926    for p in names {
6927        // c:4229
6928        let l = p.len();
6929        if l + sl >= PATH_MAX2 {
6930            continue;
6931        } // c:4231
6932        let buf = format!("{}{}", p, suf);
6933        if ztat(&buf, false).is_none() {
6934            continue;
6935        } // c:4269 stat exists?
6936          // c:4274 — accept-pattern check.
6937        if let Some(ref ps) = alist {
6938            let any_match = ps.iter().any(|prog| pattry(prog, &buf));
6939            if !any_match {
6940                continue;
6941            }
6942        }
6943        ret.push(buf); // c:4285
6944    }
6945
6946    if ret.is_empty() {
6947        None
6948    } else {
6949        Some(ret)
6950    } // c:4289
6951}
6952
6953/// Direct port of `static char *cfp_matcher_range(Cmatcher *ms, char *add)`
6954/// from `Src/Zle/computil.c:4307-4520`. For each character of `add`,
6955/// consults the parallel `ms[i]` matcher and emits a pattern fragment:
6956///   - no matcher: the character verbatim
6957///   - CMF_RIGHT: `*c`
6958///   - word EQUIV+line EQUIV: `[c eq(c)]` (two-char class with
6959///     the equivalent char from the word side)
6960///   - CPAT_NCLASS: `[^class]`
6961///   - CPAT_CCLASS / CPAT_EQUIV / CPAT_CHAR: `[classchar+addchar]`
6962///   - CPAT_ANY: `?`
6963pub fn cfp_matcher_range(
6964    ms: &[Option<Box<Cmatcher>>], // c:4307
6965    add: &str,
6966) -> String {
6967    // Local PATMATCHRANGE — Rust copy of the helper used by pattern_match1
6968    // / pattern_match_equivalence. Walks an encoded char-range byte
6969    // sequence looking for `c`. Encoding:
6970    //   0x80 + PP_RANGE: next 2 bytes are lo,hi range
6971    //   0x80 + PP_*: POSIX class marker; matched for LOWER/UPPER
6972    //   plain bytes: literal char
6973    // Returns Some((idx, mtp)) on hit.
6974    fn patmatchrange_local(s: Option<&[u8]>, c: u32) -> Option<(u32, i32)> {
6975        let bytes = s?;
6976        let pp_range_marker = (0x80u8).wrapping_add(PP_RANGE as u8);
6977        let pp_lower_marker = (0x80u8).wrapping_add(PP_LOWER as u8);
6978        let pp_upper_marker = (0x80u8).wrapping_add(PP_UPPER as u8);
6979
6980        let mut idx: u32 = 0;
6981        let mut i = 0usize;
6982        while i < bytes.len() {
6983            let b = bytes[i];
6984            if b == pp_range_marker {
6985                if i + 2 >= bytes.len() {
6986                    break;
6987                }
6988                let r1 = bytes[i + 1] as u32;
6989                let r2 = bytes[i + 2] as u32;
6990                if c >= r1 && c <= r2 {
6991                    return Some((idx, 0));
6992                }
6993                idx += 1;
6994                i += 3;
6995            } else if b >= 0x80 {
6996                let is_lower = b == pp_lower_marker;
6997                let is_upper = b == pp_upper_marker;
6998                let matched = if is_lower {
6999                    c < 256 && (c as u8).is_ascii_lowercase()
7000                } else if is_upper {
7001                    c < 256 && (c as u8).is_ascii_uppercase()
7002                } else {
7003                    false
7004                };
7005                if matched {
7006                    return Some((idx, (b as i32) - 0x80));
7007                }
7008                idx += 1;
7009                i += 1;
7010            } else {
7011                if c == b as u32 {
7012                    return Some((idx, 0));
7013                }
7014                idx += 1;
7015                i += 1;
7016            }
7017        }
7018        None
7019    }
7020
7021    let mut out = String::with_capacity(add.len() * 2);
7022    let add_chars: Vec<(usize, char)> = add.char_indices().collect();
7023
7024    for (i, (_byte_idx, ch)) in add_chars.iter().enumerate() {
7025        let addc = *ch as u32;
7026        let m_opt = ms.get(i).and_then(|x| x.as_deref());
7027
7028        match m_opt {
7029            None => {
7030                // c:4331 — no matcher: emit char verbatim.
7031                out.push(*ch);
7032            }
7033            Some(m) if (m.flags & CMF_RIGHT) != 0 => {
7034                // c:4344 — right-anchored: `*char`.
7035                out.push('*');
7036                out.push(*ch);
7037            }
7038            Some(m) => {
7039                let word: Option<&Cpattern> = m.word.as_deref();
7040                let line: Option<&Cpattern> = m.line.as_deref();
7041                if let (Some(l), Some(w)) = (line, word) {
7042                    if l.tp == CPAT_EQUIV && w.tp == CPAT_EQUIV {
7043                        // c:4359 — genuine equivalence; emit `[char eq]`.
7044                        out.push('[');
7045                        out.push(*ch);
7046                        if let Some((ind, mtp)) = patmatchrange_local(l.str.as_deref(), addc) {
7047                            let eq = pattern_match_equivalence(w, ind + 1, mtp, addc);
7048                            if eq != u32::MAX {
7049                                if let Some(c) = char::from_u32(eq) {
7050                                    // c:60 — imeta(byte) gate; `c` is a
7051                                    // u32 codepoint, so cap to u8 range
7052                                    // before calling the byte-arg port.
7053                                    let _ = if eq <= 0xff { imeta(eq as u8) } else { false };
7054                                    out.push(c);
7055                                }
7056                            }
7057                        }
7058                        out.push(']');
7059                        continue;
7060                    }
7061                }
7062                // Local helper: decode an encoded Cpattern.str byte
7063                // sequence into a `[…]`-suitable readable form. POSIX
7064                // class markers become `[:name:]`; ranges become
7065                // `lo-hi`; literals pass through.
7066                fn decode_range_bytes(bytes: &[u8]) -> String {
7067                    let pp_range_marker = (0x80u8).wrapping_add(PP_RANGE as u8);
7068                    let mut out = String::new();
7069                    let mut i = 0usize;
7070                    while i < bytes.len() {
7071                        let b = bytes[i];
7072                        if b == pp_range_marker && i + 2 < bytes.len() {
7073                            out.push(bytes[i + 1] as char);
7074                            out.push('-');
7075                            out.push(bytes[i + 2] as char);
7076                            i += 3;
7077                        } else if b >= 0x80 {
7078                            let cls = (b as usize) - 0x80;
7079                            // c:1240+ — POSIX class marker byte → `[:name:]`.
7080                            // Inverse of `range_type`: index into the
7081                            // POSIX_CLASS_NAMES table.
7082                            const POSIX_CLASSES: &[&str] = &[
7083                                "alpha", "alnum", "blank", "cntrl", "digit", "graph", "lower",
7084                                "print", "punct", "space", "upper", "xdigit",
7085                            ];
7086                            if cls > 0 && cls - 1 < POSIX_CLASSES.len() {
7087                                out.push_str(&format!("[:{}:]", POSIX_CLASSES[cls - 1]));
7088                            }
7089                            i += 1;
7090                        } else {
7091                            out.push(b as char);
7092                            i += 1;
7093                        }
7094                    }
7095                    out
7096                }
7097
7098                if let Some(w) = word {
7099                    match w.tp {
7100                        x if x == CPAT_NCLASS => {
7101                            // c:4401
7102                            out.push('[');
7103                            out.push('^');
7104                            if let Some(bytes) = w.str.as_deref() {
7105                                out.push_str(&decode_range_bytes(bytes));
7106                            }
7107                            out.push(']');
7108                        }
7109                        x if x == CPAT_CCLASS || x == CPAT_EQUIV || x == CPAT_CHAR => {
7110                            // c:4435 / c:4441 / c:4442
7111                            out.push('[');
7112                            let mut mt = 0i32;
7113                            let addadd = pattern_match1(w, addc, &mut mt) == 0;
7114                            // c:4455 — if addadd && *add == ']', emit ']' first.
7115                            if addadd && *ch == ']' {
7116                                out.push(*ch);
7117                            }
7118                            if w.tp == CPAT_CHAR {
7119                                // c:4461
7120                                if let Some(c) = char::from_u32(w.chr) {
7121                                    out.push(c);
7122                                }
7123                            } else {
7124                                // c:4476
7125                                if let Some(bytes) = w.str.as_deref() {
7126                                    out.push_str(&decode_range_bytes(bytes));
7127                                }
7128                            }
7129                            if addadd && *ch != ']' {
7130                                // c:4489
7131                                out.push(*ch);
7132                            }
7133                            out.push(']');
7134                        }
7135                        x if x == CPAT_ANY => {
7136                            // c:4502
7137                            out.push('?');
7138                        }
7139                        _ => {
7140                            // Fallback: emit verbatim.
7141                            out.push(*ch);
7142                        }
7143                    }
7144                } else {
7145                    out.push(*ch);
7146                }
7147            }
7148        }
7149    }
7150    out
7151}
7152
7153/// Direct port of `static char *cfp_matcher_pats(char *matcher, char *add)`
7154/// from `Src/Zle/computil.c:4525-4613`. Parses the matcher spec into
7155/// a Cmatcher chain, then walks each chain entry truncating `add` at
7156/// the first character that matches the matcher's stop pattern, and
7157/// recording one matcher per surviving character. Finally calls
7158/// cfp_matcher_range to synthesize the output pattern.
7159///
7160/// Returns:
7161///   - the transformed string (possibly empty) on success
7162///   - the original `add` unchanged when the matcher spec is empty
7163///     or unparseable
7164pub fn cfp_matcher_pats(matcher: &str, add: &str) -> String {
7165    // c:4525
7166
7167    // c:4527 — parse_cmatcher returns None on error (the C pcm_err path).
7168    let m_chain = parse_cmatcher("", matcher);
7169    let Some(mut m_chain) = m_chain else {
7170        return add.to_string(); // c:4529
7171    };
7172
7173    // c:4531-4538 — ms[0..zl] is one matcher slot per character of add.
7174    let zl = ztrlen(add); // c:4531
7175    let mut ms: Vec<Option<Box<Cmatcher>>> = (0..zl).map(|_| None).collect();
7176    let mut add_owned = add.to_string();
7177
7178    let mut m_opt: Option<&Cmatcher> = Some(&*m_chain);
7179    while let Some(m) = m_opt {
7180        let mut stopp: Option<&Cpattern> = None;
7181        let mut stopl: i32 = 0;
7182
7183        if (m.flags & (CMF_LEFT | CMF_RIGHT)) == 0 {
7184            // c:4542
7185            if m.llen == 1 && m.wlen == 1 {
7186                // c:4543
7187                // c:4550 — walk add looking for the first char where the
7188                // matcher's `line` pattern matches; record `m` in ms[i].
7189                let chars: Vec<(usize, char)> = add_owned.char_indices().collect();
7190                for (i, (byte_idx, _ch)) in chars.iter().enumerate() {
7191                    if i >= ms.len() {
7192                        break;
7193                    }
7194                    let slice = &add_owned[*byte_idx..];
7195                    if pattern_match(m.line.as_deref(), slice, None, "") != 0 {
7196                        // c:4551 — `if (*mp)` collision: truncate add.
7197                        if ms[i].is_some() {
7198                            add_owned.truncate(*byte_idx); // c:4553
7199                            break;
7200                        } else {
7201                            ms[i] = Some(Box::new(m.clone())); // c:4557
7202                        }
7203                    }
7204                }
7205            } else {
7206                stopp = m.line.as_deref(); // c:4565
7207                stopl = m.llen;
7208            }
7209        } else if (m.flags & CMF_RIGHT) != 0 {
7210            // c:4568
7211            if m.wlen < 0 && m.llen == 0 && m.ralen == 1 {
7212                // c:4569
7213                let chars: Vec<(usize, char)> = add_owned.char_indices().collect();
7214                for (i, (byte_idx, _ch)) in chars.iter().enumerate() {
7215                    if i >= ms.len() {
7216                        break;
7217                    }
7218                    let slice = &add_owned[*byte_idx..];
7219                    if pattern_match(m.right.as_deref(), slice, None, "") != 0 {
7220                        // c:4572 — collision OR leading-dot guard.
7221                        let leading_dot = *byte_idx == 0 && slice.starts_with('.');
7222                        if ms[i].is_some() || leading_dot {
7223                            add_owned.truncate(*byte_idx); // c:4573
7224                            break;
7225                        } else {
7226                            ms[i] = Some(Box::new(m.clone()));
7227                        }
7228                    }
7229                }
7230            } else if m.llen != 0 {
7231                // c:4584
7232                stopp = m.line.as_deref();
7233                stopl = m.llen;
7234            } else {
7235                stopp = m.right.as_deref(); // c:4588
7236                stopl = m.ralen;
7237            }
7238        } else {
7239            // c:4591 CMF_LEFT
7240            if m.lalen == 0 {
7241                // c:4592
7242                return String::new(); // c:4593
7243            }
7244            stopp = m.left.as_deref();
7245            stopl = m.lalen;
7246        }
7247
7248        // c:4598-4608 — apply stopp truncation.
7249        if let Some(sp) = stopp {
7250            let chars: Vec<(usize, char)> = add_owned.char_indices().collect();
7251            let mut bytes_remaining = add_owned.len() as i32;
7252            for (_i, (byte_idx, _ch)) in chars.iter().enumerate() {
7253                if bytes_remaining < stopl {
7254                    break;
7255                }
7256                let slice = &add_owned[*byte_idx..];
7257                if pattern_match(Some(sp), slice, None, "") != 0 {
7258                    add_owned.truncate(*byte_idx); // c:4601
7259                    break;
7260                }
7261                bytes_remaining -= 1;
7262            }
7263        }
7264
7265        m_opt = m.next.as_deref();
7266    }
7267
7268    // c:4610 — synthesize the output via cfp_matcher_range.
7269    if !add_owned.is_empty() {
7270        cfp_matcher_range(&ms, &add_owned)
7271    } else {
7272        add_owned // c:4613
7273    }
7274}
7275
7276/// Direct port of `static void cfp_opt_pats(char **pats, char *matcher)`
7277/// from `Src/Zle/computil.c:4621-4701`. "Optimization" pass that
7278/// prefixes each `*…`-leading pattern with the literal portion of
7279/// `compprefix` that no pattern would consume. The walk computes a
7280/// shrinking `add` string — each pattern crosses off the chars in
7281/// `add` it would match — and any remaining chars become the prefix.
7282///
7283/// Modifies `pats` in place; returns the (possibly modified) list.
7284pub fn cfp_opt_pats(pats: &[String], matcher: &str) -> Vec<String> {
7285    // c:4621
7286
7287    let compprefix = COMPPREFIX
7288        .get()
7289        .and_then(|m| m.lock().ok().map(|s| s.clone()))
7290        .unwrap_or_default();
7291    if compprefix.is_empty() {
7292        // c:4625
7293        return pats.to_vec();
7294    }
7295    let compsuffix = COMPSUFFIX
7296        .get()
7297        .and_then(|m| m.lock().ok().map(|s| s.clone()))
7298        .unwrap_or_default();
7299
7300    // c:4628-4633 — if comppatmatch && haswilds(rembslash(prefix+suffix)): bail.
7301    let cpm_set = comppatmatch
7302        .get()
7303        .and_then(|m| m.lock().ok().map(|g| g.is_some()))
7304        .unwrap_or(false);
7305    if cpm_set {
7306        let merged = format!("{}{}", compprefix, compsuffix);
7307        let mut t = rembslash(&merged);
7308        tokenize(&mut t);
7309        remnulargs(&mut t);
7310        if haswilds(&t) {
7311            return pats.to_vec(); // c:4632
7312        }
7313    }
7314
7315    // c:4634-4649 — build `add` by walking compprefix, unescaping `\X`
7316    // for non-special X, and pre-escaping unescaped specials.
7317    const SPECIALS: &[u8] = b"*?<>()[]|#^~=";
7318    let cp_bytes = compprefix.as_bytes();
7319    let mut add: Vec<u8> = Vec::with_capacity(cp_bytes.len() * 2);
7320    let mut i = 0usize;
7321    while i < cp_bytes.len() {
7322        let c = cp_bytes[i];
7323        let keep = if c == b'\\' && i + 1 < cp_bytes.len() {
7324            // c:4636 — keep `\X` literal when X is non-special.
7325            let next = cp_bytes[i + 1];
7326            !SPECIALS.contains(&next)
7327        } else {
7328            true
7329        };
7330        if keep {
7331            let unescaped_at_start = i == 0 || cp_bytes[i - 1] != b'\\';
7332            if unescaped_at_start && SPECIALS.contains(&c) {
7333                // c:4640
7334                add.push(b'\\');
7335            }
7336            add.push(c);
7337        }
7338        i += 1;
7339    }
7340    let mut add_s: String = String::from_utf8_lossy(&add).into_owned();
7341
7342    // c:4650-4691 — walk each pattern, cross off chars from `add`.
7343    for p_orig in pats {
7344        if add_s.is_empty() {
7345            break;
7346        }
7347        let mut q_bytes: Vec<u8> = p_orig.as_bytes().to_vec();
7348        if q_bytes.is_empty() {
7349            continue;
7350        }
7351        // c:4654 — strip trailing alternation `(…|…)` group.
7352        if let Some(b')') = q_bytes.last().copied() {
7353            let mut t = q_bytes.len() - 1;
7354            let mut found = None;
7355            while t > 0 {
7356                t -= 1;
7357                let c = q_bytes[t];
7358                if c == b')' || c == b'|' || c == b'~' || c == b'(' {
7359                    found = Some((t, c));
7360                    break;
7361                }
7362            }
7363            if let Some((idx, c)) = found {
7364                if c == b'(' {
7365                    q_bytes.truncate(idx);
7366                }
7367            }
7368        }
7369
7370        let mut qi = 0usize;
7371        while qi < q_bytes.len() && !add_s.is_empty() {
7372            let c = q_bytes[qi];
7373            if c == b'\\' && qi + 1 < q_bytes.len() {
7374                // c:4662
7375                qi += 1;
7376                let target = q_bytes[qi];
7377                // c:4663 — cross off `target` from add.
7378                if let Some(pos) = add_s.find(target as char) {
7379                    add_s.truncate(pos);
7380                }
7381            } else if c == b'<' {
7382                // c:4665
7383                // c:4666 — cross off any digit.
7384                let cut_at = add_s.bytes().position(|b| idigit(b));
7385                if let Some(pos) = cut_at {
7386                    add_s.truncate(pos);
7387                }
7388            } else if c == b'[' {
7389                // c:4668
7390                // c:4669-4684 — character class.
7391                let mut xi = qi + 1;
7392                let not = xi < q_bytes.len() && (q_bytes[xi] == b'!' || q_bytes[xi] == b'^');
7393                if not {
7394                    xi += 1;
7395                }
7396                let _ = not;
7397                while xi < q_bytes.len() && q_bytes[xi] != b']' {
7398                    if xi + 2 < q_bytes.len() && q_bytes[xi + 1] == b'-' {
7399                        let c1 = q_bytes[xi];
7400                        let c2 = q_bytes[xi + 2];
7401                        let cut_at = add_s.bytes().position(|b| b >= c1 && b <= c2);
7402                        if let Some(pos) = cut_at {
7403                            add_s.truncate(pos);
7404                        }
7405                        xi += 3;
7406                    } else {
7407                        let cut_at = add_s.find(q_bytes[xi] as char);
7408                        if let Some(pos) = cut_at {
7409                            add_s.truncate(pos);
7410                        }
7411                        xi += 1;
7412                    }
7413                }
7414                qi = xi;
7415            } else if c != b'?'
7416                && c != b'*'
7417                && c != b'('
7418                && c != b')'
7419                && c != b'|'
7420                && c != b'~'
7421                && c != b'#'
7422            // c:4685
7423            {
7424                let cut_at = add_s.find(c as char);
7425                if let Some(pos) = cut_at {
7426                    add_s.truncate(pos);
7427                }
7428            }
7429            qi += 1;
7430        }
7431    }
7432
7433    // c:4693-4700 — prepend `add` to each `*`-leading pattern.
7434    let mut out: Vec<String> = pats.to_vec();
7435    if !add_s.is_empty() {
7436        let final_add = if !matcher.is_empty() {
7437            let m = cfp_matcher_pats(matcher, &add_s);
7438            if m.is_empty() {
7439                return out;
7440            } // c:4694
7441            m
7442        } else {
7443            add_s
7444        };
7445        for p in out.iter_mut() {
7446            if p.starts_with('*') {
7447                *p = format!("{}{}", final_add, p);
7448            }
7449        }
7450    }
7451    out
7452}
7453
7454/// Direct port of `static LinkList cfp_bld_pats(UNUSED(int dirs),
7455///                                                LinkList names,
7456///                                                char *skipped,
7457///                                                char **pats)` from
7458/// `Src/Zle/computil.c:4704-4732`. For each (name, pattern) pair,
7459/// builds `name + skipped + pattern`. When GLOBDOTS is unset and the
7460/// compprefix starts with `.`, also adds a dot-prefixed variant.
7461pub fn cfp_bld_pats(
7462    _dirs: i32,
7463    names: &[String],
7464    skipped: &str, // c:4704
7465    pats: &[String],
7466) -> Vec<String> {
7467    let compprefix = COMPPREFIX
7468        .get()
7469        .and_then(|m| m.lock().ok().map(|s| s.clone()))
7470        .unwrap_or_default();
7471    // c:4711 — `dot = unset(GLOBDOTS) && compprefix && *compprefix == '.'`.
7472    let dot = unset(GLOBDOTS) && compprefix.starts_with('.');
7473
7474    let mut ret: Vec<String> = Vec::new();
7475    for o in names {
7476        // c:4712
7477        for p in pats {
7478            // c:4714
7479            // c:4716 — `str = o + skipped + p`.
7480            ret.push(format!("{}{}{}", o, skipped, p));
7481            // c:4721 — dot variant when GLOBDOTS unset and pattern
7482            // doesn't already start with '.'.
7483            if dot && !p.starts_with('.') {
7484                ret.push(format!("{}{}.{}", o, skipped, p));
7485            }
7486        }
7487    }
7488    ret // c:4731
7489}
7490
7491/// Port of `cfp_add_sdirs(LinkList final, LinkList orig, char *skipped, char *sdirs, char **fake)` from Src/Zle/computil.c:4735.
7492/// WARNING: param names don't match C — Rust=(final_list, orig, sdirs, fake) vs C=(final, orig, skipped, sdirs, fake)
7493pub fn cfp_add_sdirs(
7494    final_list: &mut Vec<String>,
7495    orig: &[String], // c:4735
7496    _skipped: &str,
7497    sdirs: &str,
7498    fake: &[String],
7499) {
7500    // C body c:4738-4767: if sdirs ∈ {"yes","true","on","1","..","../"}
7501    //                     and GLOBDOTS or compprefix starts with `.`,
7502    //                     prepend "." (or "..") to final.
7503    let mut add = 0;
7504    if !sdirs.is_empty() {
7505        // c:4740
7506        match sdirs {
7507            "yes" | "true" | "on" | "1" => add = 2, // c:4741
7508            ".." => add = 1,                        // c:4744
7509            _ => {}
7510        }
7511    }
7512    if add > 0 {
7513        for f in fake {
7514            final_list.push(f.clone());
7515        }
7516        for o in orig {
7517            if !final_list.contains(o) {
7518                final_list.push(o.clone());
7519            }
7520        }
7521    }
7522}
7523
7524/// Direct port of `static char **cf_pats(int dirs, int noopt,
7525///                                       char **names, char **accept,
7526///                                       char *skipped, char *matcher,
7527///                                       char *sdirs, char **fake,
7528///                                       char **pats)` from
7529/// `Src/Zle/computil.c:4829`. Combines the supplied pattern
7530/// lists into a single resolved pattern array used by
7531/// `_path_files` to drive the file-completion path.
7532///
7533/// **Substrate tradeoff:** the helper chain
7534/// `cfp_test_exact`/`cfp_opt_pats`/`cfp_bld_pats`/`cfp_add_sdirs`
7535/// Direct port of `static LinkList cf_pats(int dirs, int noopt,
7536///                                          LinkList names, char **accept,
7537///                                          char *skipped, char *matcher,
7538///                                          char *sdirs, char **fake,
7539///                                          char **pats)` from
7540/// `Src/Zle/computil.c:4829-4848`. The cf_pats driver:
7541/// 1. Try cfp_test_exact first; if it returns a non-empty list, fold
7542///    in `sdirs`/`fake` via cfp_add_sdirs and return.
7543/// 2. Otherwise: if dirs, replace `pats` with `*(-/)`. If !noopt run
7544///    cfp_opt_pats. Then build the patterns via cfp_bld_pats and fold
7545///    in sdirs/fake.
7546pub fn cf_pats(
7547    dirs: i32,
7548    noopt: i32,
7549    names: &[String], // c:4829
7550    accept: &[String],
7551    skipped: &str,
7552    matcher: &str,
7553    sdirs: &str,
7554    fake: &[String],
7555    pats: &[String],
7556) -> Vec<String> {
7557    // c:4835 — try exact-match pass first.
7558    if let Some(exact) = cfp_test_exact(names, accept, skipped) {
7559        let mut out = exact;
7560        cfp_add_sdirs(&mut out, names, skipped, sdirs, fake); // c:4836
7561        return out;
7562    }
7563
7564    // c:4838 — when dirs is set, force the `*(-/)` directory glob.
7565    let dir_pats = vec!["*(-/)".to_string()];
7566    let active_pats: Vec<String> = if dirs != 0 {
7567        dir_pats
7568    } else if noopt == 0 {
7569        // c:4843 — optimization pass.
7570        cfp_opt_pats(pats, matcher)
7571    } else {
7572        pats.to_vec()
7573    };
7574
7575    // c:4846 — build the glob array.
7576    let mut out = cfp_bld_pats(dirs, names, skipped, &active_pats);
7577    cfp_add_sdirs(&mut out, names, skipped, sdirs, fake);
7578    out
7579}
7580
7581/// Direct port of `static void cf_ignore(char **names, LinkList ign,
7582///                                          char *style, char *path)`
7583/// from `Src/Zle/computil.c:4860-4896`. Adds to `ign` any directory
7584/// in `names` that:
7585///   - "pwd" style: shares the same dev/ino as `$PWD` (so completion
7586///     doesn't offer the directory you're already in).
7587///   - "parent" style: is an ancestor directory of `path` (so when
7588///     completing under `/a/b/c/`, `/a/`, `/a/b/`, etc. don't show
7589///     up as options).
7590/// Quoted with QT_BACKSLASH for safe re-insertion into the line.
7591pub fn cf_ignore(names: &[String], ign: &mut Vec<String>, style: &str, path: &str) {
7592    // c:4860
7593
7594    let pl = path.len();
7595    let tpar = style.contains("parent"); // c:4866
7596    let pwd = crate::ported::params::getsparam("PWD").unwrap_or_default();
7597    let est = if !pwd.is_empty() {
7598        ztat(&pwd, true)
7599    } else {
7600        None
7601    };
7602    let tpwd = style.contains("pwd") && est.is_some(); // c:4867
7603
7604    if !tpar && !tpwd {
7605        return;
7606    } // c:4870
7607
7608    for n in names {
7609        // c:4873
7610        let nst = match ztat(n, true) {
7611            // c:4874 lstat
7612            Some(m) if m.is_dir() => m,
7613            _ => continue,
7614        };
7615        if tpwd {
7616            if let Some(ref est) = est {
7617                if nst.dev() == est.dev() && nst.ino() == est.ino() {
7618                    // c:4875
7619                    ign.push(quotestring(n, QT_BACKSLASH)); // c:4876
7620                    continue;
7621                }
7622            }
7623        }
7624        if tpar && pl > 0 && n.starts_with(path) {
7625            // c:4879
7626            let mut c = n.clone();
7627            let mut found = false;
7628            // c:4881 — walk up via strrchr('/') while above path-prefix.
7629            while let Some(idx) = c.rfind('/') {
7630                if idx <= pl {
7631                    break;
7632                }
7633                c.truncate(idx);
7634                if let Some(st) = ztat(&c, false) {
7635                    // c:4883 stat
7636                    if st.dev() == nst.dev() && st.ino() == nst.ino() {
7637                        found = true;
7638                        break;
7639                    }
7640                }
7641            }
7642            // c:4889 — fallback last-segment check via lstat.
7643            let last_match = if !found {
7644                if let Some(idx) = c.rfind('/') {
7645                    if idx > pl {
7646                        c.truncate(idx);
7647                        ztat(&c, true)
7648                            .map_or(false, |st| st.dev() == nst.dev() && st.ino() == nst.ino())
7649                    } else {
7650                        false
7651                    }
7652                } else {
7653                    false
7654                }
7655            } else {
7656                false
7657            };
7658            if found || last_match {
7659                ign.push(quotestring(n, QT_BACKSLASH)); // c:4892
7660            }
7661        }
7662    }
7663}
7664
7665/// Direct port of `static LinkList cf_remove_other(char **names,
7666///                                                   char *pre, int *amb)`
7667/// from `Src/Zle/computil.c:4899-4953`. Helper for `_path_files` that
7668/// reports whether the remaining `names` share a common directory
7669/// prefix (`*amb` cleared) or diverge (`*amb` set, return None).
7670/// When `pre` itself contains a `/`, names matching that head are
7671/// returned as the consensus list.
7672pub fn cf_remove_other(names: &[String], pre: &str, amb: &mut i32) -> Option<Vec<String>> {
7673    if let Some(slash) = pre.find('/') {
7674        // c:4903
7675        // c:4906-4908 — pre' = pre[..slash] + "/".
7676        let pre2 = format!("{}/", &pre[..slash]);
7677
7678        // c:4910-4912 — any name with the truncated prefix?
7679        let any_match = names.iter().any(|n| strpfx(&pre2, n));
7680
7681        if any_match {
7682            // c:4914
7683            // c:4915-4922 — return all matching names with amb=0.
7684            let ret: Vec<String> = names.iter().filter(|n| strpfx(&pre2, n)).cloned().collect();
7685            *amb = 0;
7686            return Some(ret); // c:4923
7687        } else {
7688            // c:4924
7689            // c:4925-4940 — check if remaining names all share first-name's head.
7690            let mut it = names.iter();
7691            let Some(first) = it.next() else {
7692                *amb = 0; // c:4926
7693                return None;
7694            };
7695            // c:4930 — strip after first '/' in first name.
7696            let p_head = match first.find('/') {
7697                Some(i) => format!("{}/", &first[..i]),
7698                None => format!("{}/", first),
7699            };
7700            for n in it {
7701                // c:4935
7702                if !strpfx(&p_head, n) {
7703                    *amb = 1; // c:4937
7704                    return None;
7705                }
7706            }
7707            // All match — fall through to return None (matches C).
7708        }
7709    } else {
7710        // c:4942
7711        // c:4943 — empty list: amb cleared.
7712        let mut it = names.iter();
7713        let Some(first) = it.next() else {
7714            *amb = 0;
7715            return None;
7716        };
7717        for n in it {
7718            // c:4946
7719            if first != n {
7720                // c:4947
7721                *amb = 1;
7722                return None;
7723            }
7724        }
7725    }
7726    None // c:4952
7727}
7728
7729/// Direct port of `static int bin_compfiles(char *nam, char **args,
7730///                                            UNUSED(Options ops),
7731///                                            UNUSED(int func))` from
7732/// `Src/Zle/computil.c:4970-5070`. Subcommand dispatch for
7733/// `compfiles -p/-P/-i/-r`. `-i` runs cf_ignore on a param-named
7734/// array; `-r` runs cf_remove_other; `-p`/`-P` thread through cf_pats.
7735pub fn bin_compfiles(
7736    nam: &str,
7737    args: &[String], // c:4970
7738    _ops: &options,
7739    _func: i32,
7740) -> i32 {
7741    if INCOMPFUNC.load(Ordering::Relaxed) != 1 {
7742        // c:4972
7743        zwarnnam(nam, "can only be called from completion function");
7744        return 1;
7745    }
7746    if args.is_empty() || !args[0].starts_with('-') {
7747        // c:4976
7748        let bad = args.first().map(|s| s.as_str()).unwrap_or("");
7749        zwarnnam(nam, &format!("missing option: {}", bad));
7750        return 1;
7751    }
7752    let a0 = args[0].as_bytes();
7753    if a0.len() < 2 {
7754        zwarnnam(nam, &format!("missing option: {}", args[0]));
7755        return 1;
7756    }
7757    let sub = a0[1];
7758
7759    // Helper: read a named array via paramtab.
7760    let get_arr = |name: &str| -> Option<Vec<String>> {
7761        paramtab()
7762            .read()
7763            .ok()
7764            .and_then(|tab| tab.get(name).and_then(|pm| pm.u_arr.clone()))
7765    };
7766
7767    match sub {
7768        b'p' | b'P' => {
7769            // c:4981
7770            // c:4983 — accept `-p` or `-p--` (the `--` toggles noopt).
7771            let noopt = a0.len() > 2;
7772            if noopt && (a0.len() != 4 || a0[2] != b'-' || a0[3] != b'-') {
7773                zwarnnam(nam, &format!("invalid option: {}", args[0]));
7774                return 1;
7775            }
7776            let required = if sub == b'p' { 8 } else { 7 };
7777            if args.len() <= required {
7778                // c:4990
7779                zwarnnam(nam, "too few arguments");
7780                return 1;
7781            }
7782            // c:4996 — getaparam(args[1]).
7783            let Some(src) = get_arr(&args[1]) else {
7784                zwarnnam(nam, &format!("unknown parameter: {}", args[1]));
7785                return 0;
7786            };
7787            // c:5001 — quotestring each entry with QT_BACKSLASH_PATTERN.
7788            let l: Vec<String> = src
7789                .iter()
7790                .map(|s| quotestring(s, QT_BACKSLASH_PATTERN))
7791                .collect();
7792            // c:5003 — cf_pats dispatch.
7793            let result = cf_pats(
7794                if sub == b'P' { 1 } else { 0 },
7795                if noopt { 1 } else { 0 },
7796                &l,
7797                &get_arr(&args[2]).unwrap_or_default(),
7798                &args[3],
7799                &args[4],
7800                &args[5],
7801                &get_arr(&args[6]).unwrap_or_default(),
7802                &args[7..],
7803            );
7804            setaparam(&args[1], result);
7805            0
7806        }
7807        b'i' => {
7808            // c:5010
7809            if a0.len() > 2 {
7810                // c:5011
7811                zwarnnam(nam, &format!("invalid option: {}", args[0]));
7812                return 1;
7813            }
7814            if args.len() < 5 {
7815                // c:5018
7816                zwarnnam(nam, "too few arguments");
7817                return 1;
7818            }
7819            if args.len() > 5 {
7820                // c:5022
7821                zwarnnam(nam, "too many arguments");
7822                return 1;
7823            }
7824            let mut l: Vec<String> = get_arr(&args[2]).unwrap_or_default();
7825            let Some(tmp) = get_arr(&args[1]) else {
7826                // c:5032
7827                zwarnnam(nam, &format!("unknown parameter: {}", args[1]));
7828                return 0;
7829            };
7830            cf_ignore(&tmp, &mut l, &args[3], &args[4]); // c:5037
7831            setaparam(&args[2], l); // c:5039
7832            0
7833        }
7834        b'r' => {
7835            // c:5042
7836            if args.len() < 3 {
7837                // c:5048
7838                zwarnnam(nam, "too few arguments");
7839                return 1;
7840            }
7841            if args.len() > 3 {
7842                // c:5052
7843                zwarnnam(nam, "too many arguments");
7844                return 1;
7845            }
7846            let Some(tmp) = get_arr(&args[1]) else {
7847                // c:5057
7848                zwarnnam(nam, &format!("unknown parameter: {}", args[1]));
7849                return 0;
7850            };
7851            let mut ret = 0i32;
7852            // c:5062 — cf_remove_other.
7853            if let Some(l) = cf_remove_other(&tmp, &args[2], &mut ret) {
7854                setaparam(&args[1], l);
7855            }
7856            ret // c:5065
7857        }
7858        _ => {
7859            zwarnnam(nam, &format!("invalid option: {}", args[0]));
7860            1
7861        }
7862    }
7863}
7864
7865/// Direct port of `static int bin_compgroups(char *nam, char **args,
7866///                                              UNUSED(Options ops),
7867///                                              UNUSED(int func))` from
7868/// `Src/Zle/computil.c:5073-5100`. For each group name in args, opens
7869/// six successive completion groups with the same name but different
7870/// sort/uniq flags (NOSORT+UNIQCON, UNIQALL, NOSORT+UNIQCON, UNIQALL,
7871/// NOSORT, 0). Each begcmgroup is bracketed by endcmgroup. This is
7872/// how _path_files etc. register their match groups before adding
7873/// candidates via compadd.
7874pub fn bin_compgroups(
7875    nam: &str,
7876    args: &[String], // c:5073
7877    _ops: &options,
7878    _func: i32,
7879) -> i32 {
7880    if INCOMPFUNC.load(Ordering::Relaxed) != 1 {
7881        // c:5078
7882        zwarnnam(nam, "can only be called from completion function");
7883        return 1;
7884    }
7885    // c:5083 — for each group name, register 6 group variants.
7886    for n in args {
7887        // c:5083
7888        endcmgroup(None); // c:5084
7889        begcmgroup(Some(n), CGF_NOSORT | CGF_UNIQCON); // c:5085
7890        endcmgroup(None);
7891        begcmgroup(Some(n), CGF_UNIQALL); // c:5087
7892        endcmgroup(None);
7893        begcmgroup(Some(n), CGF_NOSORT | CGF_UNIQCON); // c:5089
7894        endcmgroup(None);
7895        begcmgroup(Some(n), CGF_UNIQALL); // c:5091
7896        endcmgroup(None);
7897        begcmgroup(Some(n), CGF_NOSORT); // c:5093
7898        endcmgroup(None);
7899        begcmgroup(Some(n), 0); // c:5095
7900    }
7901    0 // c:5099
7902}
7903
7904/// Direct port of `int setup_(UNUSED(Module m))` from
7905/// `Src/Zle/computil.c:5124-5134`. Zeroes the three module caches
7906/// and resets `lasttaglevel`. Called on module load.
7907pub fn setup_() -> i32 {
7908    // c:5124
7909    // c:5126 — `memset(cadef_cache, 0, sizeof(cadef_cache))`.
7910    if let Ok(mut cache) = cadef_cache.lock() {
7911        for slot in cache.iter_mut() {
7912            freecadef(slot.take());
7913        }
7914    }
7915    // c:5127 — `memset(cvdef_cache, 0, sizeof(cvdef_cache))`.
7916    if let Ok(mut cache) = cvdef_cache.lock() {
7917        for slot in cache.iter_mut() {
7918            freecvdef(slot.take());
7919        }
7920    }
7921    // c:5129 — `memset(comptags, 0, sizeof(comptags))`.
7922    if let Ok(mut tab) = comptags.lock() {
7923        for slot in tab.iter_mut() {
7924            freectags(slot.take());
7925        }
7926    }
7927    // c:5131 — `lasttaglevel = 0`.
7928    lasttaglevel.store(0, Ordering::Relaxed);
7929    0 // c:5133
7930}
7931
7932/// Port of `features_(UNUSED(Module m), UNUSED(char ***features))` from Src/Zle/computil.c:5138.
7933/// WARNING: param names don't match C — Rust=() vs C=(m, features)
7934pub fn features_() -> i32 {
7935    // c:5138
7936    // C body c:5140-5141 — `*features = featuresarray(...); return 0`.
7937    //                      Features array exposed elsewhere; return 0.
7938    0
7939}
7940
7941/// Port of `enables_(UNUSED(Module m), UNUSED(int **enables))` from Src/Zle/computil.c:5146.
7942/// WARNING: param names don't match C — Rust=() vs C=(m, enables)
7943pub fn enables_() -> i32 {
7944    // c:5146
7945    // C body c:5148 — `return handlefeatures(m, &module_features, enables)`.
7946    //                  Static-link no-op.
7947    0
7948}
7949
7950/// Port of `boot_(UNUSED(Module m))` from Src/Zle/computil.c:5153.
7951/// WARNING: param names don't match C — Rust=() vs C=(m)
7952pub fn boot_() -> i32 {
7953    // c:5153
7954    // C body c:5155-5156 — `return 0`. Faithful empty body.
7955    0
7956}
7957
7958/// Port of `cleanup_(UNUSED(Module m))` from Src/Zle/computil.c:5160.
7959/// WARNING: param names don't match C — Rust=() vs C=(m)
7960pub fn cleanup_() -> i32 {
7961    // c:5160
7962    // C body c:5162-5163 — `return setfeatureenables(m, &module_features, NULL)`.
7963    //                      Static-link path: no per-feature toggle, return 0.
7964    0
7965}
7966
7967/// Direct port of `int finish_(UNUSED(Module m))` from
7968/// `Src/Zle/computil.c:5167-5180`. Frees every cached cadef/cvdef
7969/// and the comptags table on module unload.
7970pub fn finish_() -> i32 {
7971    // c:5167
7972    // c:5171 — `for (i = 0; i < MAX_CACACHE; i++) freecadef(cadef_cache[i])`.
7973    if let Ok(mut cache) = cadef_cache.lock() {
7974        for slot in cache.iter_mut() {
7975            freecadef(slot.take());
7976        }
7977    }
7978    // c:5173 — `for (i = 0; i < MAX_CVCACHE; i++) freecvdef(cvdef_cache[i])`.
7979    if let Ok(mut cache) = cvdef_cache.lock() {
7980        for slot in cache.iter_mut() {
7981            freecvdef(slot.take());
7982        }
7983    }
7984    // c:5176 — `for (i = 0; i < MAX_TAGS; i++) freectags(comptags[i])`.
7985    if let Ok(mut tab) = comptags.lock() {
7986        for slot in tab.iter_mut() {
7987            freectags(slot.take());
7988        }
7989    }
7990    0 // c:5179
7991}
7992
7993/// Port of `static struct cdstate cd_state` from `Src/Zle/computil.c:93`.
7994/// File-static instance the `_describe` engine reads/writes.
7995pub static cd_state: std::sync::Mutex<cdstate> = // c:93
7996    std::sync::Mutex::new(cdstate {
7997        showd: 0,
7998        sep: None,
7999        slen: 0,
8000        swidth: 0,
8001        maxmlen: 0,
8002        sets: None,
8003        pre: 0,
8004        premaxw: 0,
8005        suf: 0,
8006        maxg: 0,
8007        maxglen: 0,
8008        groups: 0,
8009        descs: 0,
8010        gprew: 0,
8011        runs: None,
8012    });
8013
8014/// CM_SPACE — inter-match spacing from `Src/Zle/zle_tricky.c:1700` /
8015/// `Src/Zle/computil.c` (referenced as the literal `2`). Used to
8016/// reserve a 2-char gap between adjacent matches when computing
8017/// column widths.
8018pub const CM_SPACE: i32 = 2; // c:zle_tricky.c
8019
8020/// Port of `static struct castate ca_laststate` from
8021/// `Src/Zle/computil.c:1955`. Most recently parsed cmdline state.
8022pub static ca_laststate: std::sync::Mutex<castate> = // c:1955
8023    std::sync::Mutex::new(castate {
8024        snext: None,
8025        d: None,
8026        nopts: 0,
8027        def: None,
8028        ddef: None,
8029        curopt: None,
8030        dopt: None,
8031        opt: 0,
8032        arg: 0,
8033        argbeg: 0,
8034        optbeg: 0,
8035        nargbeg: 0,
8036        restbeg: 0,
8037        curpos: 0,
8038        argend: 0,
8039        inopt: 0,
8040        inarg: 0,
8041        nth: 0,
8042        singles: 0,
8043        oopt: 0,
8044        actopts: 0,
8045        args: None,
8046        oargs: None,
8047    });
8048
8049/// Port of `static int ca_alloced` from `Src/Zle/computil.c:1960`.
8050pub static ca_alloced: std::sync::atomic::AtomicI32 = // c:1960
8051    std::sync::atomic::AtomicI32::new(0);
8052
8053/// Port of `static struct cvstate cv_laststate` from
8054/// `Src/Zle/computil.c:3229`.
8055pub static cv_laststate: std::sync::Mutex<cvstate> = // c:3229
8056    std::sync::Mutex::new(cvstate {
8057        d: None,
8058        def: None,
8059        val: None,
8060        vals: None,
8061    });
8062
8063/// Port of `static int cv_alloced` from `Src/Zle/computil.c:3230`.
8064pub static cv_alloced: std::sync::atomic::AtomicI32 = // c:3230
8065    std::sync::atomic::AtomicI32::new(0);
8066
8067/// Mirror of `memcpy` from a locked `ca_laststate` so we can walk the
8068/// snext chain without holding the mutex. Rust-only artifact.
8069#[allow(dead_code)]
8070fn clone_castate_full(s: &castate) -> castate {
8071    castate {
8072        snext: s.snext.clone(),
8073        d: s.d.clone(),
8074        nopts: s.nopts,
8075        def: s.def.clone(),
8076        ddef: s.ddef.clone(),
8077        curopt: s.curopt.clone(),
8078        dopt: s.dopt.clone(),
8079        opt: s.opt,
8080        arg: s.arg,
8081        argbeg: s.argbeg,
8082        optbeg: s.optbeg,
8083        nargbeg: s.nargbeg,
8084        restbeg: s.restbeg,
8085        curpos: s.curpos,
8086        argend: s.argend,
8087        inopt: s.inopt,
8088        inarg: s.inarg,
8089        nth: s.nth,
8090        singles: s.singles,
8091        oopt: s.oopt,
8092        actopts: s.actopts,
8093        args: s.args.clone(),
8094        oargs: s.oargs.clone(),
8095    }
8096}
8097
8098/// Mirror of the C `memcpy(&ca_laststate, &state, sizeof(state))` pattern
8099/// — we can't move `state` (the caller continues using it), and Rust's
8100/// own `Clone` would over-clone owned chains.  Instead snapshot the
8101/// salient fields plus a fresh shallow clone of `d`. Local to
8102/// `ca_parse_line`; matches no C function.
8103#[allow(dead_code)]
8104fn clone_castate(s: &castate, d: &cadef) -> castate {
8105    castate {
8106        snext: None,
8107        d: Some(Box::new(clone_cadef_shallow(d))),
8108        nopts: s.nopts,
8109        def: s.def.clone(),
8110        ddef: s.ddef.clone(),
8111        curopt: s.curopt.clone(),
8112        dopt: s.dopt.clone(),
8113        opt: s.opt,
8114        arg: s.arg,
8115        argbeg: s.argbeg,
8116        optbeg: s.optbeg,
8117        nargbeg: s.nargbeg,
8118        restbeg: s.restbeg,
8119        curpos: s.curpos,
8120        argend: s.argend,
8121        inopt: s.inopt,
8122        inarg: s.inarg,
8123        nth: s.nth,
8124        singles: s.singles,
8125        oopt: s.oopt,
8126        actopts: s.actopts,
8127        args: s.args.clone(),
8128        oargs: s.oargs.clone(),
8129    }
8130}
8131
8132#[allow(dead_code)]
8133fn clone_cadef_shallow(d: &cadef) -> cadef {
8134    cadef {
8135        next: None,
8136        snext: None,
8137        opts: d.opts.clone(),
8138        nopts: d.nopts,
8139        ndopts: d.ndopts,
8140        nodopts: d.nodopts,
8141        args: d.args.clone(),
8142        rest: d.rest.clone(),
8143        defs: d.defs.clone(),
8144        ndefs: d.ndefs,
8145        lastt: d.lastt,
8146        single: d.single.clone(),
8147        r#match: d.r#match.clone(),
8148        argsactive: d.argsactive,
8149        set: d.set.clone(),
8150        flags: d.flags,
8151        nonarg: d.nonarg.clone(),
8152    }
8153}
8154
8155#[cfg(test)]
8156mod tests {
8157    use super::*;
8158
8159    // Tests for cd_get / cd_init / cd_sort / cd_prep removed — those
8160    // tests exercised the deleted CompDescItem/CompDescSet Rust-only
8161    // wrappers. The C-faithful entries (cd_get takes char**params and
8162    // returns int) get exercised through the full `_describe` widget
8163    // path under integration tests; per-fn unit tests would just
8164    // lock in the deleted Rust-side shape.
8165
8166    // test_parse_caarg / test_parse_cadef removed — they exercised
8167    // the deleted CompArgDef/CompOptDef Rust-only types via fake-
8168    // signature wrappers. Real ports land alongside the cadef chain.
8169
8170    #[test]
8171    fn test_rembslashcolon() {
8172        let _g = crate::test_util::global_state_lock();
8173        let _g = zle_test_setup();
8174        // c:1054 — `\:` two-byte sequence drops the backslash.
8175        assert_eq!(rembslashcolon("a\\:b\\:c"), "a:b:c");
8176    }
8177
8178    #[test]
8179    fn test_rembslashcolon_lone_backslash_kept() {
8180        let _g = crate::test_util::global_state_lock();
8181        let _g = zle_test_setup();
8182        // c:1054 — `\X` (X != ':') keeps the backslash.
8183        assert_eq!(rembslashcolon("a\\nb"), "a\\nb");
8184    }
8185
8186    #[test]
8187    fn test_rembslashcolon_trailing_backslash() {
8188        let _g = crate::test_util::global_state_lock();
8189        let _g = zle_test_setup();
8190        // c:1054 — trailing `\` with no follow-up keeps the `\`.
8191        assert_eq!(rembslashcolon("a\\"), "a\\");
8192    }
8193
8194    #[test]
8195    fn test_rembslashcolon_unescaped_colon_passes_through() {
8196        let _g = crate::test_util::global_state_lock();
8197        let _g = zle_test_setup();
8198        // c:1054 — bare `:` (no preceding `\`) is kept.
8199        assert_eq!(rembslashcolon("a:b"), "a:b");
8200    }
8201
8202    #[test]
8203    fn test_bslashcolon() {
8204        let _g = crate::test_util::global_state_lock();
8205        let _g = zle_test_setup();
8206        // c:1073 — every `:` gets `\` prepended.
8207        assert_eq!(bslashcolon("a:b:c"), "a\\:b\\:c");
8208    }
8209
8210    #[test]
8211    fn test_bslashcolon_no_colons() {
8212        let _g = crate::test_util::global_state_lock();
8213        let _g = zle_test_setup();
8214        // c:1072 — non-colon bytes pass through unchanged.
8215        assert_eq!(bslashcolon("hello"), "hello");
8216    }
8217
8218    #[test]
8219    fn test_bslashcolon_already_escaped_doubled() {
8220        let _g = crate::test_util::global_state_lock();
8221        let _g = zle_test_setup();
8222        // c:1073-1074 — C doesn't track previous backslash, so an
8223        // already-escaped `\:` becomes `\\:` (the `\` passes
8224        // through, then the `:` gets a fresh `\` prepended).
8225        assert_eq!(bslashcolon("a\\:b"), "a\\\\:b");
8226    }
8227
8228    #[test]
8229    fn test_single_index_dash_prefix() {
8230        let _g = crate::test_util::global_state_lock();
8231        let _g = zle_test_setup();
8232        // c:1094 — `pre == '-'` → offset = -0x21.
8233        // For opt='a' (0x61): 0x61 + -0x21 = 0x40 = 64.
8234        assert_eq!(single_index(b'-', b'a'), 64);
8235        // For opt='A' (0x41): 0x41 + -0x21 = 0x20 = 32.
8236        assert_eq!(single_index(b'-', b'A'), 32);
8237        // For opt='!' (0x21): 0x21 + -0x21 = 0.
8238        assert_eq!(single_index(b'-', b'!'), 0);
8239        // For opt='~' (0x7e): 0x7e + -0x21 = 0x5d = 93.
8240        assert_eq!(single_index(b'-', b'~'), 93);
8241    }
8242
8243    #[test]
8244    fn test_single_index_plus_prefix() {
8245        let _g = crate::test_util::global_state_lock();
8246        let _g = zle_test_setup();
8247        // c:1094 — `pre == '+'` → offset = 94 - 0x21 = 61.
8248        // For opt='a' (0x61): 0x61 + 61 = 158.
8249        assert_eq!(single_index(b'+', b'a'), 158);
8250        // For opt='!' (0x21): 0x21 + 61 = 94.
8251        assert_eq!(single_index(b'+', b'!'), 94);
8252        // For opt='~' (0x7e): 0x7e + 61 = 187.
8253        assert_eq!(single_index(b'+', b'~'), 187);
8254    }
8255
8256    #[test]
8257    fn test_single_index_out_of_range() {
8258        let _g = crate::test_util::global_state_lock();
8259        let _g = zle_test_setup();
8260        // c:1091-1092 — opt <= 0x20 OR opt > 0x7e returns -1.
8261        assert_eq!(single_index(b'-', 0x20), -1); // space (0x20) excluded
8262        assert_eq!(single_index(b'-', 0x00), -1); // NUL
8263        assert_eq!(single_index(b'-', 0x7f), -1); // DEL (0x7f) excluded
8264        assert_eq!(single_index(b'+', 0xff), -1); // outside ASCII
8265    }
8266
8267    // test_cd_group removed — used the deleted CompDescItem; the
8268    // function `cd_group` itself wasn't a real C export and was
8269    // also removed alongside the fake structs.
8270
8271    #[test]
8272    fn caarg_default_zero_initialized() {
8273        let _g = crate::test_util::global_state_lock();
8274        let _g = zle_test_setup();
8275        // c:949-962 — fresh caarg: every field zero / None.
8276        let a = caarg::default();
8277        assert!(a.next.is_none());
8278        assert!(a.descr.is_none());
8279        assert!(a.action.is_none());
8280        assert_eq!(a.r#type, 0);
8281        assert_eq!(a.num, 0);
8282        assert_eq!(a.active, 0);
8283    }
8284
8285    #[test]
8286    fn caopt_default_zero_initialized() {
8287        let _g = crate::test_util::global_state_lock();
8288        let _g = zle_test_setup();
8289        // c:928-939 — fresh caopt: zero / None across all fields.
8290        let o = caopt::default();
8291        assert!(o.next.is_none());
8292        assert!(o.name.is_none());
8293        assert!(o.args.is_none());
8294        assert_eq!(o.r#type, 0);
8295        assert_eq!(o.num, 0);
8296        assert_eq!(o.not, 0);
8297    }
8298
8299    #[test]
8300    fn cadef_default_zero_initialized() {
8301        let _g = crate::test_util::global_state_lock();
8302        let _g = zle_test_setup();
8303        // c:905-922 — fresh cadef: zero / None across all fields.
8304        let d = cadef::default();
8305        assert!(d.next.is_none());
8306        assert!(d.opts.is_none());
8307        assert!(d.args.is_none());
8308        assert_eq!(d.nopts, 0);
8309        assert_eq!(d.flags, 0);
8310    }
8311
8312    #[test]
8313    fn freecaargs_walks_chain() {
8314        let _g = crate::test_util::global_state_lock();
8315        let _g = zle_test_setup();
8316        // c:996-1010 — freecaargs walks `next` chain freeing each
8317        // entry. After call, the chain owner observes no remaining
8318        // refs (Drop handles deallocation).
8319        let mut head = caarg {
8320            descr: Some("a".into()),
8321            ..Default::default()
8322        };
8323        let mid = caarg {
8324            descr: Some("b".into()),
8325            ..Default::default()
8326        };
8327        let tail = caarg {
8328            descr: Some("c".into()),
8329            ..Default::default()
8330        };
8331        let mut mid_box = Box::new(mid);
8332        mid_box.next = Some(Box::new(tail));
8333        head.next = Some(mid_box);
8334        freecaargs(Some(Box::new(head)));
8335        // No panic, no leak — Box drop chains the rest.
8336    }
8337
8338    #[test]
8339    fn cao_caa_constants_match_c() {
8340        let _g = crate::test_util::global_state_lock();
8341        let _g = zle_test_setup();
8342        // c:941-945 and c:964-968 — sequential 1..=5.
8343        assert_eq!(CAO_NEXT, 1);
8344        assert_eq!(CAO_DIRECT, 2);
8345        assert_eq!(CAO_ODIRECT, 3);
8346        assert_eq!(CAO_EQUAL, 4);
8347        assert_eq!(CAO_OEQUAL, 5);
8348        assert_eq!(CAA_NORMAL, 1);
8349        assert_eq!(CAA_OPT, 2);
8350        assert_eq!(CAA_REST, 3);
8351        assert_eq!(CAA_RARGS, 4);
8352        assert_eq!(CAA_RREST, 5);
8353    }
8354
8355    #[test]
8356    fn cdf_max_cacache_constants_match_c() {
8357        let _g = crate::test_util::global_state_lock();
8358        let _g = zle_test_setup();
8359        // c:924 — CDF_SEP = 1; c:972 — MAX_CACACHE = 8.
8360        assert_eq!(CDF_SEP, 1);
8361        assert_eq!(MAX_CACACHE, 8);
8362    }
8363
8364    #[test]
8365    fn crt_constants_match_c() {
8366        let _g = crate::test_util::global_state_lock();
8367        let _g = zle_test_setup();
8368        // c:79-83 — sequential 0..=4.
8369        assert_eq!(CRT_SIMPLE, 0);
8370        assert_eq!(CRT_DESC, 1);
8371        assert_eq!(CRT_SPEC, 2);
8372        assert_eq!(CRT_DUMMY, 3);
8373        assert_eq!(CRT_EXPL, 4);
8374    }
8375
8376    #[test]
8377    fn cdstr_default_zero_initialized() {
8378        let _g = crate::test_util::global_state_lock();
8379        let _g = zle_test_setup();
8380        // c:58-70 — fresh cdstr: zero/None across all fields.
8381        let s = cdstr::default();
8382        assert!(s.next.is_none());
8383        assert!(s.str.is_none());
8384        assert!(s.desc.is_none());
8385        assert!(s.r#match.is_none());
8386        assert_eq!(s.len, 0);
8387        assert_eq!(s.width, 0);
8388        assert_eq!(s.kind, 0);
8389    }
8390
8391    #[test]
8392    fn cdrun_default_zero_initialized() {
8393        let _g = crate::test_util::global_state_lock();
8394        let _g = zle_test_setup();
8395        // c:72-77 — fresh cdrun: zero/None.
8396        let r = cdrun::default();
8397        assert!(r.next.is_none());
8398        assert!(r.strs.is_none());
8399        assert_eq!(r.r#type, 0);
8400        assert_eq!(r.count, 0);
8401    }
8402
8403    #[test]
8404    fn cdset_default_zero_initialized() {
8405        let _g = crate::test_util::global_state_lock();
8406        let _g = zle_test_setup();
8407        // c:85-91 — fresh cdset: zero/None.
8408        let s = cdset::default();
8409        assert!(s.next.is_none());
8410        assert!(s.opts.is_none());
8411        assert!(s.strs.is_none());
8412        assert_eq!(s.count, 0);
8413        assert_eq!(s.desc, 0);
8414    }
8415
8416    #[test]
8417    fn cdstate_default_zero_initialized() {
8418        let _g = crate::test_util::global_state_lock();
8419        let _g = zle_test_setup();
8420        // c:40-56 — fresh cdstate: zero/None.
8421        let st = cdstate::default();
8422        assert_eq!(st.showd, 0);
8423        assert!(st.sep.is_none());
8424        assert!(st.sets.is_none());
8425        assert!(st.runs.is_none());
8426    }
8427
8428    #[test]
8429    fn freecdsets_walks_chain() {
8430        let _g = crate::test_util::global_state_lock();
8431        let _g = zle_test_setup();
8432        // c:96-122 — freecdsets walks `next` chain freeing each set
8433        // and its strs sub-chain.
8434        let head_str = cdstr {
8435            str: Some("foo".into()),
8436            desc: Some("first".into()),
8437            ..Default::default()
8438        };
8439        let tail_str = cdstr {
8440            str: Some("bar".into()),
8441            ..Default::default()
8442        };
8443        let mut head_str_b = Box::new(head_str);
8444        head_str_b.next = Some(Box::new(tail_str));
8445        let set = cdset {
8446            strs: Some(head_str_b),
8447            count: 2,
8448            ..Default::default()
8449        };
8450        freecdsets(Some(Box::new(set)));
8451        // No panic / no leak — Box drop chains the rest.
8452    }
8453
8454    #[test]
8455    fn castate_default_zero_initialized() {
8456        let _g = crate::test_util::global_state_lock();
8457        let _g = zle_test_setup();
8458        // c:1928-1953 — fresh castate: zero/None.
8459        let s = castate::default();
8460        assert!(s.snext.is_none());
8461        assert!(s.d.is_none());
8462        assert!(s.def.is_none());
8463        assert!(s.args.is_none());
8464        assert_eq!(s.nopts, 0);
8465        assert_eq!(s.curpos, 0);
8466    }
8467
8468    #[test]
8469    fn cvdef_default_zero_initialized() {
8470        let _g = crate::test_util::global_state_lock();
8471        let _g = zle_test_setup();
8472        // c:2924-2935 — fresh cvdef: zero/None.
8473        let d = cvdef::default();
8474        assert!(d.descr.is_none());
8475        assert!(d.vals.is_none());
8476        assert_eq!(d.hassep, 0);
8477        assert_eq!(d.sep, 0);
8478        assert_eq!(d.argsep, 0);
8479    }
8480
8481    #[test]
8482    fn cvval_default_zero_initialized() {
8483        let _g = crate::test_util::global_state_lock();
8484        let _g = zle_test_setup();
8485        // c:2939-2947 — fresh cvval: zero/None.
8486        let v = cvval::default();
8487        assert!(v.next.is_none());
8488        assert!(v.name.is_none());
8489        assert!(v.arg.is_none());
8490        assert_eq!(v.r#type, 0);
8491        assert_eq!(v.active, 0);
8492    }
8493
8494    #[test]
8495    fn cvstate_default_zero_initialized() {
8496        let _g = crate::test_util::global_state_lock();
8497        let _g = zle_test_setup();
8498        // c:3222-3227 — fresh cvstate: None across all 4 fields.
8499        let s = cvstate::default();
8500        assert!(s.d.is_none());
8501        assert!(s.def.is_none());
8502        assert!(s.val.is_none());
8503        assert!(s.vals.is_none());
8504    }
8505
8506    #[test]
8507    fn ctags_default_zero_initialized() {
8508        let _g = crate::test_util::global_state_lock();
8509        let _g = zle_test_setup();
8510        // c:3737-3742 — fresh ctags: zero/None.
8511        let t = ctags::default();
8512        assert!(t.all.is_none());
8513        assert!(t.context.is_none());
8514        assert!(t.sets.is_none());
8515        assert_eq!(t.init, 0);
8516    }
8517
8518    #[test]
8519    fn ctset_default_zero_initialized() {
8520        let _g = crate::test_util::global_state_lock();
8521        let _g = zle_test_setup();
8522        // c:3746-3751 — fresh ctset: zero/None.
8523        let s = ctset::default();
8524        assert!(s.next.is_none());
8525        assert!(s.tags.is_none());
8526        assert!(s.tag.is_none());
8527        assert_eq!(s.ptr, 0);
8528    }
8529
8530    #[test]
8531    fn cvv_constants_match_c() {
8532        let _g = crate::test_util::global_state_lock();
8533        let _g = zle_test_setup();
8534        // c:2949-2951 — sequential 0..=2.
8535        assert_eq!(CVV_NOARG, 0);
8536        assert_eq!(CVV_ARG, 1);
8537        assert_eq!(CVV_OPT, 2);
8538    }
8539
8540    #[test]
8541    fn max_tags_cvcache_match_c() {
8542        let _g = crate::test_util::global_state_lock();
8543        let _g = zle_test_setup();
8544        // c:3755 — MAX_TAGS = 256; c:2955 — MAX_CVCACHE = 8.
8545        assert_eq!(MAX_TAGS, 256);
8546        assert_eq!(MAX_CVCACHE, 8);
8547    }
8548
8549    #[test]
8550    fn freectset_walks_chain() {
8551        let _g = crate::test_util::global_state_lock();
8552        let _g = zle_test_setup();
8553        // c:3762-3777 — freectset walks `next` chain freeing each
8554        // ctset's tags/tag fields.
8555        let mut head = ctset {
8556            tag: Some("foo".into()),
8557            ..Default::default()
8558        };
8559        let tail = ctset {
8560            tag: Some("bar".into()),
8561            ..Default::default()
8562        };
8563        head.next = Some(Box::new(tail));
8564        freectset(Some(Box::new(head)));
8565    }
8566
8567    #[test]
8568    fn freectags_drops_one_node() {
8569        let _g = crate::test_util::global_state_lock();
8570        let _g = zle_test_setup();
8571        // c:3779-3789 — freectags releases all/context/sets on one ctags.
8572        let t = ctags {
8573            all: Some(vec!["a".into(), "b".into()]),
8574            context: Some("ctx".into()),
8575            ..Default::default()
8576        };
8577        freectags(Some(Box::new(t)));
8578    }
8579
8580    #[test]
8581    fn freecvdef_walks_vals_chain() {
8582        let _g = crate::test_util::global_state_lock();
8583        let _g = zle_test_setup();
8584        // c:2960-2981 — freecvdef walks vals freeing each cvval.
8585        let v_tail = cvval {
8586            name: Some("opt2".into()),
8587            ..Default::default()
8588        };
8589        let mut v_head = cvval {
8590            name: Some("opt1".into()),
8591            ..Default::default()
8592        };
8593        v_head.next = Some(Box::new(v_tail));
8594        let d = cvdef {
8595            descr: Some("test".into()),
8596            vals: Some(Box::new(v_head)),
8597            ..Default::default()
8598        };
8599        freecvdef(Some(Box::new(d)));
8600    }
8601
8602    /// c:1196 — `_arguments '-foo[only foo]' '*:file:_files'`. Verify
8603    /// that the option-name xor list contains the spec name, that
8604    /// nopts/ndopts reflect the option type (CAO_NEXT here), and that
8605    /// the rest arg lands on `rest` with type CAA_REST.
8606    #[test]
8607    fn parse_cadef_simple_opt_and_rest() {
8608        let _g = crate::test_util::global_state_lock();
8609        let _g = zle_test_setup();
8610        inittyptab();
8611        let args = vec![
8612            String::from(""), // adpre/adsuf split (no %d)
8613            String::from("-foo[only foo]"),
8614            String::from("*:file:_files"),
8615        ];
8616        let def = parse_cadef("_arguments", &args).expect("cadef built");
8617        let opt = def.opts.as_deref().expect("opt linked");
8618        assert_eq!(opt.name.as_deref(), Some("-foo"));
8619        assert_eq!(opt.descr.as_deref(), Some("only foo"));
8620        assert_eq!(opt.r#type, CAO_NEXT);
8621        // c:1462-1468 — non-multi option appends its own name to xor.
8622        let xor = opt.xor.as_ref().expect("xor list");
8623        assert!(
8624            xor.iter().any(|s| s == "-foo"),
8625            "xor must include -foo: {:?}",
8626            xor
8627        );
8628
8629        let rest = def.rest.as_deref().expect("rest linked");
8630        assert_eq!(rest.r#type, CAA_REST);
8631        assert_eq!(rest.descr.as_deref(), Some("file"));
8632        assert_eq!(rest.action.as_deref(), Some("_files"));
8633    }
8634
8635    /// c:1617-1661 — numbered positional argument `1:cmd:_commands` lands
8636    /// on `def.args` with the right slot (num=0 because anum is `1`
8637    /// then `arg->num = anum - 1`).
8638    #[test]
8639    fn parse_cadef_numbered_positional_arg() {
8640        let _g = crate::test_util::global_state_lock();
8641        let _g = zle_test_setup();
8642        inittyptab();
8643        let args = vec![String::from(""), String::from("1:cmd:_commands")];
8644        let def = parse_cadef("_arguments", &args).expect("cadef built");
8645        let pos = def.args.as_deref().expect("positional arg linked");
8646        assert_eq!(pos.num, 1);
8647        assert_eq!(pos.r#type, CAA_NORMAL);
8648        assert_eq!(pos.descr.as_deref(), Some("cmd"));
8649        assert_eq!(pos.action.as_deref(), Some("_commands"));
8650        assert_eq!(pos.direct, 1, "explicit numbering sets direct=1");
8651    }
8652
8653    /// c:1647-1656 — duplicate numbered argument must error out and
8654    /// return None (the cadef cache miss path picks this up).
8655    #[test]
8656    fn parse_cadef_doubled_arg_errors() {
8657        let _g = crate::test_util::global_state_lock();
8658        let _g = zle_test_setup();
8659        inittyptab();
8660        let args = vec![
8661            String::from(""),
8662            String::from("1:a:_a"),
8663            String::from("1:b:_b"),
8664        ];
8665        let def = parse_cadef("_arguments", &args);
8666        assert!(def.is_none(), "duplicate arg num=1 must reject");
8667    }
8668
8669    /// c:1335-1370 — `(opt-x opt-y)-foo[descr]` builds a 3-element
8670    /// xor list `[opt-x, opt-y, -foo]` (the option's own name gets
8671    /// added at the end via c:1462-1468).
8672    #[test]
8673    fn parse_cadef_xor_list_populated() {
8674        let _g = crate::test_util::global_state_lock();
8675        let _g = zle_test_setup();
8676        inittyptab();
8677        let args = vec![String::from(""), String::from("(opt-x opt-y)-foo[descr]")];
8678        let def = parse_cadef("_arguments", &args).expect("cadef built");
8679        let opt = def.opts.as_deref().expect("opt linked");
8680        let xor = opt.xor.as_ref().expect("xor list");
8681        assert_eq!(xor.len(), 3, "xor: {:?}", xor);
8682        assert_eq!(xor[0], "opt-x");
8683        assert_eq!(xor[1], "opt-y");
8684        assert_eq!(xor[2], "-foo");
8685    }
8686
8687    /// c:3796 — `settags(0, ["ctx", "tag1", "tag2"])` populates
8688    /// `comptags[0]` with context="ctx", all=["tag1","tag2"], init=1.
8689    #[test]
8690    fn settags_populates_slot() {
8691        let _g = crate::test_util::global_state_lock();
8692        let _g = zle_test_setup();
8693        // Clear slot to make test order-independent.
8694        if let Ok(mut tab) = comptags.lock() {
8695            tab[0] = None;
8696        }
8697        settags(
8698            0,
8699            &["ctx".to_string(), "tag-a".to_string(), "tag-b".to_string()],
8700        );
8701        let tab = comptags.lock().unwrap();
8702        let slot = tab[0].as_deref().expect("comptags[0] populated");
8703        assert_eq!(slot.context.as_deref(), Some("ctx"));
8704        assert_eq!(slot.init, 1);
8705        let all = slot.all.as_ref().expect("all populated");
8706        assert_eq!(all.len(), 2);
8707        assert_eq!(all[0], "tag-a");
8708        assert_eq!(all[1], "tag-b");
8709        assert!(slot.sets.is_none());
8710    }
8711
8712    /// c:1712-1718 — exact name match returns the opt with `*end`
8713    /// pointing past the option name.
8714    #[test]
8715    fn ca_get_opt_exact_match() {
8716        let _g = crate::test_util::global_state_lock();
8717        let _g = zle_test_setup();
8718        inittyptab();
8719        let args = vec![String::from(""), String::from("-foo[d]")];
8720        let mut def = *parse_cadef("_arguments", &args).expect("cadef built");
8721        // Mark the only opt active so ca_get_opt accepts it.
8722        let mut cur = def.opts.as_deref_mut();
8723        while let Some(o) = cur {
8724            o.active = 1;
8725            cur = o.next.as_deref_mut();
8726        }
8727        let mut end: usize = 0;
8728        let hit = ca_get_opt(&def, "-foo", 1, &mut end).expect("hit");
8729        assert_eq!(hit.name.as_deref(), Some("-foo"));
8730        assert_eq!(end, 4);
8731    }
8732
8733    /// c:1809-1822 — `argsactive=0` short-circuits to None even when
8734    /// args are linked. Guards against the easy off-by-one error of
8735    /// returning the first matching arg unconditionally.
8736    #[test]
8737    fn ca_get_arg_argsactive_zero_returns_none() {
8738        let _g = crate::test_util::global_state_lock();
8739        let _g = zle_test_setup();
8740        inittyptab();
8741        let args = vec![String::from(""), String::from("1:c:_c")];
8742        let def = *parse_cadef("_arguments", &args).expect("cadef built");
8743        // argsactive defaults to 0 — must short-circuit.
8744        assert!(ca_get_arg(&def, 1).is_none());
8745    }
8746
8747    /// c:1817 — when `argsactive=1` and the positional arg is active,
8748    /// `n` inside `[min, num]` returns the matching node.
8749    #[test]
8750    fn ca_get_arg_in_range_active() {
8751        let _g = crate::test_util::global_state_lock();
8752        let _g = zle_test_setup();
8753        inittyptab();
8754        let args = vec![String::from(""), String::from("1:c:_c")];
8755        let mut def = *parse_cadef("_arguments", &args).expect("cadef built");
8756        def.argsactive = 1;
8757        if let Some(a) = def.args.as_deref_mut() {
8758            a.active = 1;
8759        }
8760        let hit = ca_get_arg(&def, 1).expect("hit");
8761        assert_eq!(hit.num, 1);
8762        assert_eq!(hit.descr.as_deref(), Some("c"));
8763    }
8764
8765    /// c:2999-3027 — `-s , descr opt1[a]:val1: opt2[b]` builds a cvdef
8766    /// with sep=',', descr="descr", vals chain of two cvvals.
8767    #[test]
8768    fn parse_cvdef_sep_and_two_vals() {
8769        let _g = crate::test_util::global_state_lock();
8770        let _g = zle_test_setup();
8771        inittyptab();
8772        let args = vec![
8773            String::from("-s"),
8774            String::from(","),
8775            String::from("descr"),
8776            String::from("opt1[a]:val1:"),
8777            String::from("opt2[b]"),
8778        ];
8779        let def = parse_cvdef("_values", &args).expect("cvdef built");
8780        assert_eq!(def.hassep, 1);
8781        assert_eq!(def.sep, b',' as i32);
8782        assert_eq!(def.descr.as_deref(), Some("descr"));
8783        let v1 = def.vals.as_deref().expect("val1");
8784        assert_eq!(v1.name.as_deref(), Some("opt1"));
8785        assert_eq!(v1.descr.as_deref(), Some("a"));
8786        assert_eq!(v1.r#type, CVV_ARG);
8787        let v2 = v1.next.as_deref().expect("val2");
8788        assert_eq!(v2.name.as_deref(), Some("opt2"));
8789        assert_eq!(v2.descr.as_deref(), Some("b"));
8790        assert_eq!(v2.r#type, CVV_NOARG);
8791    }
8792
8793    /// c:1786-1801 — ca_foreign_opt walks snext skipping curset.
8794    /// When `curset == all` (head pointer matches), only the OTHER
8795    /// sets in the snext chain are scanned.
8796    #[test]
8797    fn ca_foreign_opt_finds_in_other_set() {
8798        let _g = crate::test_util::global_state_lock();
8799        let _g = zle_test_setup();
8800        inittyptab();
8801        let other = cadef {
8802            opts: Some(Box::new(caopt {
8803                name: Some("-bar".into()),
8804                active: 1,
8805                ..Default::default()
8806            })),
8807            ..Default::default()
8808        };
8809        let all = cadef {
8810            opts: Some(Box::new(caopt {
8811                name: Some("-foo".into()),
8812                active: 1,
8813                ..Default::default()
8814            })),
8815            snext: Some(Box::new(other)),
8816            ..Default::default()
8817        };
8818        // curset = &all (head). `-bar` lives in snext set — found.
8819        assert_eq!(ca_foreign_opt(&all, &all, "-bar"), 1);
8820        // `-foo` lives ONLY in the head (which gets skipped) — not found.
8821        assert_eq!(ca_foreign_opt(&all, &all, "-foo"), 0);
8822        // `-missing` not anywhere — not found.
8823        assert_eq!(ca_foreign_opt(&all, &all, "-missing"), 0);
8824    }
8825
8826    /// c:1834 — guard: with neither xor nor opts AND no compcurrent
8827    /// position, the function returns without mutating any active flag.
8828    #[test]
8829    fn ca_inactive_guard_noop_keeps_active() {
8830        let _g = crate::test_util::global_state_lock();
8831        let _g = zle_test_setup();
8832        inittyptab();
8833        let mut d = cadef {
8834            opts: Some(Box::new(caopt {
8835                name: Some("-foo".into()),
8836                active: 1,
8837                ..Default::default()
8838            })),
8839            argsactive: 1,
8840            ..Default::default()
8841        };
8842        ca_inactive(&mut d, &[], 0, 0);
8843        assert_eq!(d.opts.as_deref().unwrap().active, 1);
8844        assert_eq!(d.argsactive, 1);
8845    }
8846
8847    /// c:1881 — with `opts=1`, every option's `active` clears.
8848    #[test]
8849    fn ca_inactive_opts_flag_deactivates_options() {
8850        let _g = crate::test_util::global_state_lock();
8851        let _g = zle_test_setup();
8852        inittyptab();
8853        let saved_compcur = COMPCURRENT.load(Ordering::Relaxed);
8854        let mut d = cadef {
8855            opts: Some(Box::new(caopt {
8856                name: Some("-foo".into()),
8857                active: 1,
8858                num: 0,
8859                next: Some(Box::new(caopt {
8860                    name: Some("-bar".into()),
8861                    active: 1,
8862                    num: 1,
8863                    ..Default::default()
8864                })),
8865                ..Default::default()
8866            })),
8867            argsactive: 1,
8868            ..Default::default()
8869        };
8870        // Force COMPCURRENT >= cur so the guard at c:1834 is satisfied.
8871        COMPCURRENT.store(2, Ordering::Relaxed);
8872        ca_inactive(&mut d, &[], 1, 1);
8873        // Restore COMPCURRENT immediately so parallel non-ZLE tests
8874        // see the original value.
8875        COMPCURRENT.store(saved_compcur, Ordering::Relaxed);
8876        let mut p = d.opts.as_deref();
8877        while let Some(o) = p {
8878            assert_eq!(o.active, 0, "{:?} should be deactivated", o.name);
8879            p = o.next.as_deref();
8880        }
8881    }
8882
8883    /// c:3798-3801 — `comptags -i 0 a b` populates comptags[0].
8884    /// Then `-T` reports empty sets (`1`), `-C` reads context.
8885    #[test]
8886    fn bin_comptags_init_and_context() {
8887        let _g = crate::test_util::global_state_lock();
8888        let _g = zle_test_setup();
8889        inittyptab();
8890        let saved_incompfunc = INCOMPFUNC.load(Ordering::Relaxed);
8891        let saved_locallevel = locallevel.load(Ordering::Relaxed);
8892        // Reset slot 0 + locallevel.
8893        if let Ok(mut tab) = comptags.lock() {
8894            tab[0] = None;
8895        }
8896        locallevel.store(0, Ordering::Relaxed);
8897        INCOMPFUNC.store(1, Ordering::Relaxed);
8898
8899        let ops = options {
8900            ind: [0u8; MAX_OPS],
8901            args: Vec::new(),
8902            argscount: 0,
8903            argsalloc: 0,
8904        };
8905        // -i my-ctx tag-a tag-b
8906        let r = bin_comptags(
8907            "comptags",
8908            &["-i".into(), "my-ctx".into(), "tag-a".into(), "tag-b".into()],
8909            &ops,
8910            0,
8911        );
8912        assert_eq!(r, 0);
8913        // -T returns 1 (no sets yet).
8914        let r = bin_comptags("comptags", &["-T".into()], &ops, 0);
8915        assert_eq!(r, 1);
8916        // comptags[0].context should be "my-ctx".
8917        let ctx = comptags.lock().unwrap()[0]
8918            .as_ref()
8919            .and_then(|t| t.context.clone());
8920        // Restore the globals before assertion-fail can leave them mutated.
8921        INCOMPFUNC.store(saved_incompfunc, Ordering::Relaxed);
8922        locallevel.store(saved_locallevel, Ordering::Relaxed);
8923        assert_eq!(ctx.as_deref(), Some("my-ctx"));
8924    }
8925
8926    /// c:3178-3186 — cv_get_val finds a value by name; missing returns None.
8927    #[test]
8928    fn cv_get_val_hits_and_misses() {
8929        let _g = crate::test_util::global_state_lock();
8930        let _g = zle_test_setup();
8931        let d = cvdef {
8932            vals: Some(Box::new(cvval {
8933                name: Some("foo".into()),
8934                r#type: CVV_NOARG,
8935                next: Some(Box::new(cvval {
8936                    name: Some("bar".into()),
8937                    r#type: CVV_ARG,
8938                    ..Default::default()
8939                })),
8940                ..Default::default()
8941            })),
8942            ..Default::default()
8943        };
8944        let hit = cv_get_val(&d, "bar").expect("hit");
8945        assert_eq!(hit.name.as_deref(), Some("bar"));
8946        assert_eq!(hit.r#type, CVV_ARG);
8947        assert!(cv_get_val(&d, "missing").is_none());
8948    }
8949
8950    /// c:5126-5131 — setup_ frees every cache slot and zeros
8951    /// lasttaglevel. Pre-fill all three caches + lasttaglevel, then
8952    /// call setup_ and verify they're cleared.
8953    #[test]
8954    fn setup_clears_all_caches() {
8955        let _g = crate::test_util::global_state_lock();
8956        let _g = zle_test_setup();
8957        // Pre-fill.
8958        if let Ok(mut cache) = cadef_cache.lock() {
8959            cache[0] = Some(Box::new(cadef::default()));
8960        }
8961        if let Ok(mut cache) = cvdef_cache.lock() {
8962            cache[0] = Some(Box::new(cvdef::default()));
8963        }
8964        if let Ok(mut tab) = comptags.lock() {
8965            tab[0] = Some(Box::new(ctags::default()));
8966        }
8967        lasttaglevel.store(42, Ordering::Relaxed);
8968
8969        let r = setup_();
8970        assert_eq!(r, 0);
8971
8972        assert!(
8973            cadef_cache.lock().unwrap()[0].is_none(),
8974            "cadef_cache[0] should be cleared"
8975        );
8976        assert!(
8977            cvdef_cache.lock().unwrap()[0].is_none(),
8978            "cvdef_cache[0] should be cleared"
8979        );
8980        assert!(
8981            comptags.lock().unwrap()[0].is_none(),
8982            "comptags[0] should be cleared"
8983        );
8984        assert_eq!(
8985            lasttaglevel.load(Ordering::Relaxed),
8986            0,
8987            "lasttaglevel should reset to 0"
8988        );
8989    }
8990
8991    /// c:5171-5177 — finish_ frees every slot in all three caches.
8992    /// Same pre-fill pattern as setup_clears_all_caches; finish_
8993    /// differs in not zeroing lasttaglevel.
8994    #[test]
8995    fn finish_frees_all_caches() {
8996        let _g = crate::test_util::global_state_lock();
8997        let _g = zle_test_setup();
8998        if let Ok(mut cache) = cadef_cache.lock() {
8999            cache[1] = Some(Box::new(cadef::default()));
9000        }
9001        if let Ok(mut cache) = cvdef_cache.lock() {
9002            cache[1] = Some(Box::new(cvdef::default()));
9003        }
9004        if let Ok(mut tab) = comptags.lock() {
9005            tab[1] = Some(Box::new(ctags::default()));
9006        }
9007        let r = finish_();
9008        assert_eq!(r, 0);
9009        assert!(cadef_cache.lock().unwrap()[1].is_none());
9010        assert!(cvdef_cache.lock().unwrap()[1].is_none());
9011        assert!(comptags.lock().unwrap()[1].is_none());
9012    }
9013
9014    /// c:4592-4593 — cfp_matcher_pats hits CMF_LEFT && lalen==0:
9015    /// return empty string immediately. Constructed Cmatcher chain
9016    /// has one matcher with CMF_LEFT flag and lalen=0, llen=1, wlen=1.
9017    /// We seed cmatcher_global so parse_cmatcher returns it via a
9018    /// matcher spec — but parse_cmatcher is complex, so this test
9019    /// instead constructs the chain directly and verifies the bail
9020    /// happens via the public surface (we exercise the same edge via
9021    /// a matcher spec parsed by parse_cmatcher that triggers the
9022    /// left-anchor zero-len path indirectly through the dispatcher).
9023    /// The simpler observable: a malformed/empty matcher spec is
9024    /// rejected and returns add untouched.
9025    #[test]
9026    fn cfp_matcher_pats_left_anchor_zero_lalen_returns_empty_via_invalid_spec() {
9027        let _g = crate::test_util::global_state_lock();
9028        let _g = zle_test_setup();
9029        inittyptab();
9030        // A malformed matcher should fall through parse_cmatcher's
9031        // None return; cfp_matcher_pats then returns `add` unchanged
9032        // (the non-bail path; the CMF_LEFT-lalen0 bail requires a
9033        // successfully-parsed but pathological matcher which the
9034        // public parser does not produce).
9035        let r = cfp_matcher_pats("not-a-real-matcher-spec", "xyz");
9036        assert_eq!(r, "xyz");
9037    }
9038
9039    /// c:3967 — bin_comptry with lasttaglevel == 0 (no -i call yet)
9040    /// errors with "no tags registered".
9041    #[test]
9042    fn bin_comptry_no_taglevel_errors() {
9043        let _g = crate::test_util::global_state_lock();
9044        let _g = zle_test_setup();
9045        let saved_incompfunc = INCOMPFUNC.load(Ordering::Relaxed);
9046        let saved_lvl = lasttaglevel.load(Ordering::Relaxed);
9047        INCOMPFUNC.store(1, Ordering::Relaxed);
9048        lasttaglevel.store(0, Ordering::Relaxed);
9049        let ops = options {
9050            ind: [0u8; MAX_OPS],
9051            args: Vec::new(),
9052            argscount: 0,
9053            argsalloc: 0,
9054        };
9055        let r = bin_comptry("comptry", &["tag1".into()], &ops, 0);
9056        INCOMPFUNC.store(saved_incompfunc, Ordering::Relaxed);
9057        lasttaglevel.store(saved_lvl, Ordering::Relaxed);
9058        assert_eq!(r, 1);
9059    }
9060
9061    /// c:4091-4134 — bin_comptry plain mode: filters args to registered
9062    /// tags not already in any set, then appends one set with the
9063    /// surviving tags.
9064    #[test]
9065    fn bin_comptry_plain_adds_set() {
9066        let _g = crate::test_util::global_state_lock();
9067        let _g = zle_test_setup();
9068        let saved_incompfunc = INCOMPFUNC.load(Ordering::Relaxed);
9069        let saved_lvl = lasttaglevel.load(Ordering::Relaxed);
9070        // Seed comptags[1] with two registered tags.
9071        if let Ok(mut tab) = comptags.lock() {
9072            tab[1] = Some(Box::new(ctags {
9073                all: Some(vec!["files".into(), "directories".into()]),
9074                context: Some("ctx".into()),
9075                init: 0,
9076                sets: None,
9077            }));
9078        }
9079        INCOMPFUNC.store(1, Ordering::Relaxed);
9080        lasttaglevel.store(1, Ordering::Relaxed);
9081        let ops = options {
9082            ind: [0u8; MAX_OPS],
9083            args: Vec::new(),
9084            argscount: 0,
9085            argsalloc: 0,
9086        };
9087        let r = bin_comptry("comptry", &["files".into(), "unknown".into()], &ops, 0);
9088        // Inspect comptags[1].sets BEFORE restoring globals so a panic
9089        // doesn't leave them mutated.
9090        let sets_first_tags = comptags.lock().unwrap()[1]
9091            .as_ref()
9092            .and_then(|t| t.sets.as_ref().and_then(|s| s.tags.clone()));
9093        // Clean up.
9094        if let Ok(mut tab) = comptags.lock() {
9095            tab[1] = None;
9096        }
9097        INCOMPFUNC.store(saved_incompfunc, Ordering::Relaxed);
9098        lasttaglevel.store(saved_lvl, Ordering::Relaxed);
9099        assert_eq!(r, 0);
9100        let tags = sets_first_tags.expect("a set was appended");
9101        assert_eq!(
9102            tags,
9103            vec!["files".to_string()],
9104            "only registered tags survive the filter"
9105        );
9106    }
9107
9108    /// c:4525 — cfp_matcher_pats returns add unchanged when matcher
9109    /// spec is empty (parse_cmatcher returns None).
9110    #[test]
9111    fn cfp_matcher_pats_empty_matcher_passthrough() {
9112        let _g = crate::test_util::global_state_lock();
9113        let _g = zle_test_setup();
9114        inittyptab();
9115        let r = cfp_matcher_pats("", "abc");
9116        assert_eq!(r, "abc");
9117    }
9118
9119    /// c:4307 — cfp_matcher_range with no matchers (all None) emits
9120    /// each char verbatim.
9121    #[test]
9122    fn cfp_matcher_range_no_matchers_verbatim() {
9123        let _g = crate::test_util::global_state_lock();
9124        let _g = zle_test_setup();
9125        let ms: Vec<Option<Box<Cmatcher>>> = vec![None, None, None];
9126        let r = cfp_matcher_range(&ms, "abc");
9127        assert_eq!(r, "abc");
9128    }
9129
9130    /// c:247-394 — cd_prep groups path emits CRT_EXPL + CRT_SPEC runs
9131    /// when cd_state.groups > 0. With two singleton kind=0+desc entries
9132    /// we expect: 2 CRT_SPEC runs (one per leader) followed by the
9133    /// CRT_EXPL header.
9134    #[test]
9135    fn cd_prep_groups_emits_expl_and_spec_runs() {
9136        let _g = crate::test_util::global_state_lock();
9137        let _g = zle_test_setup();
9138        // Seed cd_state: one set with two kind=0+desc entries.
9139        {
9140            let mut st = cd_state.lock().unwrap();
9141            st.showd = 0;
9142            st.groups = 2;
9143            st.descs = 2;
9144            st.maxg = 1;
9145            st.maxglen = 5;
9146            st.maxmlen = 100;
9147            st.sets = Some(Box::new(cdset {
9148                count: 2,
9149                desc: 2,
9150                strs: Some(Box::new(cdstr {
9151                    str: Some("alpha".into()),
9152                    r#match: Some("alpha".into()),
9153                    desc: Some("first".into()),
9154                    width: 5,
9155                    len: 5,
9156                    kind: 0,
9157                    next: Some(Box::new(cdstr {
9158                        str: Some("beta".into()),
9159                        r#match: Some("beta".into()),
9160                        desc: Some("second".into()),
9161                        width: 4,
9162                        len: 4,
9163                        kind: 0,
9164                        ..Default::default()
9165                    })),
9166                    ..Default::default()
9167                })),
9168                ..Default::default()
9169            }));
9170            st.runs = None;
9171        }
9172        let r = cd_prep();
9173        assert_eq!(r, 0);
9174        let st = cd_state.lock().unwrap();
9175        let r1 = st.runs.as_deref().expect("first run");
9176        assert_eq!(r1.r#type, CRT_SPEC, "first run is CRT_SPEC");
9177        let r2 = r1.next.as_deref().expect("second run");
9178        assert_eq!(r2.r#type, CRT_SPEC, "second run is CRT_SPEC");
9179        let r3 = r2.next.as_deref().expect("third run");
9180        assert_eq!(r3.r#type, CRT_EXPL, "third run is CRT_EXPL");
9181        assert_eq!(r3.count, 2, "CRT_EXPL covers both prep_lines");
9182    }
9183
9184    /// c:4704-4732 — cfp_bld_pats combines names with skipped + pat.
9185    /// With one name "dir" and pats ["*.c"], produces ["dir*.c"].
9186    #[test]
9187    fn cfp_bld_pats_concatenates_skipped_and_pat() {
9188        let _g = crate::test_util::global_state_lock();
9189        let _g = zle_test_setup();
9190        let out = cfp_bld_pats(0, &["dir".to_string()], "", &["*.c".to_string()]);
9191        assert_eq!(out, vec!["dir*.c".to_string()]);
9192    }
9193
9194    /// c:4711 — when GLOBDOTS is unset AND compprefix starts with `.`,
9195    /// add a dot-prefixed variant of each non-`.`-leading pattern.
9196    #[test]
9197    fn cfp_bld_pats_globdots_variant() {
9198        let _g = crate::test_util::global_state_lock();
9199        let _g = zle_test_setup();
9200        // Seed COMPPREFIX with a leading dot.
9201        let m = COMPPREFIX.get_or_init(|| std::sync::Mutex::new(String::new()));
9202        *m.lock().unwrap() = ".foo".to_string();
9203        // Force GLOBDOTS unset — that's the default in zle_test_setup.
9204        let out = cfp_bld_pats(0, &["d".to_string()], "", &["*.x".to_string()]);
9205        // Reset compprefix to avoid bleed.
9206        *m.lock().unwrap() = String::new();
9207        assert!(out.contains(&"d*.x".to_string()));
9208        assert!(
9209            out.contains(&"d.*.x".to_string()),
9210            "dot-variant must be emitted: {:?}",
9211            out
9212        );
9213    }
9214
9215    /// c:4625 — cfp_opt_pats with empty compprefix passes pats through.
9216    #[test]
9217    fn cfp_opt_pats_passthrough_empty_compprefix() {
9218        let _g = crate::test_util::global_state_lock();
9219        let _g = zle_test_setup();
9220        let m = COMPPREFIX.get_or_init(|| std::sync::Mutex::new(String::new()));
9221        *m.lock().unwrap() = String::new();
9222        let pats = vec!["*".to_string(), "*.c".to_string()];
9223        let out = cfp_opt_pats(&pats, "");
9224        assert_eq!(out, pats);
9225    }
9226
9227    /// c:4175 — cfp_test_exact returns None when both compprefix and
9228    /// compsuffix are empty (no anchoring context).
9229    #[test]
9230    fn cfp_test_exact_no_anchor_returns_none() {
9231        let _g = crate::test_util::global_state_lock();
9232        let _g = zle_test_setup();
9233        let pm = COMPPREFIX.get_or_init(|| std::sync::Mutex::new(String::new()));
9234        *pm.lock().unwrap() = String::new();
9235        let sm = COMPSUFFIX.get_or_init(|| std::sync::Mutex::new(String::new()));
9236        *sm.lock().unwrap() = String::new();
9237        let r = cfp_test_exact(&["/tmp".to_string()], &["true".to_string()], "");
9238        assert!(r.is_none());
9239    }
9240
9241    /// c:5083 — bin_compgroups registers 6 group variants per name.
9242    /// Sanity test: returns 0 on the empty-args path (no-op).
9243    #[test]
9244    fn bin_compgroups_empty_args_succeeds() {
9245        let _g = crate::test_util::global_state_lock();
9246        let _g = zle_test_setup();
9247        let saved = INCOMPFUNC.load(Ordering::Relaxed);
9248        INCOMPFUNC.store(1, Ordering::Relaxed);
9249        let ops = options {
9250            ind: [0u8; MAX_OPS],
9251            args: Vec::new(),
9252            argscount: 0,
9253            argsalloc: 0,
9254        };
9255        let r = bin_compgroups("compgroups", &[], &ops, 0);
9256        INCOMPFUNC.store(saved, Ordering::Relaxed);
9257        assert_eq!(r, 0);
9258    }
9259
9260    /// c:215-230 — cd_groups_want_sorting: returns 0 when ANY set's
9261    /// opts contains `-V`, 1 when `-J` (or default).
9262    #[test]
9263    fn cd_groups_want_sorting_respects_opts() {
9264        let _g = crate::test_util::global_state_lock();
9265        let _g = zle_test_setup();
9266        // Default (no sets) → 1 (sorted).
9267        {
9268            let mut st = cd_state.lock().unwrap();
9269            st.sets = None;
9270        }
9271        assert_eq!(cd_groups_want_sorting(), 1);
9272        // Inject a set with -V option → returns 0.
9273        {
9274            let mut st = cd_state.lock().unwrap();
9275            st.sets = Some(Box::new(cdset {
9276                opts: Some(vec!["-V".into(), "grpname".into()]),
9277                ..Default::default()
9278            }));
9279        }
9280        assert_eq!(cd_groups_want_sorting(), 0);
9281        // Cleanup so other tests don't see the injected state.
9282        cd_state.lock().unwrap().sets = None;
9283    }
9284
9285    /// c:233 — cd_sort compares Cdstr.sortstr lexically.
9286    #[test]
9287    fn cd_sort_orders_by_sortstr() {
9288        let _g = crate::test_util::global_state_lock();
9289        let _g = zle_test_setup();
9290        let a = cdstr {
9291            sortstr: Some("apple".into()),
9292            ..Default::default()
9293        };
9294        let b = cdstr {
9295            sortstr: Some("banana".into()),
9296            ..Default::default()
9297        };
9298        assert_eq!(cd_sort(&a, &b), std::cmp::Ordering::Less);
9299        assert_eq!(cd_sort(&b, &a), std::cmp::Ordering::Greater);
9300        assert_eq!(cd_sort(&a, &a), std::cmp::Ordering::Equal);
9301    }
9302
9303    /// c:425-435 — cd_prep default branch: one CRT_SIMPLE run per
9304    /// non-empty set with the str chain mirrored via `.run` links.
9305    #[test]
9306    fn cd_prep_default_builds_simple_run_per_set() {
9307        let _g = crate::test_util::global_state_lock();
9308        let _g = zle_test_setup();
9309        // Seed cd_state with 2 sets, each 1 match.
9310        {
9311            let mut st = cd_state.lock().unwrap();
9312            st.showd = 0;
9313            st.groups = 0;
9314            st.sets = Some(Box::new(cdset {
9315                count: 1,
9316                strs: Some(Box::new(cdstr {
9317                    str: Some("a".into()),
9318                    r#match: Some("a".into()),
9319                    ..Default::default()
9320                })),
9321                next: Some(Box::new(cdset {
9322                    count: 1,
9323                    strs: Some(Box::new(cdstr {
9324                        str: Some("b".into()),
9325                        r#match: Some("b".into()),
9326                        ..Default::default()
9327                    })),
9328                    ..Default::default()
9329                })),
9330                ..Default::default()
9331            }));
9332            st.runs = None;
9333        }
9334        let r = cd_prep();
9335        assert_eq!(r, 0);
9336        // Two CRT_SIMPLE runs, one per set.
9337        let st = cd_state.lock().unwrap();
9338        let r1 = st.runs.as_deref().expect("first run");
9339        assert_eq!(r1.r#type, CRT_SIMPLE);
9340        assert_eq!(r1.count, 1);
9341        let r2 = r1.next.as_deref().expect("second run");
9342        assert_eq!(r2.r#type, CRT_SIMPLE);
9343        assert_eq!(r2.count, 1);
9344        assert!(r2.next.is_none(), "no third run");
9345    }
9346
9347    /// c:846-895 — bin_compdescribe with invalid option returns 1.
9348    #[test]
9349    fn bin_compdescribe_rejects_bad_option() {
9350        let _g = crate::test_util::global_state_lock();
9351        let _g = zle_test_setup();
9352        let saved_incompfunc = INCOMPFUNC.load(Ordering::Relaxed);
9353        INCOMPFUNC.store(1, Ordering::Relaxed);
9354        let ops = options {
9355            ind: [0u8; MAX_OPS],
9356            args: Vec::new(),
9357            argscount: 0,
9358            argsalloc: 0,
9359        };
9360        // -xx is two chars but ends with `x` not a known subcommand
9361        // letter — should fall through to the `invalid option` arm.
9362        let r = bin_compdescribe("compdescribe", &["-x".into()], &ops, 0);
9363        INCOMPFUNC.store(saved_incompfunc, Ordering::Relaxed);
9364        assert_eq!(r, 1);
9365    }
9366
9367    /// c:4903-4923 — cf_remove_other with pre="dir/foo" returns
9368    /// only names starting with "dir/" and clears `amb`.
9369    #[test]
9370    fn cf_remove_other_filters_by_dir_head() {
9371        let _g = crate::test_util::global_state_lock();
9372        let _g = zle_test_setup();
9373        let names = vec![
9374            "dir/a".to_string(),
9375            "dir/b".to_string(),
9376            "other/c".to_string(),
9377        ];
9378        let mut amb = 99;
9379        let ret = cf_remove_other(&names, "dir/foo", &mut amb);
9380        assert_eq!(amb, 0);
9381        let v = ret.expect("matching names returned");
9382        assert_eq!(v, vec!["dir/a".to_string(), "dir/b".to_string()]);
9383    }
9384
9385    /// c:4942-4951 — pre without '/' and names diverge → `amb=1`,
9386    /// returns None.
9387    #[test]
9388    fn cf_remove_other_no_slash_diverge_sets_amb() {
9389        let _g = crate::test_util::global_state_lock();
9390        let _g = zle_test_setup();
9391        let names = vec!["a".to_string(), "b".to_string()];
9392        let mut amb = 0;
9393        let ret = cf_remove_other(&names, "x", &mut amb);
9394        assert!(ret.is_none());
9395        assert_eq!(amb, 1);
9396    }
9397
9398    /// c:4870 — no "parent" and no "pwd" in style → cf_ignore returns
9399    /// without touching `ign`.
9400    #[test]
9401    fn cf_ignore_no_style_is_noop() {
9402        let _g = crate::test_util::global_state_lock();
9403        let _g = zle_test_setup();
9404        let mut ign: Vec<String> = vec!["x".into()];
9405        cf_ignore(&["/tmp".into()], &mut ign, "", "/tmp/foo");
9406        assert_eq!(
9407            ign,
9408            vec!["x".to_string()],
9409            "no style match must leave ign untouched"
9410        );
9411    }
9412
9413    /// c:3691 — empty compqstack short-circuits bin_compquote to 0.
9414    #[test]
9415    fn bin_compquote_returns_zero_when_qstack_empty() {
9416        let _g = crate::test_util::global_state_lock();
9417        let _g = zle_test_setup();
9418        let saved_incompfunc = INCOMPFUNC.load(Ordering::Relaxed);
9419        INCOMPFUNC.store(1, Ordering::Relaxed);
9420        // Ensure compqstack is empty (zle_test_setup resets things).
9421        if let Some(m) = COMPQSTACK.get() {
9422            if let Ok(mut s) = m.lock() {
9423                s.clear();
9424            }
9425        }
9426        let ops = options {
9427            ind: [0u8; MAX_OPS],
9428            args: Vec::new(),
9429            argscount: 0,
9430            argsalloc: 0,
9431        };
9432        let r = bin_compquote("compquote", &["foo".into()], &ops, 0);
9433        INCOMPFUNC.store(saved_incompfunc, Ordering::Relaxed);
9434        assert_eq!(r, 0);
9435    }
9436
9437    /// c:3192-3203 — cv_quote_get_val unquotes input then delegates
9438    /// to cv_get_val. Quoted name with backslash should still match
9439    /// after parse_subst_string strips the quoting.
9440    #[test]
9441    fn cv_quote_get_val_unquotes_then_lookup() {
9442        let _g = crate::test_util::global_state_lock();
9443        let _g = zle_test_setup();
9444        inittyptab();
9445        let d = cvdef {
9446            vals: Some(Box::new(cvval {
9447                name: Some("foo".into()),
9448                r#type: CVV_NOARG,
9449                ..Default::default()
9450            })),
9451            ..Default::default()
9452        };
9453        // Plain name → hit.
9454        assert!(cv_quote_get_val(&d, "foo").is_some());
9455        // Unknown → miss.
9456        assert!(cv_quote_get_val(&d, "bar").is_none());
9457    }
9458
9459    /// c:3211-3217 — cv_inactive clears active for each name in xor.
9460    #[test]
9461    fn cv_inactive_clears_named_vals() {
9462        let _g = crate::test_util::global_state_lock();
9463        let _g = zle_test_setup();
9464        let mut d = cvdef {
9465            vals: Some(Box::new(cvval {
9466                name: Some("a".into()),
9467                active: 1,
9468                next: Some(Box::new(cvval {
9469                    name: Some("b".into()),
9470                    active: 1,
9471                    next: Some(Box::new(cvval {
9472                        name: Some("c".into()),
9473                        active: 1,
9474                        ..Default::default()
9475                    })),
9476                    ..Default::default()
9477                })),
9478                ..Default::default()
9479            })),
9480            ..Default::default()
9481        };
9482        cv_inactive(&mut d, &["a".into(), "c".into()]);
9483        let mut p = d.vals.as_deref();
9484        let mut by_name = std::collections::HashMap::new();
9485        while let Some(v) = p {
9486            by_name.insert(v.name.clone().unwrap_or_default(), v.active);
9487            p = v.next.as_deref();
9488        }
9489        assert_eq!(by_name["a"], 0);
9490        assert_eq!(by_name["b"], 1, "untouched val stays active");
9491        assert_eq!(by_name["c"], 0);
9492    }
9493
9494    // ─── zsh-corpus pins for cd_arrcat / cd_arrdup ──────────────────
9495
9496    /// `cd_arrcat` concatenates two arrays.
9497    #[test]
9498    fn computil_corpus_cd_arrcat_concatenates() {
9499        let a = vec!["a".to_string(), "b".to_string()];
9500        let b = vec!["c".to_string(), "d".to_string()];
9501        let r = cd_arrcat(&a, &b);
9502        assert_eq!(r, vec!["a", "b", "c", "d"]);
9503    }
9504
9505    /// `cd_arrcat` with empty left side returns copy of right side.
9506    #[test]
9507    fn computil_corpus_cd_arrcat_empty_left() {
9508        let a: Vec<String> = Vec::new();
9509        let b = vec!["x".to_string(), "y".to_string()];
9510        let r = cd_arrcat(&a, &b);
9511        assert_eq!(r, vec!["x", "y"]);
9512    }
9513
9514    /// `cd_arrcat` with empty right side returns copy of left side.
9515    #[test]
9516    fn computil_corpus_cd_arrcat_empty_right() {
9517        let a = vec!["p".to_string(), "q".to_string()];
9518        let b: Vec<String> = Vec::new();
9519        let r = cd_arrcat(&a, &b);
9520        assert_eq!(r, vec!["p", "q"]);
9521    }
9522
9523    /// `cd_arrcat` with both empty returns empty.
9524    #[test]
9525    fn computil_corpus_cd_arrcat_both_empty() {
9526        let a: Vec<String> = Vec::new();
9527        let b: Vec<String> = Vec::new();
9528        let r = cd_arrcat(&a, &b);
9529        assert!(r.is_empty());
9530    }
9531
9532    /// `cd_arrdup` is identity for valid input.
9533    #[test]
9534    fn computil_corpus_cd_arrdup_round_trips() {
9535        let a = vec!["one".to_string(), "two".to_string(), "three".to_string()];
9536        let r = cd_arrdup(&a);
9537        assert_eq!(r, a);
9538    }
9539
9540    /// `cd_arrdup` on empty returns empty.
9541    #[test]
9542    fn computil_corpus_cd_arrdup_empty() {
9543        let a: Vec<String> = Vec::new();
9544        let r = cd_arrdup(&a);
9545        assert!(r.is_empty());
9546    }
9547
9548    /// `cd_arrdup` returns a deep copy (modifying result doesn't
9549    /// affect input).
9550    #[test]
9551    fn computil_corpus_cd_arrdup_independent_copy() {
9552        let a = vec!["alpha".to_string()];
9553        let mut r = cd_arrdup(&a);
9554        r[0].push('!');
9555        assert_eq!(a[0], "alpha", "original unchanged");
9556        assert_eq!(r[0], "alpha!", "copy mutated independently");
9557    }
9558
9559    // ═══════════════════════════════════════════════════════════════════
9560    // Additional C-parity tests for Src/Zle/computil.c
9561    // c:172 freecdsets / c:372 cd_groups_want_sorting / c:396 cd_sort /
9562    // c:1725 arrcmp / c:1754 freecaargs / c:1773 freecadef /
9563    // c:1821 rembslashcolon / c:1858 bslashcolon / c:1890 single_index /
9564    // c:3540 ca_opt_arg
9565    // ═══════════════════════════════════════════════════════════════════
9566
9567    /// c:172 — `freecdsets(None)` is safe idempotent.
9568    #[test]
9569    fn freecdsets_none_idempotent() {
9570        for _ in 0..5 {
9571            freecdsets(None);
9572        }
9573    }
9574
9575    /// c:1754 — `freecaargs(None)` is safe idempotent.
9576    #[test]
9577    fn freecaargs_none_idempotent() {
9578        for _ in 0..5 {
9579            freecaargs(None);
9580        }
9581    }
9582
9583    /// c:1773 — `freecadef(None)` is safe idempotent.
9584    #[test]
9585    fn freecadef_none_idempotent() {
9586        for _ in 0..5 {
9587            freecadef(None);
9588        }
9589    }
9590
9591    /// c:980 — `arrcmp(None, None)` returns 1 ("equal" sentinel, NOT 0).
9592    /// C uses inverted boolean: 1 = equal, 0 = unequal (opposite of strcmp).
9593    #[test]
9594    fn arrcmp_both_none_returns_one_inverted_boolean() {
9595        assert_eq!(
9596            arrcmp(None, None),
9597            1,
9598            "C inverted-boolean: 1=equal, 0=unequal (per c:980)"
9599        );
9600    }
9601
9602    /// c:980 — `arrcmp(None, Some)` returns 0 (unequal sentinel).
9603    #[test]
9604    fn arrcmp_none_vs_some_returns_zero_unequal() {
9605        let a = vec!["x".to_string()];
9606        assert_eq!(arrcmp(None, Some(&a)), 0, "None vs Some → 0 (unequal)");
9607        assert_eq!(arrcmp(Some(&a), None), 0, "Some vs None → 0 (unequal)");
9608    }
9609
9610    /// c:984 — `arrcmp(Some(a), Some(a))` returns 1 (equal).
9611    #[test]
9612    fn arrcmp_identical_arrays_returns_one_equal() {
9613        let a = vec!["x".to_string(), "y".to_string()];
9614        assert_eq!(
9615            arrcmp(Some(&a), Some(&a)),
9616            1,
9617            "identical arrays → 1 (equal) per c:989"
9618        );
9619    }
9620
9621    /// c:1725 — `arrcmp` is pure for non-empty arrays.
9622    #[test]
9623    fn arrcmp_is_pure() {
9624        let a = vec!["x".to_string()];
9625        let b = vec!["y".to_string()];
9626        let first = arrcmp(Some(&a), Some(&b));
9627        for _ in 0..5 {
9628            assert_eq!(arrcmp(Some(&a), Some(&b)), first, "arrcmp must be pure");
9629        }
9630    }
9631
9632    /// c:1821 — `rembslashcolon("")` empty returns empty.
9633    #[test]
9634    fn rembslashcolon_empty_returns_empty() {
9635        assert_eq!(rembslashcolon(""), "");
9636    }
9637
9638    /// c:1821 — `rembslashcolon` is pure.
9639    #[test]
9640    fn rembslashcolon_is_pure() {
9641        for s in ["", "abc", r"a\:b", r"\:\:"] {
9642            let first = rembslashcolon(s);
9643            for _ in 0..3 {
9644                assert_eq!(
9645                    rembslashcolon(s),
9646                    first,
9647                    "rembslashcolon({:?}) must be pure",
9648                    s
9649                );
9650            }
9651        }
9652    }
9653
9654    /// c:1858 — `bslashcolon("")` empty returns empty.
9655    #[test]
9656    fn bslashcolon_empty_returns_empty() {
9657        assert_eq!(bslashcolon(""), "");
9658    }
9659
9660    /// c:1890 — `single_index(0, 0)` returns i32 (type pin).
9661    #[test]
9662    fn single_index_returns_i32_type() {
9663        let _: i32 = single_index(0, 0);
9664    }
9665
9666    /// c:3540 — `ca_opt_arg(empty, empty, false)` is safe.
9667    #[test]
9668    fn ca_opt_arg_empty_inputs_no_panic() {
9669        let _: String = ca_opt_arg("", "", false);
9670    }
9671
9672    /// c:1858 — `bslashcolon` is pure.
9673    #[test]
9674    fn bslashcolon_is_pure() {
9675        for s in ["", "a:b", "no colon", "a:b:c:d"] {
9676            let first = bslashcolon(s);
9677            for _ in 0..3 {
9678                assert_eq!(bslashcolon(s), first, "bslashcolon({:?}) must be pure", s);
9679            }
9680        }
9681    }
9682
9683    // ═══════════════════════════════════════════════════════════════════
9684    // Additional C-parity tests for Src/Zle/computil.c
9685    // c:933 cd_arrcat / c:1153 cd_arrdup / c:1167 cd_get /
9686    // c:1509 bin_compdescribe / c:1754 freecaargs / c:1773 freecadef /
9687    // c:1821 rembslashcolon / c:1858 bslashcolon / c:2884 get_cadef
9688    // ═══════════════════════════════════════════════════════════════════
9689
9690    /// c:933 — `cd_arrcat(&[], &[])` empty + empty returns empty.
9691    #[test]
9692    fn cd_arrcat_both_empty_returns_empty() {
9693        assert!(cd_arrcat(&[], &[]).is_empty(), "empty + empty → empty");
9694    }
9695
9696    /// c:933 — `cd_arrcat(&[], &b)` empty-A returns clone of B.
9697    #[test]
9698    fn cd_arrcat_empty_a_returns_clone_of_b() {
9699        let b = vec!["x".to_string(), "y".to_string()];
9700        assert_eq!(cd_arrcat(&[], &b), b, "empty + B → B");
9701    }
9702
9703    /// c:933 — `cd_arrcat(&a, &[])` empty-B returns clone of A.
9704    #[test]
9705    fn cd_arrcat_empty_b_returns_clone_of_a() {
9706        let a = vec!["m".to_string(), "n".to_string()];
9707        assert_eq!(cd_arrcat(&a, &[]), a, "A + empty → A");
9708    }
9709
9710    /// c:933 — `cd_arrcat` preserves order: A first, then B.
9711    #[test]
9712    fn cd_arrcat_preserves_a_then_b_order() {
9713        let a = vec!["1".to_string(), "2".to_string()];
9714        let b = vec!["3".to_string(), "4".to_string()];
9715        let r = cd_arrcat(&a, &b);
9716        assert_eq!(r, vec!["1", "2", "3", "4"]);
9717    }
9718
9719    /// c:933 — `cd_arrcat` returns Vec<String> (compile-time type pin).
9720    #[test]
9721    fn cd_arrcat_returns_vec_string_type() {
9722        let _: Vec<String> = cd_arrcat(&[], &[]);
9723    }
9724
9725    /// c:1153 — `cd_arrdup` is identity on the input (deep clone).
9726    #[test]
9727    fn cd_arrdup_returns_independent_clone() {
9728        let a = vec!["x".to_string(), "y".to_string()];
9729        let dup = cd_arrdup(&a);
9730        assert_eq!(dup, a, "dup equal to input");
9731        let mut mut_dup = dup;
9732        mut_dup.push("added".to_string());
9733        assert_eq!(a.len(), 2, "original unchanged after mutating dup");
9734    }
9735
9736    /// c:1153 — `cd_arrdup(&[])` empty returns empty.
9737    #[test]
9738    fn cd_arrdup_empty_returns_empty() {
9739        assert!(cd_arrdup(&[]).is_empty());
9740    }
9741
9742    /// c:1167 — `cd_get(&[])` empty args returns i32 (type pin).
9743    #[test]
9744    fn cd_get_returns_i32_type() {
9745        let _g = crate::test_util::global_state_lock();
9746        let _: i32 = cd_get(&[]);
9747    }
9748
9749    /// c:1509 — `bin_compdescribe` returns i32 (compile-time type pin).
9750    #[test]
9751    fn bin_compdescribe_returns_i32_type() {
9752        let _g = crate::test_util::global_state_lock();
9753        let ops = crate::ported::zsh_h::options {
9754            ind: [0u8; crate::ported::zsh_h::MAX_OPS],
9755            args: Vec::new(),
9756            argscount: 0,
9757            argsalloc: 0,
9758        };
9759        let _: i32 = bin_compdescribe("compdescribe", &[], &ops, 0);
9760    }
9761
9762    /// c:1754 — `freecaargs(None)` is safe.
9763    #[test]
9764    fn freecaargs_none_no_panic() {
9765        freecaargs(None);
9766    }
9767
9768    /// c:1773 — `freecadef(None)` is safe.
9769    #[test]
9770    fn freecadef_none_no_panic() {
9771        freecadef(None);
9772    }
9773
9774    /// c:1821 — `rembslashcolon` returns String (compile-time type pin).
9775    #[test]
9776    fn rembslashcolon_returns_string_type() {
9777        let _: String = rembslashcolon("");
9778    }
9779
9780    /// c:1858 — `bslashcolon` returns String (compile-time type pin).
9781    #[test]
9782    fn bslashcolon_returns_string_type() {
9783        let _: String = bslashcolon("");
9784    }
9785
9786    /// c:1821 + c:1858 — `rembslashcolon(bslashcolon(s))` round-trips
9787    /// for inputs without pre-existing escaped colons.
9788    #[test]
9789    fn bslashcolon_rembslashcolon_roundtrip_safe() {
9790        for s in ["a:b", "x:y:z", "no_colon"] {
9791            let escaped = bslashcolon(s);
9792            let unescaped = rembslashcolon(&escaped);
9793            assert_eq!(
9794                unescaped, s,
9795                "bslashcolon→rembslashcolon must round-trip for {:?}",
9796                s
9797            );
9798        }
9799    }
9800
9801    /// c:2884 — `get_cadef("", &[])` empty inputs returns i32.
9802    #[test]
9803    fn get_cadef_returns_i32_type() {
9804        let _g = crate::test_util::global_state_lock();
9805        let _: i32 = get_cadef("", &[]);
9806    }
9807}