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  → compsys::describe::describe()
14//! - bin_comparguments → compsys::arguments (full _arguments)
15//! - bin_compvalues    → compsys::computil::compvalues()
16//! - bin_comptags      → compsys::state::comptags()
17//! - bin_comptry       → compsys::state::comptry()
18
19use std::collections::HashMap;
20use crate::ported::utils::{quotedzputs, zwarnnam};
21use crate::ported::zle::complete::INCOMPFUNC;
22use crate::ported::zle::complete::COMPQSTACK;
23use crate::ported::zsh_h::OPT_ISSET;
24
25// =====================================================================
26// CRT_* — `_describe` row-type discriminator from `computil.c:79-83`.
27// Drives the `cdescr` table-builder switch.
28// =====================================================================
29
30/// Port of `CRT_SIMPLE` from `Src/Zle/computil.c:79`. Plain match row.
31
32// --- AUTO: cross-zle hoisted-fn use glob ---
33#[allow(unused_imports)]
34#[allow(unused_imports)]
35use crate::ported::zle::zle_main::*;
36#[allow(unused_imports)]
37use crate::ported::zle::zle_misc::*;
38#[allow(unused_imports)]
39use crate::ported::zle::zle_hist::*;
40#[allow(unused_imports)]
41use crate::ported::zle::zle_move::*;
42#[allow(unused_imports)]
43use crate::ported::zle::zle_word::*;
44#[allow(unused_imports)]
45use crate::ported::zle::zle_params::*;
46#[allow(unused_imports)]
47use crate::ported::zle::zle_vi::*;
48#[allow(unused_imports)]
49use crate::ported::zle::zle_utils::*;
50#[allow(unused_imports)]
51use crate::ported::zle::zle_refresh::*;
52#[allow(unused_imports)]
53use crate::ported::zle::zle_tricky::*;
54#[allow(unused_imports)]
55use crate::ported::zle::textobjects::*;
56#[allow(unused_imports)]
57use crate::ported::zle::deltochar::*;
58
59pub const CRT_SIMPLE: i32 = 0;                                               // c:79
60/// Port of `CRT_DESC` from `computil.c:80`. Match with description.
61pub const CRT_DESC:   i32 = 1;                                               // c:80
62/// Port of `CRT_SPEC` from `computil.c:81`. Special separator row.
63pub const CRT_SPEC:   i32 = 2;                                               // c:81
64/// Port of `CRT_DUMMY` from `computil.c:82`. Placeholder row.
65pub const CRT_DUMMY:  i32 = 3;                                               // c:82
66/// Port of `CRT_EXPL` from `computil.c:83`. Explanation header row.
67pub const CRT_EXPL:   i32 = 4;                                               // c:83
68
69/// Port of `CDF_SEP` from `Src/Zle/computil.c:924`. `-S` flag — `--`
70/// terminates options.
71pub const CDF_SEP: i32 = 1;                                                  // c:924
72
73// =====================================================================
74// CAO_* — Cadef option-argument attachment style — `computil.c:941-945`.
75// =====================================================================
76
77/// Port of `CAO_NEXT` from `computil.c:941`. Argument in next argv slot.
78pub const CAO_NEXT:    i32 = 1;                                              // c:941
79/// Port of `CAO_DIRECT` from `computil.c:942`. Argument directly attached
80/// to option (`-opt:value`).
81pub const CAO_DIRECT:  i32 = 2;                                              // c:942
82/// Port of `CAO_ODIRECT` from `computil.c:943`. Optional direct attach.
83pub const CAO_ODIRECT: i32 = 3;                                              // c:943
84/// Port of `CAO_EQUAL` from `computil.c:944`. Argument after `=`.
85pub const CAO_EQUAL:   i32 = 4;                                              // c:944
86/// Port of `CAO_OEQUAL` from `computil.c:945`. Optional `=` argument.
87pub const CAO_OEQUAL:  i32 = 5;                                              // c:945
88
89// =====================================================================
90// CAA_* — Cadef positional-argument kinds — `computil.c:964-968`.
91// =====================================================================
92
93/// Port of `CAA_NORMAL` from `computil.c:964`. Plain positional arg.
94pub const CAA_NORMAL: i32 = 1;                                               // c:964
95/// Port of `CAA_OPT` from `computil.c:965`. Optional positional arg.
96pub const CAA_OPT:    i32 = 2;                                               // c:965
97/// Port of `CAA_REST` from `computil.c:966`. Mandatory rest of args.
98pub const CAA_REST:   i32 = 3;                                               // c:966
99/// Port of `CAA_RARGS` from `computil.c:967`. Repeated args sequence.
100pub const CAA_RARGS:  i32 = 4;                                               // c:967
101/// Port of `CAA_RREST` from `computil.c:968`. Repeated rest of args.
102pub const CAA_RREST:  i32 = 5;                                               // c:968
103
104/// Port of `MAX_CACACHE` from `computil.c:972`. Cadef LRU cache size.
105pub const MAX_CACACHE: usize = 8;                                            // c:972
106
107// =====================================================================
108// CVV_* — Cvval value-kind — `computil.c:2949-2951`.
109// =====================================================================
110
111/// Port of `CVV_NOARG` from `computil.c:2949`. Value without argument.
112pub const CVV_NOARG: i32 = 0;                                                // c:2949
113/// Port of `CVV_ARG` from `computil.c:2950`. Value requires argument.
114pub const CVV_ARG:   i32 = 1;                                                // c:2950
115/// Port of `CVV_OPT` from `computil.c:2951`. Argument optional.
116pub const CVV_OPT:   i32 = 2;                                                // c:2951
117
118/// Port of `MAX_CVCACHE` from `computil.c:2955`. Cvdef LRU cache size.
119pub const MAX_CVCACHE: usize = 8;                                            // c:2955
120
121/// Port of `MAX_TAGS` from `computil.c:3755`. Maximum nested completion
122/// tags depth.
123pub const MAX_TAGS: usize = 256;                                             // c:3755
124
125/// Port of `PATH_MAX2` from `computil.c:4141`. `PATH_MAX * 2` — buffer
126/// budget for path-completion staging strings.
127pub const PATH_MAX2: usize = 8192;                                           // c:4141 (PATH_MAX*2, 4096*2)
128
129// =====================================================================
130// `_describe`-completion types — direct ports of the C structs at
131// Src/Zle/computil.c:40-91 (the cdset/cdstr/cdrun/cdstate chain
132// the `_describe` completion path builds + processes).
133// =====================================================================
134
135// CRT_* constants already declared above (file scope).
136
137/// Port of `typedef struct cdset *Cdset` from `Src/Zle/computil.c:36`.
138pub type Cdset = Box<cdset>;                                                 // c:36
139/// Port of `typedef struct cdstr *Cdstr` from `computil.c:37`.
140pub type Cdstr = Box<cdstr>;                                                 // c:37
141/// Port of `typedef struct cdrun *Cdrun` from `computil.c:38`.
142pub type Cdrun = Box<cdrun>;                                                 // c:38
143
144/// Direct port of `struct cdstr` from `Src/Zle/computil.c:58-70`.
145/// One match string inside a `_describe` group, with optional
146/// description and the same-description chain.
147#[derive(Debug, Default, Clone)]
148#[allow(non_camel_case_types)]
149pub struct cdstr {                                                           // c:58
150    pub next:    Option<Box<cdstr>>,                                         // c:59 Cdstr next
151    pub str:     Option<String>,                                             // c:60 char *str
152    pub desc:    Option<String>,                                             // c:61 char *desc
153    pub r#match: Option<String>,                                             // c:62 char *match
154    pub sortstr: Option<String>,                                             // c:63 char *sortstr
155    pub len:     i32,                                                        // c:64 int len
156    pub width:   i32,                                                        // c:65 int width
157    pub other:   Option<Box<cdstr>>,                                         // c:66 Cdstr other
158    pub kind:    i32,                                                        // c:67 int kind (0/1/2)
159    pub set:     usize,                                                      // c:68 Cdset set (raw ptr index)
160    pub run:     Option<Box<cdstr>>,                                         // c:69 Cdstr run
161}
162
163/// Direct port of `struct cdrun` from `Src/Zle/computil.c:72-77`.
164/// One contiguous "run" of cdstr entries the shell code should
165/// emit as a block.
166#[derive(Debug, Default)]
167#[allow(non_camel_case_types)]
168pub struct cdrun {                                                           // c:72
169    pub next:   Option<Box<cdrun>>,                                          // c:73 Cdrun next
170    pub r#type: i32,                                                         // c:74 int type (CRT_*)
171    pub strs:   Option<Box<cdstr>>,                                          // c:75 Cdstr strs
172    pub count:  i32,                                                         // c:76 int count
173}
174
175/// Direct port of `struct cdset` from `Src/Zle/computil.c:85-91`.
176/// One set of matches (one `compadd` invocation worth) with its
177/// compadd options + the cdstr chain.
178#[derive(Debug, Default)]
179#[allow(non_camel_case_types)]
180pub struct cdset {                                                           // c:85
181    pub next:  Option<Box<cdset>>,                                           // c:86 Cdset next
182    pub opts:  Option<Vec<String>>,                                          // c:87 char **opts
183    pub strs:  Option<Box<cdstr>>,                                           // c:88 Cdstr strs
184    pub count: i32,                                                          // c:89 int count
185    pub desc:  i32,                                                          // c:90 int desc
186}
187
188/// Direct port of `struct cdstate` from `Src/Zle/computil.c:40-56`.
189/// File-static state for the `_describe` engine — holds the active
190/// sets/runs/dimensions during a single `_describe` invocation.
191#[derive(Debug, Default)]
192#[allow(non_camel_case_types)]
193pub struct cdstate {                                                         // c:40
194    pub showd:   i32,                                                        // c:41
195    pub sep:     Option<String>,                                             // c:42 char *sep
196    pub slen:    i32,                                                        // c:43
197    pub swidth:  i32,                                                        // c:44
198    pub maxmlen: i32,                                                        // c:45
199    pub sets:    Option<Box<cdset>>,                                         // c:46 Cdset sets
200    pub pre:     i32,                                                        // c:47
201    pub premaxw: i32,                                                        // c:48
202    pub suf:     i32,                                                        // c:49
203    pub maxg:    i32,                                                        // c:50
204    pub maxglen: i32,                                                        // c:51
205    pub groups:  i32,                                                        // c:52
206    pub descs:   i32,                                                        // c:53
207    pub gprew:   i32,                                                        // c:54
208    pub runs:    Option<Box<cdrun>>,                                         // c:55 Cdrun runs
209}
210
211/// Port of `static struct cdstate cd_state` from `Src/Zle/computil.c:93`.
212/// File-static instance the `_describe` engine reads/writes.
213pub static cd_state: std::sync::Mutex<cdstate> =                             // c:93
214    std::sync::Mutex::new(cdstate {
215        showd: 0, sep: None, slen: 0, swidth: 0, maxmlen: 0,
216        sets: None, pre: 0, premaxw: 0, suf: 0, maxg: 0, maxglen: 0,
217        groups: 0, descs: 0, gprew: 0, runs: None,
218    });
219
220/// Port of `static int cd_parsed` from `Src/Zle/computil.c:188`. Flag
221/// signalling whether `cd_state` holds a parsed-but-unconsumed
222/// description set.
223pub static cd_parsed: std::sync::atomic::AtomicI32 =                         // c:94
224    std::sync::atomic::AtomicI32::new(0);
225
226/// Direct port of `static void cd_calc(void)` from
227/// `Src/Zle/computil.c:188-211`. Walks `cd_state.sets`, computing
228/// each set's `count`/`desc` and updating
229/// `cd_state.pre`/`premaxw`/`suf` (the global max widths) for the
230/// `_describe` column layout.
231pub fn cd_calc() {                                                           // c:188
232    let mut st = cd_state.lock().unwrap();
233    st.pre = 0;                                                              // c:194
234    st.suf = 0;
235    let mut max_pre = 0i32;
236    let mut max_premaxw = st.premaxw;
237    let mut max_suf = 0i32;
238
239    let mut set = st.sets.as_deref_mut();
240    while let Some(s) = set {
241        s.count = 0;                                                         // c:197
242        s.desc = 0;
243        let mut str_iter = s.strs.as_deref();
244        while let Some(st_node) = str_iter {
245            s.count += 1;                                                    // c:199
246            let str_s = st_node.str.as_deref().unwrap_or("");
247            let l = str_s.len() as i32;
248            if l > max_pre { max_pre = l; }                                  // c:200
249            // c:202 — ZMB_nicewidth(str). Rust niceztrlen returns usize.
250            let nw = crate::ported::utils::niceztrlen(str_s) as i32;
251            if nw > max_premaxw { max_premaxw = nw; }
252            if let Some(d) = st_node.desc.as_deref() {                       // c:204
253                s.desc += 1;
254                let dl = d.len() as i32;
255                if dl > max_suf { max_suf = dl; }                            // c:206
256            }
257            str_iter = st_node.next.as_deref();
258        }
259        set = s.next.as_deref_mut();
260    }
261    st.pre = max_pre;
262    st.premaxw = max_premaxw;
263    st.suf = max_suf;
264}
265
266/// Direct port of `static void cd_group(int maxg)` from
267/// `Src/Zle/computil.c:127-182`. Walks `cd_state.sets` looking for
268/// matches sharing the same description; links them via the `other`
269/// chain on the first cdstr of each group. Sets `cd_state.groups`,
270/// `descs`, `maxg`, `maxglen` accordingly.
271pub fn cd_group(maxg: i32) {                                                 // c:127
272    let mut st = cd_state.lock().unwrap();
273    st.groups = 0;                                                           // c:133
274    st.descs = 0;
275    st.maxglen = 0;
276    st.maxg = 0;
277
278    // c:136-140 — reset kind/other on every cdstr.
279    // Rust port: walk via raw pointers since we need mutable access
280    // through nested chains while holding the set borrow.
281    let st_ptr: *mut cdstate = &mut *st;
282    unsafe {
283        let mut set = (*st_ptr).sets.as_deref_mut();
284        while let Some(s) = set {
285            let mut sp = s.strs.as_deref_mut();
286            while let Some(sn) = sp {
287                sn.kind = 0;
288                sn.other = None;
289                sp = sn.next.as_deref_mut();
290            }
291            set = s.next.as_deref_mut();
292        }
293
294        // c:142-180 — find matching desc, build "other" chain.
295        let mut set1 = (*st_ptr).sets.as_deref_mut();
296        while let Some(s1) = set1 {
297            let s1_ptr: *mut cdset = s1;
298            let mut str1 = (*s1_ptr).strs.as_deref_mut();
299            while let Some(t1) = str1 {
300                if t1.desc.is_none() || t1.kind != 0 {                       // c:144
301                    str1 = t1.next.as_deref_mut();
302                    continue;
303                }
304                let mut num = 1i32;                                          // c:147
305                let mut width = t1.width + (*st_ptr).swidth;                 // c:148
306                if width > (*st_ptr).maxglen {
307                    (*st_ptr).maxglen = width;
308                }
309                // Iterate set2 from set1 onwards; str2 starts at str1.next
310                // when same set, else strs head.
311                let t1_desc = t1.desc.clone().unwrap_or_default();
312                let mut other_tail: *mut Option<Box<cdstr>> = &mut t1.other;
313                let mut hit_break = false;
314                let mut set2 = Some(&mut *s1_ptr);
315                let mut first_iter = true;
316                while let Some(s2) = set2 {
317                    let s2_ptr: *mut cdset = s2;
318                    let mut str2 = if first_iter {
319                        t1.next.as_deref_mut()
320                    } else {
321                        (*s2_ptr).strs.as_deref_mut()
322                    };
323                    first_iter = false;
324                    while let Some(t2) = str2 {
325                        if t2.desc.as_deref() == Some(t1_desc.as_str()) {
326                            width += CM_SPACE + t2.width;                    // c:157
327                            if width > (*st_ptr).maxmlen || num == maxg {    // c:158
328                                hit_break = true;
329                                break;
330                            }
331                            if width > (*st_ptr).maxglen {                   // c:160
332                                (*st_ptr).maxglen = width;
333                            }
334                            t1.kind = 1;                                     // c:162
335                            t2.kind = 2;
336                            num += 1;
337                            // Clone t2 into the other chain (Rust ownership).
338                            let clone = Box::new(cdstr {
339                                next:    None,
340                                str:     t2.str.clone(),
341                                desc:    t2.desc.clone(),
342                                r#match: t2.r#match.clone(),
343                                sortstr: t2.sortstr.clone(),
344                                len:     t2.len,
345                                width:   t2.width,
346                                other:   None,
347                                kind:    t2.kind,
348                                set:     t2.set,
349                                run:     None,
350                            });
351                            *other_tail = Some(clone);
352                            let nxt = &mut (*other_tail).as_mut().unwrap().other;
353                            other_tail = nxt as *mut _;
354                        }
355                        str2 = t2.next.as_deref_mut();
356                    }
357                    if hit_break { break; }
358                    set2 = (*s2_ptr).next.as_deref_mut();
359                }
360                if num > 1 {                                                 // c:173
361                    (*st_ptr).groups += 1;
362                } else {
363                    (*st_ptr).descs += 1;                                    // c:176
364                }
365                if num > (*st_ptr).maxg {                                    // c:178
366                    (*st_ptr).maxg = num;
367                }
368                str1 = t1.next.as_deref_mut();
369            }
370            set1 = s1.next.as_deref_mut();
371        }
372    }
373}
374
375/// CM_SPACE — inter-match spacing from `Src/Zle/zle_tricky.c:1700` /
376/// `Src/Zle/computil.c` (referenced as the literal `2`). Used to
377/// reserve a 2-char gap between adjacent matches when computing
378/// column widths.
379pub const CM_SPACE: i32 = 2;                                                 // c:zle_tricky.c
380
381/// Direct port of `static int cd_sort(const void *a, const void *b)`
382/// from `Src/Zle/computil.c:233-236`. qsort comparator over Cdstr
383/// pointers — compares the `sortstr` fields via `zstrcmp` (case-
384/// sensitive by default).
385pub fn cd_sort(a: &cdstr, b: &cdstr) -> std::cmp::Ordering {                  // c:233
386    crate::ported::sort::zstrcmp(
387        a.sortstr.as_deref().unwrap_or(""),
388        b.sortstr.as_deref().unwrap_or(""),
389        0,
390    )
391}
392
393/// Direct port of `static int cd_groups_want_sorting(void)` from
394/// `Src/Zle/computil.c:215-230`. Returns 0 if any set's opts contain
395/// `-V` (preserve order), 1 if any contain `-J` (sort), 1 default.
396pub fn cd_groups_want_sorting() -> i32 {                                     // c:215
397    let st = cd_state.lock().unwrap();
398    let mut set = st.sets.as_deref();
399    while let Some(s) = set {
400        if let Some(opts) = s.opts.as_deref() {
401            for o in opts {
402                if o.starts_with("-V") { return 0; }                         // c:222
403                if o.starts_with("-J") { return 1; }                         // c:224
404            }
405        }
406        set = s.next.as_deref();
407    }
408    1                                                                        // c:229
409}
410
411/// Direct port of `static int cd_prep(void)` from
412/// `Src/Zle/computil.c:239-439`. Builds the `cd_state.runs` chain
413/// from the parsed `cd_state.sets`.
414///
415/// Three branches:
416///   - groups (cd_state.groups > 0): build CRT_EXPL + CRT_SPEC/
417///     CRT_DUMMY interleaved + CRT_SIMPLE per set. The most complex
418///     path; depends on width tracking via cd_state.gprew. **This
419///     branch returns 1 when the laid-out group width exceeds the
420///     terminal — the caller (cd_init at c:582-586) loops with a
421///     shrunken maxg until prep succeeds.**
422///   - showd (cd_state.showd != 0): emit CRT_DESC for entries with
423///     descriptions and CRT_SIMPLE for plain matches per set.
424///   - default: one CRT_SIMPLE run per set.
425pub fn cd_prep() -> i32 {                                                    // c:239
426    // CRT_SIMPLE/DESC/SPEC/DUMMY/EXPL declared at the top of this file
427
428    // Build the new runs list as a Vec; link into cd_state.runs at the end.
429    let mut new_runs: Vec<Box<cdrun>> = Vec::new();
430    let mut st = cd_state.lock().unwrap();
431    st.runs = None;
432
433    if st.groups != 0 {
434        // c:247-394 — groups path. Full algorithm: collect leaders
435        // (kind==1 from cd_group OR kind==0+desc standalone) into a
436        // `prep_lines` Vec; sort by width inside each leader's .other
437        // chain; track per-column widths; bail-with-1 on overflow;
438        // sort by sortstr; dedup-adjacent so same-desc entries cluster;
439        // emit CRT_EXPL header + CRT_SPEC per leader column 0; for each
440        // additional column emit CRT_DUMMY/CRT_SPEC interleave; finally
441        // emit CRT_SIMPLE per set for un-described entries.
442
443        let maxg = st.maxg.max(1) as usize;
444        let maxmlen = st.maxmlen;
445        let maxglen = st.maxglen;
446        let swidth = st.swidth;
447
448        // c:256 — wids[0..maxg] tracks max width per column.
449        let mut wids: Vec<i32> = vec![0; maxg];
450
451        // c:257-287 — collect leaders into prep_lines (Vec of owned
452        // cdstr clones with their .other chains).
453        let mut prep_lines: Vec<Box<cdstr>> = Vec::new();
454        let mut set = st.sets.as_deref();
455        while let Some(s) = set {
456            let mut str_iter = s.strs.as_deref();
457            while let Some(node) = str_iter {
458                if node.kind != 1 {
459                    if node.kind == 0 && node.desc.is_some() {               // c:262
460                        if node.width > wids[0] {                            // c:263
461                            wids[0] = node.width;
462                        }
463                        let mut clone = Box::new({
464                            let n = node;
465                            cdstr {
466                                next: None, str: n.str.clone(), desc: n.desc.clone(),
467                                r#match: n.r#match.clone(), sortstr: n.sortstr.clone(),
468                                len: n.len, width: n.width, other: None,
469                                kind: n.kind, set: n.set, run: None,
470                            }
471                        });
472                        clone.other = None;                                  // c:265
473                        prep_lines.push(clone);
474                    }
475                    str_iter = node.next.as_deref();
476                    continue;
477                }
478                // c:270 — kind==1 leader: collect, sort its .other by
479                // width descending, update wids[i] per column.
480                let mut gs = Box::new({
481                            let n = node;
482                            cdstr {
483                                next: None, str: n.str.clone(), desc: n.desc.clone(),
484                                r#match: n.r#match.clone(), sortstr: n.sortstr.clone(),
485                                len: n.len, width: n.width, other: None,
486                                kind: n.kind, set: n.set, run: None,
487                            }
488                        });
489                gs.kind = 2;                                                 // c:271
490                gs.other = None;
491
492                // Walk node.other; build a sorted insert into gs.other
493                // by descending width (matches c:274-281).
494                let mut gp = node.other.as_deref();
495                while let Some(g_node) = gp {
496                    let new_clone = Box::new({
497                        let n = g_node;
498                        cdstr {
499                            next: None, str: n.str.clone(), desc: n.desc.clone(),
500                            r#match: n.r#match.clone(), sortstr: n.sortstr.clone(),
501                            len: n.len, width: n.width, other: None,
502                            kind: n.kind, set: n.set, run: None,
503                        }
504                    });
505                    // Sorted-insert by width descending.
506                    // Drain gs's .other chain into a flat Vec, sort-insert
507                    // new_clone, then rebuild the chain.
508                    let mut chain: Vec<Box<cdstr>> = Vec::new();
509                    // First entry: a clone of gs itself (without other).
510                    chain.push(Box::new(cdstr {
511                        next: None, str: gs.str.clone(), desc: gs.desc.clone(),
512                        r#match: gs.r#match.clone(), sortstr: gs.sortstr.clone(),
513                        len: gs.len, width: gs.width, other: None,
514                        kind: gs.kind, set: gs.set, run: None,
515                    }));
516                    let mut rest = gs.other.take();
517                    while let Some(mut n) = rest {
518                        rest = n.other.take();
519                        chain.push(n);
520                    }
521                    // Find insert index where existing.width <= new_clone.width.
522                    let mut ins = chain.len();
523                    for (i, c) in chain.iter().enumerate() {
524                        if c.width <= new_clone.width {
525                            ins = i;
526                            break;
527                        }
528                    }
529                    chain.insert(ins, new_clone);
530                    // Rebuild gs from chain[0]; link tail via .other.
531                    let mut new_head = chain.remove(0);
532                    let mut tail_ptr: *mut Option<Box<cdstr>> = &mut new_head.other;
533                    for entry in chain {
534                        unsafe {
535                            *tail_ptr = Some(entry);
536                            let nxt = &mut (*tail_ptr).as_mut().unwrap().other;
537                            tail_ptr = nxt as *mut _;
538                        }
539                    }
540                    gs = new_head;
541                    gp = g_node.other.as_deref();
542                }
543
544                // c:282-284 — update wids per column.
545                let mut col = 0usize;
546                let mut walker = Some(gs.as_ref());
547                while let Some(g) = walker {
548                    if col < wids.len() && g.width > wids[col] {
549                        wids[col] = g.width;
550                    }
551                    col += 1;
552                    walker = g.other.as_deref();
553                }
554
555                prep_lines.push(gs);
556                str_iter = node.next.as_deref();
557            }
558            set = s.next.as_deref();
559        }
560
561        // c:289-292 — gprew = sum(wids[i] + CM_SPACE).
562        let mut gprew = 0i32;
563        for w in &wids {
564            gprew += w + CM_SPACE;
565        }
566        st.gprew = gprew;
567
568        // c:294 — bail with retry if too wide.
569        if gprew > maxmlen && maxglen > 1 {
570            let _ = swidth;
571            return 1;
572        }
573
574        // c:297-303 — set sortstr from unmetafy(str) for each line.
575        for line in prep_lines.iter_mut() {
576            let s = line.str.clone().unwrap_or_default();
577            line.sortstr = Some(crate::ported::zle::zle_utils::unmetafy(&s));
578        }
579
580        // c:305 — sort if requested.
581        // We have to drop the lock briefly because cd_groups_want_sorting
582        // re-acquires it.
583        let want_sort = {
584            drop(st);
585            let r = cd_groups_want_sorting();
586            st = cd_state.lock().unwrap();
587            r
588        };
589        if want_sort != 0 {                                                  // c:305
590            prep_lines.sort_by(|a, b| cd_sort(a, b));                        // c:306
591        }
592
593        // c:308-322 — dedup-adjacent: shuffle same-desc entries together.
594        let mut i = 0usize;
595        while i + 1 < prep_lines.len() {
596            let strp_desc = prep_lines[i].desc.clone().unwrap_or_default();
597            let next_desc = prep_lines[i + 1].desc.clone().unwrap_or_default();
598            if strp_desc == next_desc {
599                i += 1;
600                continue;
601            }
602            // Find a later entry with matching desc; bubble it to i+1.
603            let mut found: Option<usize> = None;
604            for j in i + 2..prep_lines.len() {
605                if prep_lines[j].desc.clone().unwrap_or_default() == strp_desc {
606                    found = Some(j);
607                    break;
608                }
609            }
610            if let Some(j) = found {
611                let entry = prep_lines.remove(j);
612                prep_lines.insert(i + 1, entry);
613            }
614            i += 1;
615        }
616
617        let preplines = prep_lines.len();
618
619        // c:323-326 — CRT_EXPL header: link all preplines via .run.
620        // Build a chain of header cdstrs (desc + str only).
621        if preplines > 0 {
622            let mut expl_head: Option<Box<cdstr>> = None;
623            let mut tail_ptr: *mut Option<Box<cdstr>> = &mut expl_head;
624            for line in &prep_lines {
625                let header = Box::new(cdstr {
626                    next:    None,
627                    str:     line.str.clone(),
628                    desc:    line.desc.clone(),
629                    r#match: line.r#match.clone(),
630                    sortstr: line.sortstr.clone(),
631                    len:     line.len,
632                    width:   line.width,
633                    other:   None,
634                    kind:    line.kind,
635                    set:     line.set,
636                    run:     None,
637                });
638                unsafe {
639                    *tail_ptr = Some(header);
640                    let nxt = &mut (*tail_ptr).as_mut().unwrap().run;
641                    tail_ptr = nxt as *mut _;
642                }
643            }
644            // c:323-326 — emit CRT_EXPL run with the header chain.
645            let expl_run = Box::new(cdrun {
646                next: None,
647                r#type: CRT_EXPL,
648                strs: expl_head,
649                count: preplines as i32,
650            });
651            // Store at the END (matches c:373 `*runp = expl; runp = &(expl->next)`).
652            // We'll insert after column-emit runs below.
653
654            // c:328-340 — emit CRT_SPEC for each column-0 leader.
655            // Each line has a .other chain; we consume it column-by-column.
656            let mut grps: Vec<Option<Box<cdstr>>> = prep_lines
657                .into_iter()
658                .map(Some)
659                .collect();
660
661            for line_opt in grps.iter_mut() {
662                if let Some(line) = line_opt.take() {
663                    let mut owned = *line;
664                    let next_col = owned.other.take();
665                    owned.run = None;
666                    let spec_run = Box::new(cdrun {
667                        next: None,
668                        r#type: CRT_SPEC,
669                        strs: Some(Box::new(owned)),
670                        count: 1,
671                    });
672                    new_runs.push(spec_run);
673                    *line_opt = next_col;
674                }
675            }
676
677            // c:343-372 — for columns 1..maxg, emit CRT_DUMMY/CRT_SPEC.
678            for _col in 1..maxg {
679                let mut dummy_count = 0i32;
680                for line_opt in grps.iter_mut() {
681                    if let Some(line) = line_opt.take() {
682                        // Flush pending dummies first.
683                        if dummy_count > 0 {
684                            new_runs.push(Box::new(cdrun {
685                                next: None,
686                                r#type: CRT_DUMMY,
687                                strs: None,
688                                count: dummy_count,
689                            }));
690                            dummy_count = 0;
691                        }
692                        let mut owned = *line;
693                        let next_col = owned.other.take();
694                        owned.run = None;
695                        new_runs.push(Box::new(cdrun {
696                            next: None,
697                            r#type: CRT_SPEC,
698                            strs: Some(Box::new(owned)),
699                            count: 1,
700                        }));
701                        *line_opt = next_col;
702                    } else {
703                        dummy_count += 1;
704                    }
705                }
706                if dummy_count > 0 {                                         // c:365
707                    new_runs.push(Box::new(cdrun {
708                        next: None,
709                        r#type: CRT_DUMMY,
710                        strs: None,
711                        count: dummy_count,
712                    }));
713                }
714            }
715
716            // c:373 — append the expl run at the end of the column emits.
717            new_runs.push(expl_run);
718        }
719
720        // c:376-394 — emit CRT_SIMPLE per set for entries without
721        // kind and without desc (the un-described ones).
722        let mut set = st.sets.as_deref();
723        while let Some(s) = set {
724            let mut head: Option<Box<cdstr>> = None;
725            let mut tail_ptr: *mut Option<Box<cdstr>> = &mut head;
726            let mut count = 0i32;
727            let mut str_iter = s.strs.as_deref();
728            while let Some(node) = str_iter {
729                if node.kind == 0 && node.desc.is_none() {
730                    let clone = Box::new(cdstr {
731                        next:    None,
732                        str:     node.str.clone(),
733                        desc:    None,
734                        r#match: node.r#match.clone(),
735                        sortstr: node.sortstr.clone(),
736                        len:     node.len,
737                        width:   node.width,
738                        other:   None,
739                        kind:    0,
740                        set:     node.set,
741                        run:     None,
742                    });
743                    unsafe {
744                        *tail_ptr = Some(clone);
745                        let nxt = &mut (*tail_ptr).as_mut().unwrap().run;
746                        tail_ptr = nxt as *mut _;
747                    }
748                    count += 1;
749                }
750                str_iter = node.next.as_deref();
751            }
752            if count > 0 {
753                new_runs.push(Box::new(cdrun {
754                    next: None,
755                    r#type: CRT_SIMPLE,
756                    strs: head,
757                    count,
758                }));
759            }
760            set = s.next.as_deref();
761        }
762    } else if st.showd != 0 {
763        // c:395-423 — showd: emit CRT_DESC (described entries) then
764        // CRT_SIMPLE (undescribed) per set.
765        let mut set = st.sets.as_deref();
766        while let Some(s) = set {
767            if s.desc > 0 {
768                // c:397-409 — CRT_DESC for entries with descriptions.
769                let mut head: Option<Box<cdstr>> = None;
770                let mut tail: *mut Option<Box<cdstr>> = &mut head;
771                let mut str_iter = s.strs.as_deref();
772                while let Some(st_node) = str_iter {
773                    if st_node.desc.is_some() {
774                        let clone = Box::new(cdstr {
775                            next:    None,
776                            str:     st_node.str.clone(),
777                            desc:    st_node.desc.clone(),
778                            r#match: st_node.r#match.clone(),
779                            sortstr: st_node.sortstr.clone(),
780                            len:     st_node.len,
781                            width:   st_node.width,
782                            other:   None,
783                            kind:    st_node.kind,
784                            set:     st_node.set,
785                            run:     None,
786                        });
787                        unsafe {
788                            *tail = Some(clone);
789                            let nxt = &mut (*tail).as_mut().unwrap().run;
790                            tail = nxt as *mut _;
791                        }
792                    }
793                    str_iter = st_node.next.as_deref();
794                }
795                new_runs.push(Box::new(cdrun {
796                    next: None, r#type: CRT_DESC,
797                    strs: head, count: s.desc,
798                }));
799            }
800            if s.desc != s.count {
801                // c:410-422 — CRT_SIMPLE for undescribed entries.
802                let mut head: Option<Box<cdstr>> = None;
803                let mut tail: *mut Option<Box<cdstr>> = &mut head;
804                let mut str_iter = s.strs.as_deref();
805                while let Some(st_node) = str_iter {
806                    if st_node.desc.is_none() {
807                        let clone = Box::new(cdstr {
808                            next:    None,
809                            str:     st_node.str.clone(),
810                            desc:    st_node.desc.clone(),
811                            r#match: st_node.r#match.clone(),
812                            sortstr: st_node.sortstr.clone(),
813                            len:     st_node.len,
814                            width:   st_node.width,
815                            other:   None,
816                            kind:    st_node.kind,
817                            set:     st_node.set,
818                            run:     None,
819                        });
820                        unsafe {
821                            *tail = Some(clone);
822                            let nxt = &mut (*tail).as_mut().unwrap().run;
823                            tail = nxt as *mut _;
824                        }
825                    }
826                    str_iter = st_node.next.as_deref();
827                }
828                new_runs.push(Box::new(cdrun {
829                    next: None, r#type: CRT_SIMPLE,
830                    strs: head, count: s.count - s.desc,
831                }));
832            }
833            set = s.next.as_deref();
834        }
835    } else {
836        // c:424-435 — default: one CRT_SIMPLE per non-empty set.
837        let mut set = st.sets.as_deref();
838        while let Some(s) = set {
839            if s.count != 0 {
840                // c:431 — link str.run = str.next for each entry.
841                let mut head: Option<Box<cdstr>> = None;
842                let mut tail: *mut Option<Box<cdstr>> = &mut head;
843                let mut str_iter = s.strs.as_deref();
844                while let Some(st_node) = str_iter {
845                    let clone = Box::new(cdstr {
846                        next:    None,
847                        str:     st_node.str.clone(),
848                        desc:    st_node.desc.clone(),
849                        r#match: st_node.r#match.clone(),
850                        sortstr: st_node.sortstr.clone(),
851                        len:     st_node.len,
852                        width:   st_node.width,
853                        other:   None,
854                        kind:    st_node.kind,
855                        set:     st_node.set,
856                        run:     None,
857                    });
858                    unsafe {
859                        *tail = Some(clone);
860                        let nxt = &mut (*tail).as_mut().unwrap().run;
861                        tail = nxt as *mut _;
862                    }
863                    str_iter = st_node.next.as_deref();
864                }
865                new_runs.push(Box::new(cdrun {
866                    next: None, r#type: CRT_SIMPLE,
867                    strs: head, count: s.count,
868                }));
869            }
870            set = s.next.as_deref();
871        }
872    }
873
874    // Link new_runs as a chain into cd_state.runs.
875    let mut head: Option<Box<cdrun>> = None;
876    for run in new_runs.into_iter().rev() {
877        let mut run = run;
878        run.next = head;
879        head = Some(run);
880    }
881    st.runs = head;
882    0                                                                        // c:438
883}
884
885/// Direct port of `static int cd_init(char *nam, char *hide, char *mlen,
886///                                       char *sep, char **opts, char **args,
887///                                       int disp)`
888/// from `Src/Zle/computil.c:477-594`. Parses the `_describe` input
889/// (match arrays + optional display arrays) into the `cd_state.sets`
890/// chain, then runs `cd_calc` + `cd_prep` to build the run chain.
891///
892/// `args` is the consolidated arg list — match-array param name,
893/// optional disp-array name, optional `--`-separated per-set opts.
894/// `-g` prefix on `args` enables group detection (cd_group loop).
895pub fn cd_init(nam: &str, hide: &str, mlen: &str, sep: &str,                 // c:477
896               opts: &[String], args: &[String], disp: i32) -> i32 {
897    use crate::ported::zle::compcore::{get_user_var, rembslash};
898
899    // c:485 — discard prior parsed state.
900    if cd_parsed.load(std::sync::atomic::Ordering::Relaxed) != 0 {
901        let mut st = cd_state.lock().unwrap();
902        st.sep = None;
903        freecdsets(st.sets.take());
904        cd_parsed.store(0, std::sync::atomic::Ordering::Relaxed);
905    }
906
907    // c:491 — seed cd_state.
908    {
909        let mut st = cd_state.lock().unwrap();
910        st.sep = Some(sep.to_string());
911        st.slen = sep.len() as i32;
912        st.swidth = crate::ported::utils::niceztrlen(sep) as i32;
913        st.sets = None;
914        st.showd = disp;
915        st.maxg = 0;
916        st.groups = 0;
917        st.descs = 0;
918        st.maxmlen = mlen.parse::<i32>().unwrap_or(0);
919        st.premaxw = 0;
920        let cols = crate::ported::utils::adjustcolumns() as i32;
921        let itmp = cols - st.swidth - 4;                                     // c:499
922        if st.maxmlen > itmp { st.maxmlen = itmp; }
923        if st.maxmlen < 4 { st.maxmlen = 4; }
924    }
925
926    // c:504 — strip leading `-g` for group detection.
927    let mut idx = 0usize;
928    let grp = if args.first().map(|s| s.as_str()) == Some("-g") {
929        idx = 1;
930        true
931    } else {
932        false
933    };
934
935    // c:508 — walk arg pairs (match-array [disp-array] [-- opts]).
936    let mut sets_collected: Vec<Box<cdset>> = Vec::new();
937    while idx < args.len() {
938        let arg = &args[idx];
939        let Some(mat_arr) = get_user_var(Some(arg.as_str())) else {          // c:515
940            zwarnnam(nam, &format!("invalid argument: {}", arg));
941            let mut st = cd_state.lock().unwrap();
942            st.sep = None;
943            freecdsets(st.sets.take());
944            return 1;
945        };
946        idx += 1;
947
948        // c:521-543 — parse `match:desc` entries into cdstr chain.
949        let mut strs_vec: Vec<Box<cdstr>> = Vec::new();
950        for entry in &mat_arr {
951            let bytes = entry.as_bytes();
952            let mut p = 0usize;
953            while p < bytes.len() && bytes[p] != b':' {                      // c:530
954                if bytes[p] == b'\\' && p + 1 < bytes.len() { p += 1; }
955                p += 1;
956            }
957            let (match_part, desc_part) = if p < bytes.len() {
958                let m = std::str::from_utf8(&bytes[..p]).unwrap_or("");
959                let d = std::str::from_utf8(&bytes[p + 1..]).unwrap_or("");
960                (rembslash(m), Some(rembslash(d)))
961            } else {
962                (rembslash(entry), None)
963            };
964            let str_s = match_part.clone();
965            let mut new_str = Box::new(cdstr::default());
966            new_str.str = Some(str_s.clone());
967            new_str.r#match = Some(str_s.clone());
968            new_str.desc = desc_part;
969            new_str.len = str_s.len() as i32;
970            new_str.width = crate::ported::utils::niceztrlen(&str_s) as i32;
971            new_str.kind = 0;
972            strs_vec.push(new_str);
973        }
974
975        // c:547-557 — optional separate match array.
976        if idx < args.len() && !args[idx].starts_with('-') {
977            let Some(match_arr) = get_user_var(Some(args[idx].as_str())) else {
978                zwarnnam(nam, &format!("invalid argument: {}", args[idx]));
979                let mut st = cd_state.lock().unwrap();
980                st.sep = None;
981                freecdsets(st.sets.take());
982                return 1;
983            };
984            for (i, m) in match_arr.iter().enumerate() {
985                if i < strs_vec.len() {
986                    strs_vec[i].r#match = Some(m.clone());
987                }
988            }
989            idx += 1;
990        }
991
992        // c:559 — apply hide (strip leading `-`/`--` from str).
993        if !hide.is_empty() {
994            let hb = hide.as_bytes();
995            let double = hb.len() > 1;
996            for s in strs_vec.iter_mut() {
997                if let Some(cur) = s.str.clone() {
998                    let mut bytes = cur.into_bytes();
999                    if double && bytes.len() >= 2 && bytes[0] == b'-' && bytes[1] == b'-' {
1000                        bytes.drain(0..2);                                   // c:564
1001                    } else if !bytes.is_empty() && (bytes[0] == b'-' || bytes[0] == b'+') {
1002                        bytes.drain(0..1);                                   // c:566
1003                    }
1004                    s.str = String::from_utf8(bytes).ok();
1005                }
1006            }
1007        }
1008
1009        // c:569-577 — gather per-set opts up to `--`.
1010        let opt_start = idx;
1011        while idx < args.len()
1012            && !(args[idx].as_bytes().len() == 2
1013                 && args[idx].as_bytes()[0] == b'-'
1014                 && args[idx].as_bytes()[1] == b'-')
1015        {
1016            idx += 1;
1017        }
1018        let per_set: &[String] = &args[opt_start..idx];
1019        let combined = cd_arrcat(per_set, opts);
1020        if idx < args.len() { idx += 1; }                                    // c:577 skip `--`
1021
1022        // Link strs_vec as a chain into a new cdset.
1023        let mut strs_head: Option<Box<cdstr>> = None;
1024        for s in strs_vec.into_iter().rev() {
1025            let mut s = s;
1026            s.next = strs_head;
1027            strs_head = Some(s);
1028        }
1029        let mut set = Box::new(cdset::default());
1030        set.opts = Some(combined);
1031        set.strs = strs_head;
1032        sets_collected.push(set);
1033    }
1034
1035    // Link sets_collected as a chain into cd_state.sets.
1036    {
1037        let mut head: Option<Box<cdset>> = None;
1038        for s in sets_collected.into_iter().rev() {
1039            let mut s = s;
1040            s.next = head;
1041            head = Some(s);
1042        }
1043        cd_state.lock().unwrap().sets = head;
1044    }
1045
1046    // c:579 — group-aware vs simple prep.
1047    if disp != 0 && grp {
1048        let cols = crate::ported::utils::adjustcolumns() as i32;
1049        let mut mg = cols;
1050        // c:582-586 — retry cd_prep with shrinking maxg.
1051        loop {
1052            cd_group(mg);
1053            mg = {
1054                let st = cd_state.lock().unwrap();
1055                st.maxg - 1
1056            };
1057            cd_calc();
1058            if cd_prep() == 0 || mg <= 0 { break; }
1059        }
1060    } else {
1061        cd_calc();
1062        cd_prep();
1063    }
1064    cd_parsed.store(1, std::sync::atomic::Ordering::Relaxed);                // c:592
1065    0
1066}
1067
1068/// Direct port of `static int cd_get(char **params)` from
1069/// `Src/Zle/computil.c:614-841`. Pops the next `cdrun` off
1070/// `cd_state.runs` and emits its match/display arrays + per-run
1071/// compadd options into the four named params:
1072///   params[0] = csl ("" or "packed")
1073///   params[1] = opts (compadd flags)
1074///   params[2] = mats (match strings)
1075///   params[3] = dpys (display strings)
1076/// Returns 1 when no runs remain, 0 otherwise.
1077pub fn cd_get(params: &[String]) -> i32 {                                    // c:614
1078    use crate::ported::params::{setsparam, setaparam};
1079
1080    // c:618 — pop the head run.
1081    let run_opt = {
1082        let mut st = cd_state.lock().unwrap();
1083        st.runs.take().map(|mut r| {
1084            let next = r.next.take();
1085            st.runs = next;
1086            r
1087        })
1088    };
1089    let Some(run) = run_opt else { return 1; };
1090
1091    let mut mats: Vec<String> = Vec::new();
1092    let mut dpys: Vec<String> = Vec::new();
1093    let mut opts: Vec<String> = Vec::new();
1094    let mut csl: String = String::new();
1095
1096    let rtype = run.r#type;
1097
1098    // Helper: walk a cdstr chain via .run, applying f.
1099    let mut walk_run = |head: &Option<Box<cdstr>>, mut f: Box<dyn FnMut(&cdstr)>| {
1100        let mut cur = head.as_deref();
1101        while let Some(s) = cur {
1102            f(s);
1103            cur = s.run.as_deref();
1104        }
1105    };
1106
1107    if rtype == CRT_SIMPLE {                                                 // c:625
1108        let head_opts = run.strs.as_deref()
1109            .map(|s| {
1110                let st = cd_state.lock().unwrap();
1111                // c:634 — zarrdup(run->strs->set->opts). Set is an index;
1112                // we walk cd_state.sets to find the matching index.
1113                let mut set_iter = st.sets.as_deref();
1114                let mut found: Option<Vec<String>> = None;
1115                let mut idx_count = 0usize;
1116                while let Some(set) = set_iter {
1117                    if idx_count == s.set {
1118                        found = set.opts.clone();
1119                        break;
1120                    }
1121                    idx_count += 1;
1122                    set_iter = set.next.as_deref();
1123                }
1124                found.unwrap_or_default()
1125            })
1126            .unwrap_or_default();
1127        walk_run(&run.strs, Box::new(|s| {                                    // c:629
1128            mats.push(s.r#match.clone().unwrap_or_default());
1129            dpys.push(s.str.clone().or_else(|| s.r#match.clone()).unwrap_or_default());
1130        }));
1131        let groups_flag = cd_state.lock().unwrap().groups;
1132        opts = if groups_flag != 0 {                                          // c:635
1133            // c:641 — strip `-X` options.
1134            let mut filtered: Vec<String> = Vec::new();
1135            let mut skip_next = false;
1136            for o in head_opts.iter() {
1137                if skip_next { skip_next = false; continue; }
1138                if o.starts_with("-X") {                                     // c:642
1139                    if o.len() == 2 { skip_next = true; }                    // c:643
1140                    continue;
1141                }
1142                filtered.push(o.clone());
1143            }
1144            filtered
1145        } else {
1146            head_opts
1147        };
1148    } else if rtype == CRT_DESC {                                            // c:652
1149        let st_snapshot = {
1150            let st = cd_state.lock().unwrap();
1151            (st.pre, st.suf, st.premaxw, st.slen, st.swidth, st.sep.clone(),
1152             crate::ported::utils::adjustcolumns() as i32)
1153        };
1154        let (cd_pre, _cd_suf, cd_premaxw, _cd_slen, cd_swidth, cd_sep, cols) = st_snapshot;
1155        let sep_str = cd_sep.unwrap_or_default();
1156        walk_run(&run.strs, Box::new(|s| {                                    // c:669
1157            let str_s = s.str.clone().unwrap_or_default();
1158            let desc_s = s.desc.clone().unwrap_or_default();
1159            let mut buf = String::with_capacity(
1160                (cd_pre + cd_premaxw + cd_swidth + 16) as usize);
1161            // c:674 — write str.
1162            buf.push_str(&str_s);
1163            // c:676 — pad to premaxw + CM_SPACE.
1164            let pad = (cd_premaxw - s.width + CM_SPACE).max(0) as usize;
1165            for _ in 0..pad { buf.push(' '); }
1166
1167            // c:679-715 — append separator + truncated desc to fit terminal.
1168            let mut remw = cols - cd_premaxw - cd_swidth - 3;
1169            while remw < 0 && cols > 0 { remw += cols; }
1170            if (sep_str.len() as i32) < remw {                                // c:685
1171                buf.push_str(&sep_str);
1172                remw -= sep_str.len() as i32;
1173                let dw = crate::ported::utils::niceztrlen(&desc_s) as i32;
1174                if dw <= remw {
1175                    buf.push_str(&desc_s);
1176                } else {                                                     // c:701
1177                    // Truncate desc to fit. Use char boundaries.
1178                    let mut w_used = 0i32;
1179                    for ch in desc_s.chars() {
1180                        let cw = crate::ported::utils::niceztrlen(&ch.to_string()) as i32;
1181                        if w_used + cw > remw { break; }
1182                        buf.push(ch);
1183                        w_used += cw;
1184                    }
1185                }
1186            }
1187            mats.push(s.r#match.clone().unwrap_or_default());                 // c:673
1188            dpys.push(buf);
1189        }));
1190        // c:721 — opts = cd_arrdup + opts[0] = "-l".
1191        let head_opts = run.strs.as_deref()
1192            .map(|s| {
1193                let st = cd_state.lock().unwrap();
1194                let mut set_iter = st.sets.as_deref();
1195                let mut found: Option<Vec<String>> = None;
1196                let mut idx_count = 0usize;
1197                while let Some(set) = set_iter {
1198                    if idx_count == s.set { found = set.opts.clone(); break; }
1199                    idx_count += 1;
1200                    set_iter = set.next.as_deref();
1201                }
1202                found.unwrap_or_default()
1203            })
1204            .unwrap_or_default();
1205        opts = std::iter::once("-l".to_string()).chain(head_opts).collect();
1206    } else if rtype == CRT_SPEC {                                            // c:726
1207        let s = run.strs.as_deref();
1208        if let Some(s) = s {
1209            mats.push(s.r#match.clone().unwrap_or_default());
1210            dpys.push(s.str.clone().unwrap_or_default());
1211        }
1212        // c:732 — opts = cd_arrdup + flip -J/-V to -2V or insert -2V-default-.
1213        let head_opts = s.map(|s| {
1214            let st = cd_state.lock().unwrap();
1215            let mut set_iter = st.sets.as_deref();
1216            let mut found: Option<Vec<String>> = None;
1217            let mut idx_count = 0usize;
1218            while let Some(set) = set_iter {
1219                if idx_count == s.set { found = set.opts.clone(); break; }
1220                idx_count += 1;
1221                set_iter = set.next.as_deref();
1222            }
1223            found.unwrap_or_default()
1224        }).unwrap_or_default();
1225        let mut new_opts: Vec<String> = head_opts.clone();
1226        let mut found_jv = false;
1227        // c:736 — `for (dp = opts + 1; *dp; dp++)`. Skip slot 0 (the
1228        // existing first element which we'll overwrite below) and look
1229        // for the first -J/-V flag.
1230        for i in 1..new_opts.len() {
1231            if new_opts[i].starts_with("-J") || new_opts[i].starts_with("-V") {
1232                let rest = new_opts[i][2..].to_string();
1233                new_opts[i] = format!("-2V{}", rest);
1234                found_jv = true;
1235                break;
1236            }
1237        }
1238        if !found_jv {
1239            new_opts.insert(0, "-2V-default-".to_string());                  // c:750
1240        }
1241        opts = new_opts;
1242        csl = "packed".to_string();
1243    } else if rtype == CRT_DUMMY {                                           // c:754
1244        // c:758 — opts[0] = "-E<count>".
1245        let head_opts = run.strs.as_deref().map(|s| {
1246            let st = cd_state.lock().unwrap();
1247            let mut set_iter = st.sets.as_deref();
1248            let mut found: Option<Vec<String>> = None;
1249            let mut idx_count = 0usize;
1250            while let Some(set) = set_iter {
1251                if idx_count == s.set { found = set.opts.clone(); break; }
1252                idx_count += 1;
1253                set_iter = set.next.as_deref();
1254            }
1255            found.unwrap_or_default()
1256        }).unwrap_or_default();
1257        opts = std::iter::once(format!("-E{}", run.count))
1258            .chain(head_opts).collect();
1259        csl = "packed".to_string();
1260    } else if rtype == CRT_EXPL {                                            // c:772
1261        let st_snapshot = {
1262            let st = cd_state.lock().unwrap();
1263            (st.suf, st.slen, st.swidth, st.gprew, st.sep.clone(),
1264             crate::ported::utils::adjustcolumns() as i32)
1265        };
1266        let (_cd_suf, _cd_slen, cd_swidth, cd_gprew, cd_sep, cols) = st_snapshot;
1267        let sep_str = cd_sep.unwrap_or_default();
1268        let count = run.count;
1269
1270        walk_run(&run.strs, Box::new(|s| {                                    // c:785
1271            // c:786 — if run sibling has same desc, emit empty.
1272            let next_desc = s.run.as_deref().and_then(|n| n.desc.clone());
1273            if next_desc.is_some() && next_desc == s.desc {
1274                dpys.push(String::new());
1275                return;
1276            }
1277            let mut buf = String::new();
1278            buf.push_str(&sep_str);
1279            let mut remw = cols - cd_gprew - cd_swidth - CM_SPACE;
1280            let desc_s = s.desc.clone().unwrap_or_default();
1281            let dw = crate::ported::utils::niceztrlen(&desc_s) as i32;
1282            if dw <= remw {                                                   // c:797
1283                buf.push_str(&desc_s);
1284                remw -= dw;
1285            } else {
1286                for ch in desc_s.chars() {
1287                    let cw = crate::ported::utils::niceztrlen(&ch.to_string()) as i32;
1288                    if cw > remw { break; }
1289                    buf.push(ch);
1290                    remw -= cw;
1291                }
1292            }
1293            while remw > 0 {                                                  // c:817
1294                buf.push(' ');
1295                remw -= 1;
1296            }
1297            dpys.push(buf);
1298        }));
1299        // c:825 — opts[0] = "-E<count>".
1300        let head_opts = run.strs.as_deref().map(|s| {
1301            let st = cd_state.lock().unwrap();
1302            let mut set_iter = st.sets.as_deref();
1303            let mut found: Option<Vec<String>> = None;
1304            let mut idx_count = 0usize;
1305            while let Some(set) = set_iter {
1306                if idx_count == s.set { found = set.opts.clone(); break; }
1307                idx_count += 1;
1308                set_iter = set.next.as_deref();
1309            }
1310            found.unwrap_or_default()
1311        }).unwrap_or_default();
1312        opts = std::iter::once(format!("-E{}", count))
1313            .chain(head_opts).collect();
1314        csl = "packed".to_string();
1315    }
1316
1317    // c:832 — emit the four params.
1318    if params.len() >= 4 {
1319        setsparam(&params[0], &csl);
1320        setaparam(&params[1], opts);
1321        setaparam(&params[2], mats);
1322        setaparam(&params[3], dpys);
1323    }
1324    0                                                                        // c:839
1325}
1326
1327/// Direct port of `static char **cd_arrcat(char **a, char **b)` from
1328/// `Src/Zle/computil.c:599`.
1329pub fn cd_arrcat(a: &[String], b: &[String]) -> Vec<String> {                // c:444
1330    let mut out = a.to_vec();
1331    out.extend_from_slice(b);
1332    out
1333}
1334
1335/// Direct port of `static char **cd_arrdup(char **a)` from
1336/// `Src/Zle/computil.c:somewhere`. Duplicate a string array.
1337pub fn cd_arrdup(a: &[String]) -> Vec<String> {                              // c:cd_arrdup
1338    a.to_vec()
1339}
1340
1341/// Direct port of `static void freecdsets(Cdset p)` from
1342/// `Src/Zle/computil.c:97`. Walks the cdset `next` chain
1343/// freeing each set's opts/strs sub-chains and the cd_state runs
1344/// list at the end.
1345pub fn freecdsets(mut p: Option<Box<cdset>>) {                               // c:97
1346    while let Some(mut set) = p {                                            // c:97 for (; p; ...)
1347        p = set.next.take();                                                 // c:104 n = p->next
1348        // c:105-106 — `if (p->opts) freearray(p->opts)`.
1349        set.opts = None;
1350        // c:107-115 — for each cdstr: free sortstr/str/desc/match.
1351        let mut s = set.strs.take();
1352        while let Some(mut node) = s {
1353            s = node.next.take();
1354            node.sortstr = None;                                             // c:109
1355            node.str = None;                                                 // c:110
1356            node.desc = None;                                                // c:111
1357            // c:112-113 — `if (s->match != s->str) zsfree(s->match)`.
1358            // Rust's Option<String> drop is unconditional; the C
1359            // pointer-equality guard collapses out.
1360            node.r#match = None;
1361            drop(node);                                                      // c:114
1362        }
1363        // c:116-119 — drain cd_state.runs.
1364        if let Ok(mut st) = cd_state.lock() {
1365            let mut r = st.runs.take();
1366            while let Some(mut run) = r {
1367                r = run.next.take();
1368                drop(run);                                                   // c:118
1369            }
1370        }
1371        drop(set);                                                           // c:120
1372    }
1373}
1374
1375// =====================================================================
1376// `_arguments`-cache types — direct ports of the C structs at
1377// Src/Zle/computil.c:899-968. CAO_* / CAA_* / CDF_SEP /
1378// MAX_CACACHE constants already declared above (file scope).
1379// =====================================================================
1380
1381/// Port of `typedef struct cadef *Cadef` from `Src/Zle/computil.c:899`.
1382pub type Cadef = Box<cadef>;                                                 // c:899
1383/// Port of `typedef struct caopt *Caopt` from `Src/Zle/computil.c:900`.
1384pub type Caopt = Box<caopt>;                                                 // c:900
1385/// Port of `typedef struct caarg *Caarg` from `Src/Zle/computil.c:901`.
1386pub type Caarg = Box<caarg>;                                                 // c:901
1387
1388/// Direct port of `struct caarg` from `Src/Zle/computil.c:949-962`.
1389/// Description for one `_arguments` argument spec.
1390#[derive(Debug, Default, Clone)]
1391#[allow(non_camel_case_types)]
1392pub struct caarg {                                                           // c:949
1393    pub next:   Option<Box<caarg>>,                                          // c:950 Caarg next
1394    pub descr:  Option<String>,                                              // c:951 char *descr
1395    pub xor:    Option<Vec<String>>,                                         // c:952 char **xor
1396    pub action: Option<String>,                                              // c:953 char *action
1397    pub r#type: i32,                                                         // c:954 int type (CAA_*)
1398    pub end:    Option<String>,                                              // c:955 char *end
1399    pub opt:    Option<String>,                                              // c:956 char *opt
1400    pub num:    i32,                                                         // c:957 int num
1401    pub min:    i32,                                                         // c:958 int min
1402    pub direct: i32,                                                         // c:959 int direct
1403    pub active: i32,                                                         // c:960 int active
1404    pub gsname: Option<String>,                                              // c:961 char *gsname
1405}
1406
1407/// Direct port of `struct caopt` from `Src/Zle/computil.c:928-939`.
1408/// Description for one `_arguments` option spec.
1409#[derive(Debug, Default, Clone)]
1410#[allow(non_camel_case_types)]
1411pub struct caopt {                                                           // c:928
1412    pub next:   Option<Box<caopt>>,                                          // c:929 Caopt next
1413    pub name:   Option<String>,                                              // c:930 char *name
1414    pub descr:  Option<String>,                                              // c:931 char *descr
1415    pub xor:    Option<Vec<String>>,                                         // c:932 char **xor
1416    pub r#type: i32,                                                         // c:933 int type (CAO_*)
1417    pub args:   Option<Box<caarg>>,                                          // c:934 Caarg args
1418    pub active: i32,                                                         // c:935 int active
1419    pub num:    i32,                                                         // c:936 int num
1420    pub gsname: Option<String>,                                              // c:937 char *gsname
1421    pub not:    i32,                                                         // c:938 int not
1422}
1423
1424/// Direct port of `struct cadef` from `Src/Zle/computil.c:905-922`.
1425/// Cache entry for a set of `_arguments` definitions.
1426#[derive(Debug, Default, Clone)]
1427#[allow(non_camel_case_types)]
1428pub struct cadef {                                                           // c:905
1429    pub next:       Option<Box<cadef>>,                                      // c:906 Cadef next
1430    pub snext:      Option<Box<cadef>>,                                      // c:907 Cadef snext
1431    pub opts:       Option<Box<caopt>>,                                      // c:908 Caopt opts
1432    pub nopts:      i32,                                                     // c:909
1433    pub ndopts:     i32,                                                     // c:909
1434    pub nodopts:    i32,                                                     // c:909
1435    pub args:       Option<Box<caarg>>,                                      // c:910 Caarg args
1436    pub rest:       Option<Box<caarg>>,                                      // c:911 Caarg rest
1437    pub defs:       Option<Vec<String>>,                                     // c:912 char **defs
1438    pub ndefs:      i32,                                                     // c:913
1439    pub lastt:      i64,                                                     // c:914 time_t lastt
1440    pub single:     Option<Vec<Option<Box<caopt>>>>,                         // c:915 Caopt *single (188-slot)
1441    pub r#match:    Option<String>,                                          // c:916 char *match
1442    pub argsactive: i32,                                                     // c:917
1443    pub set:        Option<String>,                                          // c:919 char *set
1444    pub flags:      i32,                                                     // c:920 int flags (CDF_*)
1445    pub nonarg:     Option<String>,                                          // c:921 char *nonarg
1446}
1447
1448/// Direct port of `static void freecaargs(Caarg a)` from
1449/// `Src/Zle/computil.c:996`. Walks the `next` chain and frees
1450/// each entry. In Rust this is `Box` ownership — dropping the head
1451/// recursively drops the chain, but we mirror the C body for ABI
1452/// parity with callers that want explicit teardown.
1453pub fn freecaargs(mut a: Option<Box<caarg>>) {                               // c:996
1454    while let Some(mut node) = a {                                           // c:996 for (; a; ...)
1455        a = node.next.take();                                                // c:1001 n = a->next
1456        // c:1002-1007 — zsfree on descr/xor/action/end/opt is implicit
1457        //               via Drop on the String / Vec<String> fields.
1458        node.descr = None;                                                   // c:1013
1459        node.xor = None;                                                     // c:1013-1004
1460        node.action = None;                                                  // c:1013
1461        node.end = None;                                                     // c:1013
1462        node.opt = None;                                                     // c:1013
1463        drop(node);                                                          // c:1013 zfree(a, sizeof(*a))
1464    }
1465}
1466
1467/// Direct port of `static void freecadef(Cadef d)` from
1468/// `Src/Zle/computil.c:1013`. Walks the `snext` chain freeing
1469/// each cadef plus its opts/args/rest sub-chains.
1470pub fn freecadef(mut d: Option<Box<cadef>>) {                                // c:1013
1471    while let Some(mut node) = d {                                           // c:1013 while (d)
1472        d = node.snext.take();                                               // c:1019 s = d->snext
1473        // c:1020-1023 — zsfree match/set, freearray(defs).
1474        node.r#match = None;
1475        node.set = None;
1476        node.defs = None;
1477
1478        // c:1025-1033 — for each opt: zsfree name/descr, freearray xor,
1479        // freecaargs(opt->args), zfree opt.
1480        let mut p = node.opts.take();
1481        while let Some(mut popt) = p {
1482            p = popt.next.take();
1483            popt.name = None;
1484            popt.descr = None;
1485            popt.xor = None;
1486            freecaargs(popt.args.take());                                    // c:1031
1487            drop(popt);                                                      // c:1032
1488        }
1489        freecaargs(node.args.take());                                        // c:1034
1490        freecaargs(node.rest.take());                                        // c:1035
1491        node.nonarg = None;                                                  // c:1036
1492        node.single = None;                                                  // c:1037-1038
1493        drop(node);                                                          // c:1039 zfree(d, sizeof(*d))
1494    }
1495}
1496
1497// =====================================================================
1498// `castate` — command-line parse state for `_arguments`.
1499// Src/Zle/computil.c:1920-1957.
1500// =====================================================================
1501
1502/// Port of `typedef struct castate *Castate` from
1503/// `Src/Zle/computil.c:1922`.
1504pub type Castate = Box<castate>;                                             // c:1922
1505
1506/// Direct port of `struct castate` from `Src/Zle/computil.c:1928-1953`.
1507/// Encapsulates the parsed-command-line state for one `_arguments`
1508/// set — used as a linked list (`snext`) with one state per set.
1509#[derive(Debug, Default, Clone)]
1510#[allow(non_camel_case_types)]
1511pub struct castate {                                                         // c:1928
1512    pub snext:   Option<Box<castate>>,                                       // c:1929 Castate snext
1513    pub d:       Option<Box<cadef>>,                                         // c:1930 Cadef d
1514    pub nopts:   i32,                                                        // c:1931
1515    pub def:     Option<Box<caarg>>,                                         // c:1932 Caarg def
1516    pub ddef:    Option<Box<caarg>>,                                         // c:1933 Caarg ddef
1517    pub curopt:  Option<Box<caopt>>,                                         // c:1934 Caopt curopt
1518    pub dopt:    Option<Box<caopt>>,                                         // c:1935 Caopt dopt
1519    pub opt:     i32,                                                        // c:1936
1520    pub arg:     i32,                                                        // c:1937
1521    pub argbeg:  i32,                                                        // c:1938
1522    pub optbeg:  i32,                                                        // c:1939
1523    pub nargbeg: i32,                                                        // c:1941
1524    pub restbeg: i32,                                                        // c:1942
1525    pub curpos:  i32,                                                        // c:1943
1526    pub argend:  i32,                                                        // c:1944
1527    pub inopt:   i32,                                                        // c:1945
1528    pub inarg:   i32,                                                        // c:1946
1529    pub nth:     i32,                                                        // c:1947
1530    pub singles: i32,                                                        // c:1948
1531    pub oopt:    i32,                                                        // c:1949
1532    pub actopts: i32,                                                        // c:1950
1533    pub args:    Option<Vec<String>>,                                        // c:1951 LinkList args
1534    pub oargs:   Option<Vec<Option<Vec<String>>>>,                           // c:1952 LinkList *oargs
1535}
1536
1537/// Port of `static struct castate ca_laststate` from
1538/// `Src/Zle/computil.c:1955`. Most recently parsed cmdline state.
1539pub static ca_laststate: std::sync::Mutex<castate> =                         // c:1955
1540    std::sync::Mutex::new(castate {
1541        snext: None, d: None, nopts: 0, def: None, ddef: None,
1542        curopt: None, dopt: None, opt: 0, arg: 0, argbeg: 0, optbeg: 0,
1543        nargbeg: 0, restbeg: 0, curpos: 0, argend: 0, inopt: 0,
1544        inarg: 0, nth: 0, singles: 0, oopt: 0, actopts: 0,
1545        args: None, oargs: None,
1546    });
1547
1548/// Port of `static int ca_parsed` from `Src/Zle/computil.c:1956`.
1549pub static ca_parsed: std::sync::atomic::AtomicI32 =                         // c:1956
1550    std::sync::atomic::AtomicI32::new(0);
1551
1552/// Port of `static int ca_alloced` from `Src/Zle/computil.c:1960`.
1553pub static ca_alloced: std::sync::atomic::AtomicI32 =                        // c:1960
1554    std::sync::atomic::AtomicI32::new(0);
1555
1556/// Port of `static int ca_doff` from `Src/Zle/computil.c:1960`. Count
1557/// of chars of ignored prefix (for clumped options or arg to an
1558/// option).
1559pub static ca_doff: std::sync::atomic::AtomicI32 =                           // c:1960
1560    std::sync::atomic::AtomicI32::new(0);
1561
1562/// Direct port of `static void freecastate(Castate s)` from
1563/// `Src/Zle/computil.c:1960`. Frees the args/oargs lists.
1564pub fn freecastate(s: &mut castate) {                                        // c:1960
1565    s.args = None;                                                           // c:1960 freelinklist(s->args)
1566    s.oargs = None;                                                          // c:1966-1969 freelinklist per slot
1567}
1568
1569// =====================================================================
1570// `cvdef` / `cvval` — `_values` completion cache types.
1571// Src/Zle/computil.c:2919-2956. CVV_* and MAX_CVCACHE consts
1572// already declared above (file scope).
1573// =====================================================================
1574
1575/// Port of `typedef struct cvdef *Cvdef` from `Src/Zle/computil.c:2919`.
1576pub type Cvdef = Box<cvdef>;                                                 // c:2919
1577/// Port of `typedef struct cvval *Cvval` from `computil.c:2920`.
1578pub type Cvval = Box<cvval>;                                                 // c:2920
1579
1580/// Direct port of `struct cvdef` from `Src/Zle/computil.c:2924-2935`.
1581/// One parsed `_values` definition entry, cached for reuse.
1582#[derive(Debug, Default, Clone)]
1583#[allow(non_camel_case_types)]
1584pub struct cvdef {                                                           // c:2924
1585    pub descr:  Option<String>,                                              // c:2925 char *descr
1586    pub hassep: i32,                                                         // c:2926
1587    pub sep:    i32,                                                         // c:2927 char sep
1588    pub argsep: i32,                                                         // c:2928 char argsep
1589    pub next:   Option<Box<cvdef>>,                                          // c:2929 Cvdef next
1590    pub vals:   Option<Box<cvval>>,                                          // c:2930 Cvval vals
1591    pub defs:   Option<Vec<String>>,                                         // c:2931 char **defs
1592    pub ndefs:  i32,                                                         // c:2932
1593    pub lastt:  i64,                                                         // c:2933 time_t lastt
1594    pub words:  i32,                                                         // c:2934
1595}
1596
1597/// Direct port of `struct cvval` from `Src/Zle/computil.c:2939-2947`.
1598/// One value definition inside a cvdef.
1599#[derive(Debug, Default, Clone)]
1600#[allow(non_camel_case_types)]
1601pub struct cvval {                                                           // c:2939
1602    pub next:   Option<Box<cvval>>,                                          // c:2940 Cvval next
1603    pub name:   Option<String>,                                              // c:2961 char *name
1604    pub descr:  Option<String>,                                              // c:2961 char *descr
1605    pub xor:    Option<Vec<String>>,                                         // c:2961 char **xor
1606    pub r#type: i32,                                                         // c:2961 int type (CVV_*)
1607    pub arg:    Option<Box<caarg>>,                                          // c:2961 Caarg arg
1608    pub active: i32,                                                         // c:2961
1609}
1610
1611/// Direct port of `static void freecvdef(Cvdef d)` from
1612/// `Src/Zle/computil.c:2961`. Walks the vals chain freeing
1613/// each cvval (which frees its caarg via freecaargs).
1614pub fn freecvdef(d: Option<Box<cvdef>>) {                                    // c:2961
1615    let Some(mut node) = d else { return; };                                 // c:2961 if (d)
1616    node.descr = None;                                                       // c:2966 zsfree(d->descr)
1617    node.defs = None;                                                        // c:2967-2968 freearray(d->defs)
1618    let mut p = node.vals.take();
1619    while let Some(mut v) = p {                                              // c:2970 for (p = d->vals; ...)
1620        p = v.next.take();                                                   // c:2971 n = p->next
1621        v.name = None;                                                       // c:2972
1622        v.descr = None;                                                      // c:2973
1623        v.xor = None;                                                        // c:2974-2975
1624        freecaargs(v.arg.take());                                            // c:2976
1625        drop(v);                                                             // c:2977
1626    }
1627    drop(node);                                                              // c:2979
1628}
1629
1630// =====================================================================
1631// `cvstate` — `_values` parse state.
1632// Src/Zle/computil.c:3220-3231.
1633// =====================================================================
1634
1635/// Direct port of `struct cvstate` from `Src/Zle/computil.c:3222-3227`.
1636#[derive(Debug, Default)]
1637#[allow(non_camel_case_types)]
1638pub struct cvstate {                                                         // c:3222
1639    pub d:    Option<Box<cvdef>>,                                            // c:3223 Cvdef d
1640    pub def:  Option<Box<caarg>>,                                            // c:3224 Caarg def
1641    pub val:  Option<Box<cvval>>,                                            // c:3225 Cvval val
1642    pub vals: Option<Vec<String>>,                                           // c:3226 LinkList vals
1643}
1644
1645/// Port of `static struct cvstate cv_laststate` from
1646/// `Src/Zle/computil.c:3229`.
1647pub static cv_laststate: std::sync::Mutex<cvstate> =                         // c:3229
1648    std::sync::Mutex::new(cvstate {
1649        d: None, def: None, val: None, vals: None,
1650    });
1651
1652/// Port of `static int cv_parsed` from `Src/Zle/computil.c:3230`.
1653pub static cv_parsed: std::sync::atomic::AtomicI32 =                         // c:3230
1654    std::sync::atomic::AtomicI32::new(0);
1655
1656/// Port of `static int cv_alloced` from `Src/Zle/computil.c:3230`.
1657pub static cv_alloced: std::sync::atomic::AtomicI32 =                        // c:3230
1658    std::sync::atomic::AtomicI32::new(0);
1659
1660/// Port of `static Cadef cadef_cache[MAX_CACACHE]` from
1661/// `Src/Zle/computil.c:973`. The LRU cache holds parsed
1662/// `_arguments` defs keyed by the raw arg vector — `get_cadef`
1663/// scans linearly, returns on first match (arr-compare on `defs`),
1664/// and on miss evicts the entry with the oldest `lastt` slot before
1665/// inserting the freshly parsed result.
1666pub static cadef_cache: std::sync::Mutex<[Option<Box<cadef>>; MAX_CACACHE]> = // c:973
1667    std::sync::Mutex::new([const { None }; MAX_CACACHE]);
1668
1669/// Port of `static Cvdef cvdef_cache[MAX_CVCACHE]` from
1670/// `Src/Zle/computil.c:2956`. Same LRU layout as cadef_cache;
1671/// `get_cvdef` scans for a defs-match hit, evicts the oldest slot
1672/// on miss.
1673pub static cvdef_cache: std::sync::Mutex<[Option<Box<cvdef>>; MAX_CVCACHE]> = // c:2956
1674    std::sync::Mutex::new([const { None }; MAX_CVCACHE]);
1675
1676/// Port of `static Ctags comptags[MAX_TAGS]` from
1677/// `Src/Zle/computil.c:3756`. One ctags entry per `locallevel`;
1678/// indexed by completion level.
1679pub static comptags: std::sync::Mutex<[Option<Box<ctags>>; MAX_TAGS]> =        // c:3756
1680    std::sync::Mutex::new([const { None }; MAX_TAGS]);
1681
1682/// Port of `static int lasttaglevel` from `Src/Zle/computil.c:3760`.
1683/// "locallevel at last comptags -i".
1684pub static lasttaglevel: std::sync::atomic::AtomicI32 =                       // c:3760
1685    std::sync::atomic::AtomicI32::new(0);
1686
1687// =====================================================================
1688// `ctags` / `ctset` — `comptags` cache.
1689// Src/Zle/computil.c:3732-3760. MAX_TAGS already declared above.
1690// =====================================================================
1691
1692/// Port of `typedef struct ctags *Ctags` from `Src/Zle/computil.c:3732`.
1693pub type Ctags = Box<ctags>;                                                 // c:3732
1694/// Port of `typedef struct ctset *Ctset` from `computil.c:3733`.
1695pub type Ctset = Box<ctset>;                                                 // c:3733
1696
1697/// Direct port of `struct ctags` from `Src/Zle/computil.c:3737-3742`.
1698/// A bunch of tag sets keyed by locallevel.
1699#[derive(Debug, Default)]
1700#[allow(non_camel_case_types)]
1701pub struct ctags {                                                           // c:3737
1702    pub all:     Option<Vec<String>>,                                        // c:3738 char **all
1703    pub context: Option<String>,                                             // c:3739 char *context
1704    pub init:    i32,                                                        // c:3740
1705    pub sets:    Option<Box<ctset>>,                                         // c:3741 Ctset sets
1706}
1707
1708/// Direct port of `struct ctset` from `Src/Zle/computil.c:3763`.
1709#[derive(Debug, Default)]
1710#[allow(non_camel_case_types)]
1711pub struct ctset {                                                           // c:3763
1712    pub next: Option<Box<ctset>>,                                            // c:3763 Ctset next
1713    pub tags: Option<Vec<String>>,                                           // c:3763 char **tags
1714    pub tag:  Option<String>,                                                // c:3763 char *tag
1715    pub ptr:  i32,                                                           // c:3763 char **ptr (index)
1716}
1717
1718/// Direct port of `static void freectset(Ctset s)` from
1719/// `Src/Zle/computil.c:3780`.
1720pub fn freectset(mut s: Option<Box<ctset>>) {                                // c:3763
1721    while let Some(mut node) = s {                                           // c:3780 while (s)
1722        s = node.next.take();                                                // c:3780 n = s->next
1723        node.tags = None;                                                    // c:3780-3771
1724        node.tag = None;                                                     // c:3780
1725        drop(node);                                                          // c:3780
1726    }
1727}
1728
1729/// Direct port of `static void freectags(Ctags t)` from
1730/// `Src/Zle/computil.c:3780`.
1731pub fn freectags(t: Option<Box<ctags>>) {                                    // c:3780
1732    let Some(mut node) = t else { return; };                                 // c:3780 if (t)
1733    node.all = None;                                                         // c:3783-3784
1734    node.context = None;                                                     // c:3785
1735    freectset(node.sets.take());                                             // c:3786
1736    drop(node);                                                              // c:3787
1737}
1738
1739/// Port of `rembslashcolon(char *s)` from `Src/Zle/computil.c:1046`.
1740/// ```c
1741/// static char *
1742/// rembslashcolon(char *s)
1743/// {
1744///     char *p, *r;
1745///     r = p = s = dupstring(s);
1746///     while (*s) {
1747///         if (s[0] != '\\' || s[1] != ':')
1748///             *p++ = *s;
1749///         s++;
1750///     }
1751///     *p = '\0';
1752///     return r;
1753/// }
1754/// ```
1755/// Strip every `\:` two-byte sequence to nothing (the `\` is dropped,
1756/// the `:` follows on the next iteration). Used to unescape colon-
1757/// bearing description strings produced by `_arguments`.
1758pub fn rembslashcolon(s: &str) -> String {                                   // c:1047
1759    let bytes = s.as_bytes();                                                // c:1047 dupstring(s)
1760    let mut out = Vec::<u8>::with_capacity(bytes.len());
1761    let mut i = 0;
1762    while i < bytes.len() {                                                  // c:1053 while (*s)
1763        // c:1054 — `if (s[0] != '\\' || s[1] != ':') *p++ = *s`.
1764        let drop = bytes[i] == b'\\'
1765            && i + 1 < bytes.len()
1766            && bytes[i + 1] == b':';
1767        if !drop {
1768            out.push(bytes[i]);                                              // c:1055 *p++ = *s
1769        }
1770        i += 1;                                                              // c:1056 s++
1771    }
1772    // c:1058 — `*p = '\0'`. Rust strings are length-tracked.
1773    String::from_utf8(out).unwrap_or_default()                               // c:1060 return r
1774}
1775
1776/// Port of `bslashcolon(char *s)` from `Src/Zle/computil.c:1065`.
1777/// ```c
1778/// static char *
1779/// bslashcolon(char *s)
1780/// {
1781///     char *p, *r;
1782///     r = p = zhalloc((2 * strlen(s)) + 1);
1783///     while (*s) {
1784///         if (*s == ':')
1785///             *p++ = '\\';
1786///         *p++ = *s++;
1787///     }
1788///     *p = '\0';
1789///     return r;
1790/// }
1791/// ```
1792/// Insert a backslash before every `:`, doubling the worst-case
1793/// length. Inverse of `rembslashcolon` for description-string
1794/// emission.
1795pub fn bslashcolon(s: &str) -> String {                                      // c:1066
1796    let bytes = s.as_bytes();                                                // c:1066 zhalloc(2*strlen(s)+1)
1797    let mut out = Vec::<u8>::with_capacity(2 * bytes.len() + 1);
1798    for &b in bytes {                                                        // c:1072 while (*s)
1799        if b == b':' {                                                       // c:1073
1800            out.push(b'\\');                                                 // c:1074 *p++ = '\\'
1801        }
1802        out.push(b);                                                         // c:1075 *p++ = *s++
1803    }
1804    // c:1077 — `*p = '\0'`.
1805    String::from_utf8(out).unwrap_or_default()                               // c:1079 return r
1806}
1807
1808/// Port of `single_index(char pre, char opt)` from `Src/Zle/computil.c:1088`.
1809/// ```c
1810/// static int
1811/// single_index(char pre, char opt)
1812/// {
1813///     if (opt <= 0x20 || opt > 0x7e)
1814///         return -1;
1815///     return opt + (pre == '-' ? -0x21 : 94 - 0x21);
1816/// }
1817/// ```
1818/// Map a `(prefix, option-letter)` pair into the flat 188-slot array
1819/// that `cadef` keeps for single-letter option lookup. Returns -1
1820/// when `opt` is outside the printable-ASCII range.
1821///
1822/// `pre` is `-` for the negative-prefix slot and anything else
1823/// (typically `+`) for the positive-prefix slot.
1824pub fn single_index(pre: u8, opt: u8) -> i32 {                               // c:1089
1825    if opt <= 0x20 || opt > 0x7e {                                           // c:1089
1826        return -1;                                                           // c:1092
1827    }
1828    // c:1094 — `return opt + (pre == '-' ? -0x21 : 94 - 0x21)`.
1829    let off: i32 = if pre == b'-' { -0x21 } else { 94 - 0x21 };
1830    (opt as i32) + off
1831}
1832
1833// `freecaargs(Caarg)` + `freecadef(Cadef)` ported above with the
1834// caarg/caopt/cadef struct ports (c:996 / c:1013).
1835
1836#[cfg(test)]
1837mod cao_caa_tests {
1838    use super::*;
1839
1840    #[test]
1841    fn cao_values_match_c_source() {
1842        let _g = crate::ported::zle::zle_main::zle_test_setup();
1843        // c:941-945 — sequential 1..=5.
1844        assert_eq!(CAO_NEXT, 1);
1845        assert_eq!(CAO_DIRECT, 2);
1846        assert_eq!(CAO_ODIRECT, 3);
1847        assert_eq!(CAO_EQUAL, 4);
1848        assert_eq!(CAO_OEQUAL, 5);
1849    }
1850
1851    #[test]
1852    fn caa_values_match_c_source() {
1853        let _g = crate::ported::zle::zle_main::zle_test_setup();
1854        // c:964-968 — sequential 1..=5.
1855        assert_eq!(CAA_NORMAL, 1);
1856        assert_eq!(CAA_OPT,    2);
1857        assert_eq!(CAA_REST,   3);
1858        assert_eq!(CAA_RARGS,  4);
1859        assert_eq!(CAA_RREST,  5);
1860    }
1861
1862    #[test]
1863    fn crt_values_match_c_source() {
1864        let _g = crate::ported::zle::zle_main::zle_test_setup();
1865        // c:79-83 — sequential 0..=4.
1866        assert_eq!(CRT_SIMPLE, 0);
1867        assert_eq!(CRT_DESC,   1);
1868        assert_eq!(CRT_SPEC,   2);
1869        assert_eq!(CRT_DUMMY,  3);
1870        assert_eq!(CRT_EXPL,   4);
1871    }
1872
1873    #[test]
1874    fn cvv_values_match_c_source() {
1875        let _g = crate::ported::zle::zle_main::zle_test_setup();
1876        // c:2949-2951 — sequential 0..=2.
1877        assert_eq!(CVV_NOARG, 0);
1878        assert_eq!(CVV_ARG,   1);
1879        assert_eq!(CVV_OPT,   2);
1880    }
1881
1882    #[test]
1883    fn cache_sizes_are_8() {
1884        let _g = crate::ported::zle::zle_main::zle_test_setup();
1885        // c:972 + c:2955 — both LRU caches are 8 entries.
1886        assert_eq!(MAX_CACACHE, 8);
1887        assert_eq!(MAX_CVCACHE, 8);
1888    }
1889
1890    #[test]
1891    fn max_tags_is_256() {
1892        let _g = crate::ported::zle::zle_main::zle_test_setup();
1893        assert_eq!(MAX_TAGS, 256);
1894    }
1895
1896    #[test]
1897    fn path_max2_is_8192() {
1898        let _g = crate::ported::zle::zle_main::zle_test_setup();
1899        assert_eq!(PATH_MAX2, 8192);
1900    }
1901}
1902
1903#[cfg(test)]
1904mod tests {
1905    use super::*;
1906
1907    // Tests for cd_get / cd_init / cd_sort / cd_prep removed — those
1908    // tests exercised the deleted CompDescItem/CompDescSet Rust-only
1909    // wrappers. The C-faithful entries (cd_get takes char**params and
1910    // returns int) get exercised through the full `_describe` widget
1911    // path under integration tests; per-fn unit tests would just
1912    // lock in the deleted Rust-side shape.
1913
1914    // test_parse_caarg / test_parse_cadef removed — they exercised
1915    // the deleted CompArgDef/CompOptDef Rust-only types via fake-
1916    // signature wrappers. Real ports land alongside the cadef chain.
1917
1918    #[test]
1919    fn test_rembslashcolon() {
1920        let _g = crate::ported::zle::zle_main::zle_test_setup();
1921        // c:1054 — `\:` two-byte sequence drops the backslash.
1922        assert_eq!(rembslashcolon("a\\:b\\:c"), "a:b:c");
1923    }
1924
1925    #[test]
1926    fn test_rembslashcolon_lone_backslash_kept() {
1927        let _g = crate::ported::zle::zle_main::zle_test_setup();
1928        // c:1054 — `\X` (X != ':') keeps the backslash.
1929        assert_eq!(rembslashcolon("a\\nb"), "a\\nb");
1930    }
1931
1932    #[test]
1933    fn test_rembslashcolon_trailing_backslash() {
1934        let _g = crate::ported::zle::zle_main::zle_test_setup();
1935        // c:1054 — trailing `\` with no follow-up keeps the `\`.
1936        assert_eq!(rembslashcolon("a\\"), "a\\");
1937    }
1938
1939    #[test]
1940    fn test_rembslashcolon_unescaped_colon_passes_through() {
1941        let _g = crate::ported::zle::zle_main::zle_test_setup();
1942        // c:1054 — bare `:` (no preceding `\`) is kept.
1943        assert_eq!(rembslashcolon("a:b"), "a:b");
1944    }
1945
1946    #[test]
1947    fn test_bslashcolon() {
1948        let _g = crate::ported::zle::zle_main::zle_test_setup();
1949        // c:1073 — every `:` gets `\` prepended.
1950        assert_eq!(bslashcolon("a:b:c"), "a\\:b\\:c");
1951    }
1952
1953    #[test]
1954    fn test_bslashcolon_no_colons() {
1955        let _g = crate::ported::zle::zle_main::zle_test_setup();
1956        // c:1072 — non-colon bytes pass through unchanged.
1957        assert_eq!(bslashcolon("hello"), "hello");
1958    }
1959
1960    #[test]
1961    fn test_bslashcolon_already_escaped_doubled() {
1962        let _g = crate::ported::zle::zle_main::zle_test_setup();
1963        // c:1073-1074 — C doesn't track previous backslash, so an
1964        // already-escaped `\:` becomes `\\:` (the `\` passes
1965        // through, then the `:` gets a fresh `\` prepended).
1966        assert_eq!(bslashcolon("a\\:b"), "a\\\\:b");
1967    }
1968
1969    #[test]
1970    fn test_single_index_dash_prefix() {
1971        let _g = crate::ported::zle::zle_main::zle_test_setup();
1972        // c:1094 — `pre == '-'` → offset = -0x21.
1973        // For opt='a' (0x61): 0x61 + -0x21 = 0x40 = 64.
1974        assert_eq!(single_index(b'-', b'a'), 64);
1975        // For opt='A' (0x41): 0x41 + -0x21 = 0x20 = 32.
1976        assert_eq!(single_index(b'-', b'A'), 32);
1977        // For opt='!' (0x21): 0x21 + -0x21 = 0.
1978        assert_eq!(single_index(b'-', b'!'), 0);
1979        // For opt='~' (0x7e): 0x7e + -0x21 = 0x5d = 93.
1980        assert_eq!(single_index(b'-', b'~'), 93);
1981    }
1982
1983    #[test]
1984    fn test_single_index_plus_prefix() {
1985        let _g = crate::ported::zle::zle_main::zle_test_setup();
1986        // c:1094 — `pre == '+'` → offset = 94 - 0x21 = 61.
1987        // For opt='a' (0x61): 0x61 + 61 = 158.
1988        assert_eq!(single_index(b'+', b'a'), 158);
1989        // For opt='!' (0x21): 0x21 + 61 = 94.
1990        assert_eq!(single_index(b'+', b'!'), 94);
1991        // For opt='~' (0x7e): 0x7e + 61 = 187.
1992        assert_eq!(single_index(b'+', b'~'), 187);
1993    }
1994
1995    #[test]
1996    fn test_single_index_out_of_range() {
1997        let _g = crate::ported::zle::zle_main::zle_test_setup();
1998        // c:1091-1092 — opt <= 0x20 OR opt > 0x7e returns -1.
1999        assert_eq!(single_index(b'-', 0x20), -1);     // space (0x20) excluded
2000        assert_eq!(single_index(b'-', 0x00), -1);     // NUL
2001        assert_eq!(single_index(b'-', 0x7f), -1);     // DEL (0x7f) excluded
2002        assert_eq!(single_index(b'+', 0xff), -1);     // outside ASCII
2003    }
2004
2005    // test_cd_group removed — used the deleted CompDescItem; the
2006    // function `cd_group` itself wasn't a real C export and was
2007    // also removed alongside the fake structs.
2008
2009    #[test]
2010    fn caarg_default_zero_initialized() {
2011        let _g = crate::ported::zle::zle_main::zle_test_setup();
2012        // c:949-962 — fresh caarg: every field zero / None.
2013        let a = caarg::default();
2014        assert!(a.next.is_none());
2015        assert!(a.descr.is_none());
2016        assert!(a.action.is_none());
2017        assert_eq!(a.r#type, 0);
2018        assert_eq!(a.num, 0);
2019        assert_eq!(a.active, 0);
2020    }
2021
2022    #[test]
2023    fn caopt_default_zero_initialized() {
2024        let _g = crate::ported::zle::zle_main::zle_test_setup();
2025        // c:928-939 — fresh caopt: zero / None across all fields.
2026        let o = caopt::default();
2027        assert!(o.next.is_none());
2028        assert!(o.name.is_none());
2029        assert!(o.args.is_none());
2030        assert_eq!(o.r#type, 0);
2031        assert_eq!(o.num, 0);
2032        assert_eq!(o.not, 0);
2033    }
2034
2035    #[test]
2036    fn cadef_default_zero_initialized() {
2037        let _g = crate::ported::zle::zle_main::zle_test_setup();
2038        // c:905-922 — fresh cadef: zero / None across all fields.
2039        let d = cadef::default();
2040        assert!(d.next.is_none());
2041        assert!(d.opts.is_none());
2042        assert!(d.args.is_none());
2043        assert_eq!(d.nopts, 0);
2044        assert_eq!(d.flags, 0);
2045    }
2046
2047    #[test]
2048    fn freecaargs_walks_chain() {
2049        let _g = crate::ported::zle::zle_main::zle_test_setup();
2050        // c:996-1010 — freecaargs walks `next` chain freeing each
2051        // entry. After call, the chain owner observes no remaining
2052        // refs (Drop handles deallocation).
2053        let mut head = caarg { descr: Some("a".into()), ..Default::default() };
2054        let mid     = caarg { descr: Some("b".into()), ..Default::default() };
2055        let tail    = caarg { descr: Some("c".into()), ..Default::default() };
2056        let mut mid_box = Box::new(mid);
2057        mid_box.next = Some(Box::new(tail));
2058        head.next = Some(mid_box);
2059        freecaargs(Some(Box::new(head)));
2060        // No panic, no leak — Box drop chains the rest.
2061    }
2062
2063    #[test]
2064    fn cao_caa_constants_match_c() {
2065        let _g = crate::ported::zle::zle_main::zle_test_setup();
2066        // c:941-945 and c:964-968 — sequential 1..=5.
2067        assert_eq!(CAO_NEXT,    1);
2068        assert_eq!(CAO_DIRECT,  2);
2069        assert_eq!(CAO_ODIRECT, 3);
2070        assert_eq!(CAO_EQUAL,   4);
2071        assert_eq!(CAO_OEQUAL,  5);
2072        assert_eq!(CAA_NORMAL,  1);
2073        assert_eq!(CAA_OPT,     2);
2074        assert_eq!(CAA_REST,    3);
2075        assert_eq!(CAA_RARGS,   4);
2076        assert_eq!(CAA_RREST,   5);
2077    }
2078
2079    #[test]
2080    fn cdf_max_cacache_constants_match_c() {
2081        let _g = crate::ported::zle::zle_main::zle_test_setup();
2082        // c:924 — CDF_SEP = 1; c:972 — MAX_CACACHE = 8.
2083        assert_eq!(CDF_SEP, 1);
2084        assert_eq!(MAX_CACACHE, 8);
2085    }
2086
2087    #[test]
2088    fn crt_constants_match_c() {
2089        let _g = crate::ported::zle::zle_main::zle_test_setup();
2090        // c:79-83 — sequential 0..=4.
2091        assert_eq!(CRT_SIMPLE, 0);
2092        assert_eq!(CRT_DESC,   1);
2093        assert_eq!(CRT_SPEC,   2);
2094        assert_eq!(CRT_DUMMY,  3);
2095        assert_eq!(CRT_EXPL,   4);
2096    }
2097
2098    #[test]
2099    fn cdstr_default_zero_initialized() {
2100        let _g = crate::ported::zle::zle_main::zle_test_setup();
2101        // c:58-70 — fresh cdstr: zero/None across all fields.
2102        let s = cdstr::default();
2103        assert!(s.next.is_none());
2104        assert!(s.str.is_none());
2105        assert!(s.desc.is_none());
2106        assert!(s.r#match.is_none());
2107        assert_eq!(s.len, 0);
2108        assert_eq!(s.width, 0);
2109        assert_eq!(s.kind, 0);
2110    }
2111
2112    #[test]
2113    fn cdrun_default_zero_initialized() {
2114        let _g = crate::ported::zle::zle_main::zle_test_setup();
2115        // c:72-77 — fresh cdrun: zero/None.
2116        let r = cdrun::default();
2117        assert!(r.next.is_none());
2118        assert!(r.strs.is_none());
2119        assert_eq!(r.r#type, 0);
2120        assert_eq!(r.count, 0);
2121    }
2122
2123    #[test]
2124    fn cdset_default_zero_initialized() {
2125        let _g = crate::ported::zle::zle_main::zle_test_setup();
2126        // c:85-91 — fresh cdset: zero/None.
2127        let s = cdset::default();
2128        assert!(s.next.is_none());
2129        assert!(s.opts.is_none());
2130        assert!(s.strs.is_none());
2131        assert_eq!(s.count, 0);
2132        assert_eq!(s.desc, 0);
2133    }
2134
2135    #[test]
2136    fn cdstate_default_zero_initialized() {
2137        let _g = crate::ported::zle::zle_main::zle_test_setup();
2138        // c:40-56 — fresh cdstate: zero/None.
2139        let st = cdstate::default();
2140        assert_eq!(st.showd, 0);
2141        assert!(st.sep.is_none());
2142        assert!(st.sets.is_none());
2143        assert!(st.runs.is_none());
2144    }
2145
2146    #[test]
2147    fn freecdsets_walks_chain() {
2148        let _g = crate::ported::zle::zle_main::zle_test_setup();
2149        // c:96-122 — freecdsets walks `next` chain freeing each set
2150        // and its strs sub-chain.
2151        let head_str = cdstr {
2152            str: Some("foo".into()),
2153            desc: Some("first".into()),
2154            ..Default::default()
2155        };
2156        let tail_str = cdstr {
2157            str: Some("bar".into()),
2158            ..Default::default()
2159        };
2160        let mut head_str_b = Box::new(head_str);
2161        head_str_b.next = Some(Box::new(tail_str));
2162        let set = cdset {
2163            strs: Some(head_str_b),
2164            count: 2,
2165            ..Default::default()
2166        };
2167        freecdsets(Some(Box::new(set)));
2168        // No panic / no leak — Box drop chains the rest.
2169    }
2170
2171    #[test]
2172    fn castate_default_zero_initialized() {
2173        let _g = crate::ported::zle::zle_main::zle_test_setup();
2174        // c:1928-1953 — fresh castate: zero/None.
2175        let s = castate::default();
2176        assert!(s.snext.is_none());
2177        assert!(s.d.is_none());
2178        assert!(s.def.is_none());
2179        assert!(s.args.is_none());
2180        assert_eq!(s.nopts, 0);
2181        assert_eq!(s.curpos, 0);
2182    }
2183
2184    #[test]
2185    fn cvdef_default_zero_initialized() {
2186        let _g = crate::ported::zle::zle_main::zle_test_setup();
2187        // c:2924-2935 — fresh cvdef: zero/None.
2188        let d = cvdef::default();
2189        assert!(d.descr.is_none());
2190        assert!(d.vals.is_none());
2191        assert_eq!(d.hassep, 0);
2192        assert_eq!(d.sep, 0);
2193        assert_eq!(d.argsep, 0);
2194    }
2195
2196    #[test]
2197    fn cvval_default_zero_initialized() {
2198        let _g = crate::ported::zle::zle_main::zle_test_setup();
2199        // c:2939-2947 — fresh cvval: zero/None.
2200        let v = cvval::default();
2201        assert!(v.next.is_none());
2202        assert!(v.name.is_none());
2203        assert!(v.arg.is_none());
2204        assert_eq!(v.r#type, 0);
2205        assert_eq!(v.active, 0);
2206    }
2207
2208    #[test]
2209    fn cvstate_default_zero_initialized() {
2210        let _g = crate::ported::zle::zle_main::zle_test_setup();
2211        // c:3222-3227 — fresh cvstate: None across all 4 fields.
2212        let s = cvstate::default();
2213        assert!(s.d.is_none());
2214        assert!(s.def.is_none());
2215        assert!(s.val.is_none());
2216        assert!(s.vals.is_none());
2217    }
2218
2219    #[test]
2220    fn ctags_default_zero_initialized() {
2221        let _g = crate::ported::zle::zle_main::zle_test_setup();
2222        // c:3737-3742 — fresh ctags: zero/None.
2223        let t = ctags::default();
2224        assert!(t.all.is_none());
2225        assert!(t.context.is_none());
2226        assert!(t.sets.is_none());
2227        assert_eq!(t.init, 0);
2228    }
2229
2230    #[test]
2231    fn ctset_default_zero_initialized() {
2232        let _g = crate::ported::zle::zle_main::zle_test_setup();
2233        // c:3746-3751 — fresh ctset: zero/None.
2234        let s = ctset::default();
2235        assert!(s.next.is_none());
2236        assert!(s.tags.is_none());
2237        assert!(s.tag.is_none());
2238        assert_eq!(s.ptr, 0);
2239    }
2240
2241    #[test]
2242    fn cvv_constants_match_c() {
2243        let _g = crate::ported::zle::zle_main::zle_test_setup();
2244        // c:2949-2951 — sequential 0..=2.
2245        assert_eq!(CVV_NOARG, 0);
2246        assert_eq!(CVV_ARG,   1);
2247        assert_eq!(CVV_OPT,   2);
2248    }
2249
2250    #[test]
2251    fn max_tags_cvcache_match_c() {
2252        let _g = crate::ported::zle::zle_main::zle_test_setup();
2253        // c:3755 — MAX_TAGS = 256; c:2955 — MAX_CVCACHE = 8.
2254        assert_eq!(MAX_TAGS, 256);
2255        assert_eq!(MAX_CVCACHE, 8);
2256    }
2257
2258    #[test]
2259    fn freectset_walks_chain() {
2260        let _g = crate::ported::zle::zle_main::zle_test_setup();
2261        // c:3762-3777 — freectset walks `next` chain freeing each
2262        // ctset's tags/tag fields.
2263        let mut head = ctset { tag: Some("foo".into()), ..Default::default() };
2264        let tail     = ctset { tag: Some("bar".into()), ..Default::default() };
2265        head.next = Some(Box::new(tail));
2266        freectset(Some(Box::new(head)));
2267    }
2268
2269    #[test]
2270    fn freectags_drops_one_node() {
2271        let _g = crate::ported::zle::zle_main::zle_test_setup();
2272        // c:3779-3789 — freectags releases all/context/sets on one ctags.
2273        let t = ctags {
2274            all: Some(vec!["a".into(), "b".into()]),
2275            context: Some("ctx".into()),
2276            ..Default::default()
2277        };
2278        freectags(Some(Box::new(t)));
2279    }
2280
2281    #[test]
2282    fn freecvdef_walks_vals_chain() {
2283        let _g = crate::ported::zle::zle_main::zle_test_setup();
2284        // c:2960-2981 — freecvdef walks vals freeing each cvval.
2285        let v_tail = cvval { name: Some("opt2".into()), ..Default::default() };
2286        let mut v_head = cvval { name: Some("opt1".into()), ..Default::default() };
2287        v_head.next = Some(Box::new(v_tail));
2288        let d = cvdef {
2289            descr: Some("test".into()),
2290            vals: Some(Box::new(v_head)),
2291            ..Default::default()
2292        };
2293        freecvdef(Some(Box::new(d)));
2294    }
2295
2296    /// c:1196 — `_arguments '-foo[only foo]' '*:file:_files'`. Verify
2297    /// that the option-name xor list contains the spec name, that
2298    /// nopts/ndopts reflect the option type (CAO_NEXT here), and that
2299    /// the rest arg lands on `rest` with type CAA_REST.
2300    #[test]
2301    fn parse_cadef_simple_opt_and_rest() {
2302        let _g = crate::ported::zle::zle_main::zle_test_setup();
2303        crate::ported::utils::inittyptab();
2304        let args = vec![
2305            String::from(""),             // adpre/adsuf split (no %d)
2306            String::from("-foo[only foo]"),
2307            String::from("*:file:_files"),
2308        ];
2309        let def = parse_cadef("_arguments", &args).expect("cadef built");
2310        let opt = def.opts.as_deref().expect("opt linked");
2311        assert_eq!(opt.name.as_deref(), Some("-foo"));
2312        assert_eq!(opt.descr.as_deref(), Some("only foo"));
2313        assert_eq!(opt.r#type, CAO_NEXT);
2314        // c:1462-1468 — non-multi option appends its own name to xor.
2315        let xor = opt.xor.as_ref().expect("xor list");
2316        assert!(xor.iter().any(|s| s == "-foo"), "xor must include -foo: {:?}", xor);
2317
2318        let rest = def.rest.as_deref().expect("rest linked");
2319        assert_eq!(rest.r#type, CAA_REST);
2320        assert_eq!(rest.descr.as_deref(), Some("file"));
2321        assert_eq!(rest.action.as_deref(), Some("_files"));
2322    }
2323
2324    /// c:1617-1661 — numbered positional argument `1:cmd:_commands` lands
2325    /// on `def.args` with the right slot (num=0 because anum is `1`
2326    /// then `arg->num = anum - 1`).
2327    #[test]
2328    fn parse_cadef_numbered_positional_arg() {
2329        let _g = crate::ported::zle::zle_main::zle_test_setup();
2330        crate::ported::utils::inittyptab();
2331        let args = vec![
2332            String::from(""),
2333            String::from("1:cmd:_commands"),
2334        ];
2335        let def = parse_cadef("_arguments", &args).expect("cadef built");
2336        let pos = def.args.as_deref().expect("positional arg linked");
2337        assert_eq!(pos.num, 1);
2338        assert_eq!(pos.r#type, CAA_NORMAL);
2339        assert_eq!(pos.descr.as_deref(), Some("cmd"));
2340        assert_eq!(pos.action.as_deref(), Some("_commands"));
2341        assert_eq!(pos.direct, 1, "explicit numbering sets direct=1");
2342    }
2343
2344    /// c:1647-1656 — duplicate numbered argument must error out and
2345    /// return None (the cadef cache miss path picks this up).
2346    #[test]
2347    fn parse_cadef_doubled_arg_errors() {
2348        let _g = crate::ported::zle::zle_main::zle_test_setup();
2349        crate::ported::utils::inittyptab();
2350        let args = vec![
2351            String::from(""),
2352            String::from("1:a:_a"),
2353            String::from("1:b:_b"),
2354        ];
2355        let def = parse_cadef("_arguments", &args);
2356        assert!(def.is_none(), "duplicate arg num=1 must reject");
2357    }
2358
2359    /// c:1335-1370 — `(opt-x opt-y)-foo[descr]` builds a 3-element
2360    /// xor list `[opt-x, opt-y, -foo]` (the option's own name gets
2361    /// added at the end via c:1462-1468).
2362    #[test]
2363    fn parse_cadef_xor_list_populated() {
2364        let _g = crate::ported::zle::zle_main::zle_test_setup();
2365        crate::ported::utils::inittyptab();
2366        let args = vec![
2367            String::from(""),
2368            String::from("(opt-x opt-y)-foo[descr]"),
2369        ];
2370        let def = parse_cadef("_arguments", &args).expect("cadef built");
2371        let opt = def.opts.as_deref().expect("opt linked");
2372        let xor = opt.xor.as_ref().expect("xor list");
2373        assert_eq!(xor.len(), 3, "xor: {:?}", xor);
2374        assert_eq!(xor[0], "opt-x");
2375        assert_eq!(xor[1], "opt-y");
2376        assert_eq!(xor[2], "-foo");
2377    }
2378
2379    /// c:3796 — `settags(0, ["ctx", "tag1", "tag2"])` populates
2380    /// `comptags[0]` with context="ctx", all=["tag1","tag2"], init=1.
2381    #[test]
2382    fn settags_populates_slot() {
2383        let _g = crate::ported::zle::zle_main::zle_test_setup();
2384        // Clear slot to make test order-independent.
2385        if let Ok(mut tab) = comptags.lock() {
2386            tab[0] = None;
2387        }
2388        settags(0, &[
2389            "ctx".to_string(),
2390            "tag-a".to_string(),
2391            "tag-b".to_string(),
2392        ]);
2393        let tab = comptags.lock().unwrap();
2394        let slot = tab[0].as_deref().expect("comptags[0] populated");
2395        assert_eq!(slot.context.as_deref(), Some("ctx"));
2396        assert_eq!(slot.init, 1);
2397        let all = slot.all.as_ref().expect("all populated");
2398        assert_eq!(all.len(), 2);
2399        assert_eq!(all[0], "tag-a");
2400        assert_eq!(all[1], "tag-b");
2401        assert!(slot.sets.is_none());
2402    }
2403
2404    /// c:1712-1718 — exact name match returns the opt with `*end`
2405    /// pointing past the option name.
2406    #[test]
2407    fn ca_get_opt_exact_match() {
2408        let _g = crate::ported::zle::zle_main::zle_test_setup();
2409        crate::ported::utils::inittyptab();
2410        let args = vec![
2411            String::from(""),
2412            String::from("-foo[d]"),
2413        ];
2414        let mut def = *parse_cadef("_arguments", &args).expect("cadef built");
2415        // Mark the only opt active so ca_get_opt accepts it.
2416        let mut cur = def.opts.as_deref_mut();
2417        while let Some(o) = cur {
2418            o.active = 1;
2419            cur = o.next.as_deref_mut();
2420        }
2421        let mut end: usize = 0;
2422        let hit = ca_get_opt(&def, "-foo", 1, &mut end).expect("hit");
2423        assert_eq!(hit.name.as_deref(), Some("-foo"));
2424        assert_eq!(end, 4);
2425    }
2426
2427    /// c:1809-1822 — `argsactive=0` short-circuits to None even when
2428    /// args are linked. Guards against the easy off-by-one error of
2429    /// returning the first matching arg unconditionally.
2430    #[test]
2431    fn ca_get_arg_argsactive_zero_returns_none() {
2432        let _g = crate::ported::zle::zle_main::zle_test_setup();
2433        crate::ported::utils::inittyptab();
2434        let args = vec![
2435            String::from(""),
2436            String::from("1:c:_c"),
2437        ];
2438        let def = *parse_cadef("_arguments", &args).expect("cadef built");
2439        // argsactive defaults to 0 — must short-circuit.
2440        assert!(ca_get_arg(&def, 1).is_none());
2441    }
2442
2443    /// c:1817 — when `argsactive=1` and the positional arg is active,
2444    /// `n` inside `[min, num]` returns the matching node.
2445    #[test]
2446    fn ca_get_arg_in_range_active() {
2447        let _g = crate::ported::zle::zle_main::zle_test_setup();
2448        crate::ported::utils::inittyptab();
2449        let args = vec![
2450            String::from(""),
2451            String::from("1:c:_c"),
2452        ];
2453        let mut def = *parse_cadef("_arguments", &args).expect("cadef built");
2454        def.argsactive = 1;
2455        if let Some(a) = def.args.as_deref_mut() {
2456            a.active = 1;
2457        }
2458        let hit = ca_get_arg(&def, 1).expect("hit");
2459        assert_eq!(hit.num, 1);
2460        assert_eq!(hit.descr.as_deref(), Some("c"));
2461    }
2462
2463    /// c:2999-3027 — `-s , descr opt1[a]:val1: opt2[b]` builds a cvdef
2464    /// with sep=',', descr="descr", vals chain of two cvvals.
2465    #[test]
2466    fn parse_cvdef_sep_and_two_vals() {
2467        let _g = crate::ported::zle::zle_main::zle_test_setup();
2468        crate::ported::utils::inittyptab();
2469        let args = vec![
2470            String::from("-s"),
2471            String::from(","),
2472            String::from("descr"),
2473            String::from("opt1[a]:val1:"),
2474            String::from("opt2[b]"),
2475        ];
2476        let def = parse_cvdef("_values", &args).expect("cvdef built");
2477        assert_eq!(def.hassep, 1);
2478        assert_eq!(def.sep, b',' as i32);
2479        assert_eq!(def.descr.as_deref(), Some("descr"));
2480        let v1 = def.vals.as_deref().expect("val1");
2481        assert_eq!(v1.name.as_deref(), Some("opt1"));
2482        assert_eq!(v1.descr.as_deref(), Some("a"));
2483        assert_eq!(v1.r#type, CVV_ARG);
2484        let v2 = v1.next.as_deref().expect("val2");
2485        assert_eq!(v2.name.as_deref(), Some("opt2"));
2486        assert_eq!(v2.descr.as_deref(), Some("b"));
2487        assert_eq!(v2.r#type, CVV_NOARG);
2488    }
2489
2490    /// c:1786-1801 — ca_foreign_opt walks snext skipping curset.
2491    /// When `curset == all` (head pointer matches), only the OTHER
2492    /// sets in the snext chain are scanned.
2493    #[test]
2494    fn ca_foreign_opt_finds_in_other_set() {
2495        let _g = crate::ported::zle::zle_main::zle_test_setup();
2496        crate::ported::utils::inittyptab();
2497        let other = cadef {
2498            opts: Some(Box::new(caopt {
2499                name: Some("-bar".into()), active: 1,
2500                ..Default::default()
2501            })),
2502            ..Default::default()
2503        };
2504        let all = cadef {
2505            opts: Some(Box::new(caopt {
2506                name: Some("-foo".into()), active: 1,
2507                ..Default::default()
2508            })),
2509            snext: Some(Box::new(other)),
2510            ..Default::default()
2511        };
2512        // curset = &all (head). `-bar` lives in snext set — found.
2513        assert_eq!(ca_foreign_opt(&all, &all, "-bar"), 1);
2514        // `-foo` lives ONLY in the head (which gets skipped) — not found.
2515        assert_eq!(ca_foreign_opt(&all, &all, "-foo"), 0);
2516        // `-missing` not anywhere — not found.
2517        assert_eq!(ca_foreign_opt(&all, &all, "-missing"), 0);
2518    }
2519
2520    /// c:1834 — guard: with neither xor nor opts AND no compcurrent
2521    /// position, the function returns without mutating any active flag.
2522    #[test]
2523    fn ca_inactive_guard_noop_keeps_active() {
2524        let _g = crate::ported::zle::zle_main::zle_test_setup();
2525        crate::ported::utils::inittyptab();
2526        let mut d = cadef {
2527            opts: Some(Box::new(caopt {
2528                name: Some("-foo".into()), active: 1, ..Default::default()
2529            })),
2530            argsactive: 1,
2531            ..Default::default()
2532        };
2533        ca_inactive(&mut d, &[], 0, 0);
2534        assert_eq!(d.opts.as_deref().unwrap().active, 1);
2535        assert_eq!(d.argsactive, 1);
2536    }
2537
2538    /// c:1881 — with `opts=1`, every option's `active` clears.
2539    #[test]
2540    fn ca_inactive_opts_flag_deactivates_options() {
2541        use std::sync::atomic::Ordering;
2542        let _g = crate::ported::zle::zle_main::zle_test_setup();
2543        crate::ported::utils::inittyptab();
2544        let saved_compcur = crate::ported::zle::complete::COMPCURRENT.load(Ordering::Relaxed);
2545        let mut d = cadef {
2546            opts: Some(Box::new(caopt {
2547                name: Some("-foo".into()), active: 1, num: 0,
2548                next: Some(Box::new(caopt {
2549                    name: Some("-bar".into()), active: 1, num: 1,
2550                    ..Default::default()
2551                })),
2552                ..Default::default()
2553            })),
2554            argsactive: 1,
2555            ..Default::default()
2556        };
2557        // Force COMPCURRENT >= cur so the guard at c:1834 is satisfied.
2558        crate::ported::zle::complete::COMPCURRENT.store(2, Ordering::Relaxed);
2559        ca_inactive(&mut d, &[], 1, 1);
2560        // Restore COMPCURRENT immediately so parallel non-ZLE tests
2561        // see the original value.
2562        crate::ported::zle::complete::COMPCURRENT.store(saved_compcur, Ordering::Relaxed);
2563        let mut p = d.opts.as_deref();
2564        while let Some(o) = p {
2565            assert_eq!(o.active, 0,
2566                "{:?} should be deactivated", o.name);
2567            p = o.next.as_deref();
2568        }
2569    }
2570
2571    /// c:3798-3801 — `comptags -i 0 a b` populates comptags[0].
2572    /// Then `-T` reports empty sets (`1`), `-C` reads context.
2573    #[test]
2574    fn bin_comptags_init_and_context() {
2575        use std::sync::atomic::Ordering;
2576        let _g = crate::ported::zle::zle_main::zle_test_setup();
2577        crate::ported::utils::inittyptab();
2578        let saved_incompfunc = INCOMPFUNC.load(Ordering::Relaxed);
2579        let saved_locallevel = crate::ported::builtin::LOCALLEVEL.load(Ordering::Relaxed);
2580        // Reset slot 0 + locallevel.
2581        if let Ok(mut tab) = comptags.lock() {
2582            tab[0] = None;
2583        }
2584        crate::ported::builtin::LOCALLEVEL.store(0, Ordering::Relaxed);
2585        INCOMPFUNC.store(1, Ordering::Relaxed);
2586
2587        let ops = crate::ported::zsh_h::options {
2588            ind: [0u8; crate::ported::zsh_h::MAX_OPS],
2589            args: Vec::new(),
2590            argscount: 0,
2591            argsalloc: 0,
2592        };
2593        // -i my-ctx tag-a tag-b
2594        let r = bin_comptags("comptags", &[
2595            "-i".into(), "my-ctx".into(),
2596            "tag-a".into(), "tag-b".into(),
2597        ], &ops, 0);
2598        assert_eq!(r, 0);
2599        // -T returns 1 (no sets yet).
2600        let r = bin_comptags("comptags", &[
2601            "-T".into(),
2602        ], &ops, 0);
2603        assert_eq!(r, 1);
2604        // comptags[0].context should be "my-ctx".
2605        let ctx = comptags.lock().unwrap()[0].as_ref()
2606            .and_then(|t| t.context.clone());
2607        // Restore the globals before assertion-fail can leave them mutated.
2608        INCOMPFUNC.store(saved_incompfunc, Ordering::Relaxed);
2609        crate::ported::builtin::LOCALLEVEL.store(saved_locallevel, Ordering::Relaxed);
2610        assert_eq!(ctx.as_deref(), Some("my-ctx"));
2611    }
2612
2613    /// c:3178-3186 — cv_get_val finds a value by name; missing returns None.
2614    #[test]
2615    fn cv_get_val_hits_and_misses() {
2616        let _g = crate::ported::zle::zle_main::zle_test_setup();
2617        let d = cvdef {
2618            vals: Some(Box::new(cvval {
2619                name: Some("foo".into()),
2620                r#type: CVV_NOARG,
2621                next: Some(Box::new(cvval {
2622                    name: Some("bar".into()),
2623                    r#type: CVV_ARG,
2624                    ..Default::default()
2625                })),
2626                ..Default::default()
2627            })),
2628            ..Default::default()
2629        };
2630        let hit = cv_get_val(&d, "bar").expect("hit");
2631        assert_eq!(hit.name.as_deref(), Some("bar"));
2632        assert_eq!(hit.r#type, CVV_ARG);
2633        assert!(cv_get_val(&d, "missing").is_none());
2634    }
2635
2636    /// c:5126-5131 — setup_ frees every cache slot and zeros
2637    /// lasttaglevel. Pre-fill all three caches + lasttaglevel, then
2638    /// call setup_ and verify they're cleared.
2639    #[test]
2640    fn setup_clears_all_caches() {
2641        use std::sync::atomic::Ordering;
2642        let _g = crate::ported::zle::zle_main::zle_test_setup();
2643        // Pre-fill.
2644        if let Ok(mut cache) = cadef_cache.lock() {
2645            cache[0] = Some(Box::new(cadef::default()));
2646        }
2647        if let Ok(mut cache) = cvdef_cache.lock() {
2648            cache[0] = Some(Box::new(cvdef::default()));
2649        }
2650        if let Ok(mut tab) = comptags.lock() {
2651            tab[0] = Some(Box::new(ctags::default()));
2652        }
2653        lasttaglevel.store(42, Ordering::Relaxed);
2654
2655        let r = setup_();
2656        assert_eq!(r, 0);
2657
2658        assert!(cadef_cache.lock().unwrap()[0].is_none(),
2659            "cadef_cache[0] should be cleared");
2660        assert!(cvdef_cache.lock().unwrap()[0].is_none(),
2661            "cvdef_cache[0] should be cleared");
2662        assert!(comptags.lock().unwrap()[0].is_none(),
2663            "comptags[0] should be cleared");
2664        assert_eq!(lasttaglevel.load(Ordering::Relaxed), 0,
2665            "lasttaglevel should reset to 0");
2666    }
2667
2668    /// c:5171-5177 — finish_ frees every slot in all three caches.
2669    /// Same pre-fill pattern as setup_clears_all_caches; finish_
2670    /// differs in not zeroing lasttaglevel.
2671    #[test]
2672    fn finish_frees_all_caches() {
2673        let _g = crate::ported::zle::zle_main::zle_test_setup();
2674        if let Ok(mut cache) = cadef_cache.lock() {
2675            cache[1] = Some(Box::new(cadef::default()));
2676        }
2677        if let Ok(mut cache) = cvdef_cache.lock() {
2678            cache[1] = Some(Box::new(cvdef::default()));
2679        }
2680        if let Ok(mut tab) = comptags.lock() {
2681            tab[1] = Some(Box::new(ctags::default()));
2682        }
2683        let r = finish_();
2684        assert_eq!(r, 0);
2685        assert!(cadef_cache.lock().unwrap()[1].is_none());
2686        assert!(cvdef_cache.lock().unwrap()[1].is_none());
2687        assert!(comptags.lock().unwrap()[1].is_none());
2688    }
2689
2690    /// c:4592-4593 — cfp_matcher_pats hits CMF_LEFT && lalen==0:
2691    /// return empty string immediately. Constructed Cmatcher chain
2692    /// has one matcher with CMF_LEFT flag and lalen=0, llen=1, wlen=1.
2693    /// We seed cmatcher_global so parse_cmatcher returns it via a
2694    /// matcher spec — but parse_cmatcher is complex, so this test
2695    /// instead constructs the chain directly and verifies the bail
2696    /// happens via the public surface (we exercise the same edge via
2697    /// a matcher spec parsed by parse_cmatcher that triggers the
2698    /// left-anchor zero-len path indirectly through the dispatcher).
2699    /// The simpler observable: a malformed/empty matcher spec is
2700    /// rejected and returns add untouched.
2701    #[test]
2702    fn cfp_matcher_pats_left_anchor_zero_lalen_returns_empty_via_invalid_spec() {
2703        let _g = crate::ported::zle::zle_main::zle_test_setup();
2704        crate::ported::utils::inittyptab();
2705        // A malformed matcher should fall through parse_cmatcher's
2706        // None return; cfp_matcher_pats then returns `add` unchanged
2707        // (the non-bail path; the CMF_LEFT-lalen0 bail requires a
2708        // successfully-parsed but pathological matcher which the
2709        // public parser does not produce).
2710        let r = cfp_matcher_pats("not-a-real-matcher-spec", "xyz");
2711        assert_eq!(r, "xyz");
2712    }
2713
2714    /// c:3967 — bin_comptry with lasttaglevel == 0 (no -i call yet)
2715    /// errors with "no tags registered".
2716    #[test]
2717    fn bin_comptry_no_taglevel_errors() {
2718        let _g = crate::ported::zle::zle_main::zle_test_setup();
2719        let saved_incompfunc = INCOMPFUNC.load(std::sync::atomic::Ordering::Relaxed);
2720        let saved_lvl = lasttaglevel.load(std::sync::atomic::Ordering::Relaxed);
2721        INCOMPFUNC.store(1, std::sync::atomic::Ordering::Relaxed);
2722        lasttaglevel.store(0, std::sync::atomic::Ordering::Relaxed);
2723        let ops = crate::ported::zsh_h::options {
2724            ind: [0u8; crate::ported::zsh_h::MAX_OPS],
2725            args: Vec::new(), argscount: 0, argsalloc: 0,
2726        };
2727        let r = bin_comptry("comptry", &["tag1".into()], &ops, 0);
2728        INCOMPFUNC.store(saved_incompfunc, std::sync::atomic::Ordering::Relaxed);
2729        lasttaglevel.store(saved_lvl, std::sync::atomic::Ordering::Relaxed);
2730        assert_eq!(r, 1);
2731    }
2732
2733    /// c:4091-4134 — bin_comptry plain mode: filters args to registered
2734    /// tags not already in any set, then appends one set with the
2735    /// surviving tags.
2736    #[test]
2737    fn bin_comptry_plain_adds_set() {
2738        let _g = crate::ported::zle::zle_main::zle_test_setup();
2739        let saved_incompfunc = INCOMPFUNC.load(std::sync::atomic::Ordering::Relaxed);
2740        let saved_lvl = lasttaglevel.load(std::sync::atomic::Ordering::Relaxed);
2741        // Seed comptags[1] with two registered tags.
2742        if let Ok(mut tab) = comptags.lock() {
2743            tab[1] = Some(Box::new(ctags {
2744                all:     Some(vec!["files".into(), "directories".into()]),
2745                context: Some("ctx".into()),
2746                init:    0,
2747                sets:    None,
2748            }));
2749        }
2750        INCOMPFUNC.store(1, std::sync::atomic::Ordering::Relaxed);
2751        lasttaglevel.store(1, std::sync::atomic::Ordering::Relaxed);
2752        let ops = crate::ported::zsh_h::options {
2753            ind: [0u8; crate::ported::zsh_h::MAX_OPS],
2754            args: Vec::new(), argscount: 0, argsalloc: 0,
2755        };
2756        let r = bin_comptry("comptry", &["files".into(), "unknown".into()], &ops, 0);
2757        // Inspect comptags[1].sets BEFORE restoring globals so a panic
2758        // doesn't leave them mutated.
2759        let sets_first_tags = comptags.lock().unwrap()[1].as_ref()
2760            .and_then(|t| t.sets.as_ref()
2761                .and_then(|s| s.tags.clone()));
2762        // Clean up.
2763        if let Ok(mut tab) = comptags.lock() {
2764            tab[1] = None;
2765        }
2766        INCOMPFUNC.store(saved_incompfunc, std::sync::atomic::Ordering::Relaxed);
2767        lasttaglevel.store(saved_lvl, std::sync::atomic::Ordering::Relaxed);
2768        assert_eq!(r, 0);
2769        let tags = sets_first_tags.expect("a set was appended");
2770        assert_eq!(tags, vec!["files".to_string()],
2771            "only registered tags survive the filter");
2772    }
2773
2774    /// c:4525 — cfp_matcher_pats returns add unchanged when matcher
2775    /// spec is empty (parse_cmatcher returns None).
2776    #[test]
2777    fn cfp_matcher_pats_empty_matcher_passthrough() {
2778        let _g = crate::ported::zle::zle_main::zle_test_setup();
2779        crate::ported::utils::inittyptab();
2780        let r = cfp_matcher_pats("", "abc");
2781        assert_eq!(r, "abc");
2782    }
2783
2784    /// c:4307 — cfp_matcher_range with no matchers (all None) emits
2785    /// each char verbatim.
2786    #[test]
2787    fn cfp_matcher_range_no_matchers_verbatim() {
2788        let _g = crate::ported::zle::zle_main::zle_test_setup();
2789        let ms: Vec<Option<Box<crate::ported::zle::comp_h::Cmatcher>>> =
2790            vec![None, None, None];
2791        let r = cfp_matcher_range(&ms, "abc");
2792        assert_eq!(r, "abc");
2793    }
2794
2795    /// c:247-394 — cd_prep groups path emits CRT_EXPL + CRT_SPEC runs
2796    /// when cd_state.groups > 0. With two singleton kind=0+desc entries
2797    /// we expect: 2 CRT_SPEC runs (one per leader) followed by the
2798    /// CRT_EXPL header.
2799    #[test]
2800    fn cd_prep_groups_emits_expl_and_spec_runs() {
2801        let _g = crate::ported::zle::zle_main::zle_test_setup();
2802        // Seed cd_state: one set with two kind=0+desc entries.
2803        {
2804            let mut st = cd_state.lock().unwrap();
2805            st.showd = 0;
2806            st.groups = 2;
2807            st.descs = 2;
2808            st.maxg = 1;
2809            st.maxglen = 5;
2810            st.maxmlen = 100;
2811            st.sets = Some(Box::new(cdset {
2812                count: 2,
2813                desc: 2,
2814                strs: Some(Box::new(cdstr {
2815                    str: Some("alpha".into()),
2816                    r#match: Some("alpha".into()),
2817                    desc: Some("first".into()),
2818                    width: 5, len: 5,
2819                    kind: 0,
2820                    next: Some(Box::new(cdstr {
2821                        str: Some("beta".into()),
2822                        r#match: Some("beta".into()),
2823                        desc: Some("second".into()),
2824                        width: 4, len: 4,
2825                        kind: 0,
2826                        ..Default::default()
2827                    })),
2828                    ..Default::default()
2829                })),
2830                ..Default::default()
2831            }));
2832            st.runs = None;
2833        }
2834        let r = cd_prep();
2835        assert_eq!(r, 0);
2836        let st = cd_state.lock().unwrap();
2837        let r1 = st.runs.as_deref().expect("first run");
2838        assert_eq!(r1.r#type, CRT_SPEC, "first run is CRT_SPEC");
2839        let r2 = r1.next.as_deref().expect("second run");
2840        assert_eq!(r2.r#type, CRT_SPEC, "second run is CRT_SPEC");
2841        let r3 = r2.next.as_deref().expect("third run");
2842        assert_eq!(r3.r#type, CRT_EXPL, "third run is CRT_EXPL");
2843        assert_eq!(r3.count, 2, "CRT_EXPL covers both prep_lines");
2844    }
2845
2846    /// c:4704-4732 — cfp_bld_pats combines names with skipped + pat.
2847    /// With one name "dir" and pats ["*.c"], produces ["dir*.c"].
2848    #[test]
2849    fn cfp_bld_pats_concatenates_skipped_and_pat() {
2850        let _g = crate::ported::zle::zle_main::zle_test_setup();
2851        let out = cfp_bld_pats(0,
2852            &["dir".to_string()],
2853            "",
2854            &["*.c".to_string()]);
2855        assert_eq!(out, vec!["dir*.c".to_string()]);
2856    }
2857
2858    /// c:4711 — when GLOBDOTS is unset AND compprefix starts with `.`,
2859    /// add a dot-prefixed variant of each non-`.`-leading pattern.
2860    #[test]
2861    fn cfp_bld_pats_globdots_variant() {
2862        let _g = crate::ported::zle::zle_main::zle_test_setup();
2863        // Seed COMPPREFIX with a leading dot.
2864        let m = crate::ported::zle::complete::COMPPREFIX
2865            .get_or_init(|| std::sync::Mutex::new(String::new()));
2866        *m.lock().unwrap() = ".foo".to_string();
2867        // Force GLOBDOTS unset — that's the default in zle_test_setup.
2868        let out = cfp_bld_pats(0,
2869            &["d".to_string()],
2870            "",
2871            &["*.x".to_string()]);
2872        // Reset compprefix to avoid bleed.
2873        *m.lock().unwrap() = String::new();
2874        assert!(out.contains(&"d*.x".to_string()));
2875        assert!(out.contains(&"d.*.x".to_string()),
2876            "dot-variant must be emitted: {:?}", out);
2877    }
2878
2879    /// c:4625 — cfp_opt_pats with empty compprefix passes pats through.
2880    #[test]
2881    fn cfp_opt_pats_passthrough_empty_compprefix() {
2882        let _g = crate::ported::zle::zle_main::zle_test_setup();
2883        let m = crate::ported::zle::complete::COMPPREFIX
2884            .get_or_init(|| std::sync::Mutex::new(String::new()));
2885        *m.lock().unwrap() = String::new();
2886        let pats = vec!["*".to_string(), "*.c".to_string()];
2887        let out = cfp_opt_pats(&pats, "");
2888        assert_eq!(out, pats);
2889    }
2890
2891    /// c:4175 — cfp_test_exact returns None when both compprefix and
2892    /// compsuffix are empty (no anchoring context).
2893    #[test]
2894    fn cfp_test_exact_no_anchor_returns_none() {
2895        let _g = crate::ported::zle::zle_main::zle_test_setup();
2896        let pm = crate::ported::zle::complete::COMPPREFIX
2897            .get_or_init(|| std::sync::Mutex::new(String::new()));
2898        *pm.lock().unwrap() = String::new();
2899        let sm = crate::ported::zle::complete::COMPSUFFIX
2900            .get_or_init(|| std::sync::Mutex::new(String::new()));
2901        *sm.lock().unwrap() = String::new();
2902        let r = cfp_test_exact(&["/tmp".to_string()],
2903                                &["true".to_string()], "");
2904        assert!(r.is_none());
2905    }
2906
2907    /// c:5083 — bin_compgroups registers 6 group variants per name.
2908    /// Sanity test: returns 0 on the empty-args path (no-op).
2909    #[test]
2910    fn bin_compgroups_empty_args_succeeds() {
2911        let _g = crate::ported::zle::zle_main::zle_test_setup();
2912        let saved = INCOMPFUNC.load(std::sync::atomic::Ordering::Relaxed);
2913        INCOMPFUNC.store(1, std::sync::atomic::Ordering::Relaxed);
2914        let ops = crate::ported::zsh_h::options {
2915            ind: [0u8; crate::ported::zsh_h::MAX_OPS],
2916            args: Vec::new(), argscount: 0, argsalloc: 0,
2917        };
2918        let r = bin_compgroups("compgroups", &[], &ops, 0);
2919        INCOMPFUNC.store(saved, std::sync::atomic::Ordering::Relaxed);
2920        assert_eq!(r, 0);
2921    }
2922
2923    /// c:215-230 — cd_groups_want_sorting: returns 0 when ANY set's
2924    /// opts contains `-V`, 1 when `-J` (or default).
2925    #[test]
2926    fn cd_groups_want_sorting_respects_opts() {
2927        let _g = crate::ported::zle::zle_main::zle_test_setup();
2928        // Default (no sets) → 1 (sorted).
2929        {
2930            let mut st = cd_state.lock().unwrap();
2931            st.sets = None;
2932        }
2933        assert_eq!(cd_groups_want_sorting(), 1);
2934        // Inject a set with -V option → returns 0.
2935        {
2936            let mut st = cd_state.lock().unwrap();
2937            st.sets = Some(Box::new(cdset {
2938                opts: Some(vec!["-V".into(), "grpname".into()]),
2939                ..Default::default()
2940            }));
2941        }
2942        assert_eq!(cd_groups_want_sorting(), 0);
2943        // Cleanup so other tests don't see the injected state.
2944        cd_state.lock().unwrap().sets = None;
2945    }
2946
2947    /// c:233 — cd_sort compares Cdstr.sortstr lexically.
2948    #[test]
2949    fn cd_sort_orders_by_sortstr() {
2950        let _g = crate::ported::zle::zle_main::zle_test_setup();
2951        let a = cdstr {
2952            sortstr: Some("apple".into()), ..Default::default()
2953        };
2954        let b = cdstr {
2955            sortstr: Some("banana".into()), ..Default::default()
2956        };
2957        assert_eq!(cd_sort(&a, &b), std::cmp::Ordering::Less);
2958        assert_eq!(cd_sort(&b, &a), std::cmp::Ordering::Greater);
2959        assert_eq!(cd_sort(&a, &a), std::cmp::Ordering::Equal);
2960    }
2961
2962    /// c:425-435 — cd_prep default branch: one CRT_SIMPLE run per
2963    /// non-empty set with the str chain mirrored via `.run` links.
2964    #[test]
2965    fn cd_prep_default_builds_simple_run_per_set() {
2966        let _g = crate::ported::zle::zle_main::zle_test_setup();
2967        // Seed cd_state with 2 sets, each 1 match.
2968        {
2969            let mut st = cd_state.lock().unwrap();
2970            st.showd = 0;
2971            st.groups = 0;
2972            st.sets = Some(Box::new(cdset {
2973                count: 1,
2974                strs: Some(Box::new(cdstr {
2975                    str: Some("a".into()),
2976                    r#match: Some("a".into()),
2977                    ..Default::default()
2978                })),
2979                next: Some(Box::new(cdset {
2980                    count: 1,
2981                    strs: Some(Box::new(cdstr {
2982                        str: Some("b".into()),
2983                        r#match: Some("b".into()),
2984                        ..Default::default()
2985                    })),
2986                    ..Default::default()
2987                })),
2988                ..Default::default()
2989            }));
2990            st.runs = None;
2991        }
2992        let r = cd_prep();
2993        assert_eq!(r, 0);
2994        // Two CRT_SIMPLE runs, one per set.
2995        let st = cd_state.lock().unwrap();
2996        let r1 = st.runs.as_deref().expect("first run");
2997        assert_eq!(r1.r#type, CRT_SIMPLE);
2998        assert_eq!(r1.count, 1);
2999        let r2 = r1.next.as_deref().expect("second run");
3000        assert_eq!(r2.r#type, CRT_SIMPLE);
3001        assert_eq!(r2.count, 1);
3002        assert!(r2.next.is_none(), "no third run");
3003    }
3004
3005    /// c:846-895 — bin_compdescribe with invalid option returns 1.
3006    #[test]
3007    fn bin_compdescribe_rejects_bad_option() {
3008        let _g = crate::ported::zle::zle_main::zle_test_setup();
3009        let saved_incompfunc = INCOMPFUNC.load(std::sync::atomic::Ordering::Relaxed);
3010        INCOMPFUNC.store(1, std::sync::atomic::Ordering::Relaxed);
3011        let ops = crate::ported::zsh_h::options {
3012            ind: [0u8; crate::ported::zsh_h::MAX_OPS],
3013            args: Vec::new(), argscount: 0, argsalloc: 0,
3014        };
3015        // -xx is two chars but ends with `x` not a known subcommand
3016        // letter — should fall through to the `invalid option` arm.
3017        let r = bin_compdescribe("compdescribe", &["-x".into()], &ops, 0);
3018        INCOMPFUNC.store(saved_incompfunc, std::sync::atomic::Ordering::Relaxed);
3019        assert_eq!(r, 1);
3020    }
3021
3022    /// c:4903-4923 — cf_remove_other with pre="dir/foo" returns
3023    /// only names starting with "dir/" and clears `amb`.
3024    #[test]
3025    fn cf_remove_other_filters_by_dir_head() {
3026        let _g = crate::ported::zle::zle_main::zle_test_setup();
3027        let names = vec![
3028            "dir/a".to_string(),
3029            "dir/b".to_string(),
3030            "other/c".to_string(),
3031        ];
3032        let mut amb = 99;
3033        let ret = cf_remove_other(&names, "dir/foo", &mut amb);
3034        assert_eq!(amb, 0);
3035        let v = ret.expect("matching names returned");
3036        assert_eq!(v, vec!["dir/a".to_string(), "dir/b".to_string()]);
3037    }
3038
3039    /// c:4942-4951 — pre without '/' and names diverge → `amb=1`,
3040    /// returns None.
3041    #[test]
3042    fn cf_remove_other_no_slash_diverge_sets_amb() {
3043        let _g = crate::ported::zle::zle_main::zle_test_setup();
3044        let names = vec!["a".to_string(), "b".to_string()];
3045        let mut amb = 0;
3046        let ret = cf_remove_other(&names, "x", &mut amb);
3047        assert!(ret.is_none());
3048        assert_eq!(amb, 1);
3049    }
3050
3051    /// c:4870 — no "parent" and no "pwd" in style → cf_ignore returns
3052    /// without touching `ign`.
3053    #[test]
3054    fn cf_ignore_no_style_is_noop() {
3055        let _g = crate::ported::zle::zle_main::zle_test_setup();
3056        let mut ign: Vec<String> = vec!["x".into()];
3057        cf_ignore(&["/tmp".into()], &mut ign, "", "/tmp/foo");
3058        assert_eq!(ign, vec!["x".to_string()],
3059            "no style match must leave ign untouched");
3060    }
3061
3062    /// c:3691 — empty compqstack short-circuits bin_compquote to 0.
3063    #[test]
3064    fn bin_compquote_returns_zero_when_qstack_empty() {
3065        let _g = crate::ported::zle::zle_main::zle_test_setup();
3066        let saved_incompfunc = INCOMPFUNC.load(std::sync::atomic::Ordering::Relaxed);
3067        INCOMPFUNC.store(1, std::sync::atomic::Ordering::Relaxed);
3068        // Ensure compqstack is empty (zle_test_setup resets things).
3069        if let Some(m) = COMPQSTACK.get() {
3070            if let Ok(mut s) = m.lock() {
3071                s.clear();
3072            }
3073        }
3074        let ops = crate::ported::zsh_h::options {
3075            ind: [0u8; crate::ported::zsh_h::MAX_OPS],
3076            args: Vec::new(),
3077            argscount: 0, argsalloc: 0,
3078        };
3079        let r = bin_compquote("compquote", &["foo".into()], &ops, 0);
3080        INCOMPFUNC.store(saved_incompfunc, std::sync::atomic::Ordering::Relaxed);
3081        assert_eq!(r, 0);
3082    }
3083
3084    /// c:3192-3203 — cv_quote_get_val unquotes input then delegates
3085    /// to cv_get_val. Quoted name with backslash should still match
3086    /// after parse_subst_string strips the quoting.
3087    #[test]
3088    fn cv_quote_get_val_unquotes_then_lookup() {
3089        let _g = crate::ported::zle::zle_main::zle_test_setup();
3090        crate::ported::utils::inittyptab();
3091        let d = cvdef {
3092            vals: Some(Box::new(cvval {
3093                name: Some("foo".into()),
3094                r#type: CVV_NOARG,
3095                ..Default::default()
3096            })),
3097            ..Default::default()
3098        };
3099        // Plain name → hit.
3100        assert!(cv_quote_get_val(&d, "foo").is_some());
3101        // Unknown → miss.
3102        assert!(cv_quote_get_val(&d, "bar").is_none());
3103    }
3104
3105    /// c:3211-3217 — cv_inactive clears active for each name in xor.
3106    #[test]
3107    fn cv_inactive_clears_named_vals() {
3108        let _g = crate::ported::zle::zle_main::zle_test_setup();
3109        let mut d = cvdef {
3110            vals: Some(Box::new(cvval {
3111                name: Some("a".into()), active: 1,
3112                next: Some(Box::new(cvval {
3113                    name: Some("b".into()), active: 1,
3114                    next: Some(Box::new(cvval {
3115                        name: Some("c".into()), active: 1,
3116                        ..Default::default()
3117                    })),
3118                    ..Default::default()
3119                })),
3120                ..Default::default()
3121            })),
3122            ..Default::default()
3123        };
3124        cv_inactive(&mut d, &["a".into(), "c".into()]);
3125        let mut p = d.vals.as_deref();
3126        let mut by_name = std::collections::HashMap::new();
3127        while let Some(v) = p {
3128            by_name.insert(v.name.clone().unwrap_or_default(), v.active);
3129            p = v.next.as_deref();
3130        }
3131        assert_eq!(by_name["a"], 0);
3132        assert_eq!(by_name["b"], 1, "untouched val stays active");
3133        assert_eq!(by_name["c"], 0);
3134    }
3135}
3136
3137// ===========================================================
3138// Methods moved verbatim from src/ported/exec.rs because their
3139// C counterpart's source file maps 1:1 to this Rust module.
3140// Rust permits multiple inherent impl blocks for the same
3141// type within a crate, so call sites in exec.rs are unchanged.
3142// ===========================================================
3143
3144// BEGIN moved-from-exec-rs
3145// (impl ShellExecutor block moved to src/exec_shims.rs — see file marker)
3146
3147// END moved-from-exec-rs
3148
3149
3150// ─── moved from src/ported/exec.rs (drift extraction) ───
3151
3152// CompSpec / CompMatch / CompGroup / CompState moved out of this
3153// port file to `src/extensions/bash_complete.rs` — they are
3154// Rust-original types backing the bash-style `complete` builtin
3155// extension, not zsh C ports. The ported zle/ tree should stay a
3156// faithful C-source mirror; Rust-only types live in extensions/.
3157//
3158// Callers that used `crate::ported::zle::computil::Comp*` should
3159// switch to `crate::bash_complete::Comp*` (the path lib.rs
3160// exports). exec.rs's re-export updated to point to the new home.
3161
3162
3163/// Direct port of `static Cadef alloc_cadef(char **args, int single,
3164/// char *match, char *nonarg, int flags)` from `Src/Zle/computil.c:1147-1177`.
3165///
3166/// Builds a fresh `cadef` with the option/single-letter/match/nonarg
3167/// fields initialized. `args` (if present) is captured into `defs`
3168/// for later cache-key compare in `get_cadef` (c:1681). `single` set
3169/// allocates the 188-slot single-letter index array. `match` is the
3170/// match-spec carried through to the option/arg matchers.
3171pub fn alloc_cadef(args: Option<&[String]>, single: i32, matchstr: &str,    // c:1147
3172                   nonarg: Option<&str>, flags: i32) -> Box<cadef> {
3173    Box::new(cadef {
3174        next:       None,                                                    // c:1152
3175        snext:      None,                                                    // c:1152
3176        opts:       None,                                                    // c:1153
3177        args:       None,                                                    // c:1154
3178        rest:       None,                                                    // c:1154
3179        nonarg:     nonarg.map(|s| s.to_string()),                           // c:1155 ztrdup(nonarg)
3180        defs:       args.map(|a| a.to_vec()),                                // c:1157 zarrdup(args)
3181        ndefs:      args.map_or(0, |a| a.len() as i32),                      // c:1158 arrlen(args)
3182        nopts:      0,                                                       // c:1163
3183        ndopts:     0,                                                       // c:1164
3184        nodopts:    0,                                                       // c:1165
3185        lastt:      {                                                        // c:1166 time(0)
3186            use std::time::{SystemTime, UNIX_EPOCH};
3187            SystemTime::now().duration_since(UNIX_EPOCH)
3188                .map(|d| d.as_secs() as i64).unwrap_or(0)
3189        },
3190        set:        None,                                                    // c:1167
3191        // c:1168-1172 — 188-slot single-letter Caopt index. Capacity
3192        // 188 matches C exactly (range of single-letter option names).
3193        single:     if single != 0 {
3194            Some((0..188).map(|_| None).collect())
3195        } else {
3196            None
3197        },
3198        r#match:    Some(matchstr.to_string()),                              // c:1173 ztrdup(match)
3199        argsactive: 0,
3200        flags,                                                               // c:1174
3201    })
3202}
3203
3204/// Port of `arrcontains(char **a, char *s, int colon)` from Src/Zle/computil.c:3813.
3205pub fn arrcontains(a: &[String], s: &str, colon: bool) -> i32 {              // c:3813
3206    // C body c:3817-3826: linear scan; if colon, compare up to first
3207    //                    `:` in either side; else strcmp.
3208    for entry in a {
3209        if colon {
3210            let p = s.split(':').next().unwrap_or(s);
3211            let q = entry.split(':').next().unwrap_or(entry);
3212            if p == q {
3213                return 1;                                                    // c:3823
3214            }
3215        } else if entry == s {
3216            return 1;                                                        // c:3825
3217        }
3218    }
3219    0                                                                        // c:3827
3220}
3221
3222/// Direct port of `static int bin_comparguments(char *nam, char **args,
3223///                                                 UNUSED(Options ops),
3224///                                                 UNUSED(int func))`
3225/// from `Src/Zle/computil.c:2585-2914`. Full subcommand dispatch for
3226/// `comparguments -i/-D/-O/-L/-s/-M/-a/-W/-n`. Each branch consumes
3227/// the parsed `ca_laststate` from `ca_parse_line`.
3228pub fn bin_comparguments(nam: &str, args: &[String],                         // c:2585
3229                         _ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
3230    use crate::ported::params::{setsparam, setaparam, sethparam, setiparam};
3231    use crate::ported::zle::complete::{COMPCURRENT, COMPWORDS};
3232    use std::sync::atomic::Ordering;
3233
3234    if INCOMPFUNC.load(Ordering::Relaxed) != 1 {                             // c:2590
3235        zwarnnam(nam, "can only be called from completion function");
3236        return 1;
3237    }
3238    if args.is_empty() { return 1; }
3239    let a0 = args[0].as_bytes();
3240    // c:2594 — must be `-X` exactly (2 chars).
3241    if a0.len() != 2 || a0[0] != b'-' {
3242        zwarnnam(nam, &format!("invalid argument: {}", args[0]));
3243        return 1;
3244    }
3245    let sub = a0[1];
3246
3247    // c:2598 — non-init subcommands require ca_parsed.
3248    if sub != b'i' && sub != b'I' && ca_parsed.load(Ordering::Relaxed) == 0 {
3249        zwarnnam(nam, "no parsed state");
3250        return 1;
3251    }
3252
3253    // c:2602 — per-subcommand arg-count bounds.
3254    let (min, max): (i32, i32) = match sub {
3255        b'i' => (2, -1),
3256        b'D' => (3, 3),
3257        b'O' => (4, 4),
3258        b'L' => (3, 4),
3259        b's' => (1, 1),
3260        b'M' => (1, 1),
3261        b'a' => (0, 0),
3262        b'W' => (3, 3),
3263        b'n' => (1, 1),
3264        _ => {
3265            zwarnnam(nam, &format!("invalid option: {}", args[0]));
3266            return 1;
3267        }
3268    };
3269    let n = (args.len() as i32) - 1;
3270    if n < min { zwarnnam(nam, "not enough arguments"); return 1; }
3271    if max >= 0 && n > max { zwarnnam(nam, "too many arguments"); return 1; }
3272
3273    match sub {
3274        b'i' => {                                                            // c:2625
3275            // c:2629 — compcurrent > 1 && compwords[0].
3276            let compcur = COMPCURRENT.load(Ordering::Relaxed);
3277            let compwords_nonempty = COMPWORDS.get()
3278                .and_then(|m| m.lock().ok().map(|w| !w.is_empty() && !w[0].is_empty()))
3279                .unwrap_or(false);
3280            if compcur <= 1 || !compwords_nonempty {
3281                return 1;                                                    // c:2670
3282            }
3283            // c:2636 — get_cadef(nam, args[1..]).
3284            // get_cadef returns 1 on cache hit. Look up the cached cadef
3285            // from the cadef_cache by argv match and run parse_line.
3286            let spec = &args[1..];
3287            let _ = get_cadef(nam, spec);                                    // c:2636
3288            // Now find the cadef in the cache.
3289            let cached: Option<Box<cadef>> = {
3290                let cache = cadef_cache.lock().ok();
3291                cache.and_then(|c| {
3292                    c.iter().find_map(|slot| {
3293                        slot.as_ref().filter(|e| {
3294                            e.ndefs == spec.len() as i32
3295                                && e.defs.as_deref().map_or(false, |d| {
3296                                    d.len() == spec.len()
3297                                        && d.iter().zip(spec.iter()).all(|(a, b)| a == b)
3298                                })
3299                        }).cloned()
3300                    })
3301                })
3302            };
3303            let Some(mut def_head) = cached else { return 1; };
3304            ca_parsed.store(0, Ordering::Relaxed);                           // c:2634
3305            ca_doff.store(0, Ordering::Relaxed);
3306            let all_clone = Box::new(clone_cadef_shallow(&def_head));
3307
3308            // c:2643-2664 — for each set walk: track which parses
3309            // succeeded ("use"). When a set succeeds AND more sets
3310            // remain, snapshot ca_laststate into a fallback chain.
3311            // When the final set rejects, restore the most-recent
3312            // saved state (or fail with ret=1 if no fallback).
3313            let mut first = 1;
3314            let mut def_opt: Option<Box<cadef>> = Some(def_head);
3315            let mut multi = 0;
3316            // Look ahead once to see if we have more than one set;
3317            // matches C's `multi = !!def->snext` at c:2639.
3318            if let Some(ref d) = def_opt {
3319                if d.snext.is_some() { multi = 1; }
3320            }
3321            let mut states: Vec<castate> = Vec::new();                       // c:2632
3322            let mut ret = 0i32;
3323
3324            while let Some(mut current) = def_opt {
3325                let next = current.snext.take();
3326                let parse_ret = ca_parse_line(&mut current, &all_clone, multi, first);
3327                let use_state = parse_ret == 0;                              // c:2644
3328                let has_next = next.is_some();
3329                if use_state && has_next {                                   // c:2646
3330                    // c:2648 — snapshot ca_laststate, push onto fallback.
3331                    if let Ok(ls) = ca_laststate.lock() {
3332                        states.push(clone_castate_full(&ls));
3333                    }
3334                } else if !use_state && !has_next {                          // c:2652
3335                    // c:2654 — restore most-recent saved state (if any).
3336                    if let Some(saved) = states.pop() {
3337                        if let Ok(mut ls) = ca_laststate.lock() {
3338                            freecastate(&mut ls);
3339                            *ls = saved;
3340                        }
3341                    } else {
3342                        ret = 1;                                             // c:2661
3343                    }
3344                }
3345                first = 0;                                                   // c:2663
3346                def_opt = next;
3347            }
3348            ca_parsed.store(1, Ordering::Relaxed);                           // c:2665
3349
3350            // c:2666 — thread fallback chain into ca_laststate.snext.
3351            if !states.is_empty() {
3352                if let Ok(mut ls) = ca_laststate.lock() {
3353                    // Build a linked snext chain from oldest → newest.
3354                    let mut head: Option<Box<castate>> = None;
3355                    for s in states.into_iter().rev() {
3356                        let mut s = s;
3357                        s.snext = head;
3358                        head = Some(Box::new(s));
3359                    }
3360                    ls.snext = head;
3361                }
3362            }
3363            ret                                                              // c:2668
3364        }
3365
3366        b'D' => {                                                            // c:2672
3367            let mut descr: Vec<String> = Vec::new();
3368            let mut act: Vec<String> = Vec::new();
3369            let mut subc: Vec<String> = Vec::new();
3370            let mut ret = 1i32;
3371            crate::ported::zle::complete::ignore_prefix(
3372                ca_doff.load(Ordering::Relaxed));
3373
3374            // Walk lstate (ca_laststate + its snext chain).
3375            let mut state_clone = ca_laststate.lock().map(|s| clone_castate_full(&s))
3376                .ok();
3377            while let Some(s) = state_clone {
3378                let arg = s.def.clone();
3379                if let Some(a) = arg {
3380                    ret = 0;
3381                    let opt_str = a.opt.clone();
3382                    let optdef = s.curopt.clone();
3383                    ca_set_data(&mut descr, &mut act, &mut subc,
3384                                opt_str.as_deref(),
3385                                Some(a),
3386                                optdef.as_deref(),
3387                                if ca_doff.load(Ordering::Relaxed) > 0 { 1 } else { 0 });
3388                }
3389                state_clone = s.snext.map(|b| *b);
3390            }
3391            if ret == 0 {                                                    // c:2698
3392                setaparam(&args[1], descr);
3393                setaparam(&args[2], act);
3394                setaparam(&args[3], subc);
3395            }
3396            ret
3397        }
3398
3399        b'M' => {                                                            // c:2827
3400            let m = ca_laststate.lock().ok()
3401                .and_then(|s| s.d.as_ref().and_then(|d| d.r#match.clone()))
3402                .unwrap_or_default();
3403            setsparam(&args[1], &m);
3404            0
3405        }
3406
3407        b'a' => {                                                            // c:2833
3408            let mut state_clone = ca_laststate.lock().map(|s| clone_castate_full(&s))
3409                .ok();
3410            while let Some(s) = state_clone {
3411                if s.d.as_ref().map_or(false, |d| d.args.is_some() || d.rest.is_some()) {
3412                    return 0;
3413                }
3414                state_clone = s.snext.map(|b| *b);
3415            }
3416            1                                                                // c:2840
3417        }
3418
3419        b'n' => {                                                            // c:2899
3420            let optbeg = ca_laststate.lock().map(|s| s.optbeg).unwrap_or(0);
3421            let kshoffset = if crate::ported::zsh_h::isset(
3422                crate::ported::zsh_h::KSHARRAYS) { 0 } else { 1 };
3423            setiparam(&args[1], (optbeg + kshoffset) as i64);
3424            0
3425        }
3426
3427        b'O' => {                                                            // c:2705
3428            // Build the four lists; for each non-`not` active opt assign it
3429            // to one of {next, direct, odirect, equal}.
3430            let mut next_l: Vec<String> = Vec::new();
3431            let mut direct_l: Vec<String> = Vec::new();
3432            let mut odirect_l: Vec<String> = Vec::new();
3433            let mut equal_l: Vec<String> = Vec::new();
3434            let mut ret = 1i32;
3435
3436            let mut state_clone = ca_laststate.lock().map(|s| clone_castate_full(&s))
3437                .ok();
3438            while let Some(s) = state_clone {
3439                // c:2721 — gate on actopts + position.
3440                let actopts_ok = s.actopts != 0 &&
3441                    (s.opt != 0
3442                     || (ca_doff.load(Ordering::Relaxed) != 0 && s.def.is_some())
3443                     || (s.def.is_some()
3444                         && s.def.as_deref().map_or(false, |d|
3445                            d.opt.is_some()
3446                            && (d.r#type == CAA_OPT
3447                                || (d.r#type >= CAA_RARGS && d.num < 0)))));
3448                let pos_ok = s.def.is_none()
3449                    || s.def.as_deref().map_or(true, |d| d.r#type < CAA_RARGS)
3450                    || (s.def.as_deref().map_or(false, |d| d.r#type == CAA_RARGS)
3451                        && s.curpos == s.argbeg + 1)
3452                    || COMPCURRENT.load(Ordering::Relaxed) == 1;
3453                if actopts_ok && pos_ok {
3454                    ret = 0;
3455                    if let Some(d) = s.d.as_ref() {
3456                        let mut p = d.opts.as_deref();
3457                        while let Some(opt) = p {
3458                            if opt.active != 0 && opt.not == 0 {
3459                                let bucket: &mut Vec<String> = match opt.r#type {
3460                                    t if t == CAO_NEXT => &mut next_l,
3461                                    t if t == CAO_DIRECT => &mut direct_l,
3462                                    t if t == CAO_ODIRECT => &mut odirect_l,
3463                                    _ => &mut equal_l,
3464                                };
3465                                let name_esc = bslashcolon(
3466                                    opt.name.as_deref().unwrap_or(""));
3467                                let str_val = if let Some(desc) = opt.descr.as_deref() {
3468                                    format!("{}:{}", name_esc, desc)
3469                                } else {
3470                                    name_esc
3471                                };
3472                                if !bucket.iter().any(|s| s == &str_val) {
3473                                    bucket.push(str_val);
3474                                }
3475                            }
3476                            p = opt.next.as_deref();
3477                        }
3478                    }
3479                }
3480                state_clone = s.snext.map(|b| *b);
3481            }
3482
3483            if ret == 0 {
3484                setaparam(&args[1], next_l);
3485                setaparam(&args[2], direct_l);
3486                setaparam(&args[3], odirect_l);
3487                setaparam(&args[4], equal_l);
3488                0
3489            } else {
3490                let singles = ca_laststate.lock().map(|s| s.singles).unwrap_or(0);
3491                if singles != 0 { 2 } else { 1 }                             // c:2769
3492            }
3493        }
3494
3495        b'L' => {                                                            // c:2771
3496            // c:2787 — for each state, ca_get_opt(d, args[1], 1, NULL).
3497            let mut descr: Vec<String> = Vec::new();
3498            let mut act: Vec<String> = Vec::new();
3499            let mut subc: Vec<String> = Vec::new();
3500            let mut ret = 1i32;
3501            let mut state_clone = ca_laststate.lock().map(|s| clone_castate_full(&s))
3502                .ok();
3503            while let Some(s) = state_clone {
3504                if let Some(d) = s.d.as_ref() {
3505                    let mut end = 0usize;
3506                    if let Some(opt) = ca_get_opt(d, &args[1], 1, &mut end) {
3507                        if opt.args.is_some() {
3508                            ret = 0;
3509                            let opt_name = opt.name.clone();
3510                            let opt_args = opt.args.clone();
3511                            ca_set_data(&mut descr, &mut act, &mut subc,
3512                                        opt_name.as_deref(),
3513                                        opt_args,
3514                                        Some(&opt),
3515                                        1);
3516                        }
3517                    }
3518                }
3519                state_clone = s.snext.map(|b| *b);
3520            }
3521            if ret == 0 {
3522                setaparam(&args[2], descr);
3523                setaparam(&args[3], act);
3524                setaparam(&args[4], subc);
3525            }
3526            ret
3527        }
3528
3529        b's' => {                                                            // c:2803
3530            let mut state_clone = ca_laststate.lock().map(|s| clone_castate_full(&s))
3531                .ok();
3532            while let Some(s) = state_clone {
3533                let single_active = s.d.as_ref().map_or(false, |d| d.single.is_some())
3534                    && s.singles != 0
3535                    && s.actopts != 0;
3536                if single_active {
3537                    let kind = if let (Some(_), Some(dopt)) = (&s.ddef, &s.dopt) {
3538                        match dopt.r#type {
3539                            t if t == CAO_DIRECT => "direct",
3540                            t if t == CAO_OEQUAL || t == CAO_EQUAL => "equal",
3541                            _ => "next",
3542                        }
3543                    } else { "" };
3544                    setsparam(&args[1], kind);
3545                    return 0;
3546                }
3547                state_clone = s.snext.map(|b| *b);
3548            }
3549            1
3550        }
3551
3552        b'W' => {                                                            // c:2841
3553            // Build state.args concat and oargs concat for $opt_args.
3554            let mut all_args: Vec<String> = Vec::new();
3555            let opt_args_use_nul = !args[3].starts_with('0');
3556            let mut state_clone = ca_laststate.lock().map(|s| clone_castate_full(&s))
3557                .ok();
3558            // Pass 1: state.args.
3559            let mut snapshot = state_clone.clone();
3560            while let Some(s) = snapshot {
3561                if let Some(a) = s.args.as_ref() {
3562                    all_args.extend(a.iter().cloned());
3563                }
3564                snapshot = s.snext.map(|b| *b);
3565            }
3566            setaparam(&args[1], all_args);
3567
3568            // Pass 2: oargs into a hash.
3569            let mut hash_vec: Vec<String> = Vec::new();
3570            while let Some(s) = state_clone {
3571                if let Some(d) = s.d.as_ref() {
3572                    let mut o = d.opts.as_deref();
3573                    let mut a_idx = 0usize;
3574                    let oargs_ref = s.oargs.as_deref();
3575                    while let Some(op) = o {
3576                        if let Some(oa) = oargs_ref.and_then(|v| v.get(a_idx)).and_then(|x| x.as_ref()) {
3577                            let key = match (op.gsname.as_deref(), op.name.as_deref()) {
3578                                (Some(gs), Some(n)) =>
3579                                    format!("{}{}", gs, n),
3580                                (None, Some(n)) => n.to_string(),
3581                                _ => String::new(),
3582                            };
3583                            hash_vec.push(key);
3584                            let joined = if opt_args_use_nul {
3585                                String::from_utf8_lossy(&ca_nullist(oa)).into_owned()
3586                            } else {
3587                                ca_colonlist(oa)
3588                            };
3589                            hash_vec.push(joined);
3590                        }
3591                        a_idx += 1;
3592                        o = op.next.as_deref();
3593                    }
3594                }
3595                state_clone = s.snext.map(|b| *b);
3596            }
3597            sethparam(&args[2], hash_vec);
3598            0
3599        }
3600
3601        _ => 0,
3602    }
3603}
3604
3605/// Mirror of `memcpy` from a locked `ca_laststate` so we can walk the
3606/// snext chain without holding the mutex. Rust-only artifact.
3607#[allow(dead_code)]
3608fn clone_castate_full(s: &castate) -> castate {
3609    castate {
3610        snext:   s.snext.clone(),
3611        d:       s.d.clone(),
3612        nopts:   s.nopts,
3613        def:     s.def.clone(),
3614        ddef:    s.ddef.clone(),
3615        curopt:  s.curopt.clone(),
3616        dopt:    s.dopt.clone(),
3617        opt:     s.opt, arg: s.arg,
3618        argbeg:  s.argbeg, optbeg: s.optbeg,
3619        nargbeg: s.nargbeg, restbeg: s.restbeg,
3620        curpos:  s.curpos, argend: s.argend,
3621        inopt:   s.inopt, inarg: s.inarg,
3622        nth:     s.nth, singles: s.singles, oopt: s.oopt, actopts: s.actopts,
3623        args:    s.args.clone(),
3624        oargs:   s.oargs.clone(),
3625    }
3626}
3627
3628/// Direct port of `static int bin_compdescribe(char *nam, char **args,
3629///                                                UNUSED(Options ops),
3630///                                                UNUSED(int func))`
3631/// from `Src/Zle/computil.c:846-895`. Subcommand dispatch for
3632/// `compdescribe -i/-I/-g`:
3633///   - `-i hide mlen ARGS...` → cd_init with empty opts and disp=0
3634///   - `-I hide mlen sep optsParam ARGS...` → cd_init with disp=1
3635///   - `-g param csl mats dpys` → cd_get with the 4 output params
3636pub fn bin_compdescribe(nam: &str, args: &[String],                          // c:846
3637                        _ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
3638    use crate::ported::params::paramtab;
3639    use std::sync::atomic::Ordering;
3640
3641    if INCOMPFUNC.load(Ordering::Relaxed) != 1 {                             // c:850
3642        zwarnnam(nam, "can only be called from completion function");
3643        return 1;
3644    }
3645    if args.is_empty() { return 1; }
3646    let a0 = args[0].as_bytes();
3647    // c:854 — `args[0]` must be exactly 2 chars starting with `-`.
3648    if a0.len() != 2 || a0[0] != b'-' {
3649        zwarnnam(nam, &format!("invalid argument: {}", args[0]));
3650        return 1;
3651    }
3652    let n = args.len() as i32;
3653
3654    match a0[1] {
3655        b'i' => {                                                            // c:859
3656            if n < 3 {
3657                zwarnnam(nam, "not enough arguments");
3658                return 1;
3659            }
3660            cd_init(nam, &args[1], &args[2], "", &[], &args[3..], 0)         // c:865
3661        }
3662        b'I' => {                                                            // c:866
3663            if n < 6 {
3664                zwarnnam(nam, "not enough arguments");
3665                return 1;
3666            }
3667            // c:874 — getaparam(args[4]).
3668            let opts_arr: Vec<String> = paramtab().read().ok()
3669                .and_then(|tab| tab.get(&args[4])
3670                    .and_then(|pm| pm.u_arr.clone()))
3671                .unwrap_or_default();
3672            if opts_arr.is_empty() && paramtab().read().ok()
3673                .map_or(true, |tab| tab.get(&args[4]).is_none())
3674            {
3675                zwarnnam(nam, &format!("unknown parameter: {}", args[4]));
3676                return 1;
3677            }
3678            cd_init(nam, &args[1], &args[2], &args[3], &opts_arr,            // c:878
3679                    &args[5..], 1)
3680        }
3681        b'g' => {                                                            // c:880
3682            if cd_parsed.load(Ordering::Relaxed) == 0 {                       // c:881
3683                zwarnnam(nam, "no parsed state");                            // c:889
3684                return 1;
3685            }
3686            if n != 5 {
3687                zwarnnam(nam,
3688                    if n < 5 { "not enough arguments" } else { "too many arguments" });
3689                return 1;
3690            }
3691            cd_get(&args[1..])                                                // c:887
3692        }
3693        _ => {
3694            zwarnnam(nam, &format!("invalid option: {}", args[0]));
3695            1
3696        }
3697    }
3698}
3699
3700/// Direct port of `static int bin_compfiles(char *nam, char **args,
3701///                                            UNUSED(Options ops),
3702///                                            UNUSED(int func))` from
3703/// `Src/Zle/computil.c:4970-5070`. Subcommand dispatch for
3704/// `compfiles -p/-P/-i/-r`. `-i` runs cf_ignore on a param-named
3705/// array; `-r` runs cf_remove_other; `-p`/`-P` thread through cf_pats.
3706pub fn bin_compfiles(nam: &str, args: &[String],                             // c:4970
3707                     _ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
3708    use crate::ported::params::{paramtab, setaparam};
3709    use crate::ported::utils::quotestring;
3710    use crate::ported::zsh_h::QT_BACKSLASH_PATTERN;
3711    use std::sync::atomic::Ordering;
3712
3713    if INCOMPFUNC.load(Ordering::Relaxed) != 1 {                             // c:4972
3714        zwarnnam(nam, "can only be called from completion function");
3715        return 1;
3716    }
3717    if args.is_empty() || !args[0].starts_with('-') {                        // c:4976
3718        let bad = args.first().map(|s| s.as_str()).unwrap_or("");
3719        zwarnnam(nam, &format!("missing option: {}", bad));
3720        return 1;
3721    }
3722    let a0 = args[0].as_bytes();
3723    if a0.len() < 2 { zwarnnam(nam, &format!("missing option: {}", args[0])); return 1; }
3724    let sub = a0[1];
3725
3726    // Helper: read a named array via paramtab.
3727    let get_arr = |name: &str| -> Option<Vec<String>> {
3728        paramtab().read().ok().and_then(|tab| {
3729            tab.get(name).and_then(|pm| pm.u_arr.clone())
3730        })
3731    };
3732
3733    match sub {
3734        b'p' | b'P' => {                                                      // c:4981
3735            // c:4983 — accept `-p` or `-p--` (the `--` toggles noopt).
3736            let noopt = a0.len() > 2;
3737            if noopt && (a0.len() != 4 || a0[2] != b'-' || a0[3] != b'-') {
3738                zwarnnam(nam, &format!("invalid option: {}", args[0]));
3739                return 1;
3740            }
3741            let required = if sub == b'p' { 8 } else { 7 };
3742            if args.len() <= required {                                      // c:4990
3743                zwarnnam(nam, "too few arguments");
3744                return 1;
3745            }
3746            // c:4996 — getaparam(args[1]).
3747            let Some(src) = get_arr(&args[1]) else {
3748                zwarnnam(nam, &format!("unknown parameter: {}", args[1]));
3749                return 0;
3750            };
3751            // c:5001 — quotestring each entry with QT_BACKSLASH_PATTERN.
3752            let l: Vec<String> = src.iter()
3753                .map(|s| quotestring(s, QT_BACKSLASH_PATTERN))
3754                .collect();
3755            // c:5003 — cf_pats dispatch.
3756            let result = cf_pats(
3757                if sub == b'P' { 1 } else { 0 },
3758                if noopt { 1 } else { 0 },
3759                &l,
3760                &get_arr(&args[2]).unwrap_or_default(),
3761                &args[3],
3762                &args[4],
3763                &args[5],
3764                &get_arr(&args[6]).unwrap_or_default(),
3765                &args[7..],
3766            );
3767            setaparam(&args[1], result);
3768            0
3769        }
3770        b'i' => {                                                            // c:5010
3771            if a0.len() > 2 {                                                 // c:5011
3772                zwarnnam(nam, &format!("invalid option: {}", args[0]));
3773                return 1;
3774            }
3775            if args.len() < 5 {                                              // c:5018
3776                zwarnnam(nam, "too few arguments");
3777                return 1;
3778            }
3779            if args.len() > 5 {                                              // c:5022
3780                zwarnnam(nam, "too many arguments");
3781                return 1;
3782            }
3783            let mut l: Vec<String> = get_arr(&args[2]).unwrap_or_default();
3784            let Some(tmp) = get_arr(&args[1]) else {                          // c:5032
3785                zwarnnam(nam, &format!("unknown parameter: {}", args[1]));
3786                return 0;
3787            };
3788            cf_ignore(&tmp, &mut l, &args[3], &args[4]);                      // c:5037
3789            setaparam(&args[2], l);                                           // c:5039
3790            0
3791        }
3792        b'r' => {                                                            // c:5042
3793            if args.len() < 3 {                                              // c:5048
3794                zwarnnam(nam, "too few arguments");
3795                return 1;
3796            }
3797            if args.len() > 3 {                                              // c:5052
3798                zwarnnam(nam, "too many arguments");
3799                return 1;
3800            }
3801            let Some(tmp) = get_arr(&args[1]) else {                          // c:5057
3802                zwarnnam(nam, &format!("unknown parameter: {}", args[1]));
3803                return 0;
3804            };
3805            let mut ret = 0i32;
3806            // c:5062 — cf_remove_other.
3807            if let Some(l) = cf_remove_other(&tmp, &args[2], &mut ret) {
3808                setaparam(&args[1], l);
3809            }
3810            ret                                                              // c:5065
3811        }
3812        _ => {
3813            zwarnnam(nam, &format!("invalid option: {}", args[0]));
3814            1
3815        }
3816    }
3817}
3818
3819/// Direct port of `static int bin_compgroups(char *nam, char **args,
3820///                                              UNUSED(Options ops),
3821///                                              UNUSED(int func))` from
3822/// `Src/Zle/computil.c:5073-5100`. For each group name in args, opens
3823/// six successive completion groups with the same name but different
3824/// sort/uniq flags (NOSORT+UNIQCON, UNIQALL, NOSORT+UNIQCON, UNIQALL,
3825/// NOSORT, 0). Each begcmgroup is bracketed by endcmgroup. This is
3826/// how _path_files etc. register their match groups before adding
3827/// candidates via compadd.
3828pub fn bin_compgroups(nam: &str, args: &[String],                            // c:5073
3829                      _ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
3830    use crate::ported::zle::comp_h::{CGF_NOSORT, CGF_UNIQALL, CGF_UNIQCON};
3831    use crate::ported::zle::compcore::{begcmgroup, endcmgroup};
3832
3833    if INCOMPFUNC.load(std::sync::atomic::Ordering::Relaxed) != 1 {          // c:5078
3834        zwarnnam(nam, "can only be called from completion function");
3835        return 1;
3836    }
3837    // c:5083 — for each group name, register 6 group variants.
3838    for n in args {                                                          // c:5083
3839        endcmgroup(None);                                                    // c:5084
3840        begcmgroup(Some(n), CGF_NOSORT | CGF_UNIQCON);                       // c:5085
3841        endcmgroup(None);
3842        begcmgroup(Some(n), CGF_UNIQALL);                                    // c:5087
3843        endcmgroup(None);
3844        begcmgroup(Some(n), CGF_NOSORT | CGF_UNIQCON);                       // c:5089
3845        endcmgroup(None);
3846        begcmgroup(Some(n), CGF_UNIQALL);                                    // c:5091
3847        endcmgroup(None);
3848        begcmgroup(Some(n), CGF_NOSORT);                                     // c:5093
3849        endcmgroup(None);
3850        begcmgroup(Some(n), 0);                                              // c:5095
3851    }
3852    0                                                                        // c:5099
3853}
3854
3855/// Port of `boot_(UNUSED(Module m))` from Src/Zle/computil.c:5153.
3856/// WARNING: param names don't match C — Rust=() vs C=(m)
3857pub fn boot_() -> i32 {                                                      // c:5153
3858    // C body c:5155-5156 — `return 0`. Faithful empty body.
3859    0
3860}
3861
3862/// Port of `ca_colonlist(LinkList l)` from Src/Zle/computil.c:2428.
3863pub fn ca_colonlist(l: &[String]) -> String {                            // c:2428
3864    // C body c:2430-2459 — joins l with `:`, escapes `:` and `\`
3865    //                      with `\` per item.
3866    if l.is_empty() {
3867        return String::new();                                                // c:2459
3868    }
3869    let mut out = String::new();
3870    for (i, item) in l.iter().enumerate() {                              // c:2444
3871        if i > 0 {
3872            out.push(':');                                                   // c:2452
3873        }
3874        for ch in item.chars() {
3875            if ch == ':' || ch == '\\' {                                     // c:2447
3876                out.push('\\');
3877            }
3878            out.push(ch);
3879        }
3880    }
3881    out
3882}
3883
3884/// Direct port of `static int ca_foreign_opt(Cadef curset, Cadef all,
3885///                                            char *option)` from
3886/// `Src/Zle/computil.c:1786-1802`. Walks the `snext` chain of `all`
3887/// skipping `curset` and reports whether any other set defines an
3888/// option with the requested name. Returns 1 on match, 0 otherwise.
3889pub fn ca_foreign_opt(curset: &cadef, all: &cadef, option: &str) -> i32 {    // c:1787
3890    let curset_ptr = curset as *const cadef;
3891    let mut d_opt = Some(all);
3892    while let Some(d) = d_opt {                                              // c:1792
3893        if std::ptr::addr_eq(d as *const cadef, curset_ptr) {                // c:1793
3894            d_opt = d.snext.as_deref();
3895            continue;
3896        }
3897        let mut p = d.opts.as_deref();
3898        while let Some(opt) = p {                                            // c:1796
3899            if opt.name.as_deref() == Some(option) {                         // c:1797
3900                return 1;                                                    // c:1798
3901            }
3902            p = opt.next.as_deref();
3903        }
3904        d_opt = d.snext.as_deref();
3905    }
3906    0                                                                        // c:1801
3907}
3908
3909/// Direct port of `static Caarg ca_get_arg(Cadef d, int n)` from
3910/// `Src/Zle/computil.c:1807-1823`. Walks `d->args` looking for the
3911/// arg whose `[min, num]` range contains `n`. Falls back to `d->rest`
3912/// when no positional matches. Returns a shallow clone (no `next`)
3913/// of the matched arg.
3914pub fn ca_get_arg(d: &cadef, mut n: i32) -> Option<Box<caarg>> {             // c:1807
3915    if d.argsactive == 0 {                                                   // c:1809
3916        return None;                                                         // c:1822
3917    }
3918
3919    // c:1810-1816 — skip inactive entries (advance `n` to compensate for
3920    // each skipped one, mirroring the C `n++` inside the loop).
3921    let mut a = d.args.as_deref();
3922    while let Some(node) = a {                                               // c:1812
3923        let in_range = node.active != 0 && n >= node.min && n <= node.num;
3924        if in_range { break; }                                               // c:1812 inverted
3925        if node.active == 0 {                                                // c:1813
3926            n += 1;                                                          // c:1814
3927        }
3928        a = node.next.as_deref();                                            // c:1815
3929    }
3930
3931    if let Some(node) = a {                                                  // c:1817
3932        if node.active != 0 && node.min <= n && node.num >= n {
3933            return Some(Box::new(caarg {                                     // c:1818
3934                next:   None,
3935                descr:  node.descr.clone(),
3936                xor:    node.xor.clone(),
3937                action: node.action.clone(),
3938                r#type: node.r#type,
3939                end:    node.end.clone(),
3940                opt:    node.opt.clone(),
3941                num:    node.num,
3942                min:    node.min,
3943                direct: node.direct,
3944                active: node.active,
3945                gsname: node.gsname.clone(),
3946            }));
3947        }
3948    }
3949
3950    // c:1820 — rest fallback.
3951    if let Some(r) = d.rest.as_deref() {
3952        if r.active != 0 {
3953            return Some(Box::new(caarg {
3954                next:   None,
3955                descr:  r.descr.clone(),
3956                xor:    r.xor.clone(),
3957                action: r.action.clone(),
3958                r#type: r.r#type,
3959                end:    r.end.clone(),
3960                opt:    r.opt.clone(),
3961                num:    r.num,
3962                min:    r.min,
3963                direct: r.direct,
3964                active: r.active,
3965                gsname: r.gsname.clone(),
3966            }));
3967        }
3968    }
3969    None                                                                     // c:1820
3970}
3971
3972/// Direct port of `static Caopt ca_get_opt(Cadef d, char *line, int full,
3973///                                          char **end)` from
3974/// `Src/Zle/computil.c:1706-1742`. Looks up an option-spec by name
3975/// against `line`. With `full=0`, also accepts a prefix-of-`line`
3976/// match where the option name is a prefix and the rest of `line` is
3977/// the option's argument (handles `=` / `--name=value` shapes per
3978/// `CAO_OEQUAL` / `CAO_EQUAL`). Sets `*end` to the byte offset past
3979/// the option text (and past the `=` separator when applicable).
3980/// Returns a cloned shallow copy of the matched `caopt` (without its
3981/// `next` chain) — Rust ownership artifact, equivalent to C returning
3982/// the aliased `Caopt` pointer.
3983pub fn ca_get_opt(d: &cadef, line: &str, full: i32,                          // c:1706
3984                  end: &mut usize) -> Option<Box<caopt>> {
3985    let line_bytes = line.as_bytes();
3986
3987    // c:1712-1718 — exact match against an active option name.
3988    let mut cur = d.opts.as_deref();
3989    while let Some(p) = cur {                                                // c:1712
3990        if p.active != 0 {                                                   // c:1713
3991            if let Some(name) = p.name.as_deref() {
3992                if name == line {
3993                    *end = line_bytes.len();                                 // c:1715
3994                    return Some(Box::new(caopt {                             // c:1717
3995                        next: None,
3996                        name: p.name.clone(),
3997                        descr: p.descr.clone(),
3998                        xor: p.xor.clone(),
3999                        r#type: p.r#type,
4000                        args: None,
4001                        active: p.active,
4002                        num: p.num,
4003                        gsname: p.gsname.clone(),
4004                        not: p.not,
4005                    }));
4006                }
4007            }
4008        }
4009        cur = p.next.as_deref();
4010    }
4011
4012    if full == 0 {                                                           // c:1720
4013        // c:1722-1739 — prefix-match path for `name=value` / `nameSPC value`.
4014        let mut cur = d.opts.as_deref();
4015        while let Some(p) = cur {
4016            if p.active != 0 {                                               // c:1723
4017                if let Some(name) = p.name.as_deref() {
4018                    // c:1723-1724 — short args/NEXT → exact match, else strpfx.
4019                    let is_match = if p.args.is_none() || p.r#type == CAO_NEXT {
4020                        name == line
4021                    } else {
4022                        crate::ported::utils::strpfx(name, line)
4023                    };
4024                    if is_match {
4025                        let l = name.len();
4026                        // c:1726-1728 — for OEQUAL/EQUAL, the char at name's
4027                        // end must be `=` or absent; otherwise skip.
4028                        if (p.r#type == CAO_OEQUAL || p.r#type == CAO_EQUAL)
4029                            && l < line_bytes.len() && line_bytes[l] != b'='
4030                        {
4031                            cur = p.next.as_deref();
4032                            continue;                                        // c:1728
4033                        }
4034                        // c:1731-1736 — set end past the option (+= 1 for `=`).
4035                        let mut at = l;
4036                        if (p.r#type == CAO_OEQUAL || p.r#type == CAO_EQUAL)
4037                            && l < line_bytes.len() && line_bytes[l] == b'='
4038                        {
4039                            at += 1;                                         // c:1734
4040                        }
4041                        *end = at;                                           // c:1736
4042                        return Some(Box::new(caopt {                         // c:1738
4043                            next: None,
4044                            name: p.name.clone(),
4045                            descr: p.descr.clone(),
4046                            xor: p.xor.clone(),
4047                            r#type: p.r#type,
4048                            args: None,
4049                            active: p.active,
4050                            num: p.num,
4051                            gsname: p.gsname.clone(),
4052                            not: p.not,
4053                        }));
4054                    }
4055                }
4056            }
4057            cur = p.next.as_deref();
4058        }
4059    }
4060    None                                                                     // c:1741
4061}
4062
4063/// Direct port of `static Caopt ca_get_sopt(Cadef d, char *line,
4064///                                           char **end, LinkList *lp)`
4065/// from `Src/Zle/computil.c:1747-1781`. Single-letter option lookup
4066/// for clumped flags like `-abc`. Walks `line[1..]` consulting
4067/// `d->single[]` for each char; CAO_NEXT matches accumulate in `lp`,
4068/// the first non-NEXT match terminates and sets `*end` past it.
4069/// Returns the terminating Caopt (cloned, no chain) or None.
4070pub fn ca_get_sopt(d: &cadef, line: &str,                                    // c:1747
4071                   end: &mut usize,
4072                   lp: &mut Option<Vec<Box<caopt>>>) -> Option<Box<caopt>> {
4073    let line_bytes = line.as_bytes();
4074    if line_bytes.is_empty() {
4075        *lp = None;
4076        return None;
4077    }
4078    let pre = line_bytes[0];                                                 // c:1750
4079    let mut idx: usize = 1;
4080    *lp = None;                                                              // c:1754
4081
4082    let single = match d.single.as_ref() {                                   // c:1757
4083        Some(s) => s,
4084        None => return None,
4085    };
4086
4087    let mut p_cur: Option<&caopt> = None;                                    // c:1755 p = NULL
4088    let mut pp_cur: Option<&caopt> = None;
4089    let mut list_acc: Option<Vec<Box<caopt>>> = None;
4090
4091    while idx < line_bytes.len() {                                           // c:1755 for (;*line;line++)
4092        let ch = line_bytes[idx];
4093        let sidx = single_index(pre, ch);                                    // c:1756
4094
4095        // c:1756 — d->single[sidx] lookup (assigns to p if valid).
4096        let lookup: Option<&caopt> = if sidx >= 0 && (sidx as usize) < single.len() {
4097            single[sidx as usize].as_deref()
4098        } else {
4099            None
4100        };
4101        if lookup.is_some() {
4102            p_cur = lookup;
4103        }
4104        let active_with_args = lookup
4105            .filter(|p| p.active != 0 && p.args.is_some());
4106
4107        if let Some(p) = active_with_args {                                  // c:1757
4108            if p.r#type == CAO_NEXT {                                        // c:1758
4109                let list = list_acc.get_or_insert_with(Vec::new);
4110                list.push(Box::new(caopt {                                   // c:1761 addlinknode
4111                    next: None,
4112                    name: p.name.clone(),
4113                    descr: p.descr.clone(),
4114                    xor: p.xor.clone(),
4115                    r#type: p.r#type,
4116                    args: None,
4117                    active: p.active,
4118                    num: p.num,
4119                    gsname: p.gsname.clone(),
4120                    not: p.not,
4121                }));
4122            } else {                                                         // c:1762
4123                idx += 1;                                                    // c:1764 line++
4124                if (p.r#type == CAO_OEQUAL || p.r#type == CAO_EQUAL)         // c:1765
4125                    && idx < line_bytes.len() && line_bytes[idx] == b'='
4126                {
4127                    idx += 1;                                                // c:1767
4128                }
4129                *end = idx;                                                  // c:1768
4130                pp_cur = Some(p);                                            // c:1770
4131                break;                                                       // c:1771
4132            }
4133        } else if p_cur.is_none() || p_cur.map_or(true, |p| p.active == 0) { // c:1773
4134            return None;                                                     // c:1774
4135        }
4136
4137        // c:1775 — pp = (p->name[0] == pre ? p : NULL); p = NULL.
4138        pp_cur = p_cur.filter(|p| {
4139            p.name.as_deref()
4140                .and_then(|n| n.as_bytes().first().copied())
4141                .map_or(false, |b| b == pre)
4142        });
4143        p_cur = None;
4144        idx += 1;                                                            // c:1755 line++
4145    }
4146
4147    // c:1778 — pp && end: *end = line.
4148    if pp_cur.is_some() {
4149        *end = idx;
4150    }
4151
4152    *lp = list_acc;
4153
4154    pp_cur.map(|p| Box::new(caopt {                                          // c:1780
4155        next: None,
4156        name: p.name.clone(),
4157        descr: p.descr.clone(),
4158        xor: p.xor.clone(),
4159        r#type: p.r#type,
4160        args: None,
4161        active: p.active,
4162        num: p.num,
4163        gsname: p.gsname.clone(),
4164        not: p.not,
4165    }))
4166}
4167
4168/// Direct port of `static void ca_inactive(Cadef d, char **xor, int cur,
4169///                                          int opts)` from
4170/// `Src/Zle/computil.c:1832-1918`. Marks options/args inactive based
4171/// on a xor-list (or, when `opts=1`, deactivates all options except
4172/// rest-of-line args). Each xor entry can be:
4173///   - bare name → exact-match opt or numeric positional or `:`/`-`/`*`/group
4174///   - `group-name-:` / `group-name--` → group-scoped exclusion
4175///   - excludeall path (just a set/group name) → kills the whole set
4176pub fn ca_inactive(d: &mut cadef, xor: &[String], cur: i32, opts: i32) {     // c:1832
4177    use crate::ported::ztype_h::idigit;
4178    use crate::ported::zle::complete::COMPCURRENT;
4179
4180    if (xor.is_empty() && opts == 0)                                         // c:1834
4181        || cur > COMPCURRENT.load(std::sync::atomic::Ordering::Relaxed)
4182    {
4183        return;
4184    }
4185
4186    // c:1839 — single-letter exclusions only when at compcurrent (option
4187    // clumping safety: a prefix-of-longer-opt at cursor mustn't kill the
4188    // multi-letter form prematurely).
4189    let single = opts == 0
4190        && cur == COMPCURRENT.load(std::sync::atomic::Ordering::Relaxed);
4191
4192    // c:1841 — iterate xor entries. When opts=1 we synthesize a "-" pass.
4193    let iter_xor: Vec<String> = if opts != 0 {
4194        vec!["-".to_string()]
4195    } else {
4196        xor.to_vec()
4197    };
4198
4199    for x_orig in iter_xor.iter() {
4200        let mut x = x_orig.as_str();
4201        let mut excludeall = 0;                                              // c:1842
4202        let mut grp: Option<&str> = None;
4203        let mut grplen: usize = 0;
4204
4205        // c:1845-1858 — split off optional `group-name-` prefix.
4206        let xb = x.as_bytes();
4207        let mut sep_byte = if xb.is_empty() { 0u8 } else { xb[0] };
4208        let mut sep_pos = 0usize;
4209        loop {
4210            if sep_pos >= xb.len() { break; }
4211            sep_byte = xb[sep_pos];
4212            if sep_byte == b'+' || sep_byte == b'-' || sep_byte == b':'
4213                || sep_byte == b'*' || idigit(sep_byte)
4214            {
4215                break;
4216            }
4217            // Find next '-'.
4218            let after = &xb[sep_pos..];
4219            let dash_off = after.iter().position(|&b| b == b'-');
4220            match dash_off {
4221                None => {
4222                    excludeall = 1;                                          // c:1850
4223                    sep_pos = xb.len();
4224                    break;
4225                }
4226                Some(d) => {
4227                    let next = sep_pos + d + 1;
4228                    if next >= xb.len() {                                    // c:1848
4229                        excludeall = 1;
4230                        sep_pos = xb.len();
4231                        break;
4232                    }
4233                    sep_pos = next;
4234                }
4235            }
4236        }
4237        if sep_pos > 0 && sep_pos < xb.len() {                               // c:1859
4238            grp = Some(&x[..sep_pos]);
4239            grplen = sep_pos;
4240            x = &x[sep_pos..];
4241        } else if sep_pos > 0 && excludeall != 0 && sep_pos == xb.len() {
4242            // c:1850 path — the whole string was a group name.
4243            grp = Some(x);
4244            grplen = sep_pos;
4245            x = "";
4246        }
4247        let xb = x.as_bytes();
4248
4249        // c:1865 — excludeall or `:` alone.
4250        if excludeall != 0 || (xb.len() == 1 && xb[0] == b':') {
4251            if let Some(g) = grp {                                           // c:1866
4252                let mut cur_arg = d.args.as_deref_mut();
4253                while let Some(a) = cur_arg {
4254                    let matches = a.gsname.as_deref().map_or(false, |gn| {
4255                        let gnb = gn.as_bytes();
4256                        gnb.len() == grplen + (excludeall as usize)
4257                            && gn.starts_with(g)
4258                    });
4259                    if matches {
4260                        a.active = 0;                                        // c:1872
4261                    }
4262                    cur_arg = a.next.as_deref_mut();
4263                }
4264                if let Some(r) = d.rest.as_deref_mut() {
4265                    let matches = r.gsname.as_deref().map_or(false, |gn| {
4266                        let gnb = gn.as_bytes();
4267                        gnb.len() == grplen + (excludeall as usize)
4268                            && gn.starts_with(g)
4269                    });
4270                    if matches {
4271                        r.active = 0;                                        // c:1876
4272                    }
4273                }
4274            } else {
4275                d.argsactive = 0;                                            // c:1878
4276            }
4277        }
4278
4279        // c:1881 — excludeall or `-` alone: kill options.
4280        if excludeall != 0 || (xb.len() == 1 && xb[0] == b'-') {
4281            let mut cur_opt = d.opts.as_deref_mut();
4282            while let Some(p) = cur_opt {
4283                let grp_ok = grp.map_or(true, |g| {
4284                    p.gsname.as_deref().map_or(false, |gn| {
4285                        gn.len() == grplen + (excludeall as usize)
4286                            && gn.starts_with(g)
4287                    })
4288                });
4289                let single_skip = single && p.name.as_deref().map_or(false, |n| {
4290                    let nb = n.as_bytes();
4291                    nb.len() >= 3 && nb[0] != 0
4292                });
4293                if grp_ok && !single_skip {
4294                    p.active = 0;                                            // c:1888
4295                }
4296                cur_opt = p.next.as_deref_mut();
4297            }
4298        }
4299
4300        // c:1891 — excludeall or `*` alone: kill rest.
4301        if excludeall != 0 || (xb.len() == 1 && xb[0] == b'*') {
4302            if let Some(r) = d.rest.as_deref_mut() {
4303                let grp_ok = grp.map_or(true, |g| {
4304                    r.gsname.as_deref().map_or(false, |gn| {
4305                        gn.len() == grplen + (excludeall as usize)
4306                            && gn.starts_with(g)
4307                    })
4308                });
4309                if grp_ok {
4310                    r.active = 0;                                            // c:1895
4311                }
4312            }
4313        }
4314
4315        if excludeall == 0 {                                                 // c:1898
4316            if !xb.is_empty() && idigit(xb[0]) {                             // c:1899
4317                let n: i32 = x.bytes().take_while(|b| idigit(*b))
4318                    .fold(0i32, |acc, b| acc * 10 + (b - b'0') as i32);
4319                let mut cur_arg = d.args.as_deref_mut();
4320                let mut hit: Option<&mut caarg> = None;
4321                while let Some(a) = cur_arg {
4322                    if a.num >= n {
4323                        hit = Some(a);
4324                        break;
4325                    }
4326                    cur_arg = a.next.as_deref_mut();
4327                }
4328                if let Some(a) = hit {
4329                    if a.num == n {
4330                        let grp_ok = grp.map_or(true, |g| {
4331                            a.gsname.as_deref().map_or(false, |gn| gn.starts_with(g))
4332                        });
4333                        if grp_ok {
4334                            a.active = 0;                                    // c:1908
4335                        }
4336                    }
4337                }
4338            } else {
4339                // c:1909 — ca_get_opt for full match.
4340                let mut end_unused = 0usize;
4341                if let Some(matched) = ca_get_opt(d, x, 1, &mut end_unused) {
4342                    let grp_ok = grp.map_or(true, |g| {
4343                        matched.gsname.as_deref().map_or(false, |gn| gn.starts_with(g))
4344                    });
4345                    let single_skip = single
4346                        && matched.name.as_deref().map_or(false, |n| {
4347                            let nb = n.as_bytes();
4348                            nb.len() >= 3 && nb[0] != 0
4349                        });
4350                    if grp_ok && !single_skip {
4351                        // Walk d.opts to find the actual node and clear its active.
4352                        let target_name = matched.name.clone();
4353                        let mut cur_opt = d.opts.as_deref_mut();
4354                        while let Some(p) = cur_opt {
4355                            if p.name == target_name {
4356                                p.active = 0;                                // c:1912
4357                                break;
4358                            }
4359                            cur_opt = p.next.as_deref_mut();
4360                        }
4361                    }
4362                }
4363            }
4364            if opts != 0 {
4365                break;                                                       // c:1914
4366            }
4367        }
4368        let _ = sep_byte;
4369    }
4370}
4371
4372/// Port of `ca_nullist(LinkList l)` from Src/Zle/computil.c:2411.
4373pub fn ca_nullist(l: &[String]) -> Vec<u8> {                             // c:2411
4374    // C body c:2413-2419 — `if (l) { array = zlinklist2array(l, 0);
4375    //                              ret = zjoin(array, '\\0', 0); free(array);
4376    //                              return ret; } else return ztrdup("")`.
4377    //                      Returns NUL-joined byte buffer.
4378    if l.is_empty() {
4379        return Vec::new();                                                   // c:2419
4380    }
4381    let mut out = Vec::new();
4382    for (i, item) in l.iter().enumerate() {
4383        if i > 0 {
4384            out.push(0);
4385        }
4386        out.extend_from_slice(item.as_bytes());
4387    }
4388    out
4389}
4390
4391/// Port of `ca_opt_arg(Caopt opt, char *line)` from Src/Zle/computil.c:1976.
4392/// WARNING: param names don't match C — Rust=(opt_name, line, equal_kind) vs C=(opt, line)
4393pub fn ca_opt_arg(opt_name: &str, line: &str, equal_kind: bool) -> String {  // c:1976
4394    // C body c:1978-1996: walks `o = opt->name` and `line` byte-by-byte,
4395    //                     skipping `\\` escapes; if any quote (`\\` `'` `"`)
4396    //                     in line, advance line; once they diverge, return
4397    //                     dup of remaining line minus optional `=` if
4398    //                     opt is CAO_EQUAL/CAO_OEQUAL.
4399    let o_bytes = opt_name.as_bytes();
4400    let l_bytes = line.as_bytes();
4401    let mut oi = 0usize;
4402    let mut li = 0usize;
4403    loop {                                                                   // c:1980
4404        if oi >= o_bytes.len() || li >= l_bytes.len() {
4405            break;
4406        }
4407        let mut oc = o_bytes[oi];
4408        if oc == b'\\' {                                                     // c:1981
4409            oi += 1;
4410            if oi >= o_bytes.len() {
4411                break;
4412            }
4413            oc = o_bytes[oi];
4414        }
4415        let mut lc = l_bytes[li];
4416        if matches!(lc, b'\\' | b'\'' | b'"') {                              // c:1983
4417            li += 1;
4418            if li >= l_bytes.len() {
4419                break;
4420            }
4421            lc = l_bytes[li];
4422        }
4423        if oc != lc {                                                        // c:1985
4424            break;
4425        }
4426        oi += 1;
4427        li += 1;
4428    }
4429    let rest = &l_bytes[li..];
4430    let mut s = String::from_utf8_lossy(rest).into_owned();
4431    if equal_kind && s.starts_with('\\') {                                   // c:2004
4432        s.remove(0);
4433    }
4434    if equal_kind {
4435        s = s.strip_prefix('=').map(|t| t.to_string()).unwrap_or(s);         // c:2004
4436    }
4437    s
4438}
4439
4440/// Direct port of `static int ca_parse_line(Cadef d, Cadef all, int multi,
4441///                                            int first)` from
4442/// `Src/Zle/computil.c:2004-2403`. Walks `compwords[1..]` matching
4443/// each word against the cadef's option/arg specs, updating active
4444/// flags via `ca_inactive` and populating `ca_laststate` with the
4445/// completion-point info downstream subcommands need. Returns 1 when
4446/// the set should be skipped (foreign option spotted in multi-set
4447/// mode), 0 otherwise.
4448///
4449/// Substrate notes:
4450/// - `endpat` (CAA_RREST/RARGS end-pattern matching) is compiled via
4451///   `patcompile`; the resulting `Patprog` is held in the local
4452///   `endpat` slot exactly like the C code.
4453/// - `napat` (the `-A` "non-arg" pattern) is also compiled via
4454///   `patcompile`.
4455/// - The `sopts` (clumped single-letter remainders) LinkList is
4456///   represented as a `Vec<Box<caopt>>` queue.
4457pub fn ca_parse_line(d: &mut cadef, all: &cadef, multi: i32, first: i32) -> i32 { // c:2004
4458    use crate::ported::zle::complete::{COMPCURRENT, COMPWORDS};
4459    use crate::ported::pattern::{patcompile, pattry};
4460    use crate::ported::glob::remnulargs;
4461    use crate::ported::lex::untokenize;
4462    use std::sync::atomic::Ordering;
4463
4464    let compcur = COMPCURRENT.load(Ordering::Relaxed);
4465
4466    // c:2019 — free old state if this is the first set.
4467    if first != 0 && ca_alloced.load(Ordering::Relaxed) != 0 {
4468        if let Ok(mut ls) = ca_laststate.lock() {
4469            freecastate(&mut ls);
4470            ls.snext = None;
4471        }
4472    }
4473
4474    // c:2030-2036 — mark everything active.
4475    let mut p = d.opts.as_deref_mut();
4476    while let Some(o) = p {
4477        o.active = 1;
4478        p = o.next.as_deref_mut();
4479    }
4480    d.argsactive = 1;
4481    if let Some(r) = d.rest.as_deref_mut() { r.active = 1; }
4482    let mut a = d.args.as_deref_mut();
4483    while let Some(ar) = a {
4484        ar.active = 1;
4485        a = ar.next.as_deref_mut();
4486    }
4487
4488    // c:2040-2056 — build the initial castate.
4489    let compwords: Vec<String> = COMPWORDS
4490        .get()
4491        .and_then(|m| m.lock().ok().map(|w| w.clone()))
4492        .unwrap_or_default();
4493    let argend_init = (compwords.len() as i32) - 1;
4494
4495    // Set up the working state. Note d is stored as None in the
4496    // working `state`; we re-populate ca_laststate.d from a clone at
4497    // the end (we can't move d into state since the caller still owns
4498    // it).
4499    let mut state = castate {
4500        snext:   None,
4501        d:       None,
4502        nopts:   d.nopts,
4503        def:     None,
4504        ddef:    None,
4505        curopt:  None,
4506        dopt:    None,
4507        opt:     1, arg: 1, argbeg: 1, optbeg: 1, nargbeg: 1, restbeg: 1,
4508        actopts: 1,
4509        nth:     1, inarg: 1, inopt: 0,
4510        singles: 0, oopt: 0,
4511        argend:  argend_init,
4512        curpos:  compcur,
4513        args:    Some(Vec::new()),
4514        oargs:   Some((0..d.nopts as usize).map(|_| None).collect()),
4515    };
4516    ca_alloced.store(1, Ordering::Relaxed);
4517
4518    // Snapshot state → ca_laststate (the "early return on empty"
4519    // path uses this).
4520    if let Ok(mut ls) = ca_laststate.lock() {
4521        *ls = clone_castate(&state, d);
4522    }
4523
4524    if compwords.len() < 2 {                                                 // c:2058
4525        if let Ok(mut ls) = ca_laststate.lock() {
4526            ls.opt = 0; ls.arg = 0;
4527        }
4528        // c:2061 goto end — fall through to actopts count.
4529    } else {
4530        // c:2063-2064 — compile -A nonarg pattern.
4531        let napat = d.nonarg.as_deref().and_then(|s| {
4532            patcompile(s, 0, None::<&mut String>)
4533        });
4534        let mut endpat: Option<crate::ported::pattern::Patprog> = None;
4535
4536        // c:2068 — walk words.
4537        let mut cur = 2i32;
4538        let mut argxor: Option<Vec<String>> = None;
4539        let mut sopts: Vec<Box<caopt>> = Vec::new();
4540        let mut wasopt_idx: Option<usize> = None;
4541        let mut doff: i32 = 0;
4542        let mut adef: Option<Box<caarg>> = None;
4543        let mut ddef: Option<Box<caarg>> = None;
4544        let mut dopt: Option<Box<caopt>> = None;
4545        state.curopt = None; state.def = None;
4546
4547        loop {
4548            let line_idx = (cur - 1) as usize;
4549            if line_idx >= compwords.len() { break; }
4550            let oline = compwords[line_idx].clone();
4551            let mut line = oline.clone();
4552            ddef = None;
4553            adef = None;
4554            dopt = None;
4555            state.singles = 0;
4556            let mut arglast = 0;
4557
4558            remnulargs(&mut line);
4559            line = untokenize(&line);
4560
4561            // c:2095 — apply pending arg-xor.
4562            if let Some(xor) = argxor.take() {
4563                ca_inactive(d, &xor, cur - 1, 0);
4564            }
4565
4566            // c:2099 — CDF_SEP `--` separator turns off option parsing.
4567            if (d.flags & CDF_SEP) != 0 && cur != compcur && state.actopts != 0
4568                && line == "--"
4569            {
4570                ca_inactive(d, &[], cur, 1);
4571                state.actopts = 0;
4572                cur += 1;
4573                continue;
4574            }
4575
4576            // c:2108 — already have a def from previous opt, collect args.
4577            if state.def.is_some() {
4578                state.arg = 0;
4579                if let Some(co) = state.curopt.as_deref() {
4580                    let cn = co.num as usize;
4581                    if let Some(oargs) = state.oargs.as_mut() {
4582                        if cn < oargs.len() {
4583                            oargs[cn].get_or_insert_with(Vec::new).push(oline.clone());
4584                        }
4585                    }
4586                }
4587                let def_type = state.def.as_deref().map_or(0, |d| d.r#type);
4588                let def_is_opt = def_type == CAA_OPT;
4589                state.opt = if def_is_opt { 1 } else { 0 };
4590                if def_is_opt {
4591                    if state.def.as_deref().map_or(false, |d| d.opt.is_some()) {
4592                        state.oopt += 1;
4593                    }
4594                }
4595
4596                if def_type == CAA_REST || def_type == CAA_RARGS
4597                    || def_type == CAA_RREST
4598                {
4599                    // c:2118 — end-pattern check.
4600                    let matched_end = state.def.as_deref()
4601                        .and_then(|d| d.end.as_deref())
4602                        .map_or(false, |_| endpat.as_ref()
4603                            .map_or(false, |ep| pattry(ep, &line)));
4604                    if matched_end {
4605                        state.def = None;
4606                        state.curopt = None;
4607                        state.opt = 1; state.arg = 1;
4608                        state.argend = cur - 1;
4609                        if let Ok(mut ls) = ca_laststate.lock() {
4610                            ls.argend = cur - 1;
4611                        }
4612                        // c:2124 goto cont.
4613                    }
4614                } else {
4615                    // c:2125 — advance to next arg slot.
4616                    let next = state.def.as_deref().and_then(|d| d.next.clone());
4617                    if next.is_some() {
4618                        state.def = next;
4619                        state.argbeg = cur;
4620                        state.argend = argend_init;
4621                    } else if let Some(s) = sopts.first().cloned() {
4622                        // c:2128 — pop a queued single-letter opt arg.
4623                        sopts.remove(0);
4624                        state.curopt = Some(s);
4625                        state.def = state.curopt.as_deref().and_then(|c| c.args.clone());
4626                        state.opt = 0;
4627                        state.argbeg = cur; state.optbeg = cur; state.inopt = cur;
4628                        state.argend = argend_init;
4629                        doff = 0;
4630                        state.singles = 1;
4631                        if let Some(co) = state.curopt.as_deref() {
4632                            let cn = co.num as usize;
4633                            if let Some(oargs) = state.oargs.as_mut() {
4634                                if cn < oargs.len() && oargs[cn].is_none() {
4635                                    oargs[cn] = Some(Vec::new());
4636                                }
4637                            }
4638                        }
4639                        // c:2138 goto cont.
4640                    } else {
4641                        state.curopt = None;
4642                        state.opt = 1;
4643                    }
4644                }
4645            } else {
4646                state.opt = 1; state.arg = 1;
4647                state.curopt = None;
4648            }
4649            if state.opt != 0 {
4650                let lb = line.as_bytes();
4651                state.opt = if lb.is_empty() { 0 }
4652                            else if lb.len() == 1 { 1 } else { 2 };
4653            }
4654
4655            let mut pe_off: i32 = 0;
4656            wasopt_idx = None;
4657
4658            // c:2156 — option lookup.
4659            let opt_match = if state.opt == 2 {
4660                let lb = line.as_bytes();
4661                if !lb.is_empty() && (lb[0] == b'-' || lb[0] == b'+') {
4662                    let mut end = 0usize;
4663                    if let Some(found) = ca_get_opt(d, &line, 0, &mut end) {
4664                        pe_off = end as i32;
4665                        // c:2158 — for OEQUAL/EQUAL check `=` boundary.
4666                        let pe_ok = match found.r#type {
4667                            t if t == CAO_OEQUAL =>
4668                                (line_idx + 1 < compwords.len())
4669                                    || (pe_off > 0
4670                                        && lb.get(pe_off as usize - 1) == Some(&b'=')),
4671                            t if t == CAO_EQUAL =>
4672                                pe_off > 0
4673                                    && (lb.get(pe_off as usize - 1) == Some(&b'=')
4674                                        || pe_off as usize >= lb.len()),
4675                            _ => true,
4676                        };
4677                        if pe_ok { Some(found) } else { None }
4678                    } else { None }
4679                } else { None }
4680            } else { None };
4681
4682            if let Some(co) = opt_match {
4683                // Bind found opt details for later.
4684                let co_name = co.name.clone().unwrap_or_default();
4685                let co_type = co.r#type;
4686                let co_num = co.num;
4687                let co_xor = co.xor.clone().unwrap_or_default();
4688                let co_args = co.args.clone();
4689                state.curopt = Some(co);
4690                let pe_at_eq = pe_off > 0
4691                    && line.as_bytes().get(pe_off as usize - 1) == Some(&b'=');
4692                let pe_tail_present = (pe_off as usize) < line.as_bytes().len();
4693
4694                let take_args = co_type != CAO_EQUAL || pe_at_eq;
4695                state.def = if take_args { co_args.clone() } else { None };
4696                if state.def.is_some() { ddef = state.def.clone(); dopt = state.curopt.clone(); }
4697
4698                doff = pe_off;
4699                state.optbeg = cur; state.argbeg = cur; state.inopt = cur;
4700                state.argend = argend_init;
4701                let single_ok = d.single.is_some()
4702                    && !pe_tail_present
4703                    && co_name.as_bytes().len() >= 2
4704                    && co_name.as_bytes()[1] != b'-'
4705                    && co_name.as_bytes().get(2).is_none();
4706                state.singles = if single_ok { 1 } else { 0 };
4707
4708                let cn = co_num as usize;
4709                if let Some(oargs) = state.oargs.as_mut() {
4710                    if cn < oargs.len() && oargs[cn].is_none() {
4711                        oargs[cn] = Some(Vec::new());
4712                    }
4713                }
4714                ca_inactive(d, &co_xor, cur, 0);                             // c:2179
4715
4716                let collect_arg = state.def.is_some() && (
4717                    co_type == CAO_DIRECT
4718                    || co_type == CAO_EQUAL
4719                    || (co_type == CAO_ODIRECT && pe_tail_present)
4720                    || (co_type == CAO_OEQUAL && (pe_tail_present || pe_at_eq))
4721                );
4722                if collect_arg {
4723                    let dtype = state.def.as_deref().map_or(0, |d| d.r#type);
4724                    if dtype != CAA_REST && dtype != CAA_RARGS && dtype != CAA_RREST {
4725                        let next = state.def.as_deref().and_then(|d| d.next.clone());
4726                        state.def = next;
4727                    }
4728                    let arg_str = ca_opt_arg(&co_name, &oline, false);
4729                    if let Some(oargs) = state.oargs.as_mut() {
4730                        if cn < oargs.len() {
4731                            oargs[cn].get_or_insert_with(Vec::new).push(arg_str);
4732                        }
4733                    }
4734                }
4735                if state.def.is_some() {
4736                    state.opt = 0;
4737                } else {
4738                    if d.single.is_none()
4739                        || (co_name.as_bytes().len() >= 3
4740                            && co_name.as_bytes()[1] != 0)
4741                    {
4742                        wasopt_idx = Some(co_num as usize);                  // c:2201
4743                    }
4744                    state.curopt = None;
4745                }
4746            } else if state.opt == 2 && d.single.is_some()
4747                && line.as_bytes().first().copied().map_or(false,
4748                    |b| b == b'-' || b == b'+')
4749            {
4750                // c:2204 — single-letter clump.
4751                let mut end = 0usize;
4752                let mut tmp_sopts: Option<Vec<Box<caopt>>> = None;
4753                let s_match = ca_get_sopt(d, &line, &mut end, &mut tmp_sopts);
4754                if let Some(queued) = tmp_sopts {
4755                    sopts.extend(queued);
4756                }
4757                let active_sopt = s_match.or_else(|| {
4758                    if cur != compcur && !sopts.is_empty() {
4759                        Some(sopts.remove(0))
4760                    } else { None }
4761                });
4762                if let Some(co) = active_sopt {
4763                    let co_name = co.name.clone().unwrap_or_default();
4764                    let co_type = co.r#type;
4765                    let co_num = co.num;
4766                    let co_xor = co.xor.clone().unwrap_or_default();
4767                    let co_args = co.args.clone();
4768                    state.curopt = Some(co);
4769
4770                    let cn = co_num as usize;
4771                    if let Some(oargs) = state.oargs.as_mut() {
4772                        if cn < oargs.len() && oargs[cn].is_none() {
4773                            oargs[cn] = Some(Vec::new());
4774                        }
4775                    }
4776                    state.def = co_args.clone();
4777                    if co_type == CAO_NEXT && cur == compcur {
4778                        ddef = None;
4779                    } else {
4780                        ddef = state.def.clone();
4781                    }
4782                    dopt = state.curopt.clone();
4783                    doff = end as i32;
4784                    state.optbeg = cur; state.argbeg = cur; state.inopt = cur;
4785                    state.argend = argend_init;
4786                    state.singles = if end >= line.as_bytes().len() { 1 } else { 0 };
4787
4788                    let lb = line.as_bytes();
4789                    let pre = lb.first().copied().unwrap_or(0);
4790                    let mut p_idx = 1usize;
4791                    while p_idx < end.min(lb.len()) {
4792                        let sidx = single_index(pre, lb[p_idx]);
4793                        if sidx >= 0 && (sidx as usize)
4794                            < d.single.as_ref().map_or(0, |s| s.len())
4795                        {
4796                            let tmp_xor = d.single.as_ref()
4797                                .and_then(|s| s.get(sidx as usize))
4798                                .and_then(|so| so.as_ref())
4799                                .and_then(|so| so.xor.clone());
4800                            let tmp_num = d.single.as_ref()
4801                                .and_then(|s| s.get(sidx as usize))
4802                                .and_then(|so| so.as_ref())
4803                                .map_or(-1, |so| so.num);
4804                            let tn = tmp_num as usize;
4805                            if tmp_num >= 0 {
4806                                if let Some(oargs) = state.oargs.as_mut() {
4807                                    if tn < oargs.len() && oargs[tn].is_none() {
4808                                        oargs[tn] = Some(Vec::new());
4809                                    }
4810                                }
4811                            }
4812                            if let Some(xor) = tmp_xor {
4813                                ca_inactive(d, &xor, cur, 0);
4814                            }
4815                        }
4816                        p_idx += 1;
4817                    }
4818
4819                    let pe_tail_present = end < line.as_bytes().len();
4820                    let pe_at_eq = end > 0
4821                        && line.as_bytes().get(end - 1) == Some(&b'=');
4822                    let collect_arg = state.def.is_some() && (
4823                        co_type == CAO_DIRECT
4824                        || co_type == CAO_EQUAL
4825                        || (co_type == CAO_ODIRECT && pe_tail_present)
4826                        || (co_type == CAO_OEQUAL && (pe_tail_present || pe_at_eq))
4827                    );
4828                    if collect_arg {
4829                        let dtype = state.def.as_deref().map_or(0, |d| d.r#type);
4830                        if dtype != CAA_REST && dtype != CAA_RARGS && dtype != CAA_RREST {
4831                            let next = state.def.as_deref().and_then(|d| d.next.clone());
4832                            state.def = next;
4833                        }
4834                        let arg_str = ca_opt_arg(&co_name, &line, false);
4835                        if let Some(oargs) = state.oargs.as_mut() {
4836                            if cn < oargs.len() {
4837                                oargs[cn].get_or_insert_with(Vec::new).push(arg_str);
4838                            }
4839                        }
4840                    }
4841                    if state.def.is_some() {
4842                        state.opt = 0;
4843                    } else {
4844                        state.curopt = None;
4845                    }
4846                }
4847            } else if multi != 0
4848                && line.as_bytes().first().copied().map_or(false,
4849                    |b| b == b'-' || b == b'+')
4850                && cur != compcur
4851                && ca_foreign_opt(d, all, &line) != 0
4852            {
4853                return 1;                                                    // c:2258
4854            } else if state.arg != 0 && cur <= compcur {                     // c:2259
4855                // c:2264 — napat -A pattern.
4856                if let Some(np) = napat.as_ref() {
4857                    if cur < compcur && state.actopts != 0 {
4858                        if pattry(np, &line) {
4859                            cur += 1;
4860                            continue;
4861                        }
4862                        ca_inactive(d, &[], cur + 1, 1);
4863                        state.actopts = 0;
4864                    }
4865                }
4866
4867                arglast = 1;
4868                if state.inopt != 0 {                                        // c:2274
4869                    state.inopt = 0;
4870                    state.nargbeg = cur - 1;
4871                    state.argend = argend_init;
4872                }
4873                // c:2279 — no args/rest + non-empty non-flag line → skip set.
4874                let lb = line.as_bytes();
4875                let non_flag = !lb.is_empty() && lb[0] != b'-' && lb[0] != b'+';
4876                if d.args.is_none() && d.rest.is_none() && non_flag {
4877                    if multi == 0 && cur > compcur { break; }
4878                    return 1;
4879                }
4880
4881                adef = ca_get_arg(d, state.nth);
4882                state.def = adef.clone();
4883                let dtype = state.def.as_deref().map_or(0, |d| d.r#type);
4884                if state.def.is_some() && (dtype == CAA_RREST || dtype == CAA_RARGS) {
4885                    if ca_laststate.lock().map(|ls| ls.def.is_some()).unwrap_or(false) {
4886                        break;
4887                    }
4888                    state.opt = if cur == state.nargbeg + 1
4889                        && (multi == 0
4890                            || line.is_empty()
4891                            || lb[0] == b'-'
4892                            || lb[0] == b'+')
4893                    { 1 } else { 0 };
4894                    state.optbeg = state.nargbeg;
4895                    state.argbeg = cur - 1;
4896                    state.argend = argend_init;
4897                    // c:2311 — gather remaining words into state.args.
4898                    while line_idx < compwords.len() {
4899                        state.args.get_or_insert_with(Vec::new)
4900                            .push(compwords[line_idx].clone());
4901                        cur += 1;
4902                        if (cur - 1) as usize >= compwords.len() { break; }
4903                    }
4904                    if let Ok(mut ls) = ca_laststate.lock() {
4905                        *ls = clone_castate(&state, d);
4906                        ls.ddef = None;
4907                        ls.dopt = None;
4908                    }
4909                    break;
4910                }
4911                state.args.get_or_insert_with(Vec::new).push(line.clone());
4912                if let Some(a) = adef.as_deref() {
4913                    state.oopt = a.num - state.nth;
4914                }
4915
4916                if state.def.is_some() && cur != compcur {                   // c:2323
4917                    argxor = state.def.as_deref().and_then(|d| d.xor.clone());
4918                }
4919                let dtype2 = state.def.as_deref().map_or(0, |d| d.r#type);
4920                if state.def.is_some() && dtype2 != CAA_NORMAL && dtype2 != CAA_OPT
4921                    && state.inarg != 0
4922                {
4923                    state.restbeg = cur;
4924                    state.inarg = 0;
4925                } else if state.def.is_none() || dtype2 == CAA_NORMAL || dtype2 == CAA_OPT {
4926                    state.inarg = 1;
4927                }
4928                state.nth += 1;
4929                state.def = None;
4930            }
4931
4932            // c:2338 — end-pattern compile for rest-args.
4933            if state.def.is_some() && state.curopt.is_some() {
4934                let dt = state.def.as_deref().map_or(0, |d| d.r#type);
4935                if dt == CAA_RREST || dt == CAA_RARGS {
4936                    let end_pat_str = state.def.as_deref()
4937                        .and_then(|d| d.end.clone());
4938                    if let Some(eps) = end_pat_str {
4939                        endpat = patcompile(&eps, 0, None::<&mut String>);
4940                    } else {
4941                        // c:2342-2353 — no end-pattern: gather rest into oargs.
4942                        if cur < compcur {
4943                            if let Ok(mut ls) = ca_laststate.lock() {
4944                                *ls = clone_castate(&state, d);
4945                            }
4946                        }
4947                        let cn = state.curopt.as_deref().map(|c| c.num as usize);
4948                        if let Some(cn) = cn {
4949                            if let Some(oargs) = state.oargs.as_mut() {
4950                                if cn < oargs.len() {
4951                                    let bucket = oargs[cn].get_or_insert_with(Vec::new);
4952                                    let mut k = line_idx;
4953                                    while k < compwords.len() {
4954                                        bucket.push(compwords[k].clone());
4955                                        k += 1;
4956                                    }
4957                                }
4958                            }
4959                        }
4960                        if let Ok(mut ls) = ca_laststate.lock() {
4961                            ls.ddef = None;
4962                            ls.dopt = None;
4963                        }
4964                        break;
4965                    }
4966                }
4967            } else if state.def.is_some() {
4968                let eps = state.def.as_deref().and_then(|d| d.end.clone());
4969                if let Some(eps) = eps {
4970                    endpat = patcompile(&eps, 0, None::<&mut String>);
4971                }
4972            }
4973
4974            // c:2360 cont: — checkpoint to ca_laststate.
4975            if cur + 1 == compcur {
4976                if let Ok(mut ls) = ca_laststate.lock() {
4977                    *ls = clone_castate(&state, d);
4978                    ls.ddef = None;
4979                    ls.dopt = None;
4980                }
4981            } else if cur == compcur {
4982                let mut ls = ca_laststate.lock().unwrap();
4983                if ls.def.is_none() {
4984                    if let Some(ddef_v) = ddef.clone() {
4985                        ls.def = Some(ddef_v);
4986                        ls.singles = state.singles;
4987                        if state.curopt.as_deref().map_or(false, |c| c.r#type == CAO_NEXT) {
4988                            ls.ddef = ddef.clone();
4989                            ls.dopt = dopt.clone();
4990                            ls.def = None;
4991                            ls.opt = 1;
4992                            // Mark curopt active again in d.
4993                            if let Some(co) = state.curopt.as_deref() {
4994                                let target_name = co.name.clone();
4995                                let mut p = d.opts.as_deref_mut();
4996                                while let Some(op) = p {
4997                                    if op.name == target_name {
4998                                        op.active = 1;
4999                                        break;
5000                                    }
5001                                    p = op.next.as_deref_mut();
5002                                }
5003                            }
5004                        } else {
5005                            ca_doff.store(doff, Ordering::Relaxed);
5006                            ls.opt = 0;
5007                        }
5008                    } else {
5009                        ls.def = adef.clone();
5010                        ls.opt = if arglast == 0
5011                            || multi == 0
5012                            || line.is_empty()
5013                            || line.as_bytes()[0] == b'-'
5014                            || line.as_bytes()[0] == b'+'
5015                        { 1 } else { 0 };
5016                        ls.ddef = None;
5017                        ls.dopt = None;
5018                        ls.optbeg = state.nargbeg;
5019                        ls.argbeg = state.restbeg;
5020                        ls.argend = state.argend;
5021                        ls.singles = state.singles;
5022                        ls.oopt = state.oopt;
5023                        if let Some(wi) = wasopt_idx {
5024                            let mut p = d.opts.as_deref_mut();
5025                            while let Some(op) = p {
5026                                if op.num as usize == wi {
5027                                    op.active = 1;
5028                                    break;
5029                                }
5030                                p = op.next.as_deref_mut();
5031                            }
5032                        }
5033                    }
5034                }
5035            }
5036            cur += 1;
5037        }
5038        let _ = (endpat, ddef, dopt, adef);
5039    }
5040
5041    // c:2397-2400 — count active opts.
5042    let mut actopts = 0i32;
5043    let mut p = d.opts.as_deref();
5044    while let Some(o) = p {
5045        if o.active != 0 { actopts += 1; }
5046        p = o.next.as_deref();
5047    }
5048    if let Ok(mut ls) = ca_laststate.lock() {
5049        ls.actopts = actopts;
5050        // Make sure ls.d reflects the (now-mutated) d.
5051        ls.d = Some(Box::new(clone_cadef_shallow(d)));
5052    }
5053    0
5054}
5055
5056/// Mirror of the C `memcpy(&ca_laststate, &state, sizeof(state))` pattern
5057/// — we can't move `state` (the caller continues using it), and Rust's
5058/// own `Clone` would over-clone owned chains.  Instead snapshot the
5059/// salient fields plus a fresh shallow clone of `d`. Local to
5060/// `ca_parse_line`; matches no C function.
5061#[allow(dead_code)]
5062fn clone_castate(s: &castate, d: &cadef) -> castate {
5063    castate {
5064        snext:   None,
5065        d:       Some(Box::new(clone_cadef_shallow(d))),
5066        nopts:   s.nopts,
5067        def:     s.def.clone(),
5068        ddef:    s.ddef.clone(),
5069        curopt:  s.curopt.clone(),
5070        dopt:    s.dopt.clone(),
5071        opt:     s.opt, arg: s.arg,
5072        argbeg:  s.argbeg, optbeg: s.optbeg,
5073        nargbeg: s.nargbeg, restbeg: s.restbeg,
5074        curpos:  s.curpos, argend: s.argend,
5075        inopt:   s.inopt, inarg: s.inarg,
5076        nth:     s.nth,
5077        singles: s.singles, oopt: s.oopt, actopts: s.actopts,
5078        args:    s.args.clone(),
5079        oargs:   s.oargs.clone(),
5080    }
5081}
5082
5083#[allow(dead_code)]
5084fn clone_cadef_shallow(d: &cadef) -> cadef {
5085    cadef {
5086        next:       None,
5087        snext:      None,
5088        opts:       d.opts.clone(),
5089        nopts:      d.nopts,
5090        ndopts:     d.ndopts,
5091        nodopts:    d.nodopts,
5092        args:       d.args.clone(),
5093        rest:       d.rest.clone(),
5094        defs:       d.defs.clone(),
5095        ndefs:      d.ndefs,
5096        lastt:      d.lastt,
5097        single:     d.single.clone(),
5098        r#match:    d.r#match.clone(),
5099        argsactive: d.argsactive,
5100        set:        d.set.clone(),
5101        flags:      d.flags,
5102        nonarg:     d.nonarg.clone(),
5103    }
5104}
5105
5106/// Direct port of `static void ca_set_data(LinkList descr, LinkList act,
5107///                                           LinkList subc, char *opt,
5108///                                           Caarg arg, Caopt optdef,
5109///                                           int single)` from
5110/// `Src/Zle/computil.c:2472-2582`. Appends to descr/act/subc the
5111/// description/action/subcontext for each `arg` whose `[min,num]`
5112/// range covers `ca_laststate.nth`. When `opt` is non-None, all
5113/// args are treated as option args; otherwise positional. Recurses
5114/// via the goto-rec C path to retry after the first loop when more
5115/// state remains.
5116pub fn ca_set_data(descr: &mut Vec<String>,                                  // c:2472
5117                   act: &mut Vec<String>,
5118                   subc: &mut Vec<String>,
5119                   opt: Option<&str>,
5120                   start_arg: Option<Box<caarg>>,
5121                   optdef: Option<&caopt>,
5122                   single: i32)
5123{
5124    use crate::ported::zle::complete::restrict_range;
5125
5126    let mut arg: Option<Box<caarg>> = start_arg;
5127    let mut opt = opt.map(|s| s.to_string());
5128    let mut restr = 0;
5129    let mut miss = 0;
5130    let mut oopt = 1i32;
5131    let mut lopt;
5132
5133    'rec: loop {
5134        // c:2481 — addopt = (opt ? 0 : ca_laststate.oopt).
5135        let addopt = if opt.is_some() {
5136            0
5137        } else {
5138            ca_laststate.lock().map(|s| s.oopt).unwrap_or(0)
5139        };
5140
5141        // c:2483 — main arg walk.
5142        while let Some(a) = arg.as_ref() {
5143            let cont = {
5144                let nth = ca_laststate.lock().map(|s| s.nth).unwrap_or(0);
5145                opt.is_some()
5146                    || a.num < 0
5147                    || (a.min <= nth + addopt && a.num >= nth)
5148            };
5149            if !cont { break; }
5150
5151            lopt = a.r#type == CAA_OPT;                                      // c:2486
5152            if opt.is_none() && !lopt && oopt > 0 {                          // c:2487
5153                oopt = 0;
5154            }
5155
5156            // c:2490 — dedup: skip if (descr, act) pair already present.
5157            let mut dup = false;
5158            let descr_str = a.descr.clone().unwrap_or_default();
5159            let act_str = a.action.clone().unwrap_or_default();
5160            for (d, ac) in descr.iter().zip(act.iter()) {
5161                if d == &descr_str && ac == &act_str {
5162                    dup = true;
5163                    break;
5164                }
5165            }
5166
5167            // c:2497 — with ignored prefix, no normal args.
5168            if single != 0 && a.opt.is_none() {
5169                return;
5170            }
5171
5172            if !dup {                                                        // c:2500
5173                descr.push(descr_str.clone());                               // c:2501
5174                act.push(act_str.clone());
5175
5176                if restr == 0 {                                              // c:2504
5177                    let nrestr = if a.r#type == CAA_RARGS {                  // c:2506
5178                        let (optbeg, argend) = ca_laststate.lock()
5179                            .map(|s| (s.optbeg, s.argend)).unwrap_or((0, 0));
5180                        restrict_range(optbeg, argend);
5181                        1
5182                    } else if a.r#type == CAA_RREST {                        // c:2508
5183                        let (argbeg, argend) = ca_laststate.lock()
5184                            .map(|s| (s.argbeg, s.argend)).unwrap_or((0, 0));
5185                        restrict_range(argbeg, argend);
5186                        1
5187                    } else { 0 };
5188                    restr = nrestr;
5189                }
5190
5191                // c:2511 — build subcontext string.
5192                let buf = if let Some(o) = a.opt.as_deref() {                // c:2511
5193                    let gs = a.gsname.as_deref().unwrap_or("");
5194                    if a.num > 0 && a.r#type < CAA_REST {                    // c:2514
5195                        format!("{}option{}-{}", gs, o, a.num)
5196                    } else {                                                  // c:2518
5197                        format!("{}option{}-rest", gs, o)
5198                    }
5199                } else if a.num > 0 {                                        // c:2520
5200                    if let Some(gs) = a.gsname.as_deref() {
5201                        format!("{}argument-{}", gs, a.num)
5202                    } else {
5203                        format!("argument-{}", a.num)
5204                    }
5205                } else {                                                     // c:2523
5206                    if let Some(gs) = a.gsname.as_deref() {
5207                        format!("{}argument-rest", gs)
5208                    } else {
5209                        "argument-rest".to_string()
5210                    }
5211                };
5212                subc.push(buf);                                              // c:2527
5213            }
5214
5215            // c:2539 — guard: NORMAL inside an opt where opt requires its
5216            // argument as a separate word — return so we don't keep trying
5217            // to match positionals.
5218            if a.r#type == CAA_NORMAL && opt.is_some() {
5219                if let Some(od) = optdef {
5220                    if od.r#type == CAO_NEXT
5221                        || od.r#type == CAO_ODIRECT
5222                        || od.r#type == CAO_OEQUAL
5223                    {
5224                        return;
5225                    }
5226                }
5227            }
5228
5229            if single != 0 { break; }                                        // c:2545
5230
5231            // c:2548-2568 — advance to the next arg.
5232            if opt.is_none() {                                               // c:2548
5233                let next_is_none_and_miss = a.num >= 0 && a.next.is_none() && miss != 0;
5234                if next_is_none_and_miss {                                   // c:2549
5235                    let rest = ca_laststate.lock().ok().and_then(|s| {
5236                        s.d.as_ref().and_then(|d| d.rest.clone())
5237                    });
5238                    arg = rest.filter(|r| r.active != 0);                    // c:2550
5239                } else {
5240                    let onum = a.num;                                        // c:2553
5241                    let nth = ca_laststate.lock().map(|s| s.nth).unwrap_or(0);
5242                    let rest_flag = onum != a.min && onum == nth;            // c:2554
5243                    let next = a.next.clone();
5244                    if let Some(n) = next {                                  // c:2555
5245                        if n.num != onum + 1 { miss = 1; }                   // c:2556
5246                        arg = Some(n);
5247                    } else if rest_flag || (oopt > 0 && opt.is_none()) {     // c:2558
5248                        let rest = ca_laststate.lock().ok().and_then(|s| {
5249                            s.d.as_ref().and_then(|d| d.rest.clone())
5250                        });
5251                        arg = rest.filter(|r| r.active != 0);
5252                        oopt = -1;
5253                    } else {
5254                        arg = None;
5255                    }
5256                }
5257            } else {                                                         // c:2564
5258                if !lopt { break; }                                          // c:2565
5259                arg = a.next.clone();                                        // c:2567
5260            }
5261        }
5262
5263        // c:2570 — retry as positional after the option args path.
5264        let laststate_oopt = ca_laststate.lock().map(|s| s.oopt).unwrap_or(0);
5265        let cur_lopt = arg.as_ref().map_or(false, |a| a.r#type == CAA_OPT);
5266        if single == 0 && opt.is_some() && (cur_lopt || laststate_oopt != 0) {
5267            opt = None;
5268            let nth = ca_laststate.lock().map(|s| s.nth).unwrap_or(0);
5269            // c:2572 — arg = ca_get_arg(ca_laststate.d, ca_laststate.nth).
5270            arg = ca_laststate.lock().ok().and_then(|s| {
5271                s.d.as_ref().and_then(|d| ca_get_arg(d, nth))
5272            });
5273            continue 'rec;
5274        }
5275        // c:2575 — retry as rest after positional path.
5276        if opt.is_none() && oopt > 0 {
5277            oopt = -1;
5278            let rest = ca_laststate.lock().ok().and_then(|s| {
5279                s.d.as_ref().and_then(|d| d.rest.clone())
5280            });
5281            arg = rest.filter(|r| r.active != 0);
5282            continue 'rec;
5283        }
5284        break 'rec;
5285    }
5286}
5287
5288/// Direct port of `static void cf_ignore(char **names, LinkList ign,
5289///                                          char *style, char *path)`
5290/// from `Src/Zle/computil.c:4860-4896`. Adds to `ign` any directory
5291/// in `names` that:
5292///   - "pwd" style: shares the same dev/ino as `$PWD` (so completion
5293///     doesn't offer the directory you're already in).
5294///   - "parent" style: is an ancestor directory of `path` (so when
5295///     completing under `/a/b/c/`, `/a/`, `/a/b/`, etc. don't show
5296///     up as options).
5297/// Quoted with QT_BACKSLASH for safe re-insertion into the line.
5298pub fn cf_ignore(names: &[String], ign: &mut Vec<String>, style: &str, path: &str) {  // c:4860
5299    use std::os::unix::fs::MetadataExt;
5300    use crate::ported::utils::quotestring;
5301    use crate::ported::zsh_h::QT_BACKSLASH;
5302    use crate::ported::zle::compresult::ztat;
5303
5304    let pl = path.len();
5305    let tpar = style.contains("parent");                                     // c:4866
5306    let pwd = crate::ported::params::getsparam("PWD").unwrap_or_default();
5307    let est = if !pwd.is_empty() { ztat(&pwd, true) } else { None };
5308    let tpwd = style.contains("pwd") && est.is_some();                       // c:4867
5309
5310    if !tpar && !tpwd { return; }                                            // c:4870
5311
5312    for n in names {                                                         // c:4873
5313        let nst = match ztat(n, true) {                                      // c:4874 lstat
5314            Some(m) if m.is_dir() => m,
5315            _ => continue,
5316        };
5317        if tpwd {
5318            if let Some(ref est) = est {
5319                if nst.dev() == est.dev() && nst.ino() == est.ino() {        // c:4875
5320                    ign.push(quotestring(n, QT_BACKSLASH));                  // c:4876
5321                    continue;
5322                }
5323            }
5324        }
5325        if tpar && pl > 0 && n.starts_with(path) {                           // c:4879
5326            let mut c = n.clone();
5327            let mut found = false;
5328            // c:4881 — walk up via strrchr('/') while above path-prefix.
5329            while let Some(idx) = c.rfind('/') {
5330                if idx <= pl { break; }
5331                c.truncate(idx);
5332                if let Some(st) = ztat(&c, false) {                          // c:4883 stat
5333                    if st.dev() == nst.dev() && st.ino() == nst.ino() {
5334                        found = true;
5335                        break;
5336                    }
5337                }
5338            }
5339            // c:4889 — fallback last-segment check via lstat.
5340            let last_match = if !found {
5341                if let Some(idx) = c.rfind('/') {
5342                    if idx > pl {
5343                        c.truncate(idx);
5344                        ztat(&c, true).map_or(false, |st|
5345                            st.dev() == nst.dev() && st.ino() == nst.ino())
5346                    } else { false }
5347                } else { false }
5348            } else { false };
5349            if found || last_match {
5350                ign.push(quotestring(n, QT_BACKSLASH));                      // c:4892
5351            }
5352        }
5353    }
5354}
5355
5356/// Direct port of `static char **cf_pats(int dirs, int noopt,
5357///                                       char **names, char **accept,
5358///                                       char *skipped, char *matcher,
5359///                                       char *sdirs, char **fake,
5360///                                       char **pats)` from
5361/// `Src/Zle/computil.c:4829`. Combines the supplied pattern
5362/// lists into a single resolved pattern array used by
5363/// `_path_files` to drive the file-completion path.
5364///
5365/// **Substrate tradeoff:** the helper chain
5366/// `cfp_test_exact`/`cfp_opt_pats`/`cfp_bld_pats`/`cfp_add_sdirs`
5367/// Direct port of `static LinkList cf_pats(int dirs, int noopt,
5368///                                          LinkList names, char **accept,
5369///                                          char *skipped, char *matcher,
5370///                                          char *sdirs, char **fake,
5371///                                          char **pats)` from
5372/// `Src/Zle/computil.c:4829-4848`. The cf_pats driver:
5373/// 1. Try cfp_test_exact first; if it returns a non-empty list, fold
5374///    in `sdirs`/`fake` via cfp_add_sdirs and return.
5375/// 2. Otherwise: if dirs, replace `pats` with `*(-/)`. If !noopt run
5376///    cfp_opt_pats. Then build the patterns via cfp_bld_pats and fold
5377///    in sdirs/fake.
5378pub fn cf_pats(dirs: i32, noopt: i32, names: &[String],                      // c:4829
5379               accept: &[String], skipped: &str, matcher: &str,
5380               sdirs: &str, fake: &[String], pats: &[String]) -> Vec<String> {
5381    // c:4835 — try exact-match pass first.
5382    if let Some(exact) = cfp_test_exact(names, accept, skipped) {
5383        let mut out = exact;
5384        cfp_add_sdirs(&mut out, names, skipped, sdirs, fake);                // c:4836
5385        return out;
5386    }
5387
5388    // c:4838 — when dirs is set, force the `*(-/)` directory glob.
5389    let dir_pats = vec!["*(-/)".to_string()];
5390    let active_pats: Vec<String> = if dirs != 0 {
5391        dir_pats
5392    } else if noopt == 0 {
5393        // c:4843 — optimization pass.
5394        cfp_opt_pats(pats, matcher)
5395    } else {
5396        pats.to_vec()
5397    };
5398
5399    // c:4846 — build the glob array.
5400    let mut out = cfp_bld_pats(dirs, names, skipped, &active_pats);
5401    cfp_add_sdirs(&mut out, names, skipped, sdirs, fake);
5402    out
5403}
5404
5405/// Direct port of `static LinkList cf_remove_other(char **names,
5406///                                                   char *pre, int *amb)`
5407/// from `Src/Zle/computil.c:4899-4953`. Helper for `_path_files` that
5408/// reports whether the remaining `names` share a common directory
5409/// prefix (`*amb` cleared) or diverge (`*amb` set, return None).
5410/// When `pre` itself contains a `/`, names matching that head are
5411/// returned as the consensus list.
5412pub fn cf_remove_other(names: &[String], pre: &str, amb: &mut i32)           // c:4899
5413                       -> Option<Vec<String>>
5414{
5415    use crate::ported::utils::strpfx;
5416
5417    if let Some(slash) = pre.find('/') {                                     // c:4903
5418        // c:4906-4908 — pre' = pre[..slash] + "/".
5419        let pre2 = format!("{}/", &pre[..slash]);
5420
5421        // c:4910-4912 — any name with the truncated prefix?
5422        let any_match = names.iter().any(|n| strpfx(&pre2, n));
5423
5424        if any_match {                                                       // c:4914
5425            // c:4915-4922 — return all matching names with amb=0.
5426            let ret: Vec<String> = names.iter()
5427                .filter(|n| strpfx(&pre2, n))
5428                .cloned()
5429                .collect();
5430            *amb = 0;
5431            return Some(ret);                                                // c:4923
5432        } else {                                                             // c:4924
5433            // c:4925-4940 — check if remaining names all share first-name's head.
5434            let mut it = names.iter();
5435            let Some(first) = it.next() else {
5436                *amb = 0;                                                    // c:4926
5437                return None;
5438            };
5439            // c:4930 — strip after first '/' in first name.
5440            let p_head = match first.find('/') {
5441                Some(i) => format!("{}/", &first[..i]),
5442                None    => format!("{}/", first),
5443            };
5444            for n in it {                                                    // c:4935
5445                if !strpfx(&p_head, n) {
5446                    *amb = 1;                                                // c:4937
5447                    return None;
5448                }
5449            }
5450            // All match — fall through to return None (matches C).
5451        }
5452    } else {                                                                 // c:4942
5453        // c:4943 — empty list: amb cleared.
5454        let mut it = names.iter();
5455        let Some(first) = it.next() else {
5456            *amb = 0;
5457            return None;
5458        };
5459        for n in it {                                                        // c:4946
5460            if first != n {                                                  // c:4947
5461                *amb = 1;
5462                return None;
5463            }
5464        }
5465    }
5466    None                                                                     // c:4952
5467}
5468
5469/// Port of `cfp_add_sdirs(LinkList final, LinkList orig, char *skipped, char *sdirs, char **fake)` from Src/Zle/computil.c:4735.
5470/// WARNING: param names don't match C — Rust=(final_list, orig, sdirs, fake) vs C=(final, orig, skipped, sdirs, fake)
5471pub fn cfp_add_sdirs(final_list: &mut Vec<String>, orig: &[String],          // c:4735
5472                     _skipped: &str, sdirs: &str, fake: &[String]) {
5473    // C body c:4738-4767: if sdirs ∈ {"yes","true","on","1","..","../"}
5474    //                     and GLOBDOTS or compprefix starts with `.`,
5475    //                     prepend "." (or "..") to final.
5476    let mut add = 0;
5477    if !sdirs.is_empty() {                                                   // c:4740
5478        match sdirs {
5479            "yes" | "true" | "on" | "1" => add = 2,                          // c:4741
5480            ".." => add = 1,                                                 // c:4744
5481            _ => {}
5482        }
5483    }
5484    if add > 0 {
5485        for f in fake {
5486            final_list.push(f.clone());
5487        }
5488        for o in orig {
5489            if !final_list.contains(o) {
5490                final_list.push(o.clone());
5491            }
5492        }
5493    }
5494}
5495
5496/// Direct port of `static LinkList cfp_bld_pats(UNUSED(int dirs),
5497///                                                LinkList names,
5498///                                                char *skipped,
5499///                                                char **pats)` from
5500/// `Src/Zle/computil.c:4704-4732`. For each (name, pattern) pair,
5501/// builds `name + skipped + pattern`. When GLOBDOTS is unset and the
5502/// compprefix starts with `.`, also adds a dot-prefixed variant.
5503pub fn cfp_bld_pats(_dirs: i32, names: &[String], skipped: &str,             // c:4704
5504                    pats: &[String]) -> Vec<String> {
5505    use crate::ported::zsh_h::{unset, GLOBDOTS};
5506    use crate::ported::zle::complete::COMPPREFIX;
5507
5508    let compprefix = COMPPREFIX.get()
5509        .and_then(|m| m.lock().ok().map(|s| s.clone()))
5510        .unwrap_or_default();
5511    // c:4711 — `dot = unset(GLOBDOTS) && compprefix && *compprefix == '.'`.
5512    let dot = unset(GLOBDOTS) && compprefix.starts_with('.');
5513
5514    let mut ret: Vec<String> = Vec::new();
5515    for o in names {                                                         // c:4712
5516        for p in pats {                                                      // c:4714
5517            // c:4716 — `str = o + skipped + p`.
5518            ret.push(format!("{}{}{}", o, skipped, p));
5519            // c:4721 — dot variant when GLOBDOTS unset and pattern
5520            // doesn't already start with '.'.
5521            if dot && !p.starts_with('.') {
5522                ret.push(format!("{}{}.{}", o, skipped, p));
5523            }
5524        }
5525    }
5526    ret                                                                      // c:4731
5527}
5528
5529/// Direct port of `static char *cfp_matcher_pats(char *matcher, char *add)`
5530/// from `Src/Zle/computil.c:4525-4613`. Parses the matcher spec into
5531/// a Cmatcher chain, then walks each chain entry truncating `add` at
5532/// the first character that matches the matcher's stop pattern, and
5533/// recording one matcher per surviving character. Finally calls
5534/// cfp_matcher_range to synthesize the output pattern.
5535///
5536/// Returns:
5537///   - the transformed string (possibly empty) on success
5538///   - the original `add` unchanged when the matcher spec is empty
5539///     or unparseable
5540pub fn cfp_matcher_pats(matcher: &str, add: &str) -> String {                // c:4525
5541    use crate::ported::zle::comp_h::{Cmatcher, CMF_LEFT, CMF_RIGHT};
5542    use crate::ported::zle::compmatch::pattern_match;
5543    use crate::ported::zle::complete::parse_cmatcher;
5544    use crate::ported::utils::ztrlen;
5545
5546    // c:4527 — parse_cmatcher returns None on error (the C pcm_err path).
5547    let m_chain = parse_cmatcher("", matcher);
5548    let Some(mut m_chain) = m_chain else {
5549        return add.to_string();                                              // c:4529
5550    };
5551
5552    // c:4531-4538 — ms[0..zl] is one matcher slot per character of add.
5553    let zl = ztrlen(add);                                                    // c:4531
5554    let mut ms: Vec<Option<Box<Cmatcher>>> = (0..zl).map(|_| None).collect();
5555    let mut add_owned = add.to_string();
5556
5557    let mut m_opt: Option<&Cmatcher> = Some(&*m_chain);
5558    while let Some(m) = m_opt {
5559        let mut stopp: Option<&crate::ported::zle::comp_h::Cpattern> = None;
5560        let mut stopl: i32 = 0;
5561
5562        if (m.flags & (CMF_LEFT | CMF_RIGHT)) == 0 {                         // c:4542
5563            if m.llen == 1 && m.wlen == 1 {                                  // c:4543
5564                // c:4550 — walk add looking for the first char where the
5565                // matcher's `line` pattern matches; record `m` in ms[i].
5566                let chars: Vec<(usize, char)> = add_owned.char_indices().collect();
5567                for (i, (byte_idx, _ch)) in chars.iter().enumerate() {
5568                    if i >= ms.len() { break; }
5569                    let slice = &add_owned[*byte_idx..];
5570                    if pattern_match(m.line.as_deref(), slice, None, "") != 0 {
5571                        // c:4551 — `if (*mp)` collision: truncate add.
5572                        if ms[i].is_some() {
5573                            add_owned.truncate(*byte_idx);                    // c:4553
5574                            break;
5575                        } else {
5576                            ms[i] = Some(Box::new(m.clone()));                // c:4557
5577                        }
5578                    }
5579                }
5580            } else {
5581                stopp = m.line.as_deref();                                   // c:4565
5582                stopl = m.llen;
5583            }
5584        } else if (m.flags & CMF_RIGHT) != 0 {                               // c:4568
5585            if m.wlen < 0 && m.llen == 0 && m.ralen == 1 {                   // c:4569
5586                let chars: Vec<(usize, char)> = add_owned.char_indices().collect();
5587                for (i, (byte_idx, _ch)) in chars.iter().enumerate() {
5588                    if i >= ms.len() { break; }
5589                    let slice = &add_owned[*byte_idx..];
5590                    if pattern_match(m.right.as_deref(), slice, None, "") != 0 {
5591                        // c:4572 — collision OR leading-dot guard.
5592                        let leading_dot = *byte_idx == 0 && slice.starts_with('.');
5593                        if ms[i].is_some() || leading_dot {
5594                            add_owned.truncate(*byte_idx);                    // c:4573
5595                            break;
5596                        } else {
5597                            ms[i] = Some(Box::new(m.clone()));
5598                        }
5599                    }
5600                }
5601            } else if m.llen != 0 {                                          // c:4584
5602                stopp = m.line.as_deref();
5603                stopl = m.llen;
5604            } else {
5605                stopp = m.right.as_deref();                                  // c:4588
5606                stopl = m.ralen;
5607            }
5608        } else {                                                             // c:4591 CMF_LEFT
5609            if m.lalen == 0 {                                                // c:4592
5610                return String::new();                                        // c:4593
5611            }
5612            stopp = m.left.as_deref();
5613            stopl = m.lalen;
5614        }
5615
5616        // c:4598-4608 — apply stopp truncation.
5617        if let Some(sp) = stopp {
5618            let chars: Vec<(usize, char)> = add_owned.char_indices().collect();
5619            let mut bytes_remaining = add_owned.len() as i32;
5620            for (_i, (byte_idx, _ch)) in chars.iter().enumerate() {
5621                if bytes_remaining < stopl { break; }
5622                let slice = &add_owned[*byte_idx..];
5623                if pattern_match(Some(sp), slice, None, "") != 0 {
5624                    add_owned.truncate(*byte_idx);                            // c:4601
5625                    break;
5626                }
5627                bytes_remaining -= 1;
5628            }
5629        }
5630
5631        m_opt = m.next.as_deref();
5632    }
5633
5634    // c:4610 — synthesize the output via cfp_matcher_range.
5635    if !add_owned.is_empty() {
5636        cfp_matcher_range(&ms, &add_owned)
5637    } else {
5638        add_owned                                                            // c:4613
5639    }
5640}
5641
5642/// Direct port of `static char *cfp_matcher_range(Cmatcher *ms, char *add)`
5643/// from `Src/Zle/computil.c:4307-4520`. For each character of `add`,
5644/// consults the parallel `ms[i]` matcher and emits a pattern fragment:
5645///   - no matcher: the character verbatim
5646///   - CMF_RIGHT: `*c`
5647///   - word EQUIV+line EQUIV: `[c eq(c)]` (two-char class with
5648///     the equivalent char from the word side)
5649///   - CPAT_NCLASS: `[^class]`
5650///   - CPAT_CCLASS / CPAT_EQUIV / CPAT_CHAR: `[classchar+addchar]`
5651///   - CPAT_ANY: `?`
5652pub fn cfp_matcher_range(ms: &[Option<Box<crate::ported::zle::comp_h::Cmatcher>>],  // c:4307
5653                          add: &str) -> String
5654{
5655    use crate::ported::zle::comp_h::{CMF_RIGHT, Cpattern};
5656    use crate::ported::zle::comp_h::{CPAT_ANY, CPAT_CCLASS, CPAT_CHAR,
5657        CPAT_EQUIV, CPAT_NCLASS};
5658    use crate::ported::zle::compmatch::{pattern_match1, pattern_match_equivalence};
5659    use crate::ported::pattern::pattern_range_to_string;
5660    use crate::ported::utils::imeta;
5661
5662    // Local PATMATCHRANGE — Rust copy of the helper used by pattern_match1
5663    // / pattern_match_equivalence. Walks an encoded char-range str
5664    // looking for `c`. On hit returns Some((idx, mtp)).
5665    fn patmatchrange_local(s: Option<&str>, c: u32) -> Option<(u32, i32)> {
5666        let s = s?;
5667        let mut idx: u32 = 0;
5668        let mut chars = s.chars().peekable();
5669        while let Some(ch) = chars.next() {
5670            if let Some(&peek) = chars.peek() {
5671                if peek == '-' {
5672                    chars.next();
5673                    if let Some(hi) = chars.next() {
5674                        if c >= ch as u32 && c <= hi as u32 {
5675                            return Some((idx, 0));
5676                        }
5677                        idx += 1;
5678                        continue;
5679                    }
5680                }
5681            }
5682            if c == ch as u32 {
5683                return Some((idx, 0));
5684            }
5685            idx += 1;
5686        }
5687        None
5688    }
5689
5690    let mut out = String::with_capacity(add.len() * 2);
5691    let add_chars: Vec<(usize, char)> = add.char_indices().collect();
5692
5693    for (i, (_byte_idx, ch)) in add_chars.iter().enumerate() {
5694        let addc = *ch as u32;
5695        let m_opt = ms.get(i).and_then(|x| x.as_deref());
5696
5697        match m_opt {
5698            None => {
5699                // c:4331 — no matcher: emit char verbatim.
5700                out.push(*ch);
5701            }
5702            Some(m) if (m.flags & CMF_RIGHT) != 0 => {
5703                // c:4344 — right-anchored: `*char`.
5704                out.push('*');
5705                out.push(*ch);
5706            }
5707            Some(m) => {
5708                let word: Option<&Cpattern> = m.word.as_deref();
5709                let line: Option<&Cpattern> = m.line.as_deref();
5710                if let (Some(l), Some(w)) = (line, word) {
5711                    if l.tp == CPAT_EQUIV && w.tp == CPAT_EQUIV {
5712                        // c:4359 — genuine equivalence; emit `[char eq]`.
5713                        out.push('[');
5714                        out.push(*ch);
5715                        if let Some((ind, mtp)) = patmatchrange_local(
5716                            l.str.as_deref(), addc)
5717                        {
5718                            let eq = pattern_match_equivalence(
5719                                w, ind + 1, mtp, addc);
5720                            if eq != u32::MAX {
5721                                if let Some(c) = char::from_u32(eq) {
5722                                    let _ = imeta(c);  // imeta handled implicitly
5723                                    out.push(c);
5724                                }
5725                            }
5726                        }
5727                        out.push(']');
5728                        continue;
5729                    }
5730                }
5731                if let Some(w) = word {
5732                    match w.tp {
5733                        x if x == CPAT_NCLASS => {                          // c:4401
5734                            out.push('[');
5735                            out.push('^');
5736                            if let Some(idx_str) = w.str.as_deref() {
5737                                if let Ok(idx) = idx_str.parse::<usize>() {
5738                                    if let Some(cls) = pattern_range_to_string(idx) {
5739                                        out.push_str(&cls);
5740                                    } else {
5741                                        out.push_str(idx_str);
5742                                    }
5743                                } else {
5744                                    out.push_str(idx_str);
5745                                }
5746                            }
5747                            out.push(']');
5748                        }
5749                        x if x == CPAT_CCLASS
5750                          || x == CPAT_EQUIV
5751                          || x == CPAT_CHAR => {                            // c:4435 / c:4441 / c:4442
5752                            out.push('[');
5753                            let mut mt = 0i32;
5754                            let addadd = pattern_match1(w, addc, &mut mt) == 0;
5755                            // c:4455 — if addadd && *add == ']', emit ']' first.
5756                            if addadd && *ch == ']' {
5757                                out.push(*ch);
5758                            }
5759                            if w.tp == CPAT_CHAR {                            // c:4461
5760                                if let Some(c) = char::from_u32(w.chr) {
5761                                    out.push(c);
5762                                }
5763                            } else {                                         // c:4476
5764                                if let Some(idx_str) = w.str.as_deref() {
5765                                    if let Ok(idx) = idx_str.parse::<usize>() {
5766                                        if let Some(cls) = pattern_range_to_string(idx) {
5767                                            out.push_str(&cls);
5768                                        } else {
5769                                            out.push_str(idx_str);
5770                                        }
5771                                    } else {
5772                                        out.push_str(idx_str);
5773                                    }
5774                                }
5775                            }
5776                            if addadd && *ch != ']' {                        // c:4489
5777                                out.push(*ch);
5778                            }
5779                            out.push(']');
5780                        }
5781                        x if x == CPAT_ANY => {                              // c:4502
5782                            out.push('?');
5783                        }
5784                        _ => {
5785                            // Fallback: emit verbatim.
5786                            out.push(*ch);
5787                        }
5788                    }
5789                } else {
5790                    out.push(*ch);
5791                }
5792            }
5793        }
5794    }
5795    out
5796}
5797
5798/// Direct port of `static void cfp_opt_pats(char **pats, char *matcher)`
5799/// from `Src/Zle/computil.c:4621-4701`. "Optimization" pass that
5800/// prefixes each `*…`-leading pattern with the literal portion of
5801/// `compprefix` that no pattern would consume. The walk computes a
5802/// shrinking `add` string — each pattern crosses off the chars in
5803/// `add` it would match — and any remaining chars become the prefix.
5804///
5805/// Modifies `pats` in place; returns the (possibly modified) list.
5806pub fn cfp_opt_pats(pats: &[String], matcher: &str) -> Vec<String> {         // c:4621
5807    use crate::ported::glob::{remnulargs, tokenize};
5808    use crate::ported::pattern::haswilds;
5809    use crate::ported::zle::compcore::comppatmatch;
5810    use crate::ported::zle::compcore::rembslash;
5811    use crate::ported::zle::complete::{COMPPREFIX, COMPSUFFIX};
5812    use crate::ported::ztype_h::idigit;
5813
5814    let compprefix = COMPPREFIX.get()
5815        .and_then(|m| m.lock().ok().map(|s| s.clone()))
5816        .unwrap_or_default();
5817    if compprefix.is_empty() {                                               // c:4625
5818        return pats.to_vec();
5819    }
5820    let compsuffix = COMPSUFFIX.get()
5821        .and_then(|m| m.lock().ok().map(|s| s.clone()))
5822        .unwrap_or_default();
5823
5824    // c:4628-4633 — if comppatmatch && haswilds(rembslash(prefix+suffix)): bail.
5825    let cpm_set = comppatmatch.get()
5826        .and_then(|m| m.lock().ok().map(|g| g.is_some()))
5827        .unwrap_or(false);
5828    if cpm_set {
5829        let merged = format!("{}{}", compprefix, compsuffix);
5830        let mut t = rembslash(&merged);
5831        tokenize(&mut t);
5832        remnulargs(&mut t);
5833        if haswilds(&t) {
5834            return pats.to_vec();                                            // c:4632
5835        }
5836    }
5837
5838    // c:4634-4649 — build `add` by walking compprefix, unescaping `\X`
5839    // for non-special X, and pre-escaping unescaped specials.
5840    const SPECIALS: &[u8] = b"*?<>()[]|#^~=";
5841    let cp_bytes = compprefix.as_bytes();
5842    let mut add: Vec<u8> = Vec::with_capacity(cp_bytes.len() * 2);
5843    let mut i = 0usize;
5844    while i < cp_bytes.len() {
5845        let c = cp_bytes[i];
5846        let keep = if c == b'\\' && i + 1 < cp_bytes.len() {
5847            // c:4636 — keep `\X` literal when X is non-special.
5848            let next = cp_bytes[i + 1];
5849            !SPECIALS.contains(&next)
5850        } else {
5851            true
5852        };
5853        if keep {
5854            let unescaped_at_start = i == 0 || cp_bytes[i - 1] != b'\\';
5855            if unescaped_at_start && SPECIALS.contains(&c) {                 // c:4640
5856                add.push(b'\\');
5857            }
5858            add.push(c);
5859        }
5860        i += 1;
5861    }
5862    let mut add_s: String = String::from_utf8_lossy(&add).into_owned();
5863
5864    // c:4650-4691 — walk each pattern, cross off chars from `add`.
5865    for p_orig in pats {
5866        if add_s.is_empty() { break; }
5867        let mut q_bytes: Vec<u8> = p_orig.as_bytes().to_vec();
5868        if q_bytes.is_empty() { continue; }
5869        // c:4654 — strip trailing alternation `(…|…)` group.
5870        if let Some(b')') = q_bytes.last().copied() {
5871            let mut t = q_bytes.len() - 1;
5872            let mut found = None;
5873            while t > 0 {
5874                t -= 1;
5875                let c = q_bytes[t];
5876                if c == b')' || c == b'|' || c == b'~' || c == b'(' {
5877                    found = Some((t, c));
5878                    break;
5879                }
5880            }
5881            if let Some((idx, c)) = found {
5882                if c == b'(' {
5883                    q_bytes.truncate(idx);
5884                }
5885            }
5886        }
5887
5888        let mut qi = 0usize;
5889        while qi < q_bytes.len() && !add_s.is_empty() {
5890            let c = q_bytes[qi];
5891            if c == b'\\' && qi + 1 < q_bytes.len() {                        // c:4662
5892                qi += 1;
5893                let target = q_bytes[qi];
5894                // c:4663 — cross off `target` from add.
5895                if let Some(pos) = add_s.find(target as char) {
5896                    add_s.truncate(pos);
5897                }
5898            } else if c == b'<' {                                            // c:4665
5899                // c:4666 — cross off any digit.
5900                let cut_at = add_s.bytes().position(|b| idigit(b));
5901                if let Some(pos) = cut_at {
5902                    add_s.truncate(pos);
5903                }
5904            } else if c == b'[' {                                            // c:4668
5905                // c:4669-4684 — character class.
5906                let mut xi = qi + 1;
5907                let not = xi < q_bytes.len()
5908                    && (q_bytes[xi] == b'!' || q_bytes[xi] == b'^');
5909                if not { xi += 1; }
5910                let _ = not;
5911                while xi < q_bytes.len() && q_bytes[xi] != b']' {
5912                    if xi + 2 < q_bytes.len() && q_bytes[xi + 1] == b'-' {
5913                        let c1 = q_bytes[xi];
5914                        let c2 = q_bytes[xi + 2];
5915                        let cut_at = add_s.bytes().position(|b| b >= c1 && b <= c2);
5916                        if let Some(pos) = cut_at {
5917                            add_s.truncate(pos);
5918                        }
5919                        xi += 3;
5920                    } else {
5921                        let cut_at = add_s.find(q_bytes[xi] as char);
5922                        if let Some(pos) = cut_at {
5923                            add_s.truncate(pos);
5924                        }
5925                        xi += 1;
5926                    }
5927                }
5928                qi = xi;
5929            } else if c != b'?' && c != b'*' && c != b'(' && c != b')'
5930                && c != b'|' && c != b'~' && c != b'#'                       // c:4685
5931            {
5932                let cut_at = add_s.find(c as char);
5933                if let Some(pos) = cut_at {
5934                    add_s.truncate(pos);
5935                }
5936            }
5937            qi += 1;
5938        }
5939    }
5940
5941    // c:4693-4700 — prepend `add` to each `*`-leading pattern.
5942    let mut out: Vec<String> = pats.to_vec();
5943    if !add_s.is_empty() {
5944        let final_add = if !matcher.is_empty() {
5945            let m = cfp_matcher_pats(matcher, &add_s);
5946            if m.is_empty() { return out; }                                  // c:4694
5947            m
5948        } else {
5949            add_s
5950        };
5951        for p in out.iter_mut() {
5952            if p.starts_with('*') {
5953                *p = format!("{}{}", final_add, p);
5954            }
5955        }
5956    }
5957    out
5958}
5959
5960/// Direct port of `static LinkList cfp_test_exact(LinkList names,
5961///                                                  char **accept,
5962///                                                  char *skipped)` from
5963/// `Src/Zle/computil.c:4160-4290`. Returns the subset of `names` whose
5964/// `name + skipped + compprefix + compsuffix` resolves to an existing
5965/// file. When `accept` is non-boolean, the resolved path must also
5966/// match at least one of the compiled accept-patterns. Returns None
5967/// when nothing matched.
5968pub fn cfp_test_exact(names: &[String], accept: &[String],                   // c:4160
5969                      skipped: &str) -> Option<Vec<String>>
5970{
5971    use crate::ported::glob::tokenize;
5972    use crate::ported::pattern::{patcompile, pattry, Patprog};
5973    use crate::ported::zle::compcore::rembslash;
5974    use crate::ported::zle::complete::{COMPPREFIX, COMPSUFFIX};
5975    use crate::ported::zle::compresult::ztat;
5976
5977    let compprefix = COMPPREFIX.get()
5978        .and_then(|m| m.lock().ok().map(|s| s.clone()))
5979        .unwrap_or_default();
5980    let compsuffix = COMPSUFFIX.get()
5981        .and_then(|m| m.lock().ok().map(|s| s.clone()))
5982        .unwrap_or_default();
5983
5984    // c:4175 — bail when both prefix and suffix are empty.
5985    if compprefix.is_empty() && compsuffix.is_empty() {
5986        return None;
5987    }
5988
5989    // c:4181 — accept-exact off?
5990    let accept_off = accept.is_empty()
5991        || (accept.len() == 1 && matches!(accept[0].as_str(),
5992            "false" | "no" | "off" | "0"));
5993    if accept_off {                                                          // c:4188
5994        return None;
5995    }
5996
5997    // c:4199-4214 — build compiled Patprog list from non-boolean accept.
5998    let mut alist: Option<Vec<Patprog>> = None;
5999    let is_boolean_true = accept.len() == 1
6000        && matches!(accept[0].as_str(), "true" | "yes" | "on" | "1");
6001    if !is_boolean_true {
6002        let mut list: Vec<Patprog> = Vec::new();
6003        let mut all_star = false;
6004        for p in accept {
6005            if p == "*" {                                                    // c:4207 wildcard short-circuit
6006                all_star = true;
6007                break;
6008            }
6009            let mut p_copy = p.clone();
6010            tokenize(&mut p_copy);
6011            if let Some(prog) = patcompile(&p_copy, 0, None::<&mut String>) {
6012                list.push(prog);
6013            }
6014        }
6015        if !all_star { alist = Some(list); }
6016    }
6017
6018    // c:4220-4227 — assemble `suf = skipped + rembslash(prefix + suffix)`.
6019    let sl = skipped.len() + compprefix.len() + compsuffix.len();
6020    if sl > PATH_MAX2 {                                                      // c:4223
6021        return None;
6022    }
6023    let suf = format!("{}{}",
6024        skipped,
6025        rembslash(&format!("{}{}", compprefix, compsuffix)));
6026
6027    let mut ret: Vec<String> = Vec::new();
6028    for p in names {                                                         // c:4229
6029        let l = p.len();
6030        if l + sl >= PATH_MAX2 { continue; }                                 // c:4231
6031        let buf = format!("{}{}", p, suf);
6032        if ztat(&buf, false).is_none() { continue; }                          // c:4269 stat exists?
6033        // c:4274 — accept-pattern check.
6034        if let Some(ref ps) = alist {
6035            let any_match = ps.iter().any(|prog| pattry(prog, &buf));
6036            if !any_match { continue; }
6037        }
6038        ret.push(buf);                                                       // c:4285
6039    }
6040
6041    if ret.is_empty() { None } else { Some(ret) }                            // c:4289
6042}
6043
6044/// Port of `cleanup_(UNUSED(Module m))` from Src/Zle/computil.c:5160.
6045/// WARNING: param names don't match C — Rust=() vs C=(m)
6046pub fn cleanup_() -> i32 {                                                   // c:5160
6047    // C body c:5162-5163 — `return setfeatureenables(m, &module_features, NULL)`.
6048    //                      Static-link path: no per-feature toggle, return 0.
6049    0
6050}
6051
6052/// Port of `comp_quote(char *str, int prefix)` from Src/Zle/computil.c:3662.
6053pub fn comp_quote(str: &str, prefix: i32) -> String {                          // c:3662
6054    // c:3667 — `x = (prefix && *str == '=')`.
6055    let (s_eff, x) = if prefix != 0 && str.starts_with('=') {                  // c:3667
6056        ("x".to_string() + &str[1..], true)                                    // c:3668
6057    } else {
6058        (str.to_string(), false)
6059    };
6060    // c:3670 — `ret = quotestring(str, *compqstack)`.
6061    //          *compqstack is the first byte of the qstack string.
6062    let qhead = COMPQSTACK.get()
6063        .and_then(|m| m.lock().ok().and_then(|str| str.bytes().next()))
6064        .unwrap_or(0);
6065    let mut ret = crate::ported::zle::zle_tricky::quotename(&s_eff, qhead as i32);
6066    // c:3672-3673 — restore `=` prefix on both ret and original.
6067    if x {
6068        if !ret.is_empty() {
6069            ret.replace_range(0..1, "=");
6070        }
6071    }
6072    ret
6073}
6074
6075/// Direct port of `static Cvval cv_get_val(Cvdef d, char *name)` from
6076/// `Src/Zle/computil.c:3178-3187`. Linear scan over `d->vals` for a
6077/// name match. Returns a shallow clone of the matched cvval.
6078pub fn cv_get_val(d: &cvdef, name: &str) -> Option<Box<cvval>> {             // c:3178
6079    let mut p = d.vals.as_deref();
6080    while let Some(v) = p {                                                  // c:3182
6081        if v.name.as_deref() == Some(name) {                                 // c:3183
6082            return Some(Box::new(cvval {
6083                next:   None,
6084                name:   v.name.clone(),
6085                descr:  v.descr.clone(),
6086                xor:    v.xor.clone(),
6087                r#type: v.r#type,
6088                arg:    v.arg.clone(),
6089                active: v.active,
6090            }));
6091        }
6092        p = v.next.as_deref();
6093    }
6094    None                                                                     // c:3186
6095}
6096
6097/// Direct port of `static void cv_inactive(Cvdef d, char **xor)` from
6098/// `Src/Zle/computil.c:3209-3218`. Clears `active` on each cvval named
6099/// in the xor list.
6100pub fn cv_inactive(d: &mut cvdef, xor: &[String]) {                          // c:3209
6101    for name in xor {                                                        // c:3214
6102        let mut p = d.vals.as_deref_mut();
6103        while let Some(v) = p {
6104            if v.name.as_deref() == Some(name.as_str()) {
6105                v.active = 0;                                                // c:3216
6106            }
6107            p = v.next.as_deref_mut();
6108        }
6109    }
6110}
6111
6112/// Direct port of `static Cvval cv_next(Cvdef d, char **sp, char **ap)`
6113/// from `Src/Zle/computil.c:3240-3331`. Splits the next value out of
6114/// `*sp` using `d->sep` / `d->argsep`, returns its matched Cvval.
6115/// On success, `*sp` advances past the consumed prefix; `*ap` is set
6116/// to the value's argument (if any) or `None`.
6117pub fn cv_next(d: &cvdef, sp: &mut Option<String>,                           // c:3240
6118               ap: &mut Option<String>) -> Option<Box<cvval>>
6119{
6120    let s_in = sp.take().unwrap_or_default();
6121    if s_in.is_empty() {                                                     // c:3245
6122        *sp = None;
6123        *ap = None;
6124        return None;
6125    }
6126    let bytes = s_in.as_bytes();
6127
6128    // Branch 1: hassep && !sep, or !argsep — greedy match (longest prefix).
6129    if (d.hassep != 0 && d.sep == 0) || d.argsep == 0 {                      // c:3250
6130        let ec_byte: u8 = if d.hassep != 0 && d.sep != 0 {
6131            d.sep as u8
6132        } else {
6133            d.argsep as u8
6134        };
6135        let mut s_idx: usize = 0;
6136        let mut r: Option<Box<cvval>> = None;
6137        // c:3255 — extend until cv_quote_get_val matches or hit ec.
6138        loop {
6139            s_idx += 1;
6140            if s_idx > bytes.len() { break; }
6141            let candidate = std::str::from_utf8(&bytes[..s_idx])
6142                .unwrap_or("");
6143            r = cv_quote_get_val(d, candidate);
6144            if r.is_some() { break; }
6145            if s_idx >= bytes.len() || bytes[s_idx] == ec_byte { break; }
6146        }
6147        let os = s_idx;
6148        // c:3268 — advance *sp.
6149        if d.hassep != 0 && d.sep != 0 {
6150            let sep_byte = d.sep as u8;
6151            if let Some(off) = bytes[s_idx.min(bytes.len())..]
6152                .iter().position(|&b| b == sep_byte)
6153            {
6154                let after = s_idx + off + 1;
6155                *sp = Some(String::from_utf8_lossy(&bytes[after..]).into_owned());
6156            } else {
6157                *sp = None;
6158            }
6159        } else {
6160            *sp = if s_idx < bytes.len() {
6161                Some(String::from_utf8_lossy(&bytes[s_idx..]).into_owned())
6162            } else { None };
6163        }
6164        // c:3275 — set *ap.
6165        let argsep_b = d.argsep as u8;
6166        if d.argsep != 0 && os < bytes.len() && bytes[os] == argsep_b {
6167            *ap = Some(String::from_utf8_lossy(&bytes[os + 1..]).into_owned());
6168            *sp = None;
6169        } else if r.as_deref().map_or(false, |v| v.r#type != CVV_NOARG) {
6170            *ap = if os < bytes.len() {
6171                Some(String::from_utf8_lossy(&bytes[os..]).into_owned())
6172            } else { None };
6173        } else {
6174            *ap = None;
6175        }
6176        return r;
6177    }
6178
6179    // Branch 2: hassep set (with both sep and argsep).
6180    if d.hassep != 0 {                                                       // c:3285
6181        let sep_b = d.sep as u8;
6182        let argsep_b = d.argsep as u8;
6183        let ns = bytes.iter().position(|&b| b == sep_b);
6184        let mut as_off: Option<usize> = None;
6185        let mut skip = false;
6186        if d.argsep != 0 {
6187            if let Some(a_pos) = bytes.iter().position(|&b| b == argsep_b) {
6188                if ns.map_or(true, |n| a_pos <= n) {                          // c:3289
6189                    as_off = Some(a_pos);
6190                    *ap = Some(String::from_utf8_lossy(&bytes[a_pos + 1..])
6191                        .into_owned());
6192                    skip = true;
6193                }
6194            }
6195        }
6196        let sap = as_off.or(ns);
6197        let head = match sap {
6198            Some(p) => std::str::from_utf8(&bytes[..p]).unwrap_or(""),
6199            None => std::str::from_utf8(&bytes).unwrap_or(""),
6200        };
6201        let r = cv_quote_get_val(d, head);
6202        // c:3302 — if NOARG/no match and skip, fall back to as.
6203        let ns_eff = if (r.as_deref().map_or(true, |v| v.r#type == CVV_NOARG)) && skip {
6204            as_off
6205        } else if skip {
6206            // skip is set ⇒ as_off was the chosen sep; ns might be after.
6207            ns.filter(|&n| as_off.map_or(true, |a| n > a))
6208        } else {
6209            ns
6210        };
6211        let next_off = match ns_eff {
6212            None => None,
6213            Some(n) if Some(n) == as_off
6214                && r.as_deref().map_or(false, |v| v.r#type != CVV_NOARG) => None,
6215            Some(n) => Some(n + 1),
6216        };
6217        *sp = next_off.map(|o|
6218            String::from_utf8_lossy(&bytes[o..]).into_owned());
6219        if !skip { *ap = None; }
6220        return r;
6221    }
6222
6223    // Branch 3: no hassep, argsep set.
6224    *sp = None;                                                              // c:3314
6225    let argsep_b = d.argsep as u8;
6226    let as_pos = bytes.iter().position(|&b| b == argsep_b);
6227    let head = match as_pos {
6228        Some(p) => {
6229            *ap = Some(String::from_utf8_lossy(&bytes[p + 1..]).into_owned());
6230            std::str::from_utf8(&bytes[..p]).unwrap_or("")
6231        }
6232        None => {
6233            *ap = None;
6234            &s_in
6235        }
6236    };
6237    cv_quote_get_val(d, head)                                                // c:3324
6238}
6239
6240/// Direct port of `static void cv_parse_word(Cvdef d)` from
6241/// `Src/Zle/computil.c:3336-3472`. Walks `compwords[1..]` (when
6242/// `d->words` set) and `compprefix` calling `cv_next` repeatedly,
6243/// accumulating recognized values into `cv_laststate.vals`.
6244pub fn cv_parse_word(d: &mut cvdef) {                                        // c:3336
6245    use crate::ported::zle::complete::{COMPCURRENT, COMPWORDS,
6246        COMPPREFIX, COMPSUFFIX, ignore_prefix, ignore_suffix};
6247    use std::sync::atomic::Ordering;
6248
6249    // c:3343 — free old vals.
6250    if cv_alloced.load(Ordering::Relaxed) != 0 {
6251        if let Ok(mut ls) = cv_laststate.lock() {
6252            ls.vals = None;
6253        }
6254    }
6255    // c:3346 — mark all vals active.
6256    let mut v = d.vals.as_deref_mut();
6257    while let Some(vv) = v {
6258        vv.active = 1;
6259        v = vv.next.as_deref_mut();
6260    }
6261
6262    let mut state_vals: Vec<String> = Vec::new();
6263    let mut state_def: Option<Box<caarg>> = None;
6264    let mut state_val: Option<Box<cvval>> = None;
6265    cv_alloced.store(1, Ordering::Relaxed);
6266
6267    let compcur = COMPCURRENT.load(Ordering::Relaxed);
6268    let compwords: Vec<String> = COMPWORDS
6269        .get()
6270        .and_then(|m| m.lock().ok().map(|w| w.clone()))
6271        .unwrap_or_default();
6272    let compprefix: String = COMPPREFIX.get()
6273        .and_then(|m| m.lock().ok().map(|s| s.clone()))
6274        .unwrap_or_default();
6275    let compsuffix: String = COMPSUFFIX.get()
6276        .and_then(|m| m.lock().ok().map(|s| s.clone()))
6277        .unwrap_or_default();
6278    let mut pign = compprefix.clone();                                       // c:3340
6279    let mut nosfx = false;
6280
6281    // c:3356 — scan compwords[1..] if d.words is set.
6282    if d.words != 0 && !compwords.is_empty() && !compwords[0].is_empty() {
6283        for i in 1..compwords.len() {
6284            if (i as i32) == compcur - 1 { continue; }
6285            let mut str_opt: Option<String> = Some(compwords[i].clone());
6286            while str_opt.as_deref().map_or(false, |s| !s.is_empty()) {
6287                let mut ap: Option<String> = None;
6288                let val = cv_next(d, &mut str_opt, &mut ap);
6289                if let Some(v) = val {
6290                    state_vals.push(v.name.clone().unwrap_or_default());
6291                    state_vals.push(ap.unwrap_or_default());
6292                    if (i as i32) + 1 < compcur {
6293                        let xor = v.xor.clone().unwrap_or_default();
6294                        cv_inactive(d, &xor);
6295                    }
6296                } else {
6297                    break;
6298                }
6299            }
6300        }
6301    }
6302
6303    // c:3385 — scan compprefix.
6304    let mut str_opt: Option<String> = Some(compprefix.clone());
6305    let mut last_arg: Option<String> = None;
6306    while str_opt.as_deref().map_or(false, |s| !s.is_empty()) {
6307        let mut ap: Option<String> = None;
6308        let val = cv_next(d, &mut str_opt, &mut ap);
6309        if let Some(v) = val {
6310            state_vals.push(v.name.clone().unwrap_or_default());
6311            match ap.as_deref() {
6312                Some(arg_v) => {
6313                    if str_opt.is_some() {
6314                        state_vals.push(arg_v.to_string());
6315                    } else {
6316                        let joined = format!("{}{}", arg_v, compsuffix);
6317                        state_vals.push(joined);
6318                        nosfx = true;
6319                    }
6320                    last_arg = ap.clone();
6321                }
6322                None => state_vals.push(String::new()),
6323            }
6324            let xor = v.xor.clone().unwrap_or_default();
6325            cv_inactive(d, &xor);
6326            if let Some(s) = str_opt.as_deref() {
6327                pign = s.to_string();
6328            } else {
6329                // c:3407 — re-activate v in the cvdef.
6330                let target_name = v.name.clone();
6331                let mut p = d.vals.as_deref_mut();
6332                while let Some(vv) = p {
6333                    if vv.name == target_name { vv.active = 1; break; }
6334                    p = vv.next.as_deref_mut();
6335                }
6336            }
6337            state_val = Some(v);
6338        } else {
6339            break;
6340        }
6341    }
6342    if state_val.is_some() && last_arg.is_some() && str_opt.is_none() {       // c:3411
6343        state_def = state_val.as_ref().and_then(|v| v.arg.clone());
6344    }
6345
6346    // c:3414 — separator handling for compsuffix.
6347    if !nosfx && d.hassep != 0 {
6348        let pign_len = pign.len();
6349        let cp_len = compprefix.len();
6350        ignore_prefix(cp_len as i32 - pign_len as i32);                       // c:3418
6351
6352        let mut ign = 0usize;
6353        let mut more: Option<String> = None;
6354        if d.sep == 0 && (state_val.is_none()
6355            || state_val.as_deref().map_or(true, |v| v.r#type == CVV_NOARG))
6356        {
6357            ign = compsuffix.len();
6358            more = Some(compsuffix.clone());
6359        } else if d.sep != 0 {
6360            let sep_b = d.sep as u8;
6361            let ns_pos = compsuffix.as_bytes().iter().position(|&b| b == sep_b);
6362            let as_pos = if d.argsep != 0 {
6363                compsuffix.as_bytes().iter().position(|&b| b == d.argsep as u8)
6364            } else { None };
6365            if let Some(a) = as_pos {
6366                if ns_pos.map_or(true, |n| a <= n) {
6367                    ign = compsuffix.len() - a;
6368                } else {
6369                    ign = ns_pos.map_or(0, |n| compsuffix.len() - n);
6370                }
6371            } else {
6372                ign = ns_pos.map_or(0, |n| compsuffix.len() - n);
6373            }
6374            more = ns_pos.map(|n| compsuffix[n + 1..].to_string());
6375        } else if d.argsep != 0 {
6376            let as_pos = compsuffix.as_bytes().iter()
6377                .position(|&b| b == d.argsep as u8);
6378            if let Some(a) = as_pos {
6379                ign = compsuffix.len() - a;
6380            }
6381        }
6382
6383        if ign > 0 {
6384            ignore_suffix(ign as i32);                                        // c:3444
6385        }
6386
6387        let mut more_opt = more;
6388        while more_opt.as_deref().map_or(false, |s| !s.is_empty()) {          // c:3446
6389            let mut ap: Option<String> = None;
6390            let val = cv_next(d, &mut more_opt, &mut ap);
6391            if let Some(v) = val {
6392                state_vals.push(v.name.clone().unwrap_or_default());
6393                match ap.as_deref() {
6394                    Some(arg_v) => {
6395                        if more_opt.is_some() {
6396                            state_vals.push(arg_v.to_string());
6397                        } else {
6398                            state_vals.push(format!("{}{}", arg_v, compsuffix));
6399                        }
6400                    }
6401                    None => state_vals.push(String::new()),
6402                }
6403                let xor = v.xor.clone().unwrap_or_default();
6404                cv_inactive(d, &xor);
6405            } else { break; }
6406        }
6407    } else if last_arg.is_some() {
6408        let cp_len = compprefix.len();
6409        let arg_off = compprefix.find(last_arg.as_deref().unwrap_or(""))
6410            .map(|i| i as i32).unwrap_or(cp_len as i32);
6411        ignore_prefix(arg_off);                                              // c:3467
6412    } else {
6413        let cp_len = compprefix.len();
6414        ignore_prefix(cp_len as i32 - pign.len() as i32);                    // c:3469
6415    }
6416
6417    // c:3471 — commit state.
6418    if let Ok(mut ls) = cv_laststate.lock() {
6419        *ls = cvstate {
6420            d:    Some(Box::new(d.clone())),
6421            def:  state_def,
6422            val:  state_val,
6423            vals: if state_vals.is_empty() { None } else { Some(state_vals) },
6424        };
6425    }
6426}
6427
6428/// Direct port of `static Cvval cv_quote_get_val(Cvdef d, char *name)`
6429/// from `Src/Zle/computil.c:3190-3204`. Unquotes `name` via the full
6430/// C chain: `parse_subst_string` (with noerrs=2 to suppress errors),
6431/// `remnulargs`, then `untokenize`; result fed to `cv_get_val`.
6432pub fn cv_quote_get_val(d: &cvdef, name: &str) -> Option<Box<cvval>> {       // c:3190
6433    // c:3195 — `name = dupstring(name)` (Rust: own a mutable copy).
6434    let mut s = name.to_string();
6435    // c:3196-3199 — `ne = noerrs; noerrs = 2; parse_subst_string(name);
6436    //                noerrs = ne`. The parse_subst_string port (lex.rs:3797)
6437    // returns Result; we discard errors so noerrs=2/restore is a no-op.
6438    crate::ported::utils::set_noerrs(2);
6439    let parsed = crate::ported::lex::parse_subst_string(&s).ok();
6440    crate::ported::utils::set_noerrs(0);
6441    if let Some(p) = parsed { s = p; }
6442    // c:3200 — `remnulargs(name)`.
6443    crate::ported::glob::remnulargs(&mut s);
6444    // c:3201 — `untokenize(name)`.
6445    let s = crate::ported::lex::untokenize(&s);
6446    // c:3203 — `return cv_get_val(d, name)`.
6447    cv_get_val(d, &s)
6448}
6449
6450/// Port of `enables_(UNUSED(Module m), UNUSED(int **enables))` from Src/Zle/computil.c:5146.
6451/// WARNING: param names don't match C — Rust=() vs C=(m, enables)
6452pub fn enables_() -> i32 {                                                   // c:5146
6453    // C body c:5148 — `return handlefeatures(m, &module_features, enables)`.
6454    //                  Static-link no-op.
6455    0
6456}
6457
6458/// Port of `features_(UNUSED(Module m), UNUSED(char ***features))` from Src/Zle/computil.c:5138.
6459/// WARNING: param names don't match C — Rust=() vs C=(m, features)
6460pub fn features_() -> i32 {                                                  // c:5138
6461    // C body c:5140-5141 — `*features = featuresarray(...); return 0`.
6462    //                      Features array exposed elsewhere; return 0.
6463    0
6464}
6465
6466/// Direct port of `int finish_(UNUSED(Module m))` from
6467/// `Src/Zle/computil.c:5167-5180`. Frees every cached cadef/cvdef
6468/// and the comptags table on module unload.
6469pub fn finish_() -> i32 {                                                    // c:5167
6470    // c:5171 — `for (i = 0; i < MAX_CACACHE; i++) freecadef(cadef_cache[i])`.
6471    if let Ok(mut cache) = cadef_cache.lock() {
6472        for slot in cache.iter_mut() {
6473            freecadef(slot.take());
6474        }
6475    }
6476    // c:5173 — `for (i = 0; i < MAX_CVCACHE; i++) freecvdef(cvdef_cache[i])`.
6477    if let Ok(mut cache) = cvdef_cache.lock() {
6478        for slot in cache.iter_mut() {
6479            freecvdef(slot.take());
6480        }
6481    }
6482    // c:5176 — `for (i = 0; i < MAX_TAGS; i++) freectags(comptags[i])`.
6483    if let Ok(mut tab) = comptags.lock() {
6484        for slot in tab.iter_mut() {
6485            freectags(slot.take());
6486        }
6487    }
6488    0                                                                        // c:5179
6489}
6490
6491/// Direct port of `int setup_(UNUSED(Module m))` from
6492/// `Src/Zle/computil.c:5124-5134`. Zeroes the three module caches
6493/// and resets `lasttaglevel`. Called on module load.
6494pub fn setup_() -> i32 {                                                     // c:5124
6495    // c:5126 — `memset(cadef_cache, 0, sizeof(cadef_cache))`.
6496    if let Ok(mut cache) = cadef_cache.lock() {
6497        for slot in cache.iter_mut() {
6498            freecadef(slot.take());
6499        }
6500    }
6501    // c:5127 — `memset(cvdef_cache, 0, sizeof(cvdef_cache))`.
6502    if let Ok(mut cache) = cvdef_cache.lock() {
6503        for slot in cache.iter_mut() {
6504            freecvdef(slot.take());
6505        }
6506    }
6507    // c:5129 — `memset(comptags, 0, sizeof(comptags))`.
6508    if let Ok(mut tab) = comptags.lock() {
6509        for slot in tab.iter_mut() {
6510            freectags(slot.take());
6511        }
6512    }
6513    // c:5131 — `lasttaglevel = 0`.
6514    lasttaglevel.store(0, std::sync::atomic::Ordering::Relaxed);
6515    0                                                                        // c:5133
6516}
6517
6518// `freecastate` / `freectags` / `freectset` / `freecvdef` real ports
6519// landed above with the castate / ctags / ctset / cvdef structs.
6520
6521/// Direct port of `static Cadef get_cadef(char *nam, char **args)`
6522/// from `Src/Zle/computil.c:1673-1694`. Walks `cadef_cache` looking
6523/// for an entry whose `defs` array matches the requested `args`
6524/// (same length + position-for-position string equality). On hit,
6525/// bumps that entry's `lastt` and returns it. On miss, parses via
6526/// `parse_cadef` and evicts the entry with the oldest `lastt`
6527/// (or the first empty slot) to make room for the new one.
6528///
6529/// Returns `1` on hit, `0` on miss-and-cache-insert. The previous
6530/// return-`i32` shape is preserved for callers; the parsed cadef
6531/// itself lives in `cadef_cache` and is looked up by separate
6532/// per-name accessors (`ca_get_opt`, `ca_get_arg`, etc.).
6533pub fn get_cadef(nam: &str, args: &[String]) -> i32 {                       // c:1673
6534    let na = args.len() as i32;
6535    let now = {                                                              // c:1681 time(0)
6536        use std::time::{SystemTime, UNIX_EPOCH};
6537        SystemTime::now().duration_since(UNIX_EPOCH)
6538            .map(|d| d.as_secs() as i64).unwrap_or(0)
6539    };
6540
6541    if let Ok(mut cache) = cadef_cache.lock() {
6542        // c:1678 — `for (i = MAX_CACACHE, p = cadef_cache, min = NULL;
6543        //                  i && *p; p++, i--)`. Linear scan; track LRU
6544        //          candidate for eviction in `min_idx`.
6545        let mut min_idx: Option<usize> = None;
6546        let mut min_lastt: i64 = i64::MAX;
6547        let mut hit_idx: Option<usize> = None;
6548        for (i, slot) in cache.iter().enumerate() {
6549            match slot {
6550                Some(entry) => {
6551                    // c:1679 — `if (*p && na == (*p)->ndefs && arrcmp(args, (*p)->defs))`.
6552                    if entry.ndefs == na
6553                        && entry.defs.as_deref()
6554                            .map_or(false, |d| d.len() == args.len()
6555                                && d.iter().zip(args.iter()).all(|(a, b)| a == b))
6556                    {
6557                        hit_idx = Some(i);
6558                        break;                                               // c:1682 break on match
6559                    }
6560                    // c:1684 — track entry with smallest lastt as eviction target.
6561                    if entry.lastt < min_lastt {
6562                        min_lastt = entry.lastt;
6563                        min_idx = Some(i);
6564                    }
6565                }
6566                None => {
6567                    // c:1684 — empty slot wins as eviction target.
6568                    min_idx = Some(i);
6569                    break;
6570                }
6571            }
6572        }
6573        if let Some(i) = hit_idx {
6574            if let Some(entry) = cache[i].as_mut() {
6575                entry.lastt = now;                                           // c:1681
6576            }
6577            return 1;                                                        // c:1683 hit
6578        }
6579        // c:1688 — parse_cadef; on success replace the chosen slot.
6580        if let Some(new) = parse_cadef(nam, args) {
6581            let idx = min_idx.unwrap_or(0);
6582            cache[idx] = Some(new);
6583        }
6584    }
6585    0                                                                        // c:1693 miss
6586}
6587
6588/// Direct port of `static Caarg parse_caarg(int mult, int type, int num,
6589///                                          int opt, char *oname, char **def,
6590///                                          char *set)` from
6591/// `Src/Zle/computil.c:1099-1144`. Parses one `:descr[:action]`
6592/// fragment of an `_arguments` spec into a freshly-allocated caarg.
6593/// On return, `*idx` points at the first byte of `bytes` not consumed
6594/// (either the separator `:` for `mult=1` or `bytes.len()` for
6595/// `mult=0` rest specs).
6596pub fn parse_caarg(mult: i32, atype: i32, num: i32, opt: i32,                // c:1099
6597                   oname: Option<&str>, bytes: &[u8], idx: &mut usize,
6598                   set: Option<&str>) -> Box<caarg> {
6599    let mut ret = Box::new(caarg::default());
6600    ret.num = num;                                                           // c:1109
6601    ret.min = num - opt;                                                     // c:1110
6602    ret.r#type = atype;                                                      // c:1111
6603    ret.opt = oname.map(|s| s.to_string());                                  // c:1112
6604    ret.direct = 0;                                                          // c:1113
6605    ret.gsname = set.map(|s| s.to_string());                                 // c:1114
6606
6607    let n = bytes.len();
6608
6609    // c:1118-1120 — scan description up to the next `:` (escaped `\:` skipped).
6610    let d_start = *idx;
6611    while *idx < n && bytes[*idx] != b':' {
6612        if bytes[*idx] == b'\\' && *idx + 1 < n {
6613            *idx += 1;
6614        }
6615        *idx += 1;
6616    }
6617    let has_sav = *idx < n;
6618    let descr_slice = &bytes[d_start..*idx];
6619    let descr_str = std::str::from_utf8(descr_slice).unwrap_or("");
6620    ret.descr = Some(rembslashcolon(descr_str));                             // c:1123
6621
6622    if has_sav {                                                             // c:1127
6623        if mult != 0 {                                                       // c:1128
6624            // c:1129-1136 — `*p == ':'` start, scan to next `:` or NUL.
6625            *idx += 1;
6626            let a_start = *idx;
6627            while *idx < n && bytes[*idx] != b':' {
6628                if bytes[*idx] == b'\\' && *idx + 1 < n {
6629                    *idx += 1;
6630                }
6631                *idx += 1;
6632            }
6633            let action_slice = &bytes[a_start..*idx];
6634            let action_str = std::str::from_utf8(action_slice).unwrap_or("");
6635            ret.action = Some(rembslashcolon(action_str));                   // c:1134
6636        } else {                                                             // c:1137
6637            // c:1138 — `ret->action = ztrdup(rembslashcolon(p + 1))`.
6638            let action_slice = &bytes[*idx + 1..];
6639            let action_str = std::str::from_utf8(action_slice).unwrap_or("");
6640            ret.action = Some(rembslashcolon(action_str));
6641            *idx = n;
6642        }
6643    } else {                                                                 // c:1139
6644        ret.action = Some(String::new());                                    // c:1140
6645    }
6646    // c:1141 — `*def = p`. Caller reads `bytes[*idx]` to decide whether to
6647    // continue scanning more `:` fragments.
6648
6649    ret
6650}
6651
6652/// Direct port of `static Cadef parse_cadef(char *nam, char **args)` from
6653/// `Src/Zle/computil.c:1196-1666`. Parses the leading auto-description
6654/// (first arg up to `%d`), the `-s/-A/-S/-M` flag block, then the
6655/// main spec-list loop that fills opts/args/rest from each remaining
6656/// `_arguments` spec entry.
6657pub fn parse_cadef(nam: &str, args: &[String]) -> Option<Box<cadef>> {      // c:1196
6658    use crate::ported::ztype_h::{iblank, idigit, inblank};
6659
6660    if args.is_empty() {
6661        return None;                                                         // c:1262 `!*args`
6662    }
6663
6664    let orig_args = args;
6665    let mut idx = 0usize;
6666    let mut single: i32 = 0;
6667    let mut flags: i32 = 0;
6668    let mut match_spec: String = "r:|[_-]=* r:|=*".to_string();              // c:1200
6669    let mut nonarg: Option<String> = None;
6670
6671    // c:1208-1216 — split args[0] on `%d` into (adpre, adsuf). Used at
6672    // c:1543-1554 to auto-derive option descriptions.
6673    let (adpre, adsuf): (Option<String>, Option<String>) = {
6674        let first = args[0].as_bytes();
6675        let mut split_at: Option<usize> = None;
6676        let mut i = 0usize;
6677        while i + 1 < first.len() {
6678            if first[i] == b'%' && first[i + 1] == b'd' {
6679                split_at = Some(i);
6680                break;
6681            }
6682            i += 1;
6683        }
6684        if let Some(at) = split_at {
6685            let pre = String::from_utf8_lossy(&first[..at]).into_owned();
6686            let suf = String::from_utf8_lossy(&first[at + 2..]).into_owned();
6687            (Some(pre), Some(suf))
6688        } else {
6689            (None, None)
6690        }
6691    };
6692
6693    idx += 1;                                                                // c:1220 args++
6694
6695    // c:1221-1259 — `-s/-A/-S/-M[arg]` flag block.
6696    while idx < args.len() {
6697        let p = &args[idx];
6698        let bytes = p.as_bytes();
6699        if bytes.len() < 2 || bytes[0] != b'-' {                             // c:1221
6700            break;
6701        }
6702        let cluster = &bytes[1..];
6703        let mut ok = true;
6704        for (i, &c) in cluster.iter().enumerate() {
6705            match c {
6706                b's' => single = 1,                                          // c:1233
6707                b'S' => flags |= CDF_SEP,                                    // c:1235
6708                b'A' => {                                                    // c:1237
6709                    if i + 1 < cluster.len() {                               // c:1238
6710                        nonarg = Some(String::from_utf8_lossy(&cluster[i + 1..]).into_owned());
6711                    } else if idx + 1 < args.len() {                         // c:1241
6712                        nonarg = Some(args[idx + 1].clone());
6713                        idx += 1;
6714                    } else {
6715                        ok = false;
6716                    }
6717                    break;
6718                }
6719                b'M' => {                                                    // c:1245
6720                    if i + 1 < cluster.len() {                               // c:1246
6721                        match_spec = String::from_utf8_lossy(&cluster[i + 1..]).into_owned();
6722                    } else if idx + 1 < args.len() {                         // c:1249
6723                        match_spec = args[idx + 1].clone();
6724                        idx += 1;
6725                    } else {
6726                        ok = false;
6727                    }
6728                    break;
6729                }
6730                _ => {
6731                    ok = false;
6732                    break;
6733                }
6734            }
6735        }
6736        if !ok {
6737            break;                                                           // c:1230
6738        }
6739        idx += 1;                                                            // c:1258
6740    }
6741
6742    if idx < args.len() && args[idx] == ":" {                                // c:1260
6743        idx += 1;
6744    }
6745    if idx >= args.len() {                                                   // c:1262
6746        return None;
6747    }
6748
6749    // c:1266 — `tokenize(nonarg = dupstring(nonarg))`. The Rust matcher
6750    // path lazily tokenizes on use; the stored bytes are the spec text.
6751
6752    // c:1269 — `all = ret = alloc_cadef(orig_args, single, match, nonarg, flags)`.
6753    let first_def = alloc_cadef(
6754        Some(orig_args),
6755        single,
6756        &match_spec,
6757        nonarg.as_deref(),
6758        flags,
6759    );
6760
6761    // ---- spec-list loop state (c:1271-1273) ----
6762    // `sets` accumulates each Cadef in `snext` order; per-set opts/args/rest
6763    // are collected in parallel Vecs and linked into the cadef at the end.
6764    let mut sets: Vec<Box<cadef>> = vec![first_def];
6765    let mut opts_per_set: Vec<Vec<Box<caopt>>> = vec![Vec::new()];
6766    let mut args_per_set: Vec<Vec<Box<caarg>>> = vec![Vec::new()];
6767    let mut rest_per_set: Vec<Option<Box<caarg>>> = vec![None];
6768
6769    let sargs = idx;                                                         // c:1271 saved set-start
6770    let mut anum: i32 = 1;                                                   // c:1203
6771    let mut doset: Option<String> = None;
6772    let mut axor: Option<String> = None;
6773    let mut curset: Option<usize> = None;                                    // c:1201
6774    let mut pendset: Option<usize> = None;
6775    let mut foreignset = false;
6776
6777    // c:1275 — `for (; *args || pendset; args++)`.
6778    'outer: loop {
6779        // c:1276 — `if (!*args)` start a fresh set (restart from sargs).
6780        if idx >= args.len() {
6781            if pendset.is_none() {
6782                break 'outer;
6783            }
6784            // c:1278-1286 — set_cadef_opts on current; alloc new cadef as snext.
6785            {
6786                let cur = sets.last_mut().unwrap();
6787                let cur_args = args_per_set.last_mut().unwrap();
6788                // Link the args list into cur so set_cadef_opts can walk it.
6789                let mut head: Option<Box<caarg>> = None;
6790                for arg_box in cur_args.drain(..).rev() {
6791                    let mut a = arg_box;
6792                    a.next = head;
6793                    head = Some(a);
6794                }
6795                cur.args = head;
6796                set_cadef_opts(cur);                                          // c:1280
6797                // Stash args back as a Vec for the rest of the loop. We need
6798                // both forms; the linked list will be rebuilt at the end.
6799                let mut walk = cur.args.take();
6800                while let Some(mut node) = walk {
6801                    walk = node.next.take();
6802                    cur_args.push(node);
6803                }
6804            }
6805            idx = sargs;                                                     // c:1278
6806            doset = None;                                                    // c:1279
6807            sets.push(alloc_cadef(None, single, &match_spec,                  // c:1281
6808                                  nonarg.as_deref(), flags));
6809            opts_per_set.push(Vec::new());
6810            args_per_set.push(Vec::new());
6811            rest_per_set.push(None);
6812            anum = 1;                                                        // c:1283
6813            foreignset = false;                                              // c:1284
6814            curset = pendset;                                                // c:1285
6815            pendset = None;                                                  // c:1286
6816        }
6817
6818        let arg = &args[idx];
6819        let arg_bytes = arg.as_bytes();
6820
6821        // c:1288 — `args[0][0] == '-' && !args[0][1] && args[1]` — set marker.
6822        if arg_bytes == b"-" && idx + 1 < args.len() {
6823            if curset.is_some() && curset != Some(idx) {                     // c:1289
6824                foreignset = true;
6825                if pendset.is_none() && Some(idx) > curset {                 // c:1290
6826                    pendset = Some(idx);
6827                }
6828                idx += 1;                                                    // c:1292 ++args
6829            } else {                                                         // c:1293
6830                foreignset = false;
6831                idx += 1;
6832                let p_str = &args[idx];                                      // c:1295 char *p = *++args
6833                let pb = p_str.as_bytes();
6834                let l = pb.len().saturating_sub(1);
6835                // c:1298 — `if (*p == '(' && p[l] == ')')` strip parens for axor.
6836                let (set_name, ax) = if !pb.is_empty()
6837                    && pb[0] == b'(' && pb[l] == b')'
6838                {
6839                    let inner = String::from_utf8_lossy(&pb[1..l]).into_owned();
6840                    (inner.clone(), Some(inner))
6841                } else {
6842                    (p_str.clone(), None)
6843                };
6844                axor = ax;
6845                if set_name.is_empty() {                                     // c:1302
6846                    zwarnnam(nam, "empty set name");
6847                    return None;
6848                }
6849                let new_set = crate::ported::string::tricat(&set_name, "-", "");// c:1307
6850                doset = Some(new_set.clone());
6851                {
6852                    let cur = sets.last_mut().unwrap();
6853                    cur.set = Some(new_set);
6854                }
6855                curset = Some(idx);                                          // c:1308
6856            }
6857            idx += 1;
6858            continue;                                                        // c:1310
6859        }
6860
6861        // c:1311 — `args[0][0] == '+' && !args[0][1] && args[1]` — group marker.
6862        if arg_bytes == b"+" && idx + 1 < args.len() {
6863            foreignset = false;                                              // c:1315
6864            idx += 1;
6865            let p_str = &args[idx];                                          // c:1316
6866            let pb = p_str.as_bytes();
6867            let l = pb.len().saturating_sub(1);
6868            let (group_name, ax) = if !pb.is_empty()
6869                && pb[0] == b'(' && pb[l] == b')'
6870            {
6871                let inner = String::from_utf8_lossy(&pb[1..l]).into_owned();
6872                (inner.clone(), Some(inner))
6873            } else {
6874                (p_str.clone(), None)
6875            };
6876            axor = ax;
6877            if group_name.is_empty() {                                       // c:1322
6878                zwarnnam(nam, "empty group name");
6879                return None;
6880            }
6881            doset = Some(crate::ported::string::tricat(&group_name, "-", ""));// c:1327
6882            idx += 1;
6883            continue;                                                        // c:1328
6884        }
6885
6886        // c:1329 — `if (foreignset) continue` — skip specs for other sets.
6887        if foreignset {
6888            idx += 1;
6889            continue;
6890        }
6891
6892        // c:1331 — parse one spec entry.
6893        let bytes = arg_bytes;
6894        let mut p = 0usize;
6895        let mut xnum: i32 = 0;                                               // c:1332
6896        let mut not_flag = false;
6897        if p < bytes.len() && bytes[p] == b'!' {                             // c:1333
6898            not_flag = true;
6899            p += 1;
6900        }
6901
6902        let mut xor: Option<Vec<String>> = None;
6903        if p < bytes.len() && bytes[p] == b'(' {                             // c:1335 xor list
6904            let mut list: Vec<String> = Vec::new();
6905            // c:1342-1354 — collect words inside parens.
6906            let mut bad = false;
6907            'paren: loop {
6908                if p >= bytes.len() || bytes[p] == b')' { break; }
6909                p += 1;                                                       // c:1343 p++
6910                while p < bytes.len() && inblank(bytes[p]) { p += 1; }        // c:1343 inblank skip
6911                if p >= bytes.len() { bad = true; break 'paren; }
6912                if bytes[p] == b')' { break 'paren; }
6913                let q = p;
6914                p += 1;
6915                while p < bytes.len() && bytes[p] != b')' && !inblank(bytes[p]) {
6916                    p += 1;
6917                }
6918                if p >= bytes.len() { bad = true; break 'paren; }            // c:1349
6919                let word = String::from_utf8_lossy(&bytes[q..p]).into_owned();
6920                list.push(word);
6921                xnum += 1;                                                    // c:1353
6922            }
6923            if bad || p >= bytes.len() || bytes[p] != b')' {                  // c:1356
6924                zwarnnam(nam, &format!("invalid argument: {}", arg));
6925                return None;
6926            }
6927            if doset.is_some() && axor.is_some() {                            // c:1361
6928                xnum += 1;
6929                list.push(axor.clone().unwrap());                             // c:1366-1367
6930            }
6931            xor = Some(list);
6932            p += 1;                                                           // c:1370
6933        } else if doset.is_some() && axor.is_some() {                        // c:1371
6934            xnum = 1;
6935            xor = Some(vec![axor.clone().unwrap()]);
6936        }
6937
6938        // c:1379 — option spec OR rest-arg OR normal-arg.
6939        let is_opt = p < bytes.len() && (
6940            bytes[p] == b'-' || bytes[p] == b'+'
6941            || (bytes[p] == b'*' && p + 1 < bytes.len()
6942                && (bytes[p + 1] == b'-' || bytes[p + 1] == b'+'))
6943        );
6944
6945        if is_opt {
6946            // ---- c:1381-1580 option spec branch ----
6947            // The `rec:` goto loop handles `-+`/`+-` duplication by
6948            // parsing the same spec twice with name[0] flipped between
6949            // `-` and `+`.
6950            let mut again_iter = 0i32;                                       // c:1384
6951            let mut againp_start: Option<usize> = None;
6952            let mut p_state = p;
6953            let mut xor_state = xor;
6954            let mut xnum_state = xnum;
6955
6956            'rec: loop {
6957                let mut multi = false;                                       // c:1390
6958                if p_state < bytes.len() && bytes[p_state] == b'*' {
6959                    multi = true;
6960                    p_state += 1;
6961                }
6962
6963                let mut name_start: usize;
6964                let mut name_buf: Vec<u8>;
6965                let need_flip = p_state + 2 < bytes.len()
6966                    && ((bytes[p_state] == b'-' && bytes[p_state + 1] == b'+')
6967                        || (bytes[p_state] == b'+' && bytes[p_state + 1] == b'-'))
6968                    && bytes[p_state + 2] != b':'
6969                    && bytes[p_state + 2] != b'['
6970                    && bytes[p_state + 2] != b'='
6971                    && bytes[p_state + 2] != b'-'
6972                    && bytes[p_state + 2] != b'+';
6973
6974                if need_flip {                                               // c:1393
6975                    if again_iter == 0 {
6976                        againp_start = Some(p_state);
6977                    }
6978                    name_start = p_state + 1;
6979                    name_buf = bytes[name_start..].to_vec();
6980                    if !name_buf.is_empty() {
6981                        name_buf[0] = if again_iter != 0 { b'-' } else { b'+' };
6982                    }
6983                    again_iter += 1;
6984                    p_state = name_start;
6985                } else {                                                     // c:1404
6986                    name_start = p_state;
6987                    name_buf = bytes[name_start..].to_vec();
6988                    if p_state + 1 < bytes.len()
6989                        && bytes[p_state] == b'-' && bytes[p_state + 1] == b'-'
6990                    {
6991                        p_state += 1;                                        // c:1407 skip 2nd '-'
6992                    }
6993                }
6994
6995                if p_state + 1 >= bytes.len() {                              // c:1409
6996                    zwarnnam(nam, &format!("invalid argument: {}", arg));
6997                    return None;
6998                }
6999
7000                // c:1416-1422 — skip option name body up to type byte.
7001                let mut np = p_state - name_start + 1;
7002                let nlen = name_buf.len();
7003                while np < nlen
7004                    && name_buf[np] != b':'
7005                    && name_buf[np] != b'['
7006                    && !((name_buf[np] == b'-' || name_buf[np] == b'+')
7007                         && np + 1 < nlen
7008                         && (name_buf[np + 1] == b':' || name_buf[np + 1] == b'['))
7009                    && !(name_buf[np] == b'='
7010                         && np + 1 < nlen
7011                         && (name_buf[np + 1] == b':'
7012                             || name_buf[np + 1] == b'['
7013                             || name_buf[np + 1] == b'-'))
7014                {
7015                    if name_buf[np] == b'\\' && np + 1 < nlen {
7016                        np += 1;
7017                    }
7018                    np += 1;
7019                }
7020
7021                let mut c_byte = if np < nlen { name_buf[np] } else { 0 };
7022                let opt_name_slice = &name_buf[..np];
7023                let opt_name = String::from_utf8_lossy(opt_name_slice).into_owned();
7024
7025                let mut otype = CAO_NEXT;                                    // c:1384
7026                if c_byte == b'-' {                                          // c:1427
7027                    otype = CAO_DIRECT;
7028                    np += 1;
7029                    c_byte = if np < nlen { name_buf[np] } else { 0 };
7030                } else if c_byte == b'+' {                                   // c:1430
7031                    otype = CAO_ODIRECT;
7032                    np += 1;
7033                    c_byte = if np < nlen { name_buf[np] } else { 0 };
7034                } else if c_byte == b'=' {                                   // c:1433
7035                    otype = CAO_OEQUAL;
7036                    np += 1;
7037                    c_byte = if np < nlen { name_buf[np] } else { 0 };
7038                    if c_byte == b'-' {
7039                        otype = CAO_EQUAL;                                   // c:1436
7040                        np += 1;
7041                        c_byte = if np < nlen { name_buf[np] } else { 0 };
7042                    }
7043                }
7044
7045                // c:1441 — optional `[descr]`.
7046                let mut descr_str: Option<String> = None;
7047                if c_byte == b'[' {                                          // c:1441
7048                    np += 1;
7049                    let d_start = np;
7050                    while np < nlen && name_buf[np] != b']' {
7051                        if name_buf[np] == b'\\' && np + 1 < nlen { np += 1; }
7052                        np += 1;
7053                    }
7054                    if np >= nlen {                                          // c:1446
7055                        zwarnnam(nam, &format!("invalid option definition: {}", arg));
7056                        return None;
7057                    }
7058                    let d_slice = &name_buf[d_start..np];
7059                    descr_str = Some(String::from_utf8_lossy(d_slice).into_owned());
7060                    np += 1;
7061                    c_byte = if np < nlen { name_buf[np] } else { 0 };
7062                }
7063
7064                if c_byte != 0 && c_byte != b':' {                           // c:1456
7065                    zwarnnam(nam, &format!("invalid option definition: {}", arg));
7066                    return None;
7067                }
7068
7069                // c:1461 — add option name to xor list if not `*-...`.
7070                let clean_name = rembslashcolon(&opt_name);
7071                if !multi {
7072                    let xv = xor_state.get_or_insert_with(Vec::new);
7073                    if xv.len() <= xnum_state as usize {
7074                        xv.resize(xnum_state as usize + 1, String::new());
7075                    }
7076                    xv[xnum_state as usize] = clean_name.clone();
7077                }
7078
7079                // c:1470-1531 — argument loop for `:descr:action[:...]`.
7080                let mut oargs: Vec<Box<caarg>> = Vec::new();
7081                if c_byte == b':' {
7082                    let mut oanum: i32 = 1;                                   // c:1473
7083                    let mut onum: i32 = 0;
7084                    while c_byte == b':' {                                    // c:1479
7085                        let mut rest = 0;
7086                        let mut end_str: Option<String> = None;
7087                        np += 1;                                              // c:1484 *++p
7088                        let atype: i32;
7089                        c_byte = if np < nlen { name_buf[np] } else { 0 };
7090                        if c_byte == b':' {                                   // c:1485
7091                            atype = CAA_OPT;
7092                            np += 1;
7093                        } else if c_byte == b'*' {                            // c:1487
7094                            np += 1;
7095                            if np < nlen && name_buf[np] != b':' {            // c:1488
7096                                let end_start = np;
7097                                while np < nlen && name_buf[np] != b':' {
7098                                    if name_buf[np] == b'\\' && np + 1 < nlen {
7099                                        np += 1;
7100                                    }
7101                                    np += 1;
7102                                }
7103                                let e_slice = &name_buf[end_start..np];
7104                                end_str = Some(String::from_utf8_lossy(e_slice).into_owned());
7105                            }
7106                            if np >= nlen || name_buf[np] != b':' {           // c:1500
7107                                zwarnnam(nam, &format!("invalid option definition: {}", arg));
7108                                return None;
7109                            }
7110                            np += 1;                                          // c:1507 *++p
7111                            if np < nlen && name_buf[np] == b':' {            // c:1508
7112                                np += 1;
7113                                if np < nlen && name_buf[np] == b':' {        // c:1509
7114                                    atype = CAA_RREST;
7115                                    np += 1;
7116                                } else {
7117                                    atype = CAA_RARGS;
7118                                }
7119                            } else {
7120                                atype = CAA_REST;
7121                            }
7122                            rest = 1;
7123                        } else {
7124                            atype = CAA_NORMAL;
7125                        }
7126
7127                        // c:1521 — parse_caarg.
7128                        let mut oarg = parse_caarg(
7129                            if rest != 0 { 0 } else { 1 },
7130                            atype, oanum, onum,
7131                            Some(&clean_name),
7132                            &name_buf, &mut np,
7133                            doset.as_deref(),
7134                        );
7135                        oanum += 1;
7136                        if atype == CAA_OPT { onum += 1; }                    // c:1524
7137                        if let Some(end) = end_str {
7138                            oarg.end = Some(end);                             // c:1526
7139                        }
7140                        oargs.push(oarg);
7141
7142                        if rest != 0 { break; }                               // c:1528
7143                        c_byte = if np < nlen { name_buf[np] } else { 0 };    // c:1530
7144                    }
7145                }
7146
7147                // c:1534 — build the caopt.
7148                let mut opt_box = Box::new(caopt::default());
7149                opt_box.gsname = doset.clone();                               // c:1539
7150                opt_box.name = Some(clean_name.clone());                      // c:1540
7151                opt_box.descr = if let Some(d) = descr_str.clone() {          // c:1542
7152                    Some(d)
7153                } else if adpre.is_some() && oargs.len() == 1 {               // c:1543
7154                    let first_arg = &oargs[0];
7155                    let d_field = first_arg.descr.as_deref().unwrap_or("");
7156                    let has_visible = d_field.bytes().any(|b| !iblank(b));
7157                    if has_visible {                                          // c:1550
7158                        Some(crate::ported::string::tricat(
7159                            adpre.as_deref().unwrap_or(""),
7160                            d_field,
7161                            adsuf.as_deref().unwrap_or(""),
7162                        ))
7163                    } else {
7164                        None                                                  // c:1553
7165                    }
7166                } else {
7167                    None
7168                };
7169                let xor_clone = if again_iter == 1 {                          // c:1556
7170                    xor_state.clone()
7171                } else {
7172                    xor_state.take()
7173                };
7174                opt_box.xor = xor_clone;
7175                opt_box.r#type = otype;                                       // c:1557
7176                opt_box.not = if not_flag { 1 } else { 0 };                   // c:1560
7177
7178                // Link in the arg list.
7179                let mut head: Option<Box<caarg>> = None;
7180                for a in oargs.into_iter().rev() {
7181                    let mut a = a;
7182                    a.next = head;
7183                    head = Some(a);
7184                }
7185                opt_box.args = head;
7186
7187                {
7188                    let cur = sets.last_mut().unwrap();
7189                    opt_box.num = cur.nopts;
7190                    cur.nopts += 1;                                           // c:1559
7191                    if otype == CAO_DIRECT || otype == CAO_EQUAL {            // c:1562
7192                        cur.ndopts += 1;
7193                    } else if otype == CAO_ODIRECT || otype == CAO_OEQUAL {   // c:1564
7194                        cur.nodopts += 1;
7195                    }
7196                    // c:1571 — single-letter lookup table.
7197                    if single != 0 {
7198                        let nb = clean_name.as_bytes();
7199                        if nb.len() == 2 && nb[1] != b'-' {
7200                            let sidx = single_index(nb[0], nb[1]);
7201                            if sidx >= 0 {
7202                                if let Some(ref mut s) = cur.single {
7203                                    if (sidx as usize) < s.len() {
7204                                        s[sidx as usize] = Some(Box::new(
7205                                            caopt {
7206                                                next: None,
7207                                                name: opt_box.name.clone(),
7208                                                descr: opt_box.descr.clone(),
7209                                                xor: opt_box.xor.clone(),
7210                                                r#type: opt_box.r#type,
7211                                                args: None,
7212                                                active: 0,
7213                                                num: opt_box.num,
7214                                                gsname: opt_box.gsname.clone(),
7215                                                not: opt_box.not,
7216                                            }
7217                                        ));
7218                                    }
7219                                }
7220                            }
7221                        }
7222                    }
7223                }
7224
7225                opts_per_set.last_mut().unwrap().push(opt_box);
7226
7227                if again_iter == 1 {                                          // c:1576
7228                    if let Some(start) = againp_start {
7229                        p_state = start;
7230                        xnum_state = xnum;                                    // restore
7231                        xor_state = xor_state.clone();
7232                        continue 'rec;
7233                    }
7234                }
7235                break 'rec;
7236            }
7237        } else if p < bytes.len() && bytes[p] == b'*' {
7238            // ---- c:1581-1607 rest-arg branch ----
7239            if not_flag {                                                    // c:1586
7240                idx += 1;
7241                continue;
7242            }
7243            p += 1;                                                          // c:1589 *++p
7244            if p >= bytes.len() || bytes[p] != b':' {
7245                zwarnnam(nam, &format!("invalid rest argument definition: {}", arg));
7246                return None;
7247            }
7248            if rest_per_set.last().unwrap().is_some() {                       // c:1594
7249                zwarnnam(nam, &format!("doubled rest argument definition: {}", arg));
7250                return None;
7251            }
7252            let mut atype = CAA_REST;                                        // c:1584
7253            p += 1;                                                          // c:1599 *++p
7254            if p < bytes.len() && bytes[p] == b':' {                         // c:1599
7255                p += 1;
7256                if p < bytes.len() && bytes[p] == b':' {                     // c:1600
7257                    atype = CAA_RREST;
7258                    p += 1;
7259                } else {
7260                    atype = CAA_RARGS;
7261                }
7262            }
7263            let mut rarg = parse_caarg(0, atype, -1, 0, None, bytes, &mut p,
7264                                        doset.as_deref());                    // c:1606
7265            rarg.xor = xor;                                                  // c:1607
7266            *rest_per_set.last_mut().unwrap() = Some(rarg);
7267        } else {
7268            // ---- c:1608-1661 normal-arg branch ----
7269            if not_flag {                                                    // c:1614
7270                idx += 1;
7271                continue;
7272            }
7273            let mut direct = 0;                                              // c:1611
7274            if p < bytes.len() && idigit(bytes[p]) {                         // c:1617
7275                direct = 1;
7276                let mut num: i32 = 0;
7277                while p < bytes.len() && idigit(bytes[p]) {
7278                    num = num * 10 + (bytes[p] - b'0') as i32;
7279                    p += 1;
7280                }
7281                anum = num + 1;                                              // c:1624
7282            } else {
7283                anum += 1;                                                   // c:1627
7284            }
7285            if p >= bytes.len() || bytes[p] != b':' {                        // c:1629
7286                zwarnnam(nam, &format!("invalid argument: {}", arg));
7287                return None;
7288            }
7289            let mut atype = CAA_NORMAL;
7290            p += 1;                                                          // c:1636 *++p
7291            if p < bytes.len() && bytes[p] == b':' {                         // c:1636
7292                atype = CAA_OPT;
7293                p += 1;
7294            }
7295            let mut narg = parse_caarg(0, atype, anum - 1, 0, None,
7296                                        bytes, &mut p, doset.as_deref());     // c:1641
7297            narg.xor = xor;                                                  // c:1642
7298            narg.direct = direct;                                            // c:1643
7299
7300            // c:1647-1661 — sorted insert by num.
7301            let target = anum - 1;
7302            let cur_args = args_per_set.last_mut().unwrap();
7303            let mut insert_at = cur_args.len();
7304            for (i, existing) in cur_args.iter().enumerate() {
7305                if existing.num >= target {
7306                    insert_at = i;
7307                    break;
7308                }
7309            }
7310            if insert_at < cur_args.len() && cur_args[insert_at].num == target {
7311                zwarnnam(nam, &format!("doubled argument definition: {}", arg));
7312                return None;
7313            }
7314            cur_args.insert(insert_at, narg);
7315        }
7316
7317        idx += 1;
7318    }
7319
7320    // c:1664 — final set_cadef_opts on the last set.
7321    {
7322        let last_idx = sets.len() - 1;
7323        let cur = &mut sets[last_idx];
7324        let cur_args = &mut args_per_set[last_idx];
7325        let mut head: Option<Box<caarg>> = None;
7326        for a in cur_args.drain(..).rev() {
7327            let mut a = a;
7328            a.next = head;
7329            head = Some(a);
7330        }
7331        cur.args = head;
7332        set_cadef_opts(cur);
7333    }
7334
7335    // ---- finalize: link opts/args/rest per set, then snext-chain ----
7336    let n_sets = sets.len();
7337    for i in 0..n_sets {
7338        // opts — append order.
7339        let mut head: Option<Box<caopt>> = None;
7340        for o in opts_per_set[i].drain(..).rev() {
7341            let mut o = o;
7342            o.next = head;
7343            head = Some(o);
7344        }
7345        sets[i].opts = head;
7346        // args was already linked in the per-set finalize step above for
7347        // every set except possibly the last (which is now done). Walk
7348        // any still-present Vec entries into the linked list for safety.
7349        if !args_per_set[i].is_empty() {
7350            let mut head: Option<Box<caarg>> = None;
7351            for a in args_per_set[i].drain(..).rev() {
7352                let mut a = a;
7353                a.next = head;
7354                head = Some(a);
7355            }
7356            sets[i].args = head;
7357        }
7358        sets[i].rest = rest_per_set[i].take();
7359    }
7360
7361    // c:1281 — snext chain links each subsequent set off the head.
7362    while sets.len() > 1 {
7363        let tail = sets.pop().unwrap();
7364        let prev = sets.last_mut().unwrap();
7365        // Walk to the end of the snext chain on prev and attach tail.
7366        let mut cursor: &mut Option<Box<cadef>> = &mut prev.snext;
7367        while cursor.is_some() {
7368            cursor = &mut cursor.as_mut().unwrap().snext;
7369        }
7370        *cursor = Some(tail);
7371    }
7372
7373    Some(sets.pop().unwrap())
7374}
7375
7376/// Direct port of `static Cvdef get_cvdef(char *nam, char **args)` from
7377/// `Src/Zle/computil.c:3154-3173`. LRU lookup over `cvdef_cache`
7378/// keyed by the raw argv. On hit bumps `lastt` and returns 1. On
7379/// miss parses via `parse_cvdef` and evicts the entry with the
7380/// oldest `lastt` (or the first empty slot) for insertion.
7381pub fn get_cvdef(nam: &str, args: &[String]) -> i32 {                       // c:3154
7382    let na = args.len() as i32;
7383    let now = {                                                              // c:3161 time(0)
7384        use std::time::{SystemTime, UNIX_EPOCH};
7385        SystemTime::now().duration_since(UNIX_EPOCH)
7386            .map(|d| d.as_secs() as i64).unwrap_or(0)
7387    };
7388
7389    if let Ok(mut cache) = cvdef_cache.lock() {
7390        let mut min_idx: Option<usize> = None;
7391        let mut min_lastt: i64 = i64::MAX;
7392        let mut hit_idx: Option<usize> = None;
7393        for (i, slot) in cache.iter().enumerate() {                          // c:3159
7394            match slot {
7395                Some(entry) => {
7396                    if entry.ndefs == na                                     // c:3160
7397                        && entry.defs.as_deref()
7398                            .map_or(false, |d| d.len() == args.len()
7399                                && d.iter().zip(args.iter()).all(|(a, b)| a == b))
7400                    {
7401                        hit_idx = Some(i);
7402                        break;
7403                    }
7404                    if entry.lastt < min_lastt {                             // c:3164
7405                        min_lastt = entry.lastt;
7406                        min_idx = Some(i);
7407                    }
7408                }
7409                None => {                                                    // c:3164 empty slot
7410                    min_idx = Some(i);
7411                    break;
7412                }
7413            }
7414        }
7415        if let Some(i) = hit_idx {                                           // c:3160
7416            if let Some(entry) = cache[i].as_mut() {
7417                entry.lastt = now;                                           // c:3161
7418            }
7419            return 1;                                                        // c:3163 hit
7420        }
7421        // c:3168 — parse_cvdef; on success replace the chosen slot.
7422        if let Some(new) = parse_cvdef(nam, args) {
7423            let idx = min_idx.unwrap_or(0);
7424            cache[idx] = Some(new);                                          // c:3170
7425        }
7426    }
7427    0                                                                        // c:3172 miss
7428}
7429
7430/// Direct port of `static Cvdef parse_cvdef(char *nam, char **args)`
7431/// from `Src/Zle/computil.c:2986-3148`. Parses the leading
7432/// `-s SEP / -S SEP / -w` flag block, then the description, then
7433/// each value spec into a cvval chain.
7434pub fn parse_cvdef(nam: &str, args: &[String]) -> Option<Box<cvdef>> {       // c:2986
7435    use crate::ported::ztype_h::inblank;
7436
7437    let orig_args = args;
7438    let mut idx = 0usize;
7439
7440    let mut sep: i32 = 0;                                                    // c:2991 char sep = '\0'
7441    let mut asep: i32 = b'=' as i32;                                         // c:2991 char asep = '='
7442    let mut hassep: i32 = 0;                                                 // c:2992
7443    let mut words: i32 = 0;                                                  // c:2992
7444
7445    // c:2994-3010 — leading flag block (-s SEP, -S SEP, -w).
7446    while idx + 1 < args.len()
7447        && args[idx].len() == 2
7448        && args[idx].starts_with('-')
7449        && (args[idx].as_bytes()[1] == b's'
7450            || args[idx].as_bytes()[1] == b'S'
7451            || args[idx].as_bytes()[1] == b'w')
7452    {
7453        let flag = args[idx].as_bytes()[1];
7454        if flag == b's' {                                                    // c:2999
7455            hassep = 1;
7456            sep = args[idx + 1].as_bytes().first().copied().unwrap_or(0) as i32;
7457            idx += 2;
7458        } else if flag == b'S' {                                             // c:3003
7459            asep = args[idx + 1].as_bytes().first().copied().unwrap_or(0) as i32;
7460            idx += 2;
7461        } else {                                                             // c:3006 -w
7462            words = 1;
7463            idx += 1;
7464        }
7465    }
7466
7467    if idx + 1 >= args.len() {                                               // c:3011
7468        zwarnnam(nam, "not enough arguments");
7469        return None;
7470    }
7471    let descr = args[idx].clone();                                           // c:3015 descr = *args++
7472    idx += 1;
7473
7474    let mut ret = Box::new(cvdef {
7475        descr:  Some(descr),                                                 // c:3018
7476        hassep,                                                              // c:3019
7477        sep,                                                                 // c:3020
7478        argsep: asep,                                                        // c:3021
7479        next:   None,                                                        // c:3022
7480        vals:   None,                                                        // c:3023
7481        defs:   Some(orig_args.to_vec()),                                    // c:3024
7482        ndefs:  orig_args.len() as i32,                                      // c:3025
7483        lastt:  {                                                            // c:3026
7484            use std::time::{SystemTime, UNIX_EPOCH};
7485            SystemTime::now().duration_since(UNIX_EPOCH)
7486                .map(|d| d.as_secs() as i64).unwrap_or(0)
7487        },
7488        words,                                                               // c:3027
7489    });
7490
7491    // c:3029-3147 — for each remaining arg, parse one value spec.
7492    let mut vals_collected: Vec<Box<cvval>> = Vec::new();
7493
7494    while idx < args.len() {
7495        let spec = &args[idx];
7496        let bytes = spec.as_bytes();
7497        let mut p: usize = 0;
7498        let mut xnum: i32 = 0;                                               // c:3032
7499        let mut bs = 0;                                                      // c:3030
7500        let mut xor: Option<Vec<String>> = None;
7501
7502        // c:3035-3068 — `(opt1 opt2)` xor list.
7503        if p < bytes.len() && bytes[p] == b'(' {                             // c:3035
7504            let mut list: Vec<String> = Vec::new();
7505            let mut bad = false;
7506            'paren: loop {
7507                if p >= bytes.len() || bytes[p] == b')' { break; }
7508                p += 1;                                                       // c:3041 p++
7509                while p < bytes.len() && inblank(bytes[p]) { p += 1; }
7510                if p >= bytes.len() { bad = true; break 'paren; }
7511                if bytes[p] == b')' { break 'paren; }
7512                let q = p;
7513                p += 1;
7514                while p < bytes.len() && bytes[p] != b')' && !inblank(bytes[p]) {
7515                    p += 1;
7516                }
7517                if p >= bytes.len() { bad = true; break 'paren; }
7518                let word = String::from_utf8_lossy(&bytes[q..p]).into_owned();
7519                list.push(word);
7520                xnum += 1;
7521            }
7522            if bad || p >= bytes.len() || bytes[p] != b')' {                  // c:3056
7523                zwarnnam(nam, &format!("invalid argument: {}", spec));
7524                return None;
7525            }
7526            xor = Some(list);
7527            p += 1;                                                           // c:3066
7528        }
7529
7530        // c:3071 — `*` (multi).
7531        let multi = p < bytes.len() && bytes[p] == b'*';
7532        if multi { p += 1; }
7533
7534        // c:3076 — scan option name up to `:` or `[`.
7535        let name_start = p;
7536        while p < bytes.len() && bytes[p] != b':' && bytes[p] != b'[' {      // c:3076
7537            if bytes[p] == b'\\' && p + 1 < bytes.len() {
7538                p += 1;
7539                bs = 1;                                                       // c:3078
7540            }
7541            p += 1;
7542        }
7543
7544        // c:3080-3085 — multi-letter check against empty separator.
7545        if hassep != 0 && sep == 0 && name_start + (bs as usize) + 1 < p {   // c:3080
7546            zwarnnam(nam,
7547                "no multi-letter values with empty separator allowed");
7548            return None;
7549        }
7550
7551        let name_bytes = &bytes[name_start..p];
7552        let name = String::from_utf8_lossy(name_bytes).into_owned();
7553
7554        // c:3087 — optional [descr].
7555        let mut value_descr: Option<String> = None;
7556        let mut c_byte = if p < bytes.len() { bytes[p] } else { 0 };
7557        if c_byte == b'[' {                                                  // c:3088
7558            p += 1;
7559            let d_start = p;
7560            while p < bytes.len() && bytes[p] != b']' {                      // c:3090
7561                if bytes[p] == b'\\' && p + 1 < bytes.len() { p += 1; }
7562                p += 1;
7563            }
7564            if p >= bytes.len() {                                            // c:3094
7565                zwarnnam(nam, &format!("invalid value definition: {}", spec));
7566                return None;
7567            }
7568            value_descr = Some(String::from_utf8_lossy(&bytes[d_start..p]).into_owned());
7569            p += 1;                                                           // c:3100
7570            c_byte = if p < bytes.len() { bytes[p] } else { 0 };
7571        }
7572
7573        if c_byte != 0 && c_byte != b':' {                                    // c:3106
7574            zwarnnam(nam, &format!("invalid value definition: {}", spec));
7575            return None;
7576        }
7577
7578        // c:3114 — :arg or ::optarg.
7579        let mut vtype = CVV_NOARG;
7580        let mut arg: Option<Box<caarg>> = None;
7581        if c_byte == b':' {                                                   // c:3114
7582            if hassep != 0 && sep == 0 {                                      // c:3115
7583                zwarnnam(nam,
7584                    "no value with argument with empty separator allowed");
7585                return None;
7586            }
7587            p += 1;                                                            // c:3121 *++p
7588            if p < bytes.len() && bytes[p] == b':' {                          // c:3121
7589                p += 1;
7590                vtype = CVV_OPT;                                              // c:3123
7591            } else {
7592                vtype = CVV_ARG;                                              // c:3125
7593            }
7594            arg = Some(parse_caarg(0, 0, 0, 0, Some(&name), bytes, &mut p, None));// c:3126
7595        }
7596
7597        // c:3131-3137 — add own name to xor list when not multi.
7598        if !multi {                                                           // c:3131
7599            let xv = xor.get_or_insert_with(Vec::new);
7600            if xv.len() <= xnum as usize {
7601                xv.resize(xnum as usize + 1, String::new());
7602            }
7603            xv[xnum as usize] = name.clone();                                 // c:3136
7604        }
7605
7606        let v = Box::new(cvval {                                              // c:3138
7607            next:   None,
7608            name:   Some(name),                                                // c:3142
7609            descr:  value_descr,                                               // c:3143
7610            xor,                                                               // c:3144
7611            r#type: vtype,                                                     // c:3145
7612            arg,                                                               // c:3146
7613            active: 0,
7614        });
7615        vals_collected.push(v);
7616
7617        idx += 1;
7618    }
7619
7620    // Link vals_collected as a chain.
7621    let mut head: Option<Box<cvval>> = None;
7622    for v in vals_collected.into_iter().rev() {
7623        let mut v = v;
7624        v.next = head;
7625        head = Some(v);
7626    }
7627    ret.vals = head;
7628
7629    Some(ret)
7630}
7631
7632/// Direct port of `static void set_cadef_opts(Cadef def)` from
7633/// `Src/Zle/computil.c:1180-1191`. After a set-of-arg-definitions has
7634/// been parsed into the cadef, walk the args linked list and update
7635/// each non-direct argp's `min` field to the cumulative number of
7636/// CAA_OPT entries that precede it. The optionality count compounds
7637/// down the chain, which determines minimum-argument-count semantics
7638/// during completion.
7639pub fn set_cadef_opts(def: &mut cadef) {                                    // c:1180
7640    let mut xnum: i32 = 0;
7641    let mut argp = def.args.as_deref_mut();                                  // c:1185 argp = def->args
7642    while let Some(node) = argp {                                            // c:1185
7643        if node.direct == 0 {                                                // c:1186 !argp->direct
7644            node.min = node.num - xnum;                                      // c:1187
7645        }
7646        if node.r#type == CAA_OPT {                                          // c:1188
7647            xnum += 1;                                                       // c:1189
7648        }
7649        argp = node.next.as_deref_mut();                                     // c:1185 argp = argp->next
7650    }
7651}
7652
7653/// Direct port of `static void settags(int level, char **tags)` from
7654/// `Src/Zle/computil.c:3794`. Replaces `comptags[level]` with a fresh
7655/// ctags carrying `tags[0]` as context and `tags[1..]` as the full
7656/// tag-list. Used at the start of every completion level transition
7657/// (`comptags -i`).
7658pub fn settags(level: i32, tags: &[String]) {                                // c:3794
7659    let idx = level as usize;
7660    if idx >= MAX_TAGS { return; }                                           // c:3756 bounds
7661
7662    if let Ok(mut tab) = comptags.lock() {
7663        if tab[idx].is_some() {                                              // c:3798
7664            freectags(tab[idx].take());                                      // c:3799
7665        }
7666        let context = tags.first().cloned();                                 // c:3804 *tags
7667        let all: Vec<String> = tags.iter().skip(1).cloned().collect();       // c:3803 tags+1
7668        tab[idx] = Some(Box::new(ctags {                                     // c:3801 zalloc
7669            all: Some(all),                                                  // c:3803
7670            context,                                                         // c:3804
7671            init: 1,                                                         // c:3806
7672            sets: None,                                                      // c:3805
7673        }));
7674    }
7675}
7676
7677// `setup_` is ported above with the cadef_cache/cvdef_cache/comptags
7678// reset body cited at Src/Zle/computil.c:5124. This duplicate shim
7679// was retired when the real port landed.
7680
7681// =====================================================================
7682// bin_compquote / bin_comptags / bin_comptry / bin_compvalues —
7683// Src/Zle/computil.c. Each is a structural port matching the C
7684// signature exactly so the dispatch surface lands; the underlying
7685// state-mutation paths (compqstack rewrite, tags-stack walk,
7686// compvalues table) depend on infrastructure (getvalue / setstrvalue
7687// / compstate hash / cv_* helpers) that's open work.
7688// =====================================================================
7689
7690/// Direct port of `bin_compquote(char *nam, char **args, Options ops, UNUSED(int func))` from `Src/Zle/computil.c:3679`.
7691/// C body (c:3683-3725):
7692/// ```c
7693/// if (incompfunc != 1) { error; return 1; }
7694/// if (!compqstack || !*compqstack) return 0;
7695/// while ((name = *args++)) {
7696///     if ((v = getvalue(...))) {
7697///         switch (PM_TYPE(v->pm->node.flags)) {
7698///         case PM_SCALAR/NAMEREF:
7699///             setstrvalue(v, comp_quote(getstrvalue(v), -p));
7700///         case PM_ARRAY:
7701///             foreach val in array: comp_quote each
7702///         default: zwarnnam("invalid parameter type");
7703///         }
7704///     }
7705/// }
7706/// ```
7707/// Quoting routes through `comp_quote()` per param type (PM_SCALAR
7708/// / PM_ARRAY); the entry validates `incompfunc` + `compqstack`
7709/// guards before dispatch.
7710/// WARNING: param names don't match C — Rust=(nam, args, _func) vs C=(nam, args, ops, func)
7711pub fn bin_compquote(nam: &str, args: &[String],                             // c:3679
7712                     ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
7713    use crate::ported::params::{getvalue, getstrvalue, getvaluearr,
7714        setstrvalue, setarrvalue};
7715    use crate::ported::zsh_h::{value, PM_ARRAY, PM_TYPE};
7716    use std::sync::atomic::Ordering;
7717
7718    if INCOMPFUNC.load(Ordering::Relaxed) != 1 {                             // c:3685
7719        zwarnnam(nam, "can only be called from completion function");
7720        return 1;
7721    }
7722    // c:3691 — `if (!compqstack || !*compqstack) return 0;`.
7723    let qstack_empty = COMPQSTACK.get()
7724        .map(|m| m.lock().map(|s| s.is_empty()).unwrap_or(true))
7725        .unwrap_or(true);
7726    if qstack_empty { return 0; }
7727    let p_flag = OPT_ISSET(ops, b'p');                                       // c:3704
7728
7729    // c:3696 — `while ((name = *args++))`. Walk param names.
7730    for name in args {                                                       // c:3696
7731        let mut vbuf = value {
7732            pm: None, arr: Vec::new(), scanflags: 0,
7733            valflags: 0, start: 0, end: 0,
7734        };
7735        let mut nameref: &str = name.as_str();
7736        let v = getvalue(Some(&mut vbuf), &mut nameref, 0);                  // c:3699
7737        if v.is_none() {                                                     // c:3724
7738            zwarnnam(nam, &format!("unknown parameter: {}", name));
7739            continue;
7740        }
7741        let v = v.unwrap();
7742        let flags = v.pm.as_ref().map(|pm| pm.node.flags).unwrap_or(0);
7743        let pm_type = PM_TYPE(flags as u32);
7744        // c:3700-3705 — PM_SCALAR / PM_NAMEREF path.
7745        if pm_type == 0 || (flags as u32 & crate::ported::zsh_h::PM_NAMEREF) != 0 {
7746            let s = getstrvalue(Some(v));
7747            let q = comp_quote(&s, p_flag as i32);
7748            let mut nameref_re: &str = name.as_str();
7749            setstrvalue(getvalue(Some(&mut vbuf), &mut nameref_re, 0), &q);
7750        } else if pm_type == PM_ARRAY {                                      // c:3706
7751            let arr = getvaluearr(Some(v));
7752            let new_arr: Vec<String> = arr.into_iter()
7753                .map(|elem| comp_quote(&elem, p_flag as i32))
7754                .collect();
7755            // Re-fetch a fresh value for the setarrvalue call (getvalue
7756            // consumed the prior borrow).
7757            let mut vbuf2 = value {
7758                pm: None, arr: Vec::new(), scanflags: 0,
7759                valflags: 0, start: 0, end: 0,
7760            };
7761            let mut nameref2: &str = name.as_str();
7762            if let Some(v2) = getvalue(Some(&mut vbuf2), &mut nameref2, 0) {
7763                setarrvalue(v2, new_arr);
7764            }
7765        } else {                                                             // c:3720
7766            zwarnnam(nam, &format!("invalid parameter type: {}", name));
7767        }
7768    }
7769    0                                                                        // c:3725
7770}
7771
7772/// Direct port of `static int bin_comptags(char *nam, char **args,
7773///                                          UNUSED(Options ops),
7774///                                          UNUSED(int func))` from
7775/// `Src/Zle/computil.c:3831-3958`. Full subcommand dispatch for
7776/// `comptags -i/-C/-T/-N/-R/-S/-A`. Reads `LOCALLEVEL` to index
7777/// `comptags[]`; `--` suffix decrements the level by one.
7778pub fn bin_comptags(nam: &str, args: &[String],                              // c:3831
7779                    _ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
7780    use crate::ported::builtin::LOCALLEVEL;
7781    use crate::ported::params::{setsparam, setaparam};
7782    use crate::ported::utils::strpfx;
7783    use std::sync::atomic::Ordering;
7784
7785    if INCOMPFUNC.load(Ordering::Relaxed) != 1 {                             // c:3835
7786        zwarnnam(nam, "can only be called from completion function");
7787        return 1;
7788    }
7789    if args.is_empty() { return 1; }
7790    let a0 = args[0].as_bytes();
7791    // c:3839 — validate `-X` or `-X--` shape.
7792    if a0.len() < 2 || a0[0] != b'-'
7793        || (a0.len() > 2 && (a0[2] != b'-' || a0.len() > 3))
7794    {
7795        zwarnnam(nam, &format!("invalid argument: {}", args[0]));
7796        return 1;
7797    }
7798
7799    let level: i32 = LOCALLEVEL.load(Ordering::Relaxed)                      // c:3844
7800        - if a0.len() > 2 { 1 } else { 0 };
7801    if level < 0 || (level as usize) >= MAX_TAGS {                           // c:3845
7802        zwarnnam(nam, "nesting level too deep");
7803        return 1;
7804    }
7805    let lvl_idx = level as usize;
7806
7807    let sub = a0[1];
7808
7809    // c:3849 — non-init subcommands require a registered comptags[level].
7810    if sub != b'i' && sub != b'I' {
7811        let registered = {
7812            let tab = comptags.lock().unwrap();
7813            tab[lvl_idx].is_some()
7814        };
7815        if !registered {
7816            zwarnnam(nam, "no tags registered");
7817            return 1;
7818        }
7819    }
7820
7821    // c:3854-3864 — per-subcommand arg-count bounds.
7822    let (min, max): (i32, i32) = match sub {
7823        b'i' => (2, -1),
7824        b'C' => (1, 1),
7825        b'T' => (0, 0),
7826        b'N' => (0, 0),
7827        b'R' => (1, 1),
7828        b'S' => (1, 1),
7829        b'A' => (2, 3),
7830        _ => {
7831            zwarnnam(nam, &format!("invalid option: {}", args[0]));
7832            return 1;
7833        }
7834    };
7835    let n = (args.len() as i32) - 1;
7836    if n < min { zwarnnam(nam, "not enough arguments"); return 1; }
7837    if max >= 0 && n > max { zwarnnam(nam, "too many arguments"); return 1; }
7838
7839    match sub {
7840        b'i' => {                                                            // c:3874
7841            settags(level, &args[1..]);
7842            lasttaglevel.store(level, Ordering::Relaxed);                    // c:3876
7843            0
7844        }
7845        b'C' => {                                                            // c:3878
7846            let ctx = {
7847                let tab = comptags.lock().unwrap();
7848                tab[lvl_idx].as_ref()
7849                    .and_then(|t| t.context.clone())
7850                    .unwrap_or_default()
7851            };
7852            setsparam(&args[1], &ctx);                                        // c:3879
7853            0
7854        }
7855        b'T' => {                                                            // c:3881
7856            let empty = {
7857                let tab = comptags.lock().unwrap();
7858                tab[lvl_idx].as_ref().map_or(true, |t| t.sets.is_none())
7859            };
7860            if empty { 1 } else { 0 }                                         // c:3882
7861        }
7862        b'N' => {                                                            // c:3883
7863            let mut tab = comptags.lock().unwrap();
7864            if let Some(t) = tab[lvl_idx].as_mut() {
7865                if t.init != 0 {                                              // c:3887
7866                    t.init = 0;
7867                } else if let Some(mut s) = t.sets.take() {                   // c:3889
7868                    t.sets = s.next.take();                                   // c:3890
7869                    freectset(Some(s));                                       // c:3892
7870                }
7871                if t.sets.is_some() { 0 } else { 1 }                          // c:3894
7872            } else {
7873                1
7874            }
7875        }
7876        b'R' => {                                                            // c:3896
7877            let tab = comptags.lock().unwrap();
7878            let hit = tab[lvl_idx].as_ref()
7879                .and_then(|t| t.sets.as_ref())
7880                .map(|s| {
7881                    s.tags.as_deref()
7882                        .map_or(false, |tgs| arrcontains(tgs, &args[1], true) != 0)
7883                })
7884                .unwrap_or(false);
7885            if hit { 0 } else { 1 }                                           // c:3900
7886        }
7887        b'A' => {                                                            // c:3903
7888            let mut tab = comptags.lock().unwrap();
7889            let Some(t) = tab[lvl_idx].as_mut() else { return 1; };
7890            let Some(s) = t.sets.as_mut() else { return 1; };
7891            // c:3911 — refresh ptr if tag changed.
7892            if s.tag.as_deref() != Some(args[1].as_str()) {
7893                s.tag = Some(args[1].clone());                                // c:3913
7894                s.ptr = 0;                                                    // c:3914
7895            }
7896            let tags_vec = s.tags.clone().unwrap_or_default();
7897            // c:3916-3925 — walk tags from ptr looking for a name match.
7898            let mut found: Option<(usize, String, String)> = None;
7899            for (i, q) in tags_vec.iter().enumerate().skip(s.ptr as usize) {
7900                if strpfx(&args[1], q) {                                      // c:3917
7901                    let l = args[1].len();
7902                    let qb = q.as_bytes();
7903                    if qb.len() == l {                                        // c:3918
7904                        found = Some((i, q.clone(), q.clone()));
7905                        break;
7906                    } else if qb.len() > l && qb[l] == b':' {                 // c:3921
7907                        let v = String::from_utf8_lossy(&qb[l + 1..]).into_owned();
7908                        found = Some((i, q.clone(), v));
7909                        break;
7910                    }
7911                }
7912            }
7913            let (q_idx, q_full, v) = match found {
7914                None => {                                                     // c:3927
7915                    s.tag = None;
7916                    return 1;
7917                }
7918                Some(t) => t,
7919            };
7920            s.ptr = (q_idx + 1) as i32;                                       // c:3932
7921            // c:3933 — `setsparam(args[2], v == '-' ? dyncat(args[1], v) : v)`.
7922            let value = if v.starts_with('-') {
7923                crate::ported::string::dyncat(&args[1], &v)
7924            } else {
7925                v.clone()
7926            };
7927            setsparam(&args[2], &value);
7928            // c:3934 — optional 3rd arg gets the "name-up-to-`:`" of q.
7929            if args.len() > 3 {
7930                let pre_colon: String = q_full
7931                    .splitn(2, ':').next().unwrap_or("").to_string();
7932                setsparam(&args[3], &pre_colon);
7933            }
7934            0                                                                 // c:3942
7935        }
7936        b'S' => {                                                            // c:3946
7937            let tab = comptags.lock().unwrap();
7938            if let Some(tags) = tab[lvl_idx].as_ref()
7939                .and_then(|t| t.sets.as_ref())
7940                .and_then(|s| s.tags.clone())
7941            {
7942                setaparam(&args[1], tags);                                    // c:3951
7943                0
7944            } else {
7945                1                                                             // c:3952
7946            }
7947        }
7948        _ => 0,
7949    }
7950}
7951
7952/// Direct port of `static int bin_comptry(char *nam, char **args,
7953///                                          UNUSED(Options ops),
7954///                                          UNUSED(int func))` from
7955/// `Src/Zle/computil.c:3961-4138`. Builds a new tag-set under the
7956/// active comptags[lasttaglevel] entry. Two forms:
7957///   - `comptry -m "pat1 pat2" [...]` — for each space-separated
7958///     tag pattern, glob-expand via braces/wildcards, match against
7959///     the registered `all` array, filter out tags already in any
7960///     existing set, then append the deduplicated matches as a new
7961///     ctset.
7962///   - `comptry [-s] tag1 tag2 ...` — filter args to keep only
7963///     registered tags not in any existing set; with `-s`, build one
7964///     ctset per arg, else one ctset for all of them.
7965pub fn bin_comptry(nam: &str, args: &[String],                               // c:3961
7966                   _ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
7967    use crate::ported::glob::{hasbraces, tokenize, xpandbraces};
7968    use crate::ported::pattern::{patcompile, pattry};
7969    use crate::ported::ztype_h::{iblank, inblank};
7970    use crate::ported::zsh_h::{Comma, Inbrace, Outbrace};
7971    use std::sync::atomic::Ordering;
7972
7973    if INCOMPFUNC.load(Ordering::Relaxed) != 1 {                             // c:3963
7974        zwarnnam(nam, "can only be called from completion function");
7975        return 1;
7976    }
7977
7978    let lvl = lasttaglevel.load(Ordering::Relaxed);
7979    if lvl <= 0 {                                                            // c:3967 — !lasttaglevel
7980        zwarnnam(nam, "no tags registered");
7981        return 1;
7982    }
7983    let lvl_idx = lvl as usize;
7984    let registered = comptags.lock().ok()
7985        .map(|t| lvl_idx < t.len() && t[lvl_idx].is_some())
7986        .unwrap_or(false);
7987    if !registered {
7988        zwarnnam(nam, "no tags registered");
7989        return 1;
7990    }
7991
7992    if args.is_empty() { return 0; }                                         // c:3971
7993
7994    // Helper: append a new ctset to comptags[lvl_idx].sets.
7995    let append_set = |tags: Vec<String>| {
7996        if let Ok(mut tab) = comptags.lock() {
7997            if let Some(t) = tab[lvl_idx].as_mut() {
7998                let new_set = Box::new(ctset {
7999                    next: None,
8000                    tags: Some(tags),
8001                    tag:  None,
8002                    ptr:  0,
8003                });
8004                // c:4082 — walk to tail of existing sets.
8005                if let Some(head) = t.sets.as_mut() {
8006                    let mut cur = head.as_mut();
8007                    while cur.next.is_some() {
8008                        cur = cur.next.as_mut().unwrap();
8009                    }
8010                    cur.next = Some(new_set);
8011                } else {
8012                    t.sets = Some(new_set);
8013                }
8014            }
8015        }
8016    };
8017
8018    if args[0] == "-m" {                                                     // c:3972
8019        // c:3973-4090 — pattern-match mode.
8020        for arg in &args[1..] {                                              // c:3978
8021            let mut s = arg.as_bytes().to_vec();
8022            let mut list: Vec<String> = Vec::new();
8023            let mut num = 0i32;
8024            let mut i = 0usize;
8025
8026            while i < s.len() {
8027                // c:3980 — skip leading blanks.
8028                while i < s.len() && iblank(s[i]) { i += 1; }
8029                if i >= s.len() { break; }
8030                // c:3982 — accumulate the token, watching for `\X` escape
8031                // and tracking the first unescaped ':' separator.
8032                let p_start = i;
8033                let mut p_pos = i;
8034                let mut colon_at: Option<usize> = None;
8035                while i < s.len() && !inblank(s[i]) {
8036                    if colon_at.is_none() && s[i] == b':' {
8037                        colon_at = Some(p_pos);
8038                    }
8039                    if s[i] == b'\\' && i + 1 < s.len() {
8040                        i += 1;
8041                    }
8042                    s[p_pos] = s[i];
8043                    p_pos += 1;
8044                    i += 1;
8045                }
8046                // Skip the trailing blank.
8047                if i < s.len() { i += 1; }
8048
8049                let token_full = String::from_utf8_lossy(&s[p_start..p_pos]).into_owned();
8050                if token_full.is_empty() { continue; }
8051
8052                // c:3997 — split at colon: q = head, c = trailing.
8053                let (q, c_opt): (String, Option<String>) = match colon_at {
8054                    Some(c_idx) => {
8055                        let head = String::from_utf8_lossy(
8056                            &s[p_start..c_idx]).into_owned();
8057                        let tail = String::from_utf8_lossy(
8058                            &s[c_idx + 1..p_pos]).into_owned();
8059                        (head, Some(tail))
8060                    }
8061                    None => (token_full.clone(), None),
8062                };
8063                if q.is_empty() { continue; }
8064
8065                // c:4001-4012 — convert `{` / `}` / `,` to Inbrace / Outbrace
8066                // / Comma tokens for glob processing.
8067                let mut qq: String = q.chars().map(|ch| match ch {
8068                    '\\' => ch,  // keep; handled below
8069                    '{'  => Inbrace,
8070                    '}'  => Outbrace,
8071                    ','  => Comma,
8072                    other => other,
8073                }).collect();
8074                // Handle `\X` — keep both bytes literal by re-walking. The
8075                // C does this inline in the same loop; we just leave them.
8076                tokenize(&mut qq);
8077
8078                // c:4013 — if hasbraces/haswilds, glob-expand.
8079                let has_meta = hasbraces(&qq, false)
8080                    || crate::ported::pattern::haswilds(&qq);
8081                let all_arr: Vec<String> = comptags.lock().ok()
8082                    .and_then(|t| t[lvl_idx].as_ref().and_then(|c| c.all.clone()))
8083                    .unwrap_or_default();
8084                let sets_clone: Vec<Vec<String>> = comptags.lock().ok()
8085                    .map(|t| {
8086                        let mut out = Vec::new();
8087                        if let Some(c) = t[lvl_idx].as_ref() {
8088                            let mut p = c.sets.as_deref();
8089                            while let Some(set) = p {
8090                                if let Some(ts) = set.tags.as_ref() {
8091                                    out.push(ts.clone());
8092                                }
8093                                p = set.next.as_deref();
8094                            }
8095                        }
8096                        out
8097                    })
8098                    .unwrap_or_default();
8099
8100                if has_meta {
8101                    // c:4015-4022 — expand braces, then compile each as a Patprog.
8102                    let mut blist: Vec<String> = vec![qq.clone()];
8103                    let mut bi = 0usize;
8104                    while bi < blist.len() {
8105                        if hasbraces(&blist[bi], false) {
8106                            let expanded = xpandbraces(&blist[bi], false);
8107                            blist.remove(bi);
8108                            for e in expanded {
8109                                blist.insert(bi, e);
8110                                bi += 1;
8111                            }
8112                        } else {
8113                            bi += 1;
8114                        }
8115                    }
8116                    for bb in &blist {                                       // c:4023
8117                        if let Some(prog) = patcompile(bb, 0, None::<&mut String>) {
8118                            for a in &all_arr {                              // c:4029
8119                                // Skip if `a:c` (or just a) already in list.
8120                                let already = list.iter().any(|item| {
8121                                    let item_head = item.split(':').next().unwrap_or(item);
8122                                    item_head == a.as_str()
8123                                });
8124                                if already { continue; }
8125                                if pattry(&prog, a) {                         // c:4043
8126                                    let entry = match &c_opt {
8127                                        Some(c) => format!("{}:{}", a, c),
8128                                        None => a.clone(),
8129                                    };
8130                                    list.push(entry);
8131                                    num += 1;
8132                                }
8133                            }
8134                        }
8135                    }
8136                } else if arrcontains(&all_arr, &q, false) != 0 {            // c:4056
8137                    // c:4057-4064 — literal token: include if not in any set.
8138                    let in_set = sets_clone.iter().any(|s|
8139                        arrcontains(s, &q, false) != 0);
8140                    if !in_set {
8141                        list.push(q.clone());
8142                        num += 1;
8143                    }
8144                }
8145            }
8146
8147            if num > 0 {                                                     // c:4072
8148                append_set(list);
8149            }
8150        }
8151    } else {                                                                 // c:4091 — plain mode
8152        let mut idx = 0usize;
8153        let sep = args[idx] == "-s";                                         // c:4095
8154        if sep { idx += 1; }
8155        let all_arr: Vec<String> = comptags.lock().ok()
8156            .and_then(|t| t[lvl_idx].as_ref().and_then(|c| c.all.clone()))
8157            .unwrap_or_default();
8158        let sets_clone: Vec<Vec<String>> = comptags.lock().ok()
8159            .map(|t| {
8160                let mut out = Vec::new();
8161                if let Some(c) = t[lvl_idx].as_ref() {
8162                    let mut p = c.sets.as_deref();
8163                    while let Some(set) = p {
8164                        if let Some(ts) = set.tags.as_ref() {
8165                            out.push(ts.clone());
8166                        }
8167                        p = set.next.as_deref();
8168                    }
8169                }
8170                out
8171            })
8172            .unwrap_or_default();
8173
8174        // c:4098-4108 — filter args, keep only registered tags not in any set.
8175        let filtered: Vec<String> = args[idx..].iter()
8176            .filter(|p| {
8177                arrcontains(&all_arr, p, true) != 0
8178                    && !sets_clone.iter().any(|s|
8179                        arrcontains(s, p, false) != 0)
8180            })
8181            .cloned()
8182            .collect();
8183
8184        if filtered.is_empty() { return 0; }
8185
8186        // c:4114-4134 — push as one set, or split (one per arg) with -s.
8187        if sep {
8188            for t in &filtered {
8189                append_set(vec![t.clone()]);
8190            }
8191        } else {
8192            append_set(filtered);
8193        }
8194    }
8195    0                                                                        // c:4138
8196}
8197
8198/// Direct port of `static int bin_compvalues(char *nam, char **args,
8199///                                              UNUSED(Options ops),
8200///                                              UNUSED(int func))` from
8201/// `Src/Zle/computil.c:3475-3658`. Full subcommand dispatch for
8202/// `compvalues -i/-D/-C/-V/-s/-S/-d/-L/-v`. Each branch consumes
8203/// `cv_laststate` populated by `cv_parse_word`.
8204pub fn bin_compvalues(nam: &str, args: &[String],                            // c:3475
8205                      _ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
8206    use crate::ported::params::{setsparam, setaparam, sethparam};
8207    use std::sync::atomic::Ordering;
8208
8209    if INCOMPFUNC.load(Ordering::Relaxed) != 1 {                             // c:3479
8210        zwarnnam(nam, "can only be called from completion function");
8211        return 1;
8212    }
8213    if args.is_empty() { return 1; }
8214    let a0 = args[0].as_bytes();
8215    if a0.len() != 2 || a0[0] != b'-' {                                      // c:3483
8216        zwarnnam(nam, &format!("invalid argument: {}", args[0]));
8217        return 1;
8218    }
8219    let sub = a0[1];
8220
8221    if sub != b'i' && cv_parsed.load(Ordering::Relaxed) == 0 {                // c:3487
8222        zwarnnam(nam, "no parsed state");
8223        return 1;
8224    }
8225
8226    let (min, max): (i32, i32) = match sub {                                 // c:3491
8227        b'i' => (2, -1),
8228        b'D' => (2, 2),
8229        b'C' => (1, 1),
8230        b'V' => (3, 3),
8231        b's' => (1, 1),
8232        b'S' => (1, 1),
8233        b'd' => (1, 1),
8234        b'L' => (3, 4),
8235        b'v' => (1, 1),
8236        _ => {
8237            zwarnnam(nam, &format!("invalid option: {}", args[0]));
8238            return 1;
8239        }
8240    };
8241    let n = (args.len() as i32) - 1;
8242    if n < min { zwarnnam(nam, "not enough arguments"); return 1; }
8243    if max >= 0 && n > max { zwarnnam(nam, "too many arguments"); return 1; }
8244
8245    match sub {
8246        b'i' => {                                                            // c:3514
8247            let spec = &args[1..];
8248            let _ = get_cvdef(nam, spec);
8249            let mut cached: Option<Box<cvdef>> = {
8250                let cache = cvdef_cache.lock().ok();
8251                cache.and_then(|c| {
8252                    c.iter().find_map(|slot| {
8253                        slot.as_ref().filter(|e| {
8254                            e.ndefs == spec.len() as i32
8255                                && e.defs.as_deref().map_or(false, |d| {
8256                                    d.len() == spec.len()
8257                                        && d.iter().zip(spec.iter()).all(|(a, b)| a == b)
8258                                })
8259                        }).cloned()
8260                    })
8261                })
8262            };
8263            let Some(ref mut def) = cached else { return 1; };
8264            cv_parsed.store(0, Ordering::Relaxed);                           // c:3521
8265            cv_parse_word(def);                                              // c:3527
8266            cv_parsed.store(1, Ordering::Relaxed);                           // c:3528
8267            0
8268        }
8269
8270        b'D' => {                                                            // c:3533
8271            let arg = cv_laststate.lock().ok().and_then(|s| s.def.clone());
8272            if let Some(a) = arg {
8273                setsparam(&args[1], a.descr.as_deref().unwrap_or(""));       // c:3541
8274                setsparam(&args[2], a.action.as_deref().unwrap_or(""));      // c:3542
8275                0
8276            } else {
8277                1                                                            // c:3546
8278            }
8279        }
8280
8281        b'C' => {                                                            // c:3548
8282            let arg = cv_laststate.lock().ok().and_then(|s| s.def.clone());
8283            if let Some(a) = arg {
8284                setsparam(&args[1], a.opt.as_deref().unwrap_or(""));         // c:3555
8285                0
8286            } else {
8287                1                                                            // c:3559
8288            }
8289        }
8290
8291        b'V' => {                                                            // c:3561
8292            let mut noarg: Vec<String> = Vec::new();
8293            let mut arg_l: Vec<String> = Vec::new();
8294            let mut opt_l: Vec<String> = Vec::new();
8295            if let Ok(ls) = cv_laststate.lock() {
8296                if let Some(d) = ls.d.as_ref() {
8297                    let mut p = d.vals.as_deref();
8298                    while let Some(v) = p {
8299                        if v.active != 0 {                                   // c:3574
8300                            let bucket: &mut Vec<String> = match v.r#type {
8301                                t if t == CVV_NOARG => &mut noarg,
8302                                t if t == CVV_ARG => &mut arg_l,
8303                                _ => &mut opt_l,
8304                            };
8305                            let name = v.name.as_deref().unwrap_or("");
8306                            let str_val = if let Some(d) = v.descr.as_deref() {
8307                                format!("{}:{}", name, d)
8308                            } else {
8309                                name.to_string()
8310                            };
8311                            bucket.push(str_val);                            // c:3589
8312                        }
8313                        p = v.next.as_deref();
8314                    }
8315                }
8316            }
8317            setaparam(&args[1], noarg);
8318            setaparam(&args[2], arg_l);
8319            setaparam(&args[3], opt_l);
8320            0                                                                // c:3596
8321        }
8322
8323        b's' => {                                                            // c:3598
8324            let (hassep, sep) = cv_laststate.lock().ok()
8325                .and_then(|ls| ls.d.as_ref().map(|d| (d.hassep, d.sep)))
8326                .unwrap_or((0, 0));
8327            if hassep != 0 {
8328                let tmp = (sep as u8 as char).to_string();
8329                setsparam(&args[1], &tmp);
8330                0                                                            // c:3608
8331            } else {
8332                1                                                            // c:3610
8333            }
8334        }
8335
8336        b'S' => {                                                            // c:3611
8337            let argsep = cv_laststate.lock().ok()
8338                .and_then(|ls| ls.d.as_ref().map(|d| d.argsep))
8339                .unwrap_or(0);
8340            let tmp = (argsep as u8 as char).to_string();
8341            setsparam(&args[1], &tmp);
8342            0                                                                // c:3620
8343        }
8344
8345        b'd' => {                                                            // c:3621
8346            let descr = cv_laststate.lock().ok()
8347                .and_then(|ls| ls.d.as_ref().and_then(|d| d.descr.clone()))
8348                .unwrap_or_default();
8349            setsparam(&args[1], &descr);
8350            0
8351        }
8352
8353        b'L' => {                                                            // c:3626
8354            let val = cv_laststate.lock().ok().and_then(|ls| {
8355                ls.d.as_ref().and_then(|d| cv_get_val(d, &args[1]))
8356            });
8357            if let Some(v) = val {
8358                if let Some(a) = v.arg.as_deref() {                          // c:3634
8359                    setsparam(&args[2], a.descr.as_deref().unwrap_or(""));
8360                    setsparam(&args[3], a.action.as_deref().unwrap_or(""));
8361                    if args.len() > 4 {                                      // c:3638
8362                        setsparam(&args[4], v.name.as_deref().unwrap_or(""));
8363                    }
8364                    return 0;
8365                }
8366            }
8367            1                                                                // c:3643
8368        }
8369
8370        b'v' => {                                                            // c:3645
8371            let vals = cv_laststate.lock().ok()
8372                .and_then(|ls| ls.vals.clone());
8373            if let Some(v) = vals {
8374                sethparam(&args[1], v);
8375                0
8376            } else {
8377                1                                                            // c:3656
8378            }
8379        }
8380
8381        _ => 1,                                                              // c:3658
8382    }
8383}