Skip to main content

zsh/ported/zle/
complete.rs

1//! Direct port of `Src/Zle/complete.c` — the ZLE completion engine.
2//!
3//! Copy a completion matcher list into permanent storage.                   // c:151
4//! Copy a completion matcher pattern.                                       // c:214
5//! Parse a character class for matcher control.                             // c:476
6//!
7//! This file holds the canonical Rust ports of complete.c's
8//! exported functions: state globals (`compprefix` / `compsuffix` /
9//! `compwords` / `incompfunc` / etc.), the Cmlist/Cmatcher/Cpattern
10//! allocator + free + deep-copy chain (freecmlist/freecmatcher/
11//! freecpattern/cpcmatcher/cp_cpattern_element/cpcpattern), the
12//! ignore_prefix/ignore_suffix/restrict_range state mutators, the
13//! special-parameter accessors that back $compstate (get_compstate /
14//! set_compstate / get_nmatches / get_complist / get_unambig and
15//! friends), the cond_psfix / cond_range condition predicates, the
16//! parse_ordering (-o) / parse_class / parse_cmatcher (-M) parsers,
17//! the addcompparams / makecompparams / compunsetfn / comp_setunset
18//! / comp_wrapper paramtab plumbing, and the bin_compadd / bin_compset
19//! / do_comp_vars top-level builtin entries.
20//!
21//! `Src/Zle/comp.h` is ported in `comp_h.rs`; the live editor /
22//! computil dispatch lives in `compcore.rs` and `computil.rs`. This
23//! file maps 1:1 to `Src/Zle/complete.c` (4 of 4 surface ported now
24//! ported faithfully, with the deeper ones still wired through the
25//! existing comp_h struct types — no Rust-only intermediate types).
26//!
27//! Per PORT.md "file freeze" rule: this file's creation was
28//! explicitly authorised by the maintainer to land the complete.c
29//! port out of compcore.rs (where it had been parked under the
30//! freeze).
31
32use std::sync::atomic::{AtomicI32, AtomicI64, Ordering};
33use std::sync::Mutex;
34
35use crate::ported::glob::{remnulargs, tokenize};
36use crate::ported::params::{createparam, getsparam, paramtab};
37use crate::ported::pattern::{patcompile, pattry, range_type};
38use crate::ported::utils::{zerr, zwarnnam};
39use crate::ported::zle::comp_h::{
40    Cmatcher, Cpattern, CAF_ALL, CAF_ARRAYS, CAF_KEYS, CAF_MATCH, CAF_MATSORT, CAF_NOSORT,
41    CAF_QUOTE, CAF_UNIQALL, CAF_UNIQCON, CLF_LINE, CLF_SUF, CMF_DISPLINE, CMF_FILE, CMF_HIDE,
42    CMF_INTER, CMF_ISPAR, CMF_LEFT, CMF_LINE, CMF_NOLIST, CMF_REMOVE, CMF_RIGHT, CPAT_ANY,
43    CPAT_CCLASS,
44    CPAT_CHAR, CPAT_EQUIV, CPAT_NCLASS,
45};
46use crate::ported::zle::{
47    compcore, compresult, deltochar::*, textobjects::*, zle_hist::*, zle_main::*, zle_misc::*,
48    zle_move::*, zle_params::*, zle_refresh::*, zle_tricky::*, zle_utils::*, zle_vi::*,
49    zle_word::*,
50};
51use crate::ported::zsh_h::{
52    eprog, funcwrap, module, options, param, PAT_HEAPDUP, PM_ARRAY, PM_HASHED, PM_INTEGER,
53    PM_LOCAL, PM_READONLY, PM_REMOVABLE, PM_SCALAR, PM_SINGLE, PM_SPECIAL, PM_TYPE, PM_UNSET,
54    PP_RANGE, PP_UNKWN,
55};
56
57// =====================================================================
58// Cmlist / Cmatcher / Cpattern allocators + freers — Src/Zle/complete.c.
59// Ported here (rather than a non-existent complete.rs) because
60// PORT.md freezes new src/ported/ file creation; compcore.rs is the
61// canonical home for completion-machinery internals.
62// =====================================================================
63
64#[allow(unused_imports)]
65/// Direct port of `freecmlist(Cmlist l)` from `Src/Zle/complete.c:98`.
66/// C body (c:101-110): walk the linked list freeing each Cmatcher
67/// via `freecmatcher()` and the per-entry `str` via `zsfree()`.
68/// Rust drop handles the deallocation; this wrapper iterates so
69/// callers can name-match the C entry point.
70
71// --- AUTO: cross-zle hoisted-fn use glob ---
72/// `freecmlist` — see implementation.
73#[allow(unused_imports)]
74#[allow(unused_imports)]
75
76pub fn freecmlist(l: Option<Box<crate::ported::zle::comp_h::Cmlist>>) {
77    // c:98
78    let mut cur = l;
79    while let Some(node) = cur {
80        // c:101
81        // c:103 — `freecmatcher(l->matcher);` — Rust Box drop frees.
82        // c:104 — `zsfree(l->str);` — String drop frees.
83        cur = node.next; // c:102 n = l->next
84    }
85}
86
87/// Direct port of `freecmatcher(Cmatcher m)` from `Src/Zle/complete.c:115`.
88/// C body (c:118-132):
89/// ```c
90/// if (!m || --(m->refc)) return;
91/// while (m) {
92///     n = m->next;
93///     freecpattern(m->line); freecpattern(m->word);
94///     freecpattern(m->left); freecpattern(m->right);
95///     zfree(m, sizeof(struct cmatcher));
96///     m = n;
97/// }
98/// ```
99/// The C source uses refcounting (`refc`); Rust port relies on Box
100/// ownership semantics — when the last reference drops, every
101/// Box-owned Cpattern in the chain drops with it.
102pub fn freecmatcher(m: Option<Box<Cmatcher>>) {
103    // c:115
104    // c:115 — `if (!m || --(m->refc)) return;` — refcount handled by
105    // Rust ownership; the function is a name-parity wrapper.
106    let mut cur = m;
107    while let Some(node) = cur {
108        // c:122
109        // c:124-127 — `freecpattern(m->line/word/left/right)` — Rust
110        // drop chains via Option<Box<Cpattern>> fields.
111        cur = node.next; // c:123
112    }
113}
114
115/// Direct port of `freecpattern(Cpattern p)` from `Src/Zle/complete.c:137`.
116/// C body (c:141-149):
117/// ```c
118/// while (p) {
119///     n = p->next;
120///     if (p->tp <= CPAT_EQUIV) free(p->u.str);
121///     zfree(p, sizeof(struct cpattern));
122///     p = n;
123/// }
124/// ```
125pub fn freecpattern(p: Option<Box<Cpattern>>) {
126    // c:137
127    let mut cur = p;
128    while let Some(node) = cur {
129        // c:141
130        // c:144 — `if (p->tp <= CPAT_EQUIV) free(p->u.str)` — String
131        // drop in Option<String> handles the conditional free.
132        cur = node.next; // c:155
133    }
134}
135
136// Copy a completion matcher list into permanent storage.                   // c:155
137/// Direct port of `cpcmatcher(Cmatcher m)` from `Src/Zle/complete.c:155`.
138/// C body (c:158-179): walks the source matcher chain, allocating a
139/// fresh Cmatcher per node with `refc = 1`, copying flags / llen /
140/// wlen / lalen / ralen, deep-copying each Cpattern via
141/// `cpcpattern()`. Returns the new chain head.
142/// WARNING: param names don't match C — Rust=() vs C=(m)
143pub fn cpcmatcher(m: Option<&Cmatcher>) -> Option<Box<Cmatcher>> // c:155
144{
145    let mut head: Option<Box<Cmatcher>> = None; // c:158
146    let mut tail_ref: *mut Option<Box<Cmatcher>> = &mut head;
147    let mut cur = m;
148    while let Some(src) = cur {
149        // c:160
150        let n = Box::new(Cmatcher {
151            // c:161 zalloc
152            refc: 1,                                 // c:163
153            next: None,                              // c:164
154            flags: src.flags,                        // c:165
155            line: cpcpattern(src.line.as_deref()),   // c:166
156            llen: src.llen,                          // c:167
157            word: cpcpattern(src.word.as_deref()),   // c:168
158            wlen: src.wlen,                          // c:169
159            left: cpcpattern(src.left.as_deref()),   // c:170
160            lalen: src.lalen,                        // c:171
161            right: cpcpattern(src.right.as_deref()), // c:172
162            ralen: src.ralen,                        // c:173
163        });
164        unsafe {
165            *tail_ref = Some(n);
166            if let Some(ref mut new_node) = *tail_ref {
167                // c:175 p = &(n->next)
168                tail_ref = &mut new_node.next as *mut _;
169            }
170        }
171        cur = src.next.as_deref(); // c:187
172    }
173    head // c:187
174}
175
176// Copy a completion matcher pattern.                                        // c:214
177/// Direct port of `cp_cpattern_element(Cpattern o)` from `Src/Zle/complete.c:187`.
178/// C body (c:189-216): allocates a fresh Cpattern, sets `next = NULL`,
179/// copies `tp`, then dispatches on `tp` to copy `u.str` (CCLASS /
180/// NCLASS / EQUIV) or `u.chr` (CHAR). Default keeps the union zero.
181/// WARNING: param names don't match C — Rust=() vs C=(o)
182pub fn cp_cpattern_element(o: &Cpattern) -> Box<Cpattern> {
183    let mut n = Cpattern::default(); // c:189 zalloc
184    n.next = None; // c:191
185    n.tp = o.tp; // c:193
186    match o.tp {
187        // c:194
188        CPAT_CCLASS | CPAT_NCLASS | CPAT_EQUIV => {
189            // c:196-198
190            n.str = o.str.clone(); // c:199 ztrdup(o->u.str)
191        }
192        CPAT_CHAR => {
193            // c:218
194            n.chr = o.chr; // c:218 o->u.chr
195        }
196        _ => {} // c:218
197    }
198    Box::new(n) // c:218 return n
199}
200
201/// Direct port of `cpcpattern(Cpattern o)` from `Src/Zle/complete.c:218`.
202/// C body (c:222-231): walk the source Cpattern chain, copying each
203/// element via `cp_cpattern_element()`. Returns the new chain head.
204pub fn cpcpattern(o: Option<&Cpattern>) -> Option<Box<Cpattern>> // c:218
205{
206    let mut head: Option<Box<Cpattern>> = None; // c:222
207    let mut tail_ref: *mut Option<Box<Cpattern>> = &mut head;
208    let mut cur = o;
209    while let Some(src) = cur {
210        // c:224
211        unsafe {
212            *tail_ref = Some(cp_cpattern_element(src)); // c:225
213            if let Some(ref mut new_node) = *tail_ref {
214                // c:226 p = &((*p)->next)
215                tail_ref = &mut new_node.next as *mut _;
216            }
217        }
218        cur = src.next.as_deref(); // c:227
219    }
220    head // c:229
221}
222
223// =====================================================================
224// Completion-state globals — port of `Src/Zle/complete.c:35-73`.
225// =====================================================================
226//
227// C declares these as bare `mod_export` globals (`char *compprefix`,
228// `int compcurrent`, etc.) accessed directly from every completion
229// helper. Rust port wraps each in a Mutex<…> / AtomicI32 so the
230// state survives across builtin calls without threading it through
231// SubstState. Names match the C globals exactly.
232
233/// Port of `int incompfunc` from comp.h. 1 while inside a
234/// completion function (set by makecompparams, cleared by
235/// compunsetfn); checked by comp_check / cond_psfix / cond_range
236/// to refuse calls outside completion context.
237pub static INCOMPFUNC: AtomicI32 = AtomicI32::new(0); // c:complete.c
238
239/// Port of `int compcurrent` — index into compwords[] of the word
240/// being completed.
241pub static COMPCURRENT: AtomicI32 = AtomicI32::new(0); // c:complete.c
242
243/// Port of `mod_export zlong complistmax` from `Src/Zle/complete.c:37`.
244/// `$LISTMAX` value — maximum number of matches to list before asking
245/// the user via asklistscroll. 0 means no limit.
246pub static COMPLISTMAX: AtomicI64 = AtomicI64::new(0); // c:37
247
248/// Port of `int nmatches` — total matches accumulated this round.
249pub static NMATCHES_GLOBAL: AtomicI64 = AtomicI64::new(0); // c:compcore.c:160
250
251/// Port of `zlong complistlines` — line count of the listed
252/// matches when paginated.
253pub static COMPLISTLINES: AtomicI64 = AtomicI64::new(0); // c:complete.c:40
254
255/// Port of `zlong compignored` — count of matches dropped per
256/// the IGNORED options.
257pub static COMPIGNORED: AtomicI64 = AtomicI64::new(0); // c:complete.c:41
258
259// String globals from c:46-73 — wrapped in Mutex<String>.
260macro_rules! comp_string_global {
261    ($vis:vis $name:ident, $cname:literal, $cline:literal) => {
262        #[doc = concat!("Port of `char *", $cname, "` from complete.c:", stringify!($cline), ".")]
263        $vis static $name: std::sync::OnceLock<Mutex<String>> = std::sync::OnceLock::new();
264    };
265}
266
267comp_string_global!(pub COMPPREFIX,    "compprefix",    47);
268comp_string_global!(pub COMPSUFFIX,    "compsuffix",    48);
269comp_string_global!(pub COMPLASTPREFIX,"complastprefix",49);
270comp_string_global!(pub COMPLASTSUFFIX,"complastsuffix",50);
271comp_string_global!(pub COMPIPREFIX,   "compiprefix",   58);
272comp_string_global!(pub COMPISUFFIX,   "compisuffix",   51);
273comp_string_global!(pub COMPQIPREFIX,  "compqiprefix",  52);
274comp_string_global!(pub COMPQISUFFIX,  "compqisuffix",  53);
275comp_string_global!(pub COMPQUOTE,     "compquote",     54);
276comp_string_global!(pub COMPQUOTING,   "compquoting",   55);
277comp_string_global!(pub COMPQSTACK,    "compqstack",    55);
278comp_string_global!(pub COMPLIST,      "complist",      65);
279comp_string_global!(pub COMPCONTEXT,   "compcontext",   59);
280comp_string_global!(pub COMPPARAMETER, "compparameter", 60);
281comp_string_global!(pub COMPREDIRECT,  "compredirect",  61);
282
283/// Port of `char **compwords` (complete.c:45) — argv-style array of
284/// the command-line words being completed.
285pub static COMPWORDS: std::sync::OnceLock<Mutex<Vec<String>>> = std::sync::OnceLock::new();
286
287/// Direct port of `parse_cmatcher(char *name, char *s)` from `Src/Zle/complete.c:242`.
288/// 162-line parser for a `compadd -M` matcher specification string.
289/// The grammar is: comma-separated rules, each like `r:|=*` /
290/// `l:|=*` / `b:[a-z]=[A-Z]` / `e:|=*` / `B:[]=[]`. Each rule
291/// builds one Cmatcher with line/word/left/right Cpattern chains
292/// via parse_pattern (line 420) + parse_class (line 480), both of
293/// which are real-bodied ports.
294/// WARNING: param names don't match C — Rust=() vs C=(name, s)
295pub fn parse_cmatcher(name: &str, s: &str) -> Option<Box<Cmatcher>> {
296    if s.is_empty() {
297        // c:249
298        return None;
299    }
300
301    let mut ret: Option<Box<Cmatcher>> = None;
302    let mut tail_ptr: *mut Option<Box<Cmatcher>> = &mut ret;
303    let mut rest = s;
304
305    while !rest.is_empty() {
306        // c:251
307        // c:255 — `while (*s && inblank(*s)) s++;`
308        rest = rest.trim_start_matches(|c: char| c == ' ' || c == '\t');
309        if rest.is_empty() {
310            break;
311        } // c:257
312
313        // c:259-285 — switch (*s) — rule-letter dispatch.
314        let c = rest.chars().next().unwrap();
315        let (fl, fl2) = match c {
316            'b' => (CMF_LEFT, CMF_INTER),             // c:262
317            'l' => (CMF_LEFT, 0),                     // c:263
318            'e' => (CMF_RIGHT, CMF_INTER),            // c:264
319            'r' => (CMF_RIGHT, 0),                    // c:265
320            'm' => (0, 0),                            // c:266
321            'B' => (CMF_LEFT | CMF_LINE, CMF_INTER),  // c:267
322            'L' => (CMF_LEFT | CMF_LINE, 0),          // c:268
323            'E' => (CMF_RIGHT | CMF_LINE, CMF_INTER), // c:269
324            'R' => (CMF_RIGHT | CMF_LINE, 0),         // c:270
325            'M' => (CMF_LINE, 0),                     // c:271
326            'x' => (0, 0),                            // c:272
327            _ => {
328                // c:280
329                if !name.is_empty() {
330                    zwarnnam(
331                        name,
332                        &format!("unknown match specification character `{}'", c),
333                    );
334                }
335                return None; // c:283 pcm_err
336            }
337        };
338
339        // c:288 — `if (s[1] != ':')` → missing-colon.
340        let mut chars = rest.chars();
341        chars.next();
342        if chars.clone().next() != Some(':') {
343            if !name.is_empty() {
344                zwarnnam(name, "missing `:'");
345            }
346            return None;
347        }
348        chars.next(); // consume `:`
349
350        // c:294-303 — `x:` early-return.
351        if c == 'x' {
352            if let Some(next) = chars.clone().next() {
353                if next != ' ' && next != '\t' {
354                    if !name.is_empty() {
355                        zwarnnam(name, "unexpected pattern following x: specification");
356                    }
357                    return None;
358                }
359            }
360            return ret;
361        }
362        rest = chars.as_str();
363
364        // c:297-313 — `(fl & CMF_LEFT) && !fl2` → parse left anchor.
365        let mut left: Option<Box<Cpattern>> = None;
366        let mut lal: i32 = 0;
367        let mut both: bool = false;
368        if (fl & CMF_LEFT) != 0 && fl2 == 0 {
369            let (lt, r2, l, err) = parse_pattern(name, rest, '|'); // c:298
370            if err {
371                return None;
372            }
373            left = lt;
374            lal = l;
375            rest = r2;
376            // c:302 — `both = (*s && s[1] == '|')`.
377            let mut peek = rest.chars();
378            peek.next();
379            if peek.clone().next() == Some('|') {
380                both = true;
381                let mut adv = rest.chars();
382                adv.next();
383                rest = adv.as_str();
384            }
385            // c:305-313 — `if (!*s || !*++s)` → missing right anchor / line pattern.
386            if rest.len() <= 1 {
387                if !name.is_empty() {
388                    zwarnnam(
389                        name,
390                        if both {
391                            "missing right anchor"
392                        } else {
393                            "missing line pattern"
394                        },
395                    );
396                }
397                return None;
398            }
399            let mut adv = rest.chars();
400            adv.next();
401            rest = adv.as_str();
402        }
403
404        // c:317-319 — `line = parse_pattern(name, &s, &ll,
405        //                              (((fl & CMF_RIGHT) && !fl2) ? '|' : '='), &err);`
406        let line_end = if (fl & CMF_RIGHT) != 0 && fl2 == 0 {
407            '|'
408        } else {
409            '='
410        };
411        let (mut line_pat, r2, mut ll, err) = parse_pattern(name, rest, line_end);
412        if err {
413            return None;
414        }
415        rest = r2;
416
417        // c:322 — `if (both) { right = line; ral = ll; line = NULL; ll = 0; }`
418        let (mut right, mut ral) = (None, 0i32);
419        if both {
420            right = line_pat;
421            ral = ll;
422            line_pat = None;
423            ll = 0;
424        }
425
426        // c:328-339 — anchor / `=` / `*` consume.
427        if (fl & CMF_RIGHT) != 0 && fl2 == 0 && rest.len() <= 1 {
428            if !name.is_empty() {
429                zwarnnam(name, "missing right anchor");
430            }
431            return None;
432        }
433        if (fl & CMF_RIGHT) == 0 || fl2 != 0 {
434            if rest.is_empty() {
435                if !name.is_empty() {
436                    zwarnnam(name, "missing word pattern");
437                }
438                return None;
439            }
440            let mut adv = rest.chars();
441            adv.next();
442            rest = adv.as_str();
443        }
444
445        // c:340-357 — RIGHT-side anchor parse.
446        if (fl & CMF_RIGHT) != 0 && fl2 == 0 {
447            if rest.chars().next() == Some('|') {
448                left = line_pat.take();
449                lal = ll;
450                ll = 0;
451                let mut adv = rest.chars();
452                adv.next();
453                rest = adv.as_str();
454            }
455            let (rt, r3, r_len, err) = parse_pattern(name, rest, '=');
456            if err {
457                return None;
458            }
459            right = rt;
460            ral = r_len;
461            rest = r3;
462            if rest.is_empty() {
463                if !name.is_empty() {
464                    zwarnnam(name, "missing word pattern");
465                }
466                return None;
467            }
468            let mut adv = rest.chars();
469            adv.next();
470            rest = adv.as_str();
471        }
472
473        // c:359-379 — word pattern, with `*` and `**` sentinels.
474        let (word_pat, wl): (Option<Box<Cpattern>>, i32);
475        if rest.chars().next() == Some('*') {
476            if (fl & (CMF_LEFT | CMF_RIGHT)) == 0 {
477                if !name.is_empty() {
478                    zwarnnam(name, "need anchor for `*'");
479                }
480                return None;
481            }
482            let mut adv = rest.chars();
483            adv.next();
484            rest = adv.as_str();
485            if rest.chars().next() == Some('*') {
486                let mut adv2 = rest.chars();
487                adv2.next();
488                rest = adv2.as_str();
489                word_pat = None;
490                wl = -2;
491            } else {
492                word_pat = None;
493                wl = -1;
494            }
495        } else {
496            let (w, r4, w_len, err) = parse_pattern(name, rest, '\0');
497            if err {
498                return None;
499            }
500            if w.is_none() && line_pat.is_none() {
501                if !name.is_empty() {
502                    zwarnnam(name, "need non-empty word or line pattern");
503                }
504                return None;
505            }
506            word_pat = w;
507            wl = w_len;
508            rest = r4;
509        }
510
511        // c:383-394 — allocate Cmatcher node.
512        let node = Box::new(Cmatcher {
513            refc: 0,
514            next: None,
515            flags: fl | fl2,
516            line: line_pat,
517            llen: ll,
518            word: word_pat,
519            wlen: wl,
520            left,
521            lalen: lal,
522            right,
523            ralen: ral,
524        });
525
526        // c:395-400 — link into chain via tail.
527        unsafe {
528            *tail_ptr = Some(node);
529            if let Some(boxed) = (*tail_ptr).as_mut() {
530                tail_ptr = &mut boxed.next as *mut _;
531            }
532        }
533    }
534    ret
535}
536
537/// Direct port of `parse_class(Cpattern p, char *iptr)` from `Src/Zle/complete.c:480`.
538/// 93-line parser for a single character-class `[...]` or
539/// equivalence-class `{...}` inside a Cpattern. Reads metafied
540/// bytes from `iptr`, allocates `p->u.str` of the right size,
541/// fills in the parsed contents (with PP_RANGE / PP_UNKWN tokens
542/// for `a-z` ranges and `[:class:]` POSIX-style entries via
543/// range_type lookup).
544///
545/// Static-link path: the metafied-byte + Meta-token + PP_*
546/// encoding doesn't translate cleanly to Rust's UTF-8 strings.
547/// Structural port returns the input pointer unmodified (signaling
548/// "consumed nothing, parse failed") so the caller can detect the
549/// stub state and skip emitting the matcher.
550/// WARNING: param names don't match C — Rust=(_p) vs C=(p, iptr)
551/// Direct port of `Cpattern parse_pattern(char *name, char **sp,
552/// int *lp, char e, int *err)` from `Src/Zle/complete.c:418`.
553/// Walks `*sp` building a Cpattern chain. Stops at end-char `e`
554/// (or whitespace if `e == 0`). For each char-position:
555///   - `[` / `{` → call `parse_class` for `[class]` / `{equiv}`
556///   - `?` → CPAT_ANY
557///   - `*` / `(` / `)` / `=` → error (invalid in matcher patterns)
558///   - `\` + char → escape, emit next char as CPAT_CHAR
559///   - else → CPAT_CHAR
560///
561/// Returns `(chain_head, new_sp, length, err)`. Error sets `err=true`
562/// and chain is None; caller bubbles up.
563/// WARNING: signature change — C returns Cpattern + writes through
564/// sp/lp/err; Rust returns the tuple.
565pub fn parse_pattern<'a>(
566    name: &str,
567    s: &'a str,
568    end: char,
569) -> (Option<Box<Cpattern>>, &'a str, i32, bool) {
570    let mut ret: Option<Box<Cpattern>> = None;
571    let mut tail_ptr: *mut Option<Box<Cpattern>> = &mut ret;
572    let mut rest = s;
573    let mut len = 0i32;
574
575    // c:430 — `while (*s && (e ? (*s != e) : !inblank(*s)))`.
576    loop {
577        let next_ch = match rest.chars().next() {
578            Some(c) => c,
579            None => break,
580        };
581        if end != '\0' {
582            if next_ch == end {
583                break;
584            }
585        } else if next_ch == ' ' || next_ch == '\t' {
586            break;
587        }
588
589        // c:432 — `n = hcalloc(sizeof(*n)); n->next = NULL;`
590        let mut node = Box::new(Cpattern::default());
591
592        if next_ch == '[' || next_ch == '{' {
593            // c:435
594            // c:436 — `s = parse_class(n, s);`.
595            //          Rust parse_class already advances past the
596            //          close bracket internally (returns slice AFTER
597            //          `]`/`}`), so we don't re-advance here. C's
598            //          `s++` at c:442 is for the C parse_class which
599            //          leaves s pointing AT the close bracket.
600            //          Unterminated → parse_class returns empty input;
601            //          treat as error.
602            let before_len = rest.len();
603            rest = parse_class(&mut node, rest);
604            if rest.len() == before_len {
605                // parse_class didn't advance — unterminated.
606                if !name.is_empty() {
607                    zwarnnam(name, "unterminated character class");
608                }
609                return (None, rest, 0, true);
610            }
611        } else if next_ch == '?' {
612            // c:443
613            node.tp = CPAT_ANY;
614            let mut it = rest.chars();
615            it.next();
616            rest = it.as_str();
617        } else if matches!(next_ch, '*' | '(' | ')' | '=') {
618            // c:446
619            if !name.is_empty() {
620                zwarnnam(name, &format!("invalid pattern character `{}'", next_ch));
621            }
622            return (None, rest, 0, true);
623        } else {
624            // c:451
625            // c:452 — `if (*s == '\\' && s[1]) s++;` skip backslash escape.
626            if next_ch == '\\' {
627                let mut it = rest.chars();
628                it.next();
629                if it.clone().next().is_some() {
630                    rest = it.as_str();
631                }
632            }
633            // c:455-461 — `inlen = MB_METACHARLENCONV(...); inchar = ...;
634            //              n->tp = CPAT_CHAR; n->u.chr = inchar; s += inlen;`
635            let ch = rest.chars().next().unwrap();
636            node.tp = CPAT_CHAR;
637            node.chr = ch as u32;
638            let mut it = rest.chars();
639            it.next();
640            rest = it.as_str();
641        }
642
643        // c:463-467 — link node into chain via tail.
644        unsafe {
645            *tail_ptr = Some(node);
646            // Advance tail to the new node's `.next` slot.
647            if let Some(boxed) = (*tail_ptr).as_mut() {
648                tail_ptr = &mut boxed.next as *mut _;
649            }
650        }
651        len += 1;
652    }
653    (ret, rest, len, false)
654}
655/// `parse_class` — see implementation.
656pub fn parse_class<'a>(
657    p: &mut Cpattern, // c:480
658    iptr: &'a str,
659) -> &'a str {
660    let bytes = iptr.as_bytes();
661    if bytes.is_empty() {
662        return iptr;
663    }
664
665    // c:485-498 — `if (*iptr++ == '[')` sets CCLASS/NCLASS; else
666    //              EQUIV (`{...}`).
667    let opener = bytes[0];
668    let endchar: u8;
669    let mut i = 1;
670    if opener == b'[' {
671        endchar = b']';
672        // c:490 — `if ((*iptr=='!' || *iptr=='^') && iptr[1] != ']') NCLASS`.
673        if i < bytes.len()
674            && (bytes[i] == b'!' || bytes[i] == b'^')
675            && i + 1 < bytes.len()
676            && bytes[i + 1] != b']'
677        {
678            p.tp = CPAT_NCLASS;
679            i += 1;
680        } else {
681            p.tp = CPAT_CCLASS;
682        }
683    } else {
684        endchar = 0x7d; // ASCII close-brace; avoid b'<close-brace>' so
685                        // the build.rs brace-scanner doesn't miscount.
686        p.tp = CPAT_EQUIV;
687    }
688
689    // c:501-505 — End character can appear literally first. Find
690    //              end position; bail with rest-of-input on no end.
691    let start = i;
692    let mut optr_idx = i;
693    while optr_idx < bytes.len() && (optr_idx == start || bytes[optr_idx] != endchar) {
694        optr_idx += 1;
695    }
696    if optr_idx >= bytes.len() {
697        // c:504 — `if (!*optr) return optr;` — unterminated class.
698        return &iptr[bytes.len()..];
699    }
700
701    // c:507-512 — `p->u.str = zhalloc((optr-iptr) + 1)`. Pre-size
702    //              output buffer; tokens always fit in input length.
703    let mut out: Vec<u8> = Vec::with_capacity(optr_idx - i + 1);
704
705    // c:514-562 — main parse loop. firsttime allows endchar at position 0.
706    let mut firsttime = true;
707    while firsttime || (i < bytes.len() && bytes[i] != endchar) {
708        // c:516-525 — `[:name:]` POSIX-class form.
709        if bytes[i] == b'[' && i + 1 < bytes.len() && bytes[i + 1] == b':' {
710            if let Some(nptr) = bytes[i + 2..].iter().position(|&b| b == b':') {
711                let nptr = i + 2 + nptr;
712                if nptr + 1 < bytes.len() && bytes[nptr + 1] == b']' {
713                    let name = std::str::from_utf8(&bytes[i + 2..nptr]).unwrap_or("");
714                    let ch = range_type(name).unwrap_or(PP_UNKWN as usize);
715                    i = nptr + 2;
716                    if ch != PP_UNKWN as usize {
717                        // c:523 — `*optr++ = Meta + ch`. Encode as a
718                        // single byte with the high bit set so callers
719                        // recognise PP_* markers (decoded in
720                        // `pattern_match_equivalence` and friends).
721                        out.push(0x80u8.wrapping_add(ch as u8));
722                    }
723                    firsttime = false;
724                    continue;
725                }
726            }
727            // Malformed `[:name:` — treat `[` literally.
728        }
729
730        // c:528-560 — single-char / range parse.
731        let ptr1 = i;
732        if bytes[i] == 0x83 {
733            // c:530 Meta
734            i += 1;
735        }
736        if i >= bytes.len() {
737            break;
738        }
739        i += 1;
740        // c:534-553 — `*iptr=='-' && iptr[1] && iptr[1]!=endchar` → range.
741        if i < bytes.len() && bytes[i] == b'-' && i + 1 < bytes.len() && bytes[i + 1] != endchar {
742            i += 1; // consume '-'
743                    // c:539 — `*optr++ = Meta + PP_RANGE;`.
744            out.push(0x80u8.wrapping_add(PP_RANGE as u8));
745            // c:543-547 — start char (with Meta decode).
746            if bytes[ptr1] == 0x83 && ptr1 + 1 < bytes.len() {
747                out.push(0x83);
748                out.push(bytes[ptr1 + 1] ^ 32);
749            } else {
750                out.push(bytes[ptr1]);
751            }
752            // c:549-554 — end char (with Meta passthrough).
753            if i < bytes.len() && bytes[i] == 0x83 && i + 1 < bytes.len() {
754                out.push(bytes[i]);
755                out.push(bytes[i + 1]);
756                i += 2;
757            } else if i < bytes.len() {
758                out.push(bytes[i]);
759                i += 1;
760            }
761        } else {
762            // c:556-560 — single char.
763            if bytes[ptr1] == 0x83 && ptr1 + 1 < bytes.len() {
764                out.push(0x83);
765                out.push(bytes[ptr1 + 1] ^ 32);
766            } else {
767                out.push(bytes[ptr1]);
768            }
769        }
770        firsttime = false;
771    }
772
773    // c:564 — `*optr = '\0';`. Rust Vec<u8> stores raw byte sequence
774    // verbatim; marker bytes (0x80 + PP_*) survive without UTF-8 munging.
775    p.str = Some(out);
776
777    // c:565 — `return iptr;` — input ptr now past the close-bracket.
778    let consumed = (i + 1).min(bytes.len());
779    &iptr[consumed..]
780}
781
782/// Direct port of `parse_ordering(const char *arg, int *flags)` from `Src/Zle/complete.c:573`.
783/// C body (c:577-599): comma-separated list of order names, each
784/// matched by minimum-abbreviation length against `orderopts[]`. On
785/// any unknown name returns -1 (and seeds `*flags = CAF_MATSORT` if
786/// flags is non-NULL); otherwise OR-accumulates the matched flags
787/// into `*flags`.
788///
789/// `arg` is the comma-separated list, `flags` is an out-parameter
790/// receiving the accumulated CAF_* bitmask. Returns 0 on success,
791/// -1 on bad name.
792pub fn parse_ordering(arg: &str, flags: &mut Option<i32>) -> i32 {
793    // c:573
794    let mut fl = 0i32; // c:575
795    for opt_token in arg.split(',') {
796        // c:578-583
797        // c:585-590 — walk orderopts[] in reverse, longest-match first.
798        let mut found = false; // c:580
799        for o in ORDEROPTS.iter().rev() {
800            // c:585
801            if opt_token.len() >= o.abbrev                                   // c:586
802                && o.name.starts_with(opt_token)
803            {
804                fl |= o.oflag; // c:588
805                found = true;
806                break;
807            }
808        }
809        if !found {
810            // c:592
811            if let Some(ref mut f) = flags {
812                // c:593
813                *f = CAF_MATSORT; // c:594 default
814            }
815            return -1; // c:595
816        }
817    }
818    if let Some(ref mut f) = flags {
819        // c:598
820        *f |= fl; // c:599
821    }
822    0 // c:600
823}
824
825// =====================================================================
826// bin_compadd / bin_compset / do_comp_vars / parse_cmatcher /
827// parse_class — Src/Zle/complete.c. The remaining big-body ported from
828// the unported list. Each is ported as a faithful structural shell:
829// canonical C signature, control-flow shape, every C-source line
830// cited, with the actual data-mutation paths (addmatch, set_comp_sep,
831// CCS_* match-engine, Cmatcher chain ops) marked DEFERRED until the
832// underlying infrastructure lands.
833// =====================================================================
834
835/// Direct port of `bin_compadd(char *name, char **argv, UNUSED(Options ops), UNUSED(int func))` from `Src/Zle/complete.c:603`.
836/// 251 lines — the main `compadd` builtin entry. Parses ~30 single-
837/// letter flags + their args (-J group, -V vgroup, -X expl, -d
838/// description, -E count, -O array, -A action, -W where, -R remfn,
839/// -F filemask, -P prefix, -S suffix, -i ipre, -I isfx, -p qpre,
840/// -s qsfx, -r rstring, -R rmatch, -a/-l/-k flags, -Q noquote,
841/// -U usemenu, -1 unique, -2 partial, -o ordering, -M matcher),
842/// builds a `cadata`/`mdata` pair, then dispatches to addmatches.
843///
844/// Cadata is typed in `comp_h.rs:566` and `addmatches` is ported in
845/// `compcore.rs`. Each flag captures into the matching Cadata field
846/// (-P→pre, -S→suf, -i→ipre, -I→isuf, -p→ppre, -s→psuf, -W→prpre,
847/// -J/-V→group, -X→exp, -x→mesg, -d→disp, -O→opar, -A→apar, -D→dpar,
848/// -E→dummies, -M→match_/CAF_MATCH, -r→rems, -R→remf, -q→CMF_REMOVE,
849/// -n/-l→CMF_NOLIST, -U→clears CAF_MATCH, -Y→CMF_ISPAR, -a→CAF_ARRAYS,
850/// -k→CAF_KEYS, -Q→CAF_QUOTE, -1→CAF_UNIQALL, -2→CAF_UNIQCON,
851/// -C→CAF_ALL, -f→CMF_FILE, -o/-e→CAF_NOSORT), then dispatches the
852/// residual argv through `compcore::addmatches`.
853/// WARNING: param names don't match C — Rust=(name, argv, _func) vs C=(name, argv, ops, func)
854pub fn bin_compadd(
855    name: &str,
856    argv: &[String], // c:603
857    _ops: &options,
858    _func: i32,
859) -> i32 {
860    if INCOMPFUNC.load(Ordering::Relaxed) != 1 {
861        // c:608
862        zwarnnam(name, "can only be called from completion function"); // c:609
863        return 1; // c:610
864    }
865    // Bug #657 gap #3 — completion-builtin override interception. In zsh a
866    // completer can install a `compadd` SHELL FUNCTION (e.g. `_complete_help`'s
867    // `compadd(){ return 1 }` at sh:13, or `_approximate`'s correction body)
868    // and `compadd` then resolves function-before-builtin. The Rust ports call
869    // `bin_compadd` directly, bypassing that override; route to it here. Gated
870    // on an active `_shadow` (`.shadow.depth` > 0) so NORMAL completion (depth
871    // 0) is completely untouched — zero overhead beyond one int read. The
872    // thread-local guard lets the override's `builtin compadd` / `compadd@suffix`
873    // (body `builtin compadd`) reach the real builtin below without re-entry.
874    thread_local! {
875        static IN_COMPADD_OVERRIDE: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
876    }
877    if !IN_COMPADD_OVERRIDE.with(|g| g.get())
878        && crate::ported::params::getiparam(".shadow.depth") > 0
879        && crate::ported::utils::getshfunc("compadd").is_some()
880    {
881        IN_COMPADD_OVERRIDE.with(|g| g.set(true));
882        let rc = crate::ported::exec::dispatch_function_call("compadd", argv).unwrap_or(1);
883        IN_COMPADD_OVERRIDE.with(|g| g.set(false));
884        return rc;
885    }
886    // sh:_approximate:57-72 — process-wide PREFIX-injection hook.
887    //   When set, `(#a${_comp_correct})` (or another caller-supplied
888    //   prefix) is prepended to `$PREFIX` for the duration of this
889    //   compadd call only. The override is mirrored exactly from the
890    //   shell-side `_approximate` compadd shadow at sh:62-71: with a
891    //   leading `~` in PREFIX the injection lands AFTER the tilde.
892    let saved_prefix_for_inject: Option<String> = {
893        let lock = COMPADD_PREFIX_INJECTOR.lock().unwrap();
894        if let Some(inj) = lock.as_ref() {
895            let cur_prefix = crate::ported::params::getsparam("PREFIX").unwrap_or_default();
896            // sh:_approximate:65 — `-p` arg-position detection. If
897            //   the user passed `-p VAL` where VAL starts with `~`,
898            //   the tilde-already-handled path triggers.
899            let p_idx = argv.iter().position(|a| a == "-p");
900            let tilde_in_p = p_idx
901                .and_then(|i| argv.get(i + 1))
902                .map(|v| v.starts_with('~'))
903                .unwrap_or(false);
904            let new_prefix = if cur_prefix.starts_with('~') && !tilde_in_p {
905                // sh:67 — `~(#a${N})${PREFIX[2,-1]}` keeps the ~
906                //   intact for tilde-expansion downstream.
907                format!("~{}{}", inj, &cur_prefix[1..])
908            } else {
909                // sh:69 — bare prefix-prepend
910                format!("{}{}", inj, cur_prefix)
911            };
912            let _ = crate::ported::params::setsparam("PREFIX", &new_prefix);
913            Some(cur_prefix)
914        } else {
915            None
916        }
917    };
918    // Run the compadd body, then restore PREFIX exactly as it was.
919    let ret = bin_compadd_body(name, argv, _ops, _func);
920    if let Some(saved) = saved_prefix_for_inject {
921        let _ = crate::ported::params::setsparam("PREFIX", &saved);
922    }
923    // sh:_complete_help:11 — `compadd() { return 1 }` trace shadow.
924    //   When the trace flag is set, record the call into the trace
925    //   buffer and short-circuit so no matches actually land.
926    if COMPADD_TRACE_ACTIVE.load(Ordering::Relaxed) {
927        let mut buf = crate::ported::params::getaparam("_complete_help_funcs").unwrap_or_default();
928        buf.push(argv.join(" "));
929        crate::ported::params::setaparam("_complete_help_funcs", buf);
930        return 1;
931    }
932    // In-editor capture shadow (Phase 0.5 of the LSP completion
933    // path — see `docs/IN_EDITOR_COMPSYS_COMPLETION.md` +
934    // `crate::compsys::in_editor::COMPADD_CAPTURE_BUFFER`).
935    // When the buffer is `Some`, every compadd call routes its
936    // proposed matches into the buffer as `CompsysMatch` records
937    // instead of into the ZLE state. The buffer's installer (the
938    // LSP / `complete_at`) drains it after `_main_complete`
939    // returns and translates each match to a `CompletionItem`.
940    // Parsing happens in `crate::compsys::in_editor` (Rust-only
941    // space) so this ported file stays a faithful C port.
942    if crate::compsys::in_editor::try_capture_compadd_argv(argv) {
943        return 0; // mimic "matches were added" status
944    }
945    ret
946}
947
948/// Process-wide PREFIX injection used by `_approximate` to inject
949/// `(#a$N)` per-iteration without modifying PREFIX outside the
950/// compadd call. Set via [`set_compadd_prefix_injector`] / cleared
951/// via [`clear_compadd_prefix_injector`].
952pub static COMPADD_PREFIX_INJECTOR: std::sync::Mutex<Option<String>> = std::sync::Mutex::new(None);
953
954/// sh:_complete_help:11 — when this flag is set, every `bin_compadd`
955/// call records its argv into `_complete_help_funcs` and returns 1
956/// without adding matches. Set / cleared via
957/// [`set_compadd_trace`].
958pub static COMPADD_TRACE_ACTIVE: std::sync::atomic::AtomicBool =
959    std::sync::atomic::AtomicBool::new(false);
960
961/// Install the PREFIX-injection string used by the next `bin_compadd`
962/// call. Returns the previously-installed injector (if any) so the
963/// caller can chain.
964pub fn set_compadd_prefix_injector(s: impl Into<String>) -> Option<String> {
965    COMPADD_PREFIX_INJECTOR.lock().unwrap().replace(s.into())
966}
967
968/// Clear the PREFIX injector.
969pub fn clear_compadd_prefix_injector() {
970    *COMPADD_PREFIX_INJECTOR.lock().unwrap() = None;
971}
972
973/// Enable [`COMPADD_TRACE_ACTIVE`] — every subsequent `bin_compadd`
974/// call records its argv into `_complete_help_funcs` and returns 1.
975pub fn set_compadd_trace(active: bool) {
976    COMPADD_TRACE_ACTIVE.store(active, std::sync::atomic::Ordering::Relaxed);
977}
978
979/// Internal: the actual compadd body. Split out so the injector
980/// wrapper can run before / after it without code duplication.
981fn bin_compadd_body(name: &str, argv: &[String], _ops: &options, _func: i32) -> i32 {
982    // c:613-820 — flag-arg parse loop. Walk argv consuming `-X arg`
983    // pairs into the `Cadata` struct; per-flag dispatch ports the C
984    // switch at c:621-820.
985    let mut dat = crate::ported::zle::comp_h::Cadata::default();
986    // c:622 — `dat.aflags = CAF_MATCH`. compadd matches candidates
987    // against the word on the line by default (only `-U` clears it).
988    // Without this seed, addmatches takes the no-match branch and adds
989    // every candidate unfiltered — `compadd -k commands` would offer all
990    // 5000+ commands instead of just those matching the typed prefix.
991    dat.aflags = CAF_MATCH;
992    dat.dummies = -1;
993    let mut idx = 0usize;
994    // c:632-820 — flag loop. C iterates EACH CHARACTER inside every `-…`
995    // argv element (`for (p = *argv+1; *p; p++)`), so bundled flags like
996    // compdescribe/_describe's `-2V-default-` parse as `-2` then `-V` whose
997    // argument is the rest of the same word (`-default-`). The earlier port
998    // read only `arg[1]` per element and dropped everything after the first
999    // flag char, so `-2V-default-` set only `-2` and lost the group name —
1000    // every described-option match fell into the implicit `default` group
1001    // instead of `-default-`, and list-colors' `(-default-)` group prefix
1002    // never matched (no colors). Now char-by-char with C's sp semantics.
1003    'outer: while idx < argv.len() {
1004        // c:632 — `for (; *argv && **argv == '-'; argv++)`
1005        let arg = argv[idx].clone();
1006        if !arg.starts_with('-') {
1007            break; // c:633 — loop cond `**argv == '-'` fails: a non-flag
1008                   // word ends option parsing; it is NOT consumed (it is the
1009                   // first positional match).
1010        }
1011        if arg.len() < 2 {
1012            // c:635-637 — a bare `-` is the end-of-options marker: CONSUME it
1013            // (`argv++`) and stop. The previous port broke WITHOUT consuming,
1014            // so the `-` from `compadd … - words` (e.g. `compadd -D var - …`,
1015            // used all over _path_files) survived as a spurious first
1016            // positional word — shifting every match/dpar element by one and
1017            // breaking path completion's `compadd -D` narrowing.
1018            idx += 1;
1019            break;
1020        }
1021        if arg == "--" {
1022            idx += 1;
1023            break;
1024        } // c:815 `case '-': argv++; goto ca_args`
1025        idx += 1;
1026        let bytes = arg.clone().into_bytes();
1027        let mut p = 1usize; // c:638 `p = *argv + 1`
1028        while p < bytes.len() {
1029            // c:638
1030            let c = bytes[p] as char;
1031            // c:812-825 — the `if (sp)` arg-extraction: pasted rest-of-word
1032            // (`-Xfoo`) or the next word (`-X foo`). Consuming the rest of
1033            // the word advances `p` to the end so the inner loop stops.
1034            let mut take = |p: &mut usize, idx: &mut usize| -> Option<String> {
1035                if *p + 1 < bytes.len() {
1036                    // c:816-818 — `-Xfoo`: value is the rest of this word.
1037                    let v = String::from_utf8_lossy(&bytes[*p + 1..]).into_owned();
1038                    *p = bytes.len(); // consume rest → inner loop ends
1039                    Some(v)
1040                } else if *idx < argv.len() {
1041                    // c:819-822 — `-X foo`: value is the next word.
1042                    let v = argv[*idx].clone();
1043                    *idx += 1;
1044                    Some(v)
1045                } else {
1046                    None // c:823-828 — missing argument.
1047                }
1048            };
1049            match c {
1050                'a' => dat.aflags |= CAF_ARRAYS, // c:658
1051                'k' => dat.aflags |= CAF_ARRAYS | CAF_KEYS, // c:661
1052                'l' => dat.flags |= CMF_DISPLINE as i32, // c:766-767
1053                'o' => {
1054                    // c:701-706 + c:812-828 — `-o` takes an OPTIONAL ordering
1055                    // spec (nosort/match/numeric/reverse, comma-joined) parsed
1056                    // via parse_ordering into aflags. C: `order = oarg?-1:1;
1057                    // sp = &oarg;`. The earlier port hard-set CAF_NOSORT and
1058                    // left the spec word as a stray positional, so
1059                    // `compadd -o nosort` — what _description emits for the
1060                    // `sort false` style — both mis-sorted (always nosort) AND
1061                    // offered "nosort"/"match,numeric" as a bogus match. A
1062                    // following token that is NOT a valid ordering is left as a
1063                    // real positional (parse_ordering defaults aflags to
1064                    // CAF_MATSORT in that case).
1065                    let mut fl: Option<i32> = Some(0);
1066                    if p + 1 < bytes.len() {
1067                        // pasted `-o<spec>`: the rest of this word is the spec.
1068                        let cand = String::from_utf8_lossy(&bytes[p + 1..]).into_owned();
1069                        let _ = parse_ordering(&cand, &mut fl);
1070                        p = bytes.len(); // c: pasted arg consumed with the word
1071                    } else if idx < argv.len() {
1072                        // separate `-o <spec>`: consume the next word only when
1073                        // it parses as a valid ordering (c:824-826 `--argv`).
1074                        let cand = argv[idx].clone();
1075                        if parse_ordering(&cand, &mut fl) == 0 {
1076                            idx += 1;
1077                        }
1078                    }
1079                    if let Some(f) = fl {
1080                        if f != 0 {
1081                            dat.aflags |= f;
1082                        }
1083                    }
1084                }
1085                'Q' => dat.aflags |= CAF_QUOTE, // c:648
1086                '1' => dat.aflags |= CAF_UNIQALL, // c:693
1087                '2' => dat.aflags |= CAF_UNIQCON, // c:697
1088                'C' => dat.aflags |= CAF_ALL, // c:651
1089                'F' => dat.ign = take(&mut p, &mut idx), // c:664 -F string
1090                'f' => dat.flags |= CMF_FILE as i32, // c:654
1091                'P' => dat.pre = take(&mut p, &mut idx), // c:709
1092                'S' => dat.suf = take(&mut p, &mut idx), // c:713
1093                'p' => dat.ppre = take(&mut p, &mut idx), // c:717
1094                's' => dat.psuf = take(&mut p, &mut idx), // c:721
1095                'W' => dat.prpre = take(&mut p, &mut idx), // c:725
1096                'i' => dat.ipre = take(&mut p, &mut idx), // c:729
1097                'I' => dat.isuf = take(&mut p, &mut idx), // c:733
1098                'J' => dat.group = take(&mut p, &mut idx), // c:737
1099                'V' => {
1100                    // c:741 -V — unsorted group.
1101                    if dat.group.is_none() {
1102                        dat.aflags |= CAF_NOSORT; // c:742-743
1103                    }
1104                    dat.group = take(&mut p, &mut idx); // c:744
1105                }
1106                'X' => dat.exp = take(&mut p, &mut idx), // c:757
1107                'x' => dat.mesg = take(&mut p, &mut idx), // c:761
1108                'd' => dat.disp = take(&mut p, &mut idx), // c:765
1109                'O' => dat.opar = take(&mut p, &mut idx), // c:749
1110                'A' => dat.apar = take(&mut p, &mut idx), // c:753
1111                'D' => {
1112                    // c:772 -D
1113                    if let Some(s) = take(&mut p, &mut idx) {
1114                        dat.dpar.push(s);
1115                    }
1116                }
1117                'E' => {
1118                    // c:777 -E <number of dummies>
1119                    if let Some(s) = take(&mut p, &mut idx) {
1120                        dat.dummies = s.parse::<i32>().unwrap_or(-1).max(0);
1121                    }
1122                }
1123                'M' => {
1124                    // c:676 -M
1125                    if let Some(s) = take(&mut p, &mut idx) {
1126                        if let Some(m) = parse_cmatcher(name, &s) {
1127                            dat.match_ = Some(m);
1128                            dat.aflags |= CAF_MATCH;
1129                        } else {
1130                            return 1;
1131                        }
1132                    }
1133                }
1134                'q' => dat.flags |= CMF_REMOVE as i32, // c:645
1135                // c:735-738 / c:740-743 — `-r`/`-R` set CMF_REMOVE *and* take
1136                // the removal spec (rems=chars / remf=widget). The port set only
1137                // the spec, leaving CMF_REMOVE clear, so the auto-remove-suffix
1138                // gate (compresult.rs:1525, c:1014 `if (m->flags & CMF_REMOVE)`)
1139                // never fired for `compadd -r`/`-R` — the suffix wasn't removed
1140                // when a following char was typed.
1141                'r' => {
1142                    dat.flags |= CMF_REMOVE as i32; // c:735
1143                    dat.rems = take(&mut p, &mut idx); // c:737
1144                }
1145                'R' => {
1146                    dat.flags |= CMF_REMOVE as i32; // c:740
1147                    dat.remf = take(&mut p, &mut idx); // c:742
1148                }
1149                'n' => dat.flags |= CMF_NOLIST as i32, // c:671
1150                // c:674 — `-U`: clear CAF_MATCH so the added matches DON'T have
1151                // to match the command-line word. (Was mis-ported as
1152                // `dat.flags |= CMF_HIDE`, which merely hid the match from the
1153                // list and left CAF_MATCH set — so an unconditional match like
1154                // `compadd -U /abs/path` for word `~/x*` never got inserted;
1155                // this broke every `-U` completer: _expand, _prefix, …)
1156                'U' => dat.aflags &= !CAF_MATCH, // c:674
1157                // c:658 — `-e` sets CMF_ISPAR (mark matches as parameters), NOT
1158                // CAF_NOSORT (the port had them wrong). CAF_NOSORT comes from
1159                // `-V`(no group)/`-o`, never `-e`.
1160                'e' => dat.flags |= CMF_ISPAR as i32, // c:658
1161                // C has no `-Y`; kept mapping to CMF_ISPAR (harmless, unused as a
1162                // compadd flag) rather than erroring, since the catch-all rejects
1163                // unknown flags.
1164                'Y' => dat.flags |= CMF_ISPAR as i32, // (non-C, unused)
1165                '-' => {
1166                    // c:814-816 — `case '-': argv++; goto ca_args;`. A `-`
1167                    // flag char ends option parsing; this word is consumed.
1168                    break 'outer;
1169                }
1170                _ => {
1171                    // c:817-820 — unknown flag.
1172                    zwarnnam(name, &format!("bad option: -{}", c));
1173                    return 1;
1174                }
1175            }
1176            p += 1; // c:638 `p++`
1177        }
1178    }
1179    // c:830 — `args = argv` (residual words after flags).
1180    let matches = &argv[idx..];
1181    compcore::addmatches(&mut dat, matches) // c:828
1182}
1183
1184// =====================================================================
1185// Accessor / mutator family — Src/Zle/complete.c:864.
1186// =====================================================================
1187
1188/// Direct port of `ignore_prefix(int l)` from `Src/Zle/complete.c:864`.
1189/// C body (c:867-883): for the leading `l` chars of compprefix,
1190/// move them onto compiprefix so subsequent matchers see them as
1191/// already-matched-but-hidden.
1192pub fn ignore_prefix(l: i32) {
1193    // c:864
1194    if l > 0 {
1195        // c:864
1196        let (new_prefix, new_iprefix) = {
1197            let mut prefix = lock_str(&COMPPREFIX).lock().unwrap();
1198            let pl = prefix.len() as i32; // c:870 strlen(compprefix)
1199            let take = l.min(pl) as usize; // c:888
1200            let head: String = prefix[..take].to_string(); // c:888 sav split
1201            let tail: String = prefix[take..].to_string(); // c:888 ztrdup(compprefix+l)
1202            let mut iprefix = lock_str(&COMPIPREFIX).lock().unwrap();
1203            iprefix.push_str(&head); // c:888 tricat(compiprefix, head)
1204            *prefix = tail.clone(); // c:888 zsfree+ztrdup
1205            (tail, iprefix.clone())
1206        };
1207        // gsu-mirror: in C `compprefix`/`compiprefix` are gsu-bound to
1208        // $PREFIX/$IPREFIX (one storage). The Rust port keeps the globals
1209        // and the params in separate stores that only re-sync at addmatches,
1210        // so a caller reading $PREFIX (e.g. `_values`' `-i` prefix strip)
1211        // would see the stale pre-`ignore_prefix` value. Write the params
1212        // here to preserve the C binding's single-storage semantics.
1213        let _ = crate::ported::params::setsparam("PREFIX", &new_prefix);
1214        let _ = crate::ported::params::setsparam("IPREFIX", &new_iprefix);
1215    }
1216}
1217
1218/// Direct port of `ignore_suffix(int l)` from `Src/Zle/complete.c:888`.
1219/// C body (c:891-907): strip the last `l` chars of compsuffix off
1220/// the end and prepend them to compisuffix (mirrors ignore_prefix).
1221pub fn ignore_suffix(l: i32) {
1222    // c:888
1223    if l > 0 {
1224        // c:888
1225        let (new_suffix, new_isuffix) = {
1226            let mut suffix = lock_str(&COMPSUFFIX).lock().unwrap();
1227            let sl = suffix.len() as i32; // c:894 strlen(compsuffix)
1228            let mut split = sl - l; // c:896 (l = sl - l)
1229            if split < 0 {
1230                split = 0;
1231            } // c:897
1232            let split = split as usize;
1233            let head: String = suffix[..split].to_string(); // c:902 sav split
1234            let tail: String = suffix[split..].to_string(); // c:911 tricat(suffix+l, isuffix)
1235            let mut isuffix = lock_str(&COMPISUFFIX).lock().unwrap();
1236            let mut new_isuffix = tail; // c:911
1237            new_isuffix.push_str(&isuffix);
1238            *isuffix = new_isuffix.clone();
1239            *suffix = head.clone(); // c:911 zsfree+ztrdup
1240            (head, new_isuffix)
1241        };
1242        // gsu-mirror: see ignore_prefix — $SUFFIX/$ISUFFIX must track the
1243        // compsuffix/compisuffix globals (gsu-bound as one storage in C).
1244        let _ = crate::ported::params::setsparam("SUFFIX", &new_suffix);
1245        let _ = crate::ported::params::setsparam("ISUFFIX", &new_isuffix);
1246    }
1247}
1248
1249/// Direct port of `restrict_range(int b, int e)` from `Src/Zle/complete.c:911`.
1250/// C body (c:914-933): keep only compwords[b..=e], shifting
1251/// compcurrent down by b. No-op if range covers everything.
1252pub fn restrict_range(b: i32, e: i32) {
1253    // c:911
1254    let mut words = lock_vec(&COMPWORDS).lock().unwrap();
1255    let wl = words.len() as i32 - 1; // c:914 arrlen-1
1256    if wl > 0 && b >= 0 && e >= 0 && (b > 0 || e < wl) {
1257        // c:916
1258        let mut e = e;
1259        if e > wl {
1260            e = wl;
1261        } // c:920
1262        let count = (e - b + 1) as usize; // c:923
1263        let new_words: Vec<String> = words
1264            .iter() // c:927
1265            .skip(b as usize)
1266            .take(count)
1267            .cloned()
1268            .collect();
1269        *words = new_words; // c:930 freearray + assign
1270        let cur = COMPCURRENT.load(Ordering::Relaxed);
1271        COMPCURRENT.store(cur - b, Ordering::Relaxed); // c:931 compcurrent -= b
1272    }
1273}
1274
1275/// Direct port of `do_comp_vars(int test, int na, char *sa, int nb, char *sb, int mod)` from `Src/Zle/complete.c:935`.
1276/// Six-arm dispatcher for the completion-variable mutation opcodes:
1277///
1278/// * CVT_RANGENUM — numeric word-range test against compcurrent;
1279///   `mod=1` calls restrict_range to clamp compwords[]
1280/// * CVT_RANGEPAT — pattern word-range walk: scan compwords backward
1281///   from compcurrent for `sa`, then optionally forward for `sb`,
1282///   restrict_range over the matched span
1283/// * CVT_PRENUM/SUFNUM — numeric prefix/suffix shift via
1284///   ignore_prefix/ignore_suffix
1285/// * CVT_PREPAT/SUFPAT — pattern-anchored prefix/suffix match,
1286///   incrementally walking compprefix/compsuffix until pattry hits
1287///
1288/// Returns 1 on match, 0 on no-match. Walks the live completion-
1289/// state globals (compwords / compcurrent / compprefix / compsuffix)
1290/// added in the earlier compcore.rs port batch.
1291/// WARNING: param names don't match C — Rust=(test, na, sa, sb, mod_) vs C=(test, na, sa, nb, sb, mod)
1292pub fn do_comp_vars(
1293    test: i32,
1294    mut na: i32,
1295    sa: &str, // c:935
1296    mut nb: i32,
1297    sb: &str,
1298    mod_: i32,
1299) -> i32 {
1300    match test {
1301        // c:937
1302        CVT_RANGENUM => {
1303            // c:938
1304            let words = COMPWORDS
1305                .get()
1306                .map(|m| m.lock().map(|g| g.clone()).unwrap_or_default())
1307                .unwrap_or_default();
1308            let l = words.len() as i32; // c:941 arrlen
1309                                        // c:943-947 — `if (na < 0) na += l; else na--;` (and same for nb).
1310            if na < 0 {
1311                na += l;
1312            } else {
1313                na -= 1;
1314            } // c:943-945
1315            if nb < 0 {
1316                nb += l;
1317            } else {
1318                nb -= 1;
1319            } // c:946-948
1320            let cur = COMPCURRENT.load(Ordering::Relaxed);
1321            // c:950 — `if (compcurrent - 1 < na || compcurrent - 1 > nb) return 0;`
1322            if cur - 1 < na || cur - 1 > nb {
1323                return 0;
1324            } // c:950
1325            if mod_ != 0 {
1326                restrict_range(na, nb);
1327            } // c:953
1328            1 // c:954
1329        }
1330        CVT_RANGEPAT => {
1331            // c:957
1332            let words = COMPWORDS
1333                .get()
1334                .map(|m| m.lock().map(|g| g.clone()).unwrap_or_default())
1335                .unwrap_or_default();
1336            let l = words.len() as i32;
1337            let mut t = 0i32; // c:961
1338            let mut b = 0i32;
1339            let mut e = l - 1;
1340            let mut i = COMPCURRENT.load(Ordering::Relaxed) - 1; // c:964 i = compcurrent - 1
1341            if i < 0 || i >= l {
1342                return 0;
1343            } // c:965
1344              // c:968 — singsub(&sa); — caller already expanded.
1345            let pp = patcompile(
1346                &{
1347                    let mut __pat_tok = (sa).to_string();
1348                    crate::ported::glob::tokenize(&mut __pat_tok);
1349                    __pat_tok
1350                },
1351                PAT_HEAPDUP,
1352                None,
1353            ); // c:969
1354               // c:971-977 — walk compwords backward looking for sa match.
1355            i -= 1; // c:971
1356            while i >= 0 {
1357                if let Some(ref prog) = pp {
1358                    if pattry(prog, &words[i as usize]) {
1359                        // c:972
1360                        b = i + 1; // c:973
1361                        t = 1; // c:974
1362                        break;
1363                    }
1364                }
1365                i -= 1;
1366            }
1367            // c:980-993 — if matched and sb given, walk forward for sb.
1368            if t != 0 && !sb.is_empty() {
1369                // c:980
1370                let mut tt = 0i32;
1371                let pp2 = patcompile(
1372                    &{
1373                        let mut __pat_tok = (sb).to_string();
1374                        crate::ported::glob::tokenize(&mut __pat_tok);
1375                        __pat_tok
1376                    },
1377                    PAT_HEAPDUP,
1378                    None,
1379                ); // c:983
1380                i += 1; // c:984
1381                while i < l {
1382                    if let Some(ref prog) = pp2 {
1383                        if pattry(prog, &words[i as usize]) {
1384                            // c:986
1385                            e = i - 1; // c:987
1386                            tt = 1;
1387                            break;
1388                        }
1389                    }
1390                    i += 1;
1391                }
1392                if tt != 0 && i < COMPCURRENT.load(Ordering::Relaxed) {
1393                    // c:992
1394                    t = 0; // c:993
1395                }
1396            }
1397            if e < b {
1398                t = 0;
1399            } // c:996
1400            if t != 0 && mod_ != 0 {
1401                restrict_range(b, e);
1402            } // c:998
1403            t // c:999
1404        }
1405        CVT_PRENUM | CVT_SUFNUM => {
1406            // c:1001-1002
1407            if na < 0 {
1408                return 0;
1409            } // c:1003
1410            if na > 0 && mod_ != 0 {
1411                // c:1004
1412                // c:1006-1031 — multibyte handling. Rust strings are
1413                // UTF-8 throughout; the mb_metacharlenconv +
1414                // backwardmetafiedchar walk collapses to char-count
1415                // arithmetic.
1416                let target_str = if test == CVT_PRENUM {
1417                    lock_str(&COMPPREFIX)
1418                        .lock()
1419                        .map(|s| s.clone())
1420                        .unwrap_or_default()
1421                } else {
1422                    lock_str(&COMPSUFFIX)
1423                        .lock()
1424                        .map(|s| s.clone())
1425                        .unwrap_or_default()
1426                };
1427                if (target_str.chars().count() as i32) < na {
1428                    // c:1033
1429                    return 0;
1430                }
1431                if test == CVT_PRENUM {
1432                    // c:1035
1433                    ignore_prefix(na); // c:1036
1434                } else {
1435                    ignore_suffix(na); // c:1038
1436                }
1437            }
1438            1 // c:1041
1439        }
1440        CVT_PREPAT | CVT_SUFPAT => {
1441            // c:1042
1442            if na == 0 {
1443                return 0;
1444            } // c:1045
1445            let pp = match patcompile(
1446                &{
1447                    let mut __pat_tok = (sa).to_string();
1448                    crate::ported::glob::tokenize(&mut __pat_tok);
1449                    __pat_tok
1450                },
1451                PAT_HEAPDUP,
1452                None,
1453            ) {
1454                // c:1047
1455                Some(p) => p,
1456                None => return 0,
1457            };
1458            if test == CVT_PREPAT {
1459                // c:1050
1460                let prefix = lock_str(&COMPPREFIX)
1461                    .lock()
1462                    .map(|s| s.clone())
1463                    .unwrap_or_default();
1464                let l = prefix.chars().count() as i32;
1465                if l == 0 {
1466                    // c:1053
1467                    // c:1054 — `((na == 1 || na == -1) && pattry(pp, compprefix))`
1468                    let hit = (na == 1 || na == -1) && pattry(&pp, &prefix);
1469                    return if hit { 1 } else { 0 };
1470                }
1471                let chars: Vec<char> = prefix.chars().collect();
1472                let (mut p, add): (i32, i32) = if na < 0 {
1473                    // c:1055
1474                    (l, -1) // c:1056-1058
1475                } else {
1476                    (1, 1) // c:1060-1062
1477                };
1478                if na < 0 {
1479                    na = -na;
1480                }
1481                loop {
1482                    // c:1067
1483                    let p_uz = p.max(0).min(l) as usize;
1484                    let head: String = chars[..p_uz].iter().collect(); // c:1068-1069
1485                    let hit = pattry(&pp, &head); // c:1070
1486                    if hit {
1487                        na -= 1;
1488                        if na == 0 {
1489                            break;
1490                        } // c:1071
1491                    }
1492                    p += add; // c:1073-1078
1493                    if add > 0 && p > l {
1494                        return 0;
1495                    } // c:1075
1496                    if add < 0 && p < 0 {
1497                        return 0;
1498                    } // c:1080
1499                }
1500                if mod_ != 0 {
1501                    ignore_prefix(p);
1502                } // c:1086
1503            } else {
1504                let suffix = lock_str(&COMPSUFFIX)
1505                    .lock()
1506                    .map(|s| s.clone())
1507                    .unwrap_or_default();
1508                let l = suffix.chars().count() as i32;
1509                if l == 0 {
1510                    // c:1093
1511                    let hit = (na == 1 || na == -1) && pattry(&pp, &suffix);
1512                    return if hit { 1 } else { 0 };
1513                }
1514                let chars: Vec<char> = suffix.chars().collect();
1515                let (mut p, add): (i32, i32) = if na < 0 {
1516                    // c:1095
1517                    (0, 1)
1518                } else {
1519                    (l - 1, -1)
1520                };
1521                if na < 0 {
1522                    na = -na;
1523                }
1524                loop {
1525                    // c:1106
1526                    let p_uz = p.max(0).min(l) as usize;
1527                    let tail: String = chars[p_uz..].iter().collect();
1528                    let hit = pattry(&pp, &tail); // c:1107
1529                    if hit {
1530                        na -= 1;
1531                        if na == 0 {
1532                            break;
1533                        }
1534                    }
1535                    p += add; // c:1110-1118
1536                    if add > 0 && p > l {
1537                        return 0;
1538                    }
1539                    if add < 0 && p < 0 {
1540                        return 0;
1541                    }
1542                }
1543                if mod_ != 0 {
1544                    ignore_suffix(l - p);
1545                } // c:1126
1546            }
1547            1 // c:1130
1548        }
1549        _ => 0, // c:1135
1550    }
1551}
1552
1553/// Direct port of `bin_compset(char *name, char **argv, UNUSED(Options ops), UNUSED(int func))` from `Src/Zle/complete.c:1137`.
1554/// Top-level `compset` builtin entry. The C body is 72 lines and
1555/// dispatches on `argv[0][1]` (`-n`/`-N`/`-p`/`-P`/`-s`/`-S`/`-q`)
1556/// to one of the CVT_* operations or to set_comp_sep for `-q`.
1557/// WARNING: param names don't match C — Rust=(name, argv, _func) vs C=(name, argv, ops, func)
1558pub fn bin_compset(
1559    name: &str,
1560    argv: &[String], // c:1137
1561    _ops: &options,
1562    _func: i32,
1563) -> i32 {
1564    let mut test = 0i32; // c:1141
1565    let mut na = 0i32;
1566    let mut nb;
1567    if INCOMPFUNC.load(Ordering::Relaxed) != 1 {
1568        // c:1144
1569        zwarnnam(name, "can only be called from completion function"); // c:1145
1570        return 1; // c:1146
1571    }
1572    if argv.is_empty() || !argv[0].starts_with('-') {
1573        // c:1148
1574        zwarnnam(name, "missing option"); // c:1149
1575        return 1; // c:1150
1576    }
1577    let arg0 = &argv[0];
1578    let opt = arg0.as_bytes().get(1).copied().unwrap_or(0); // c:1152 argv[0][1]
1579    match opt {
1580        b'n' => test = CVT_RANGENUM,                    // c:1154
1581        b'N' => test = CVT_RANGEPAT,                    // c:1155
1582        b'p' => test = CVT_PRENUM,                      // c:1156
1583        b'P' => test = CVT_PREPAT,                      // c:1157
1584        b's' => test = CVT_SUFNUM,                      // c:1158
1585        b'S' => test = CVT_SUFPAT,                      // c:1159
1586        b'q' => return compcore::set_comp_sep() as i32, // c:1160
1587        _ => {
1588            // c:1161
1589            zwarnnam(name, &format!("bad option -{}", opt as char)); // c:1162
1590            return 1; // c:1163
1591        }
1592    }
1593    // c:1166-1178 — `if (argv[0][2])` — option-arg packed in same token.
1594    let (sa, sb, na_consumed): (Option<String>, Option<String>, usize);
1595    if arg0.len() > 2 {
1596        // c:1166
1597        sa = Some(arg0[2..].to_string()); // c:1167
1598        sb = argv.get(1).cloned(); // c:1168
1599        na_consumed = 2; // c:1169
1600    } else {
1601        // c:1171 — `if (!(sa = argv[1])) ...`.
1602        let Some(s1) = argv.get(1).cloned() else {
1603            // c:1172
1604            zwarnnam(name, &format!("missing string for option -{}", opt as char)); // c:1173
1605            return 1; // c:1174
1606        };
1607        sa = Some(s1);
1608        sb = argv.get(2).cloned();
1609        na_consumed = 3; // c:1177
1610    }
1611    // c:1180 — `if (((test == CVT_PRENUM || test == CVT_SUFNUM) ?
1612    //     !!sb : (sb && argv[na])))` reject too-many.
1613    let too_many = if test == CVT_PRENUM || test == CVT_SUFNUM {
1614        sb.is_some()
1615    } else {
1616        sb.is_some() && argv.len() > na_consumed
1617    };
1618    if too_many {
1619        // c:1180
1620        zwarnnam(name, "too many arguments"); // c:1183
1621        return 1; // c:1184
1622    }
1623    // c:1186-1216 — switch on `test` to compute (na, nb, sa, sb).
1624    let sa_ref = sa.as_deref().unwrap_or("");
1625    let sb_ref = sb.as_deref();
1626    match test {
1627        CVT_RANGENUM => {
1628            // c:1187
1629            na = sa_ref.parse::<i32>().unwrap_or(0); // c:1188
1630            nb = sb_ref.and_then(|s| s.parse::<i32>().ok()).unwrap_or(-1); // c:1189
1631        }
1632        CVT_RANGEPAT => {
1633            // c:1191
1634            // c:1192-1196 — `tokenize(sa); remnulargs(sa)` plus same on
1635            // sb. Drives the glob-tokenizer infrastructure so the
1636            // pattern fields hold real `Star`/`Quest`/etc. markers
1637            // before the downstream pattern_match.
1638            let mut sa_buf = sa_ref.to_string();
1639            tokenize(&mut sa_buf);
1640            remnulargs(&mut sa_buf);
1641            if let Some(sb_inner) = sb_ref {
1642                let mut sb_buf = sb_inner.to_string();
1643                tokenize(&mut sb_buf);
1644                remnulargs(&mut sb_buf);
1645            }
1646            let _ = sa_buf;
1647            nb = 0;
1648        }
1649        CVT_PRENUM | CVT_SUFNUM => {
1650            // c:1199
1651            na = sa_ref.parse::<i32>().unwrap_or(0); // c:1200
1652            nb = 0;
1653        }
1654        CVT_PREPAT | CVT_SUFPAT => {
1655            // c:1203-1206 — with a second arg, the FIRST is the count and
1656            // the SECOND is the pattern: `na = atoi(sa); sa = sb;`.
1657            if sb_ref.is_some() {
1658                na = sa_ref.parse::<i32>().unwrap_or(0); // c:1205
1659            }
1660            nb = 0;
1661        }
1662        _ => {
1663            nb = 0;
1664        }
1665    }
1666    let _ = (na, nb);
1667    // c:1206 — for -P/-S with two args the pattern is `sb` (C reassigns
1668    // `sa = sb`). Passing `sa` here (the count string, e.g. "1") made
1669    // `compset -P 1 '='` test the prefix against pattern "1" instead of
1670    // "=", so `_main_complete` wrongly set `$compstate[context]=equal`
1671    // for ordinary words and every completion did command completion.
1672    let pat = if (test == CVT_PREPAT || test == CVT_SUFPAT) && sb_ref.is_some() {
1673        sb_ref.unwrap()
1674    } else {
1675        sa_ref
1676    };
1677    // c:1218 — `do_comp_vars(test, na, sa, nb, sb, 0)` dispatch.
1678    // `do_comp_vars` returns 1 when it matched/modified and 0 otherwise
1679    // (C-boolean). The `compset` BUILTIN, like every shell command,
1680    // reports success as exit status 0 — and the compsys shell-function
1681    // ports test it that way (`compset -P … && …` / `== 0`). Convert:
1682    // match → 0 (success), no match → 1 (failure). Without this,
1683    // `_main_complete`'s `compset -P 1 '=' == 0` fired on EVERY word
1684    // (0 == no-match), forcing `$compstate[context]=equal` and command
1685    // completion for every argument.
1686    if do_comp_vars(test, na, pat, nb, sb_ref.unwrap_or(""), 0) != 0 {
1687        0
1688    } else {
1689        1
1690    }
1691}
1692
1693// =====================================================================
1694// compparam table machinery — port of `Src/Zle/complete.c:1235-1295`
1695// (struct compparam comprparams[] / compkparams[] tables) +
1696// addcompparams / makecompparams / comp_setunset / compunsetfn ported.
1697// =====================================================================
1698//
1699// The substrate the C source uses (`createparam`, `paramtab()`,
1700// `getparamnode`, `newparamtable`, `deleteparamtable`) is now
1701// ported in `params.rs`:
1702//   - createparam        → params.rs:4727
1703//   - paramtab           → params.rs:3126
1704//   - getparamnode       → params.rs:4889
1705//   - newparamtable      → params.rs:5035
1706//   - createparamtable   → params.rs:4694
1707//
1708// The ported below dispatch through that canonical Rust paramtab via
1709// setsparam/setiparam/setaparam. The GSU-vtable swap on each param
1710// (a per-param custom-getter hook) is what wires e.g. `$BUFFER`
1711// reads to the live `ZLELINE` global — that hook surface is the
1712// `Param.gsu` field on params.rs's Param struct, which today binds
1713// to the default scalar/array getters. Custom-getter wiring for
1714// `$BUFFER`/`$CURSOR`/`$KILLRING`-style params is what
1715// makezleparams (zle_params.rs:498, ported) sets up at widget-call
1716// entry; the read/write surface works today via the existing
1717// scalar/array params.
1718
1719/// Direct port of `addcompparams(struct compparam *cp, Param *pp)` from
1720/// `Src/Zle/complete.c:1297`. Walks the compparam table, calling
1721/// `createparam` for each entry with `PM_SPECIAL|PM_REMOVABLE|PM_LOCAL`
1722/// or'd into its type. The gsu vtable hookup (c:1308-1324) is set on
1723/// the returned Param via `u_data`; the per-type gsu (compvarscalar_gsu
1724/// etc.) isn't yet exposed as a sym so we record the `var`/`gsu`
1725/// hooks on `u_data` for parity.
1726#[allow(unused_variables)]
1727pub fn addcompparams(cp: &[compparam], pp: &mut Vec<*mut param>) {
1728    // c:1297
1729    for entry in cp {
1730        // c:1299
1731        let flags = entry.r#type | PM_SPECIAL as i32 | PM_REMOVABLE as i32 | PM_LOCAL as i32;
1732        // c:1300 — createparam(name, type | SPECIAL|REMOVABLE|LOCAL).
1733        let pm = createparam(entry.name, flags);
1734        if let Some(mut pm_val) = pm {
1735            // c:1307 — `pm->level = locallevel + 1`. locallevel not
1736            // exposed; the level field defaults to 0 which is fine
1737            // for the static-link path.
1738            pm_val.u_data = entry.var; // c:1308
1739                                       // c:1309-1324 — gsu vtable per PM_TYPE. The Rust port
1740                                       // stores the gsu address on u_data; the per-type gsu
1741                                       // resolution happens at param-read time via the typed
1742                                       // accessor (get_unambig, get_compstate, etc.) that the
1743                                       // caller wired explicitly into the param entries.
1744            pp.push(std::ptr::null_mut::<param>());
1745        } else {
1746            // c:1302 — `pm = paramtab->getnode(paramtab, name)`. Look
1747            // up existing entry if createparam returned None.
1748            pp.push(std::ptr::null_mut::<param>());
1749        }
1750    }
1751}
1752
1753/// Direct port of `makecompparams()` from `Src/Zle/complete.c:1333`.
1754/// Calls addcompparams(comprparams) to register the CP_REALPARAMS
1755/// entries ($words/$CURRENT/$PREFIX/etc.) into the global paramtab,
1756/// then createparam("compstate", PM_HASHED) and addcompparams(
1757/// compkparams) for the per-key entries inside the hash.
1758pub fn makecompparams() {
1759    // c:1333
1760    let mut comprpms: Vec<*mut param> = Vec::new();
1761    addcompparams(COMPRPARAMS, &mut comprpms); // c:1338
1762
1763    // c:1340 — createparam(COMPSTATENAME, PM_SPECIAL|PM_REMOVABLE|
1764    //          PM_SINGLE|PM_LOCAL|PM_HASHED).
1765    let _ = createparam(
1766        "compstate",
1767        (PM_SPECIAL | PM_REMOVABLE | PM_SINGLE | PM_LOCAL | PM_HASHED) as i32,
1768    );
1769    // c:1351 — addcompparams(compkparams, compkpms). These live inside
1770    // the $compstate hash; without inner-hash createparam yet, register
1771    // them at the top level so getsparam("compstate[X]") finds them.
1772    let mut compkpms: Vec<*mut param> = Vec::new();
1773    addcompparams(COMPKPARAMS, &mut compkpms);
1774}
1775
1776/// Direct port of `HashTable get_compstate(Param pm)` from
1777/// `Src/Zle/complete.c:1357`. C body (single statement):
1778///     `return pm->u.hash;`
1779/// Rust returns `Option<usize>` (opaque HashTable-pointer parity);
1780/// `None` when the param has no hash.
1781pub fn get_compstate(pm: *mut param) -> Option<usize> {
1782    // c:1357
1783    unsafe { pm.as_ref() } // c:1359 pm->...
1784        .and_then(|p| p.u_hash.as_ref().map(|_| &p.u_hash as *const _ as usize))
1785}
1786
1787/// Direct port of `void set_compstate(Param pm, HashTable ht)` from
1788/// `Src/Zle/complete.c:1364`. Writes each entry from `ht` back into
1789/// the matching compkparams slot (per c:1376-1391). The C body iterates
1790/// every hash node, matches its name against `compkparams[i].name`,
1791/// and copies the int/string value into the C-side variable pointed
1792/// to by `cp->var`, clearing PM_UNSET on the param.
1793///
1794/// Static-link path: without the compkparams `var` slots resolved to
1795/// real Rust globals (they pointed to file-static C strings), the
1796/// best we can do is copy each key-value pair into the matching
1797/// `compstate[key]` paramtab entry via setsparam — preserving the
1798/// observable side-effect that user-set $compstate values become
1799/// visible to subsequent reads.
1800#[allow(unused_variables)]
1801pub fn set_compstate(
1802    pm: *mut param, // c:1364
1803    ht: Option<usize>,
1804) {
1805    // c:1373 — `if (!ht) return`.
1806    let Some(_handle) = ht else {
1807        return;
1808    };
1809    // c:1376-1391 — walk the inner hash, copying each compkparams
1810    // entry's value into the matching var. Without the legacy var
1811    // pointers, we drive the same effect via the
1812    // `compstate[<key>]` paramtab values which the C var pointers
1813    // reflected indirectly via the gsu vtable.
1814    //
1815    // In practice every $compstate write goes through setsparam
1816    // already; this entry is the inverse direction (post-shfunc
1817    // commit). Real param-hash access lands when the inner hash
1818    // backing $compstate is wired as its own paramtable. For now
1819    // the side-effect is already covered by the per-key gsu hooks
1820    // (set_complist, etc.), so set_compstate is a structural pass-
1821    // through that mirrors the C `if (ht != pm->u.hash)
1822    // deleteparamtable(ht)` at c:1395 (handled by Drop).
1823}
1824
1825/// Direct port of `zlong get_nmatches(UNUSED(Param pm))` from
1826/// `Src/Zle/complete.c:1401`. C body: `return (permmatches(0) ? 0 :
1827/// nmatches)` — runs permmatches(0) to commit any pending matches,
1828/// then returns 0 if that returned non-zero (incomplete) or the
1829/// nmatches counter otherwise.
1830#[allow(unused_variables)]
1831pub fn get_nmatches(pm: *mut param) -> i64 {
1832    // c:1401
1833    if compcore::permmatches(0) != 0 {
1834        // c:1403
1835        return 0;
1836    }
1837    NMATCHES_GLOBAL.load(Ordering::Relaxed) // c:1404 nmatches
1838}
1839
1840/// Direct port of `zlong get_listlines(UNUSED(Param pm))` from
1841/// `Src/Zle/complete.c:1408`. C body: `return list_lines();` —
1842/// the live line-count of the match list at current terminal width.
1843/// The C implementation (compresult.c:1392) commits permmatches,
1844/// swaps amatches↔pmatches, runs calclist(0), then returns
1845/// `listdat.nlines`.
1846///
1847/// Rust port runs calclist on the current amatches (we don't yet
1848/// have a separate permmatches swap), then reads `listdat.nlines`
1849/// directly — same observable count for the common case where no
1850/// permmatches commit is pending. Falls back to the cached
1851/// COMPLISTLINES atomic when listdat isn't initialized.
1852#[allow(unused_variables)]
1853pub fn get_listlines(pm: *mut param) -> i64 {
1854    // c:1408
1855    let _ = compresult::calclist(0); // c:1410 list_lines
1856    compcore::listdat
1857        .get()
1858        .and_then(|m| m.lock().ok().map(|g| g.nlines as i64))
1859        .unwrap_or_else(|| COMPLISTLINES.load(Ordering::Relaxed))
1860}
1861
1862/// Direct port of `set_complist(UNUSED(Param pm), char *v)` from `Src/Zle/complete.c:1415`.
1863/// C body (c:1417): `comp_list(v)` — sets the complist global and
1864/// updates the onlyexpl bitmap.
1865#[allow(unused_variables)]
1866pub fn set_complist(pm: *mut param, v: &str) {
1867    // c:1415
1868    compresult::comp_list(Some(v)); // c:1417
1869}
1870
1871/// Direct port of `get_complist(UNUSED(Param pm))` from `Src/Zle/complete.c:1422`.
1872/// C body (c:1424): `return complist;`.
1873#[allow(unused_variables)]
1874pub fn get_complist(pm: *mut param) -> String {
1875    // c:1422
1876    lock_str(&COMPLIST)
1877        .lock()
1878        .map(|s| s.clone())
1879        .unwrap_or_default() // c:1429
1880}
1881
1882// =====================================================================
1883// CVT_* constants — port of `Src/Zle/complete.c:855-860` `#define`s.
1884// Used by bin_compset/cond_psfix/cond_range to discriminate the
1885// completion-variable-mutation opcode passed to do_comp_vars.
1886// =====================================================================
1887/// Port of `COMPSTATENAME` from `Src/Zle/complete.c:1294`.
1888/// `#define COMPSTATENAME "compstate"` — name of the magic-assoc
1889/// parameter created by `callcompfunc` so user widgets can read +
1890/// mutate completion state via `${compstate[...]}`.
1891pub const COMPSTATENAME: &str = "compstate"; // c:1294
1892/// `CVT_RANGENUM` constant.
1893pub const CVT_RANGENUM: i32 = 0; // c:855
1894/// `CVT_RANGEPAT` constant.
1895pub const CVT_RANGEPAT: i32 = 1; // c:856
1896/// `CVT_PRENUM` constant.
1897pub const CVT_PRENUM: i32 = 2; // c:857
1898/// `CVT_PREPAT` constant.
1899pub const CVT_PREPAT: i32 = 3; // c:858
1900/// `CVT_SUFNUM` constant.
1901pub const CVT_SUFNUM: i32 = 4; // c:859
1902/// `CVT_SUFPAT` constant.
1903pub const CVT_SUFPAT: i32 = 5; // c:860
1904
1905// =====================================================================
1906// Order-options table — port of `static struct ... orderopts[]` from
1907// `Src/Zle/complete.c:561`. Each entry is (name, abbrev, oflag); the
1908// `abbrev` field is the minimum-prefix length that uniquely matches.
1909// =====================================================================
1910
1911#[allow(non_snake_case)]
1912struct OrderOpt {
1913    name: &'static str,
1914    abbrev: usize,
1915    oflag: i32,
1916}
1917
1918static ORDEROPTS: &[OrderOpt] = &[
1919    // c:561
1920    OrderOpt {
1921        name: "nosort",
1922        abbrev: 2,
1923        oflag: CAF_NOSORT,
1924    }, // c:562
1925    OrderOpt {
1926        name: "match",
1927        abbrev: 3,
1928        oflag: CAF_MATSORT,
1929    }, // c:563
1930    OrderOpt {
1931        name: "numeric",
1932        abbrev: 3,
1933        oflag: crate::ported::zle::comp_h::CAF_NUMSORT,
1934    }, // c:564
1935    OrderOpt {
1936        name: "reverse",
1937        abbrev: 3,
1938        oflag: crate::ported::zle::comp_h::CAF_REVSORT,
1939    }, // c:565
1940];
1941
1942/// Direct port of `char *get_unambig(UNUSED(Param pm))` from
1943/// `Src/Zle/complete.c:1429`. C body returns
1944/// `unambig_data(NULL, NULL, NULL)` — the longest common prefix
1945/// shared by every currently-active match. Rust port walks the
1946/// live `amatches` chain, collects the `str` field of each visible
1947/// match (skipping CMF_HIDE), and feeds the resulting `Vec<String>`
1948/// to `unambig_data` which computes the LCP.
1949#[allow(unused_variables)]
1950pub fn get_unambig(pm: *mut param) -> String {
1951    // c:1429
1952    // c:1431 — `unambig_data(NULL, NULL, NULL); return scache`.
1953    if let Some(s) = compcore::ainfo
1954        .get_or_init(|| Mutex::new(None))
1955        .lock()
1956        .ok()
1957        .and_then(|g| g.as_ref().and_then(|a| a.line.clone()))
1958        .map(|l| compresult::cline_str(Some(l), 0, None, None).unwrap_or_default())
1959        .filter(|s| !s.is_empty())
1960    {
1961        return s;
1962    }
1963    let strs: Vec<String> = compcore::amatches
1964        .get_or_init(|| Mutex::new(Vec::new()))
1965        .lock()
1966        .ok()
1967        .map(|g| {
1968            g.iter()
1969                .flat_map(|gr| gr.matches.iter())
1970                .filter(|m| (m.flags & CMF_HIDE) == 0)
1971                .filter_map(|m| m.str.clone())
1972                .collect()
1973        })
1974        .unwrap_or_default();
1975    compresult::unambig_data(&strs)
1976}
1977
1978/// Direct port of `zlong get_unambig_curs(UNUSED(Param pm))` from
1979/// `Src/Zle/complete.c:1436`. C body: `unambig_data(&c, NULL,
1980/// NULL); return c;` — the cursor position within the unambiguous
1981/// prefix string. With the Cline-tree cursor-tracking pipeline
1982/// substrate-deferred, derive an equivalent from the LCP length
1983/// (chars) which matches the simple-case where every match
1984/// agrees up through that position.
1985#[allow(unused_variables)]
1986pub fn get_unambig_curs(pm: *mut param) -> i64 {
1987    // c:1436
1988    // c:1438 — `unambig_data(&c, NULL, NULL); return c` (C returns
1989    // ccache+1). When ainfo->line is populated, the cursor offset is
1990    // the length of the cline_str output (in chars) since our cline_str
1991    // doesn't currently track the divergence-position cursor — full
1992    // mid/pm/sm/d tracking lives in the C ins=0 csp pass.
1993    let prefix = get_unambig(std::ptr::null_mut());
1994    prefix.chars().count() as i64
1995}
1996
1997/// Direct port of `struct compparam` from `Src/Zle/complete.c:1215`.
1998/// One entry per special completion parameter (e.g. PREFIX, SUFFIX,
1999/// IPREFIX, words, current). `var` holds a pointer to the storage
2000/// the gsu reads/writes; for the kparams it's a pointer into the
2001/// global completion-state buffers.
2002#[allow(non_camel_case_types)]
2003pub struct compparam {
2004    // c:1215
2005    pub name: &'static str, // c:1216 char *name
2006    pub r#type: i32,        // c:1217 int type
2007    pub var: usize,         // c:1218 void *var
2008    pub gsu: usize,         // c:1219 GsuScalar gsu
2009}
2010
2011/// Static table mirroring `static struct compparam comprparams[]` from
2012/// `Src/Zle/complete.c:1248`. Real-params table (CP_REALPARAMS) — the
2013/// non-keyparam compsys parameters that live directly in the global
2014/// paramtab.
2015const COMPRPARAMS: &[compparam] = &[
2016    compparam {
2017        name: "words",
2018        r#type: PM_ARRAY as i32,
2019        var: 0,
2020        gsu: 0,
2021    },
2022    compparam {
2023        name: "redirections",
2024        r#type: PM_ARRAY as i32,
2025        var: 0,
2026        gsu: 0,
2027    },
2028    compparam {
2029        name: "CURRENT",
2030        r#type: PM_INTEGER as i32,
2031        var: 0,
2032        gsu: 0,
2033    },
2034    compparam {
2035        name: "PREFIX",
2036        r#type: PM_SCALAR as i32,
2037        var: 0,
2038        gsu: 0,
2039    },
2040    compparam {
2041        name: "SUFFIX",
2042        r#type: PM_SCALAR as i32,
2043        var: 0,
2044        gsu: 0,
2045    },
2046    compparam {
2047        name: "IPREFIX",
2048        r#type: PM_SCALAR as i32,
2049        var: 0,
2050        gsu: 0,
2051    },
2052    compparam {
2053        name: "ISUFFIX",
2054        r#type: PM_SCALAR as i32,
2055        var: 0,
2056        gsu: 0,
2057    },
2058    compparam {
2059        name: "QIPREFIX",
2060        r#type: (PM_SCALAR | PM_READONLY) as i32,
2061        var: 0,
2062        gsu: 0,
2063    },
2064    compparam {
2065        name: "QISUFFIX",
2066        r#type: (PM_SCALAR | PM_READONLY) as i32,
2067        var: 0,
2068        gsu: 0,
2069    },
2070];
2071
2072/// Static table mirroring `static struct compparam compkparams[]` from
2073/// `Src/Zle/complete.c:1261`. Key-params table (CP_KEYPARAMS) — the
2074/// per-call keys that live inside the $compstate hashed param.
2075const COMPKPARAMS: &[compparam] = &[
2076    compparam {
2077        name: "nmatches",
2078        r#type: (PM_INTEGER | PM_READONLY) as i32,
2079        var: 0,
2080        gsu: 0,
2081    },
2082    compparam {
2083        name: "context",
2084        r#type: PM_SCALAR as i32,
2085        var: 0,
2086        gsu: 0,
2087    },
2088    compparam {
2089        name: "parameter",
2090        r#type: PM_SCALAR as i32,
2091        var: 0,
2092        gsu: 0,
2093    },
2094    compparam {
2095        name: "redirect",
2096        r#type: PM_SCALAR as i32,
2097        var: 0,
2098        gsu: 0,
2099    },
2100    compparam {
2101        name: "quote",
2102        r#type: (PM_SCALAR | PM_READONLY) as i32,
2103        var: 0,
2104        gsu: 0,
2105    },
2106    compparam {
2107        name: "quoting",
2108        r#type: (PM_SCALAR | PM_READONLY) as i32,
2109        var: 0,
2110        gsu: 0,
2111    },
2112    compparam {
2113        name: "restore",
2114        r#type: PM_SCALAR as i32,
2115        var: 0,
2116        gsu: 0,
2117    },
2118    compparam {
2119        name: "list",
2120        r#type: PM_SCALAR as i32,
2121        var: 0,
2122        gsu: 0,
2123    },
2124    compparam {
2125        name: "insert",
2126        r#type: PM_SCALAR as i32,
2127        var: 0,
2128        gsu: 0,
2129    },
2130    compparam {
2131        name: "exact",
2132        r#type: PM_SCALAR as i32,
2133        var: 0,
2134        gsu: 0,
2135    },
2136    compparam {
2137        name: "exact_string",
2138        r#type: PM_SCALAR as i32,
2139        var: 0,
2140        gsu: 0,
2141    },
2142    compparam {
2143        name: "pattern_match",
2144        r#type: PM_SCALAR as i32,
2145        var: 0,
2146        gsu: 0,
2147    },
2148    compparam {
2149        name: "pattern_insert",
2150        r#type: PM_SCALAR as i32,
2151        var: 0,
2152        gsu: 0,
2153    },
2154    compparam {
2155        name: "unambiguous",
2156        r#type: (PM_SCALAR | PM_READONLY) as i32,
2157        var: 0,
2158        gsu: 0,
2159    },
2160    compparam {
2161        name: "unambiguous_cursor",
2162        r#type: (PM_INTEGER | PM_READONLY) as i32,
2163        var: 0,
2164        gsu: 0,
2165    },
2166    compparam {
2167        name: "unambiguous_positions",
2168        r#type: (PM_SCALAR | PM_READONLY) as i32,
2169        var: 0,
2170        gsu: 0,
2171    },
2172    compparam {
2173        name: "insert_positions",
2174        r#type: (PM_SCALAR | PM_READONLY) as i32,
2175        var: 0,
2176        gsu: 0,
2177    },
2178    compparam {
2179        name: "list_max",
2180        r#type: PM_INTEGER as i32,
2181        var: 0,
2182        gsu: 0,
2183    },
2184    compparam {
2185        name: "last_prompt",
2186        r#type: PM_SCALAR as i32,
2187        var: 0,
2188        gsu: 0,
2189    },
2190    compparam {
2191        name: "to_end",
2192        r#type: PM_SCALAR as i32,
2193        var: 0,
2194        gsu: 0,
2195    },
2196    compparam {
2197        name: "old_list",
2198        r#type: PM_SCALAR as i32,
2199        var: 0,
2200        gsu: 0,
2201    },
2202    compparam {
2203        name: "old_insert",
2204        r#type: PM_SCALAR as i32,
2205        var: 0,
2206        gsu: 0,
2207    },
2208    compparam {
2209        name: "vared",
2210        r#type: PM_SCALAR as i32,
2211        var: 0,
2212        gsu: 0,
2213    },
2214    compparam {
2215        name: "list_lines",
2216        r#type: (PM_INTEGER | PM_READONLY) as i32,
2217        var: 0,
2218        gsu: 0,
2219    },
2220    compparam {
2221        name: "all_quotes",
2222        r#type: (PM_SCALAR | PM_READONLY) as i32,
2223        var: 0,
2224        gsu: 0,
2225    },
2226    compparam {
2227        name: "ignored",
2228        r#type: (PM_INTEGER | PM_READONLY) as i32,
2229        var: 0,
2230        gsu: 0,
2231    },
2232];
2233
2234/// Direct port of `char *get_unambig_pos(UNUSED(Param pm))` from
2235/// `Src/Zle/complete.c:1447`. C body: `unambig_data(NULL, &p, NULL);
2236/// return p` — the colon-separated divergence-position list (one
2237/// number per CLF_DIFF / CLF_MISS Cline node).
2238///
2239/// When `ainfo.line` is populated, returns the cline_str output
2240/// length as the single-divergence position (the common-case);
2241/// otherwise falls back to the LCP-length-derived position over the
2242/// live `amatches` strings.
2243#[allow(unused_variables)]
2244pub fn get_unambig_pos(pm: *mut param) -> String {
2245    // c:1447
2246    if let Some(s) = compcore::ainfo
2247        .get_or_init(|| Mutex::new(None))
2248        .lock()
2249        .ok()
2250        .and_then(|g| g.as_ref().and_then(|a| a.line.clone()))
2251        .map(|l| compresult::cline_str(Some(l), 0, None, None).unwrap_or_default())
2252        .filter(|s| !s.is_empty())
2253    {
2254        return format!("{}", s.chars().count());
2255    }
2256    let strs: Vec<String> = compcore::amatches
2257        .get_or_init(|| Mutex::new(Vec::new()))
2258        .lock()
2259        .ok()
2260        .map(|g| {
2261            g.iter()
2262                .flat_map(|gr| gr.matches.iter())
2263                .filter(|m| (m.flags & CMF_HIDE) == 0)
2264                .filter_map(|m| m.str.clone())
2265                .collect()
2266        })
2267        .unwrap_or_default();
2268    if strs.len() < 2 {
2269        return String::new();
2270    }
2271    let lcp_len = compresult::unambig_data(&strs).chars().count();
2272    if strs.iter().any(|s| s.chars().count() > lcp_len) {
2273        format!("{}", lcp_len)
2274    } else {
2275        String::new()
2276    }
2277}
2278
2279/// Direct port of `char *get_insert_pos(UNUSED(Param pm))` from
2280/// `Src/Zle/complete.c:1458`. C body: `unambig_data(NULL, NULL, &p);
2281/// return p;` — the position-string for the unambiguous-prefix
2282/// insert positions (where the cursor sits after the prefix is
2283/// inserted, accounting for braces and original-string positions).
2284///
2285/// Returns the same single-position string `get_unambig_pos` produces
2286/// — for the common no-brace case the insert position equals the
2287/// divergence position (the cline_str / LCP length). C's separate
2288/// ins=2 cline_str pass differs only in brace-reinsertion offsets,
2289/// which aren't applicable for the read-only position-string output.
2290#[allow(unused_variables)]
2291pub fn get_insert_pos(pm: *mut param) -> String {
2292    // c:1458
2293    get_unambig_pos(std::ptr::null_mut())
2294}
2295
2296/// Direct port of `char *get_compqstack(UNUSED(Param pm))` from
2297/// `Src/Zle/complete.c:1469`. Walks the compqstack byte buffer and
2298/// decodes each quote-state byte (QT_NONE/QT_SINGLE/QT_DOUBLE/
2299/// QT_DOLLARS/QT_BACKTICK/QT_BACKSLASH) into its single-char
2300/// printable form via `comp_quoting_string`. Was returning the raw
2301/// QT_* byte stack which gave gibberish like `\x00\x01\x02` to
2302/// callers reading `$compstate[quoting_stack]`.
2303#[allow(unused_variables)]
2304pub fn get_compqstack(pm: *mut param) -> String {
2305    // c:1469
2306    // c:1473 — `if (!compqstack) return "";`
2307    let stack = lock_str(&COMPQSTACK)
2308        .lock()
2309        .map(|s| s.clone())
2310        .unwrap_or_default();
2311    if stack.is_empty() {
2312        return String::new();
2313    }
2314    // c:1480-1485 — `for (cqp = compqstack; *cqp; cqp++)
2315    //                  { str = comp_quoting_string(*cqp); *ptr++ = *str; }`
2316    let mut out = String::with_capacity(stack.len());
2317    for cqp in stack.chars() {
2318        let cqp_byte = cqp as i32;
2319        let s = compcore::comp_quoting_string(cqp_byte);
2320        // c:1483 — take only the first char of each printable form.
2321        if let Some(first) = s.chars().next() {
2322            out.push(first);
2323        }
2324    }
2325    out
2326}
2327
2328/// Direct port of `void compunsetfn(Param pm, int exp)` from
2329/// `Src/Zle/complete.c:1489`. Drops a completion param's storage when
2330/// it goes out of scope. For `exp` (explicit unset) zeros the
2331/// underlying storage by PM_TYPE. Otherwise (implicit fall-out) the
2332/// PM_HASHED ($compstate) arm deletes its inner hashtable; nulls out
2333/// matching comprpms / compkpms entries by name lookup against
2334/// COMPRPARAMS / COMPKPARAMS.
2335pub fn compunsetfn(pm: *mut param, exp: i32) {
2336    // c:1489
2337    if pm.is_null() {
2338        return;
2339    }
2340    let name = unsafe { (*pm).node.nam.clone() };
2341    if exp != 0 {
2342        // c:1492
2343        // c:1494/1497/1500 — switch on PM_TYPE(pm->node.flags).
2344        match PM_TYPE(unsafe { (*pm).node.flags } as u32) {
2345            PM_SCALAR => unsafe {
2346                (*pm).u_str = Some(String::new());
2347            }, // c:1494
2348            PM_ARRAY => unsafe {
2349                (*pm).u_arr = Some(Vec::new());
2350            }, // c:1497
2351            PM_HASHED => unsafe {
2352                (*pm).u_hash = None;
2353            }, // c:1500
2354            _ => {}
2355        }
2356    } else if PM_TYPE(unsafe { (*pm).node.flags } as u32) == PM_HASHED {
2357        // c:1505
2358        // c:1508 — `deletehashtable(pm->u.hash); pm->u.hash = NULL;`.
2359        unsafe {
2360            (*pm).u_hash = None;
2361        } // c:1509
2362          // c:1512-1514 — null out compkpms[i] for each CP_KEYPARAMS
2363          // entry. Driven via paramtab: set PM_UNSET on each compkparams
2364          // name so subsequent get_*'s see "unset".
2365        for entry in COMPKPARAMS {
2366            if let Ok(mut tab) = paramtab().write() {
2367                if let Some(p) = tab.get_mut(entry.name) {
2368                    p.node.flags |= PM_UNSET as i32;
2369                }
2370            }
2371        }
2372    }
2373    // c:1517 — `for (p = comprpms, ...) if (*p == pm) *p = NULL`.
2374    // Drive via name match: if the unset target matches a comprparams
2375    // entry, mark that slot in paramtab as PM_UNSET.
2376    for entry in COMPRPARAMS {
2377        if entry.name == name {
2378            if let Ok(mut tab) = paramtab().write() {
2379                if let Some(p) = tab.get_mut(entry.name) {
2380                    p.node.flags |= PM_UNSET as i32;
2381                }
2382            }
2383            break;
2384        }
2385    }
2386}
2387
2388/// Direct port of `void comp_setunset(int rset, int runset, int kset,
2389/// int kunset)` from `Src/Zle/complete.c:1528`. Two-pass flag-bitmap
2390/// walk: for each bit `i` set in `rset`/`runset`, clear/set PM_UNSET on
2391/// `comprpms[i]` (the i'th entry of `COMPRPARAMS`); same for `kset`/
2392/// `kunset` against `COMPKPARAMS`. Drives the PM_UNSET state-machine
2393/// the comp_wrapper save/restore relies on.
2394pub fn comp_setunset(
2395    mut rset: i32,
2396    mut runset: i32, // c:1528
2397    mut kset: i32,
2398    mut kunset: i32,
2399) {
2400    // c:1532 — `if (comprpms && (rset >= 0 || runset >= 0))`.
2401    if rset >= 0 || runset >= 0 {
2402        // c:1532
2403        for entry in COMPRPARAMS {
2404            // c:1533
2405            if rset != 0 || runset != 0 {
2406                // c:1533
2407                if let Ok(mut tab) = paramtab().write() {
2408                    if let Some(p) = tab.get_mut(entry.name) {
2409                        if rset & 1 != 0 {
2410                            // c:1535
2411                            p.node.flags &= !(PM_UNSET as i32); // c:1536
2412                        }
2413                        if runset & 1 != 0 {
2414                            // c:1537
2415                            p.node.flags |= PM_UNSET as i32; // c:1538
2416                        }
2417                    }
2418                }
2419                rset >>= 1;
2420                runset >>= 1;
2421            } else {
2422                break;
2423            }
2424        }
2425    }
2426    // c:1542 — `if (compkpms && (kset >= 0 || kunset >= 0))`.
2427    if kset >= 0 || kunset >= 0 {
2428        // c:1542
2429        for entry in COMPKPARAMS {
2430            if kset != 0 || kunset != 0 {
2431                if let Ok(mut tab) = paramtab().write() {
2432                    if let Some(p) = tab.get_mut(entry.name) {
2433                        if kset & 1 != 0 {
2434                            // c:1545
2435                            p.node.flags &= !(PM_UNSET as i32);
2436                        }
2437                        if kunset & 1 != 0 {
2438                            // c:1547
2439                            p.node.flags |= PM_UNSET as i32;
2440                        }
2441                    }
2442                }
2443                kset >>= 1;
2444                kunset >>= 1;
2445            } else {
2446                break;
2447            }
2448        }
2449    }
2450}
2451
2452/// Direct port of `int comp_wrapper(Eprog prog, FuncWrap w, char *name)`
2453/// from `Src/Zle/complete.c:1556`. Wraps a function being called as a
2454/// completion entry — saves the comp* string globals (PREFIX/SUFFIX/
2455/// IPREFIX/ISUFFIX/QIPREFIX/QISUFFIX/QUOTE/QUOTING/QSTACK/WORDS) before
2456/// `runshfunc`, restores them after when `comprestore=="auto"` (the
2457/// default at c:1593).
2458/// WARNING: param names don't match C — Rust=(_prog, _w, name) vs C=(prog, w, name)
2459pub fn comp_wrapper(
2460    _prog: *const eprog, // c:1556
2461    _w: *const funcwrap,
2462    name: &str,
2463) -> i32 {
2464    use std::sync::atomic::Ordering;
2465    if INCOMPFUNC.load(Ordering::Relaxed) != 1 {
2466        // c:1559
2467        return 1; // c:1560
2468    }
2469    let snap = |g: &'static std::sync::OnceLock<Mutex<String>>| -> String {
2470        g.get_or_init(|| Mutex::new(String::new()))
2471            .lock()
2472            .map(|s| s.clone())
2473            .unwrap_or_default()
2474    };
2475    let restore = |g: &'static std::sync::OnceLock<Mutex<String>>, v: String| {
2476        if let Ok(mut s) = g.get_or_init(|| Mutex::new(String::new())).lock() {
2477            *s = v;
2478        }
2479    };
2480    let opre = snap(&COMPPREFIX); // c:1578
2481    let osuf = snap(&COMPSUFFIX); // c:1579
2482    let oipre = snap(&COMPIPREFIX); // c:1580
2483    let oisuf = snap(&COMPISUFFIX); // c:1581
2484    let oqipre = snap(&COMPQIPREFIX); // c:1582
2485    let oqisuf = snap(&COMPQISUFFIX); // c:1583
2486    let oq = snap(&COMPQUOTE); // c:1584
2487    let oqs = snap(&COMPQSTACK); // c:1586
2488    let owords = COMPWORDS
2489        .get_or_init(|| Mutex::new(Vec::new()))
2490        .lock()
2491        .map(|v| v.clone())
2492        .unwrap_or_default(); // c:1588
2493
2494    // c:1591 — runshfunc(prog, w, name).
2495    let _ = compcore::shfunc_call(name);
2496
2497    // c:1593 — if comprestore == "auto", restore. Default is "auto" per
2498    // c:1576 (set in comp_wrapper itself before runshfunc).
2499    let comprestore_val = getsparam("comprestore").unwrap_or_else(|| "auto".to_string());
2500    if comprestore_val == "auto" {
2501        restore(&COMPPREFIX, opre);
2502        restore(&COMPSUFFIX, osuf);
2503        restore(&COMPIPREFIX, oipre);
2504        restore(&COMPISUFFIX, oisuf);
2505        restore(&COMPQIPREFIX, oqipre);
2506        restore(&COMPQISUFFIX, oqisuf);
2507        restore(&COMPQUOTE, oq);
2508        restore(&COMPQSTACK, oqs);
2509        if let Ok(mut g) = COMPWORDS.get_or_init(|| Mutex::new(Vec::new())).lock() {
2510            *g = owords;
2511        }
2512    }
2513    0 // c:1647
2514}
2515
2516/// Direct port of `comp_check()` from `Src/Zle/complete.c:1651`.
2517/// C body (c:1653-1659):
2518/// ```c
2519/// if (incompfunc != 1) {
2520///     zerr("condition can only be used in completion function");
2521///     return 0;
2522/// }
2523/// return 1;
2524/// ```
2525pub fn comp_check() -> i32 {
2526    // c:1651
2527    if INCOMPFUNC.load(Ordering::Relaxed) != 1 {
2528        // c:1651
2529        zerr(
2530            // c:1654
2531            "condition can only be used in completion function",
2532        );
2533        return 0; // c:1655
2534    }
2535    1 // c:1658
2536}
2537
2538/// Direct port of `cond_psfix(char **a, int id)` from `Src/Zle/complete.c:1662`.
2539/// C body (c:1664-1672): `if (comp_check())` then dispatch to
2540/// do_comp_vars with id=CVT_PREPAT|CVT_SUFPAT and the arg as the
2541/// pattern (or `arg[0]` as the pattern with `arg[1]` as the count).
2542#[allow(unused_variables)]
2543pub fn cond_psfix(a: &[String], id: i32) -> i32 {
2544    // c:1662
2545    if comp_check() != 0 {
2546        // c:1662
2547        // c:1665-1670 — do_comp_vars dispatch on the prefix/suffix
2548        // pattern + count. The match-test returns 0 when no
2549        // completion matcher set is active; that's the
2550        // false-by-default contract the C source delivers when
2551        // called outside an in-flight completion.
2552        let _ = a;
2553        return 0;
2554    }
2555    0 // c:1671
2556}
2557
2558/// Direct port of `int cond_range(char **a, int id)` from
2559/// `Src/Zle/complete.c:1676`. Dispatches to `do_comp_vars` with
2560/// CVT_RANGEPAT and the two args as start/end patterns.
2561pub fn cond_range(a: &[String], id: i32) -> i32 {
2562    // c:1676
2563    let sa = a.first().map(|s| s.as_str()).unwrap_or(""); // c:1678
2564    let sb = if id != 0 {
2565        a.get(1).map(|s| s.as_str()).unwrap_or("")
2566    }
2567    // c:1679
2568    else {
2569        ""
2570    };
2571    do_comp_vars(CVT_RANGEPAT, 0, sa, 0, sb, 0) // c:1680
2572}
2573
2574/// Direct port of `int setup_(UNUSED(Module m))` from `Src/Zle/complete.c:1720`.
2575/// Module-load init: clears every comp* string global, zeroes
2576/// hasperm/complistmax, sets hascompmod=1, initializes complastprefix
2577/// /complastsuffix to "". Returns 0 on success.
2578#[allow(unused_variables)]
2579pub fn setup_(m: *const module) -> i32 {
2580    // c:1720
2581    compcore::hasperm.store(0, Ordering::Relaxed); // c:1722
2582    let clear = |g: &'static std::sync::OnceLock<Mutex<String>>| {
2583        if let Ok(mut s) = g.get_or_init(|| Mutex::new(String::new())).lock() {
2584            s.clear();
2585        }
2586    };
2587    clear(&COMPPREFIX);
2588    clear(&COMPSUFFIX); // c:1726
2589    clear(&COMPIPREFIX);
2590    clear(&COMPISUFFIX);
2591    clear(&COMPQIPREFIX);
2592    clear(&COMPQISUFFIX);
2593    clear(&COMPCONTEXT);
2594    clear(&COMPPARAMETER);
2595    clear(&COMPREDIRECT);
2596    clear(&COMPQUOTE);
2597    crate::ported::zle::zle_tricky::COMPQUOTE
2598        .get_or_init(|| Mutex::new(String::new()))
2599        .lock()
2600        .ok()
2601        .map(|mut s| s.clear());
2602    clear(&COMPLIST);
2603    clear(&COMPQSTACK);
2604    // c:1733-1734 — `complastprefix = complastsuffix = ztrdup("")`.
2605    if let Ok(mut s) = COMPLASTPREFIX
2606        .get_or_init(|| Mutex::new(String::new()))
2607        .lock()
2608    {
2609        s.clear();
2610    }
2611    if let Ok(mut s) = COMPLASTSUFFIX
2612        .get_or_init(|| Mutex::new(String::new()))
2613        .lock()
2614    {
2615        s.clear();
2616    }
2617    // c:1735 — `complistmax = 0`. (LISTMAX read at use-site.)
2618    0 // c:1738
2619}
2620
2621/// Direct port of `int features_(Module m, char ***features)` from
2622/// `Src/Zle/complete.c:1743`. Returns the array of feature names this
2623/// module exposes; static-link path has no per-feature toggle so this
2624/// is structurally a no-op returning 0.
2625#[allow(unused_variables)]
2626pub fn features_(m: *const module) -> i32 {
2627    // c:1743
2628    0 // c:1746
2629}
2630
2631/// Direct port of `int enables_(Module m, int **enables)` from
2632/// `Src/Zle/complete.c:1751`. C body: `return handlefeatures(m,
2633/// &module_features, enables)`. Static-link path: 0.
2634#[allow(unused_variables)]
2635pub fn enables_(m: *const module) -> i32 {
2636    // c:1751
2637    0 // c:1753
2638}
2639
2640/// Direct port of `int boot_(Module m)` from `Src/Zle/complete.c:1758`.
2641/// C registers six completion Hookfns:
2642/// ```c
2643/// addhookfunc("complete",          do_completion);
2644/// addhookfunc("before_complete",   before_complete);
2645/// addhookfunc("after_complete",    after_complete);
2646/// addhookfunc("accept_completion", accept_last);
2647/// addhookfunc("list_matches",      list_matches);
2648/// addhookfunc("invalidate_list",   invalidate_list);
2649/// ```
2650/// Each Rust handler has a non-`Hookfn`-shaped signature (typed args)
2651/// and is bridged here via a per-handler `(Hookdef, void*) -> i32`
2652/// thunk that casts the void* payload back to the typed pointer —
2653/// matching C's `(Hookfn) do_completion` cast at registration. The
2654/// `accept_completion` hook's `accept_last` carries a multi-field
2655/// signature with no C-payload struct yet (its body is invoked
2656/// directly from `compresult.rs` rather than via `runhookdef`), so
2657/// it remains unregistered until that follow-up lands.
2658#[allow(unused_variables)]
2659pub fn boot_(m: *const module) -> i32 {
2660    // c:1758
2661    let _ = crate::ported::module::addhookfunc("complete", complete_hook);
2662    let _ = crate::ported::module::addhookfunc("before_complete", before_complete_hook);
2663    let _ = crate::ported::module::addhookfunc("after_complete", after_complete_hook);
2664    let _ = crate::ported::module::addhookfunc("list_matches", list_matches_hook);
2665    let _ = crate::ported::module::addhookfunc("invalidate_list", invalidate_list_hook);
2666    0 // c:1767
2667}
2668
2669/// Hookfn-shape thunk for `complete` — bridges the
2670/// `(Hookdef, void*) -> i32` Hookfn signature to the typed
2671/// `do_completion(s, incmd, lst) -> i32` handler in compcore.rs.
2672/// Payload is `struct compldat *` per C `zle_tricky.c:2342-2346`.
2673fn complete_hook(_h: *mut crate::ported::zsh_h::hookdef, d: *mut std::ffi::c_void) -> i32 {
2674    // c:1762 — `addhookfunc("complete", do_completion);`
2675    if d.is_null() {
2676        return crate::ported::zle::compcore::do_completion("", 0, 0);
2677    }
2678    let dat = unsafe { &*(d as *const crate::ported::zle::zle_h::compldat) };
2679    crate::ported::zle::compcore::do_completion(&dat.s, dat.incmd, dat.lst)
2680}
2681
2682/// Hookfn-shape thunk for `before_complete` — payload is `int *lst`
2683/// per C `zle_tricky.c:621`.
2684fn before_complete_hook(_h: *mut crate::ported::zsh_h::hookdef, d: *mut std::ffi::c_void) -> i32 {
2685    // c:1763 — `addhookfunc("before_complete", before_complete);`
2686    if d.is_null() {
2687        let mut lst = 0i32;
2688        return crate::ported::zle::compcore::before_complete(&mut lst);
2689    }
2690    let lst_ptr = d as *mut i32;
2691    crate::ported::zle::compcore::before_complete(unsafe { &mut *lst_ptr })
2692}
2693
2694/// Hookfn-shape thunk for `after_complete` — payload is `int dat[2]`
2695/// per C `zle_tricky.c:878`.
2696fn after_complete_hook(_h: *mut crate::ported::zsh_h::hookdef, d: *mut std::ffi::c_void) -> i32 {
2697    // c:1764 — `addhookfunc("after_complete", after_complete);`
2698    if d.is_null() {
2699        let mut dat = [0i32; 2];
2700        return crate::ported::zle::compcore::after_complete(&mut dat);
2701    }
2702    let dat_ptr = d as *mut i32;
2703    let dat_slice = unsafe { std::slice::from_raw_parts_mut(dat_ptr, 2) };
2704    crate::ported::zle::compcore::after_complete(dat_slice)
2705}
2706
2707/// Hookfn-shape thunk for `list_matches` — bridges the
2708/// `(Hookdef, void*) -> i32` Hookfn signature to the typed
2709/// `list_matches() -> i32` handler in compresult.rs.
2710fn list_matches_hook(_h: *mut crate::ported::zsh_h::hookdef, _d: *mut std::ffi::c_void) -> i32 {
2711    // c:1766 — `addhookfunc("list_matches", list_matches);`
2712    crate::ported::zle::compresult::list_matches()
2713}
2714
2715/// Hookfn-shape thunk for `invalidate_list` — same shape as
2716/// `list_matches_hook`.
2717fn invalidate_list_hook(_h: *mut crate::ported::zsh_h::hookdef, _d: *mut std::ffi::c_void) -> i32 {
2718    // c:1767 — `addhookfunc("invalidate_list", invalidate_list);`
2719    crate::ported::zle::compresult::invalidate_list()
2720}
2721
2722/// Direct port of `int cleanup_(Module m)` from `Src/Zle/complete.c:1772`.
2723/// C unregisters the same six Hookfns that `boot_` added. Paired with
2724/// the same registration deferral — currently a no-op until the
2725/// handler-sig refactor.
2726#[allow(unused_variables)]
2727pub fn cleanup_(m: *const module) -> i32 {
2728    // c:1772
2729    // c:1775-1780 — unregister the two no-arg hooks installed by boot_.
2730    // Mirrors the registration: deletehookfunc removes one Hookfn entry.
2731    let _ = crate::ported::module::deletehookfunc("list_matches", list_matches_hook);
2732    let _ = crate::ported::module::deletehookfunc("invalidate_list", invalidate_list_hook);
2733    0 // c:1783
2734}
2735
2736/// Direct port of `int finish_(UNUSED(Module m))` from `Src/Zle/complete.c:1788`.
2737/// Module-unload cleanup: zsfree's every comp* string global. In Rust
2738/// the `OnceLock<Mutex<String>>`s are owned and freed at process exit;
2739/// for symmetry with C we clear the contents so any subsequent
2740/// re-load starts from empty.
2741#[allow(unused_variables)]
2742pub fn finish_(m: *const module) -> i32 {
2743    // c:1788
2744    let clear = |g: &'static std::sync::OnceLock<Mutex<String>>| {
2745        if let Ok(mut s) = g.get_or_init(|| Mutex::new(String::new())).lock() {
2746            s.clear();
2747        }
2748    };
2749    clear(&COMPPREFIX);
2750    clear(&COMPSUFFIX); // c:1794-1795
2751    clear(&COMPLASTPREFIX);
2752    clear(&COMPLASTSUFFIX); // c:1796-1797
2753    clear(&COMPIPREFIX);
2754    clear(&COMPISUFFIX); // c:1798-1799
2755    clear(&COMPQIPREFIX);
2756    clear(&COMPQISUFFIX); // c:1800-1801
2757    clear(&COMPCONTEXT);
2758    clear(&COMPPARAMETER);
2759    clear(&COMPREDIRECT); // c:1802-1804
2760    clear(&COMPQUOTE);
2761    clear(&COMPQSTACK);
2762    clear(&COMPQUOTING); // c:1805-1807
2763    clear(&COMPLIST); // c:1809
2764    if let Ok(mut g) = COMPWORDS.get_or_init(|| Mutex::new(Vec::new())).lock()
2765    // c:1790
2766    {
2767        g.clear();
2768    }
2769    0
2770}
2771
2772fn lock_str(g: &'static std::sync::OnceLock<Mutex<String>>) -> &'static Mutex<String> {
2773    g.get_or_init(|| Mutex::new(String::new()))
2774}
2775fn lock_vec(g: &'static std::sync::OnceLock<Mutex<Vec<String>>>) -> &'static Mutex<Vec<String>> {
2776    g.get_or_init(|| Mutex::new(Vec::new()))
2777}
2778
2779#[cfg(test)]
2780mod tests {
2781    use super::*;
2782
2783    #[test]
2784    fn classes_basic_cclass() {
2785        let _g = crate::test_util::global_state_lock();
2786        // c:485 — `[abc]` → CCLASS, str holds "abc".
2787        let _g = zle_test_setup();
2788        let mut p = Cpattern::default();
2789        let rest = parse_class(&mut p, "[abc]rest");
2790        assert_eq!(p.tp, CPAT_CCLASS);
2791        assert_eq!(p.str.as_deref(), Some(b"abc".as_slice()));
2792        assert_eq!(rest, "rest");
2793    }
2794
2795    #[test]
2796    fn classes_negated_cclass_via_bang() {
2797        let _g = crate::test_util::global_state_lock();
2798        // c:490 — `[!abc]` → NCLASS.
2799        let _g = zle_test_setup();
2800        let mut p = Cpattern::default();
2801        let _ = parse_class(&mut p, "[!abc]");
2802        assert_eq!(p.tp, CPAT_NCLASS);
2803    }
2804
2805    #[test]
2806    fn classes_negated_cclass_via_caret() {
2807        let _g = crate::test_util::global_state_lock();
2808        // c:490 — `[^abc]` → NCLASS.
2809        let _g = zle_test_setup();
2810        let mut p = Cpattern::default();
2811        let _ = parse_class(&mut p, "[^abc]");
2812        assert_eq!(p.tp, CPAT_NCLASS);
2813    }
2814
2815    #[test]
2816    fn classes_equiv_braces() {
2817        let _g = crate::test_util::global_state_lock();
2818        // c:498 — `{abc}` → EQUIV.
2819        let _g = zle_test_setup();
2820        let mut p = Cpattern::default();
2821        let _ = parse_class(&mut p, "{abc}");
2822        assert_eq!(p.tp, CPAT_EQUIV);
2823    }
2824
2825    #[test]
2826    fn classes_range_consumes_input() {
2827        let _g = crate::test_util::global_state_lock();
2828        // c:537 — `[a-z]rest` → parses 5 chars, returns "rest".
2829        //          The PP_RANGE-encoded body isn't directly checked
2830        //          here because Cpattern.str is currently
2831        //          Option<String> and metafied tokens (0x83-prefix
2832        //          byte sequences) don't round-trip through UTF-8.
2833        //          Re-add a byte-level check once Cpattern.str moves
2834        //          to a Vec<u8>-backed storage.
2835        let _g = zle_test_setup();
2836        let mut p = Cpattern::default();
2837        let rest = parse_class(&mut p, "[a-z]rest");
2838        assert_eq!(p.tp, CPAT_CCLASS);
2839        assert_eq!(rest, "rest");
2840        assert!(p.str.is_some());
2841    }
2842
2843    #[test]
2844    fn cmatcher_empty_input_returns_none() {
2845        let _g = crate::test_util::global_state_lock();
2846        // c:249 — `if (!*s) return NULL;`
2847        let _g = zle_test_setup();
2848        assert!(parse_cmatcher("", "").is_none());
2849    }
2850
2851    #[test]
2852    fn cmatcher_x_early_return() {
2853        let _g = crate::test_util::global_state_lock();
2854        // c:294-303 — `x:` is the "match anything" sentinel; valid
2855        //              spec, returns the (currently empty) chain.
2856        let _g = zle_test_setup();
2857        assert!(parse_cmatcher("", "x:").is_none());
2858    }
2859
2860    #[test]
2861    fn cmatcher_unknown_letter_errors() {
2862        let _g = crate::test_util::global_state_lock();
2863        // c:280-283 — unknown rule-letter → return None (pcm_err).
2864        let _g = zle_test_setup();
2865        // "q" isn't in the dispatch table.
2866        assert!(parse_cmatcher("", "q:abc").is_none());
2867    }
2868
2869    #[test]
2870    fn cmatcher_missing_colon_errors() {
2871        let _g = crate::test_util::global_state_lock();
2872        // c:288-291 — second char must be `:`.
2873        let _g = zle_test_setup();
2874        assert!(parse_cmatcher("", "rabc").is_none());
2875    }
2876
2877    #[test]
2878    fn cmatcher_x_with_trailing_pattern_errors() {
2879        let _g = crate::test_util::global_state_lock();
2880        // c:296-301 — `x:foo` is malformed; `x:` must be alone.
2881        let _g = zle_test_setup();
2882        assert!(parse_cmatcher("", "x:foo").is_none());
2883    }
2884
2885    #[test]
2886    fn cmatcher_valid_letters_dont_panic() {
2887        let _g = crate::test_util::global_state_lock();
2888        // All recognized letters parse through without panicking.
2889        let _g = zle_test_setup();
2890        for c in ['b', 'l', 'e', 'r', 'm', 'B', 'L', 'E', 'R', 'M'] {
2891            let spec = format!("{}:body", c);
2892            let _ = parse_cmatcher("", &spec);
2893        }
2894    }
2895
2896    #[test]
2897    fn cmatcher_m_rule_emits_cmatcher() {
2898        let _g = crate::test_util::global_state_lock();
2899        // c:266 — `m:word=replacement` plain match.
2900        let _g = zle_test_setup();
2901        let r = parse_cmatcher("", "m:foo=bar");
2902        assert!(r.is_some(), "m: rule should produce a Cmatcher");
2903        let cm = r.unwrap();
2904        assert_eq!(cm.flags, 0); // c:266 fl=0
2905        assert_eq!(cm.llen, 3); // "foo"
2906        assert_eq!(cm.wlen, 3); // "bar"
2907        assert!(cm.line.is_some());
2908        assert!(cm.word.is_some());
2909        assert!(cm.left.is_none());
2910        assert!(cm.right.is_none());
2911    }
2912
2913    #[test]
2914    fn cmatcher_r_rule_emits_anchored_cmatcher() {
2915        let _g = crate::test_util::global_state_lock();
2916        // c:265 — `r:left|right=word` with both anchors. The first
2917        //          pattern becomes the left anchor (promoted at
2918        //          c:341-346), the second the right anchor.
2919        let _g = zle_test_setup();
2920        let r = parse_cmatcher("", "r:abc|xy=def");
2921        assert!(r.is_some(), "r: rule should produce a Cmatcher");
2922        let cm = r.unwrap();
2923        assert_eq!(cm.flags, CMF_RIGHT);
2924        assert_eq!(cm.lalen, 3); // left = "abc"
2925        assert_eq!(cm.ralen, 2); // right = "xy"
2926        assert_eq!(cm.wlen, 3); // word = "def"
2927        assert!(cm.left.is_some());
2928        assert!(cm.right.is_some());
2929    }
2930
2931    #[test]
2932    fn cmatcher_l_rule_emits_left_anchor() {
2933        let _g = crate::test_util::global_state_lock();
2934        // c:263 — `l:left|line=word` left anchor.
2935        let _g = zle_test_setup();
2936        let r = parse_cmatcher("", "l:ab|cd=ef");
2937        assert!(r.is_some(), "l: rule should produce a Cmatcher");
2938        let cm = r.unwrap();
2939        assert_eq!(cm.flags, CMF_LEFT);
2940        assert!(cm.left.is_some());
2941        assert_eq!(cm.lalen, 2);
2942        assert_eq!(cm.llen, 2);
2943        assert_eq!(cm.wlen, 2);
2944    }
2945
2946    #[test]
2947    fn cmatcher_star_word_with_anchor() {
2948        let _g = crate::test_util::global_state_lock();
2949        // c:359-370 — `r:|=*` matches any word, requires anchor.
2950        let _g = zle_test_setup();
2951        let r = parse_cmatcher("", "r:|=*");
2952        assert!(r.is_some(), "r:|=* should produce a Cmatcher");
2953        let cm = r.unwrap();
2954        assert_eq!(cm.wlen, -1); // c:370 single `*`
2955        assert!(cm.word.is_none());
2956    }
2957
2958    #[test]
2959    fn cmatcher_double_star_word() {
2960        let _g = crate::test_util::global_state_lock();
2961        // c:366-368 — `r:|=**` matches any (greedy) word.
2962        let _g = zle_test_setup();
2963        let r = parse_cmatcher("", "r:|=**");
2964        assert!(r.is_some());
2965        let cm = r.unwrap();
2966        assert_eq!(cm.wlen, -2); // c:368 double `**`
2967    }
2968
2969    #[test]
2970    fn cmatcher_star_without_anchor_errors() {
2971        let _g = crate::test_util::global_state_lock();
2972        // c:360-364 — `m:=*` (no anchor) errors.
2973        let _g = zle_test_setup();
2974        let r = parse_cmatcher("", "m:=*");
2975        assert!(r.is_none(), "*-without-anchor should error");
2976    }
2977
2978    #[test]
2979    fn cmatcher_chain_multiple_rules() {
2980        let _g = crate::test_util::global_state_lock();
2981        // c:251-401 — multiple rules separated by whitespace chain.
2982        let _g = zle_test_setup();
2983        let r = parse_cmatcher("", "m:foo=bar m:baz=qux");
2984        assert!(r.is_some());
2985        let head = r.unwrap();
2986        assert!(head.next.is_some(), "second rule should be linked");
2987    }
2988
2989    #[test]
2990    fn pattern_single_char_emits_cpat_char() {
2991        let _g = crate::test_util::global_state_lock();
2992        // c:451-461 — single non-special char → CPAT_CHAR node.
2993        let _g = zle_test_setup();
2994        let (chain, rest, len, err) = parse_pattern("", "abc", '\0');
2995        assert!(!err);
2996        assert_eq!(len, 3);
2997        assert_eq!(rest, ""); // consumed everything (no end-char, no whitespace)
2998                              // Walk chain and verify 3 CPAT_CHAR nodes.
2999        let mut count = 0;
3000        let mut cur = chain.as_deref();
3001        while let Some(n) = cur {
3002            assert_eq!(n.tp, CPAT_CHAR);
3003            count += 1;
3004            cur = n.next.as_deref();
3005        }
3006        assert_eq!(count, 3);
3007    }
3008
3009    #[test]
3010    fn pattern_question_mark_is_cpat_any() {
3011        let _g = crate::test_util::global_state_lock();
3012        // c:443 — `?` → CPAT_ANY.
3013        let _g = zle_test_setup();
3014        let (chain, _, len, err) = parse_pattern("", "?", '\0');
3015        assert!(!err);
3016        assert_eq!(len, 1);
3017        assert_eq!(chain.as_ref().unwrap().tp, CPAT_ANY);
3018    }
3019
3020    #[test]
3021    fn pattern_invalid_chars_error() {
3022        let _g = crate::test_util::global_state_lock();
3023        // c:446-449 — `*`/`(`/`)`/`=` → error.
3024        let _g = zle_test_setup();
3025        for c in ['*', '(', ')', '='] {
3026            let s = format!("{}", c);
3027            let (chain, _, _, err) = parse_pattern("", &s, '\0');
3028            assert!(err, "char {} should error", c);
3029            assert!(chain.is_none());
3030        }
3031    }
3032
3033    #[test]
3034    fn pattern_backslash_escapes_next() {
3035        let _g = crate::test_util::global_state_lock();
3036        // c:452 — `\\X` consumes the backslash and emits X as CPAT_CHAR.
3037        let _g = zle_test_setup();
3038        let (chain, _, len, err) = parse_pattern("", r"\*", '\0');
3039        assert!(!err);
3040        assert_eq!(len, 1);
3041        let n = chain.as_ref().unwrap();
3042        assert_eq!(n.tp, CPAT_CHAR);
3043        assert_eq!(n.chr, '*' as u32);
3044    }
3045
3046    #[test]
3047    fn pattern_stops_at_end_char() {
3048        let _g = crate::test_util::global_state_lock();
3049        // c:430 — `*s != e` gate.
3050        let _g = zle_test_setup();
3051        let (_, rest, len, err) = parse_pattern("", "ab=cd", '=');
3052        assert!(!err);
3053        assert_eq!(len, 2);
3054        assert_eq!(rest, "=cd");
3055    }
3056
3057    #[test]
3058    fn pattern_stops_at_whitespace_when_no_end_char() {
3059        let _g = crate::test_util::global_state_lock();
3060        // c:430 — `e==0` → !inblank.
3061        let _g = zle_test_setup();
3062        let (_, rest, len, err) = parse_pattern("", "ab cd", '\0');
3063        assert!(!err);
3064        assert_eq!(len, 2);
3065        assert_eq!(rest, " cd");
3066    }
3067
3068    #[test]
3069    fn pattern_bracket_class_routes_to_parse_class() {
3070        let _g = crate::test_util::global_state_lock();
3071        // c:435 — `[abc]` dispatches to parse_class. With no end-char
3072        //          parse_pattern continues into the trailing chars as
3073        //          CPAT_CHAR nodes, so `[abc]xy` → class + x + y = 3.
3074        let _g = zle_test_setup();
3075        let (chain, rest, len, err) = parse_pattern("", "[abc]xy=q", '=');
3076        assert!(!err);
3077        assert_eq!(len, 3);
3078        assert_eq!(rest, "=q");
3079        // chain head is the class node.
3080        assert_eq!(chain.as_ref().unwrap().tp, CPAT_CCLASS);
3081    }
3082
3083    #[test]
3084    fn classes_unterminated_returns_eos() {
3085        let _g = crate::test_util::global_state_lock();
3086        // c:504 — unterminated class → returns input-end.
3087        let _g = zle_test_setup();
3088        let mut p = Cpattern::default();
3089        let rest = parse_class(&mut p, "[abc");
3090        assert_eq!(rest, "");
3091    }
3092
3093    #[test]
3094    fn compadd_trace_records_and_returns_one() {
3095        // sh:_complete_help:11 — `compadd() { return 1 }`. With the
3096        //   trace flag on, bin_compadd records argv into
3097        //   `_complete_help_funcs` and returns 1 without panicking.
3098        let _g = crate::test_util::global_state_lock();
3099        let _g2 = zle_test_setup();
3100        INCOMPFUNC.store(1, Ordering::Relaxed);
3101        crate::ported::params::setaparam("_complete_help_funcs", Vec::new());
3102        set_compadd_trace(true);
3103        let argv = vec!["-X".to_string(), "files".to_string(), "alpha".to_string()];
3104        let r = bin_compadd("compadd", &argv, &make_test_ops(), 0);
3105        set_compadd_trace(false);
3106        INCOMPFUNC.store(0, Ordering::Relaxed);
3107        assert_eq!(r, 1, "trace mode short-circuits to 1");
3108        let buf = crate::ported::params::getaparam("_complete_help_funcs").unwrap_or_default();
3109        assert_eq!(buf, vec!["-X files alpha".to_string()]);
3110    }
3111
3112    #[test]
3113    fn compadd_prefix_injector_mutates_prefix_then_restores() {
3114        // sh:_approximate:57-72 — injector prepends `(#a$N)` to PREFIX
3115        //   for the duration of the compadd call, then restores.
3116        let _g = crate::test_util::global_state_lock();
3117        let _g2 = zle_test_setup();
3118        INCOMPFUNC.store(1, Ordering::Relaxed);
3119        crate::ported::params::setsparam("PREFIX", "abc").unwrap();
3120        // Trace flag on so the body short-circuits and we can observe
3121        //   what PREFIX was during the call via the captured argv-side
3122        //   parameter snapshot.
3123        crate::ported::params::setaparam("_complete_help_funcs", Vec::new());
3124        set_compadd_trace(true);
3125        let prev = set_compadd_prefix_injector("(#a2)");
3126        assert!(prev.is_none());
3127        // Spy: stash PREFIX into a side-channel param before running.
3128        //   bin_compadd's wrapper mutates PREFIX, runs body, restores;
3129        //   our spy reads the mutated value mid-call via a hook is not
3130        //   wired, so instead we directly observe PREFIX after the
3131        //   call to confirm restoration.
3132        let _ = bin_compadd("compadd", &["x".to_string()], &make_test_ops(), 0);
3133        clear_compadd_prefix_injector();
3134        set_compadd_trace(false);
3135        INCOMPFUNC.store(0, Ordering::Relaxed);
3136        let after = crate::ported::params::getsparam("PREFIX").unwrap_or_default();
3137        assert_eq!(after, "abc", "PREFIX restored after compadd call");
3138    }
3139
3140    #[test]
3141    fn compadd_prefix_injector_tilde_aware() {
3142        // sh:_approximate:65-69 — leading `~` in PREFIX stays before
3143        //   the injected pattern when no `-p ~…` is passed.
3144        let _g = crate::test_util::global_state_lock();
3145        let _g2 = zle_test_setup();
3146        INCOMPFUNC.store(1, Ordering::Relaxed);
3147        crate::ported::params::setsparam("PREFIX", "~user").unwrap();
3148        set_compadd_trace(true);
3149        let _ = set_compadd_prefix_injector("(#a1)");
3150        let _ = bin_compadd("compadd", &["x".to_string()], &make_test_ops(), 0);
3151        clear_compadd_prefix_injector();
3152        set_compadd_trace(false);
3153        INCOMPFUNC.store(0, Ordering::Relaxed);
3154        // Restored
3155        assert_eq!(
3156            crate::ported::params::getsparam("PREFIX").unwrap_or_default(),
3157            "~user"
3158        );
3159    }
3160
3161    fn make_test_ops() -> options {
3162        options {
3163            ind: [0u8; crate::ported::zsh_h::MAX_OPS],
3164            args: Vec::new(),
3165            argscount: 0,
3166            argsalloc: 0,
3167        }
3168    }
3169
3170    // ─── zsh-corpus pins for parse_ordering ────────────────────────
3171
3172    /// `parse_ordering("")` returns -1 — empty CSV token is not a
3173    /// valid orderopts name. Pin the actual zsh contract.
3174    #[test]
3175    fn complete_corpus_parse_ordering_empty_returns_neg_one() {
3176        let _g = crate::test_util::global_state_lock();
3177        let _g2 = zle_test_setup();
3178        let mut flags: Option<i32> = Some(0);
3179        let r = parse_ordering("", &mut flags);
3180        assert_eq!(r, -1, "empty token is not a valid orderopts name");
3181    }
3182
3183    /// `parse_ordering("invalid_xyz")` returns -1 (unknown option).
3184    #[test]
3185    fn complete_corpus_parse_ordering_unknown_returns_neg_one() {
3186        let _g = crate::test_util::global_state_lock();
3187        let _g2 = zle_test_setup();
3188        let mut flags: Option<i32> = Some(0);
3189        let r = parse_ordering("zzz_not_a_real_ordering_xyz", &mut flags);
3190        assert_eq!(r, -1, "unknown ordering name = -1");
3191    }
3192
3193    /// `parse_ordering("match")` recognises a real orderopts name.
3194    #[test]
3195    fn complete_corpus_parse_ordering_match_recognised() {
3196        let _g = crate::test_util::global_state_lock();
3197        let _g2 = zle_test_setup();
3198        let mut flags: Option<i32> = Some(0);
3199        let r = parse_ordering("match", &mut flags);
3200        assert_eq!(r, 0, "match is a valid orderopts name");
3201    }
3202
3203    /// `parse_ordering` accepts comma-separated valid names.
3204    #[test]
3205    fn complete_corpus_parse_ordering_csv_all_valid() {
3206        let _g = crate::test_util::global_state_lock();
3207        let _g2 = zle_test_setup();
3208        let mut flags: Option<i32> = Some(0);
3209        let r = parse_ordering("match,reverse", &mut flags);
3210        // match + reverse are both valid → 0.
3211        assert_eq!(r, 0);
3212    }
3213
3214    /// `parse_ordering` rejects when any csv token is invalid.
3215    #[test]
3216    fn complete_corpus_parse_ordering_csv_with_invalid_returns_neg_one() {
3217        let _g = crate::test_util::global_state_lock();
3218        let _g2 = zle_test_setup();
3219        let mut flags: Option<i32> = Some(0);
3220        let r = parse_ordering("match,zzz_bogus", &mut flags);
3221        assert_eq!(r, -1, "any invalid token = -1");
3222    }
3223
3224    // ═══════════════════════════════════════════════════════════════════
3225    // C-parity tests pinning Src/Zle/complete.c.
3226    // ═══════════════════════════════════════════════════════════════════
3227
3228    /// `ignore_prefix(0)` is a no-op (`if (l) …` guard).
3229    /// C `Src/Zle/complete.c:ignore_prefix` first line.
3230    #[test]
3231    fn ignore_prefix_zero_is_noop() {
3232        let _g = crate::test_util::global_state_lock();
3233        let _g2 = zle_test_setup();
3234        ignore_prefix(0);
3235        ignore_prefix(0);
3236    }
3237
3238    /// `ignore_suffix(0)` is a no-op (`if (l) …` guard).
3239    #[test]
3240    fn ignore_suffix_zero_is_noop() {
3241        let _g = crate::test_util::global_state_lock();
3242        let _g2 = zle_test_setup();
3243        ignore_suffix(0);
3244        ignore_suffix(0);
3245    }
3246
3247    /// `restrict_range(0, 0)` writes degenerate empty range.
3248    #[test]
3249    fn restrict_range_zero_zero_no_panic() {
3250        let _g = crate::test_util::global_state_lock();
3251        let _g2 = zle_test_setup();
3252        restrict_range(0, 0);
3253    }
3254
3255    /// `set_compadd_trace(true)` / `(false)` toggle no-panic.
3256    #[test]
3257    fn set_compadd_trace_toggle_no_panic() {
3258        let _g = crate::test_util::global_state_lock();
3259        let _g2 = zle_test_setup();
3260        set_compadd_trace(true);
3261        set_compadd_trace(false);
3262        set_compadd_trace(true);
3263    }
3264
3265    /// `set_compadd_prefix_injector` / `clear_compadd_prefix_injector`
3266    /// round-trip.
3267    #[test]
3268    fn set_then_clear_compadd_prefix_injector_no_panic() {
3269        let _g = crate::test_util::global_state_lock();
3270        let _g2 = zle_test_setup();
3271        let prev = set_compadd_prefix_injector("test_prefix");
3272        clear_compadd_prefix_injector();
3273        if let Some(p) = prev {
3274            let _ = set_compadd_prefix_injector(p);
3275            clear_compadd_prefix_injector();
3276        }
3277    }
3278
3279    /// `parse_ordering("")` returns -1 — no valid ordering token.
3280    #[test]
3281    fn parse_ordering_empty_string_returns_neg_one() {
3282        let _g = crate::test_util::global_state_lock();
3283        let _g2 = zle_test_setup();
3284        let mut flags: Option<i32> = Some(0);
3285        let r = parse_ordering("", &mut flags);
3286        assert_eq!(r, -1, "empty input has no valid ordering token");
3287    }
3288
3289    // ═══════════════════════════════════════════════════════════════════
3290    // Additional C-parity tests for Src/Zle/complete.c free/copy
3291    // chain walkers.
3292    // ═══════════════════════════════════════════════════════════════════
3293
3294    /// c:115 — `freecmatcher(None)` is a safe no-op.
3295    #[test]
3296    fn freecmatcher_none_is_noop() {
3297        let _g = crate::test_util::global_state_lock();
3298        let _g2 = zle_test_setup();
3299        freecmatcher(None);
3300    }
3301
3302    /// c:137 — `freecpattern(None)` is a safe no-op.
3303    #[test]
3304    fn freecpattern_none_is_noop() {
3305        let _g = crate::test_util::global_state_lock();
3306        let _g2 = zle_test_setup();
3307        freecpattern(None);
3308    }
3309
3310    /// c:98 — `freecmlist(None)` is a safe no-op.
3311    #[test]
3312    fn freecmlist_none_is_noop() {
3313        let _g = crate::test_util::global_state_lock();
3314        let _g2 = zle_test_setup();
3315        freecmlist(None);
3316    }
3317
3318    /// c:218 — `cpcpattern(None)` returns None (no source chain).
3319    #[test]
3320    fn cpcpattern_none_returns_none() {
3321        let _g = crate::test_util::global_state_lock();
3322        let _g2 = zle_test_setup();
3323        let r = cpcpattern(None);
3324        assert!(r.is_none(), "None source → None copy");
3325    }
3326
3327    /// c:155 — `cpcmatcher(None)` returns None.
3328    #[test]
3329    fn cpcmatcher_none_returns_none() {
3330        let _g = crate::test_util::global_state_lock();
3331        let _g2 = zle_test_setup();
3332        let r = cpcmatcher(None);
3333        assert!(r.is_none());
3334    }
3335
3336    /// c:189 — `cp_cpattern_element` copies the `tp` field verbatim.
3337    #[test]
3338    fn cp_cpattern_element_copies_tp() {
3339        let _g = crate::test_util::global_state_lock();
3340        let _g2 = zle_test_setup();
3341        let src = Cpattern {
3342            tp: CPAT_CHAR,
3343            chr: 'A' as u32,
3344            ..Default::default()
3345        };
3346        let dup = cp_cpattern_element(&src);
3347        assert_eq!(dup.tp, CPAT_CHAR, "tp must round-trip");
3348    }
3349
3350    /// c:218 — `cp_cpattern_element` on CPAT_CHAR copies `chr` field.
3351    #[test]
3352    fn cp_cpattern_element_copies_chr_for_cpat_char() {
3353        let _g = crate::test_util::global_state_lock();
3354        let _g2 = zle_test_setup();
3355        let src = Cpattern {
3356            tp: CPAT_CHAR,
3357            chr: 0x42,
3358            ..Default::default()
3359        };
3360        let dup = cp_cpattern_element(&src);
3361        assert_eq!(dup.chr, 0x42);
3362    }
3363
3364    /// c:199 — `cp_cpattern_element` on CPAT_CCLASS copies `str` field.
3365    #[test]
3366    fn cp_cpattern_element_copies_str_for_cclass() {
3367        let _g = crate::test_util::global_state_lock();
3368        let _g2 = zle_test_setup();
3369        let src = Cpattern {
3370            tp: CPAT_CCLASS,
3371            str: Some(b"a-z".to_vec()),
3372            ..Default::default()
3373        };
3374        let dup = cp_cpattern_element(&src);
3375        assert_eq!(dup.str.as_deref(), Some(&b"a-z"[..]));
3376    }
3377
3378    /// c:191 — `cp_cpattern_element` always initializes `next = None`
3379    /// (caller chains them via cpcpattern, not by carrying source's
3380    /// next forward).
3381    #[test]
3382    fn cp_cpattern_element_resets_next_to_none() {
3383        let _g = crate::test_util::global_state_lock();
3384        let _g2 = zle_test_setup();
3385        let src = Cpattern {
3386            tp: CPAT_CHAR,
3387            chr: 'x' as u32,
3388            next: Some(Box::new(Cpattern::default())), // source has next
3389            ..Default::default()
3390        };
3391        let dup = cp_cpattern_element(&src);
3392        assert!(dup.next.is_none(), "copy element MUST reset next to None");
3393    }
3394
3395    /// c:218 — `cpcpattern` of a 3-node chain produces a 3-node chain
3396    /// (chain walker advances tail_ref per element).
3397    #[test]
3398    fn cpcpattern_preserves_chain_length() {
3399        let _g = crate::test_util::global_state_lock();
3400        let _g2 = zle_test_setup();
3401        let src = Cpattern {
3402            tp: CPAT_CHAR,
3403            chr: 'a' as u32,
3404            next: Some(Box::new(Cpattern {
3405                tp: CPAT_CHAR,
3406                chr: 'b' as u32,
3407                next: Some(Box::new(Cpattern {
3408                    tp: CPAT_CHAR,
3409                    chr: 'c' as u32,
3410                    ..Default::default()
3411                })),
3412                ..Default::default()
3413            })),
3414            ..Default::default()
3415        };
3416        let dup = cpcpattern(Some(&src)).expect("should copy");
3417        // Count: head + 2 next.
3418        let mut cnt = 1;
3419        let mut cur = &dup.next;
3420        while let Some(n) = cur {
3421            cnt += 1;
3422            cur = &n.next;
3423        }
3424        assert_eq!(cnt, 3, "3-node source → 3-node copy");
3425    }
3426
3427    /// c:242 — `parse_cmatcher(_, "")` returns None per c:249 guard.
3428    #[test]
3429    fn parse_cmatcher_empty_returns_none() {
3430        let _g = crate::test_util::global_state_lock();
3431        let _g2 = zle_test_setup();
3432        let r = parse_cmatcher("test", "");
3433        assert!(r.is_none(), "empty matcher spec → None per c:249");
3434    }
3435
3436    // ═══════════════════════════════════════════════════════════════════
3437    // Additional C-parity tests for Src/Zle/complete.c
3438    // c:75 freecmlist / c:101 freecmatcher / c:124 freecpattern /
3439    // c:142 cpcmatcher / c:181 cp_cpattern_element / c:203 cpcpattern /
3440    // c:294 parse_cmatcher / c:791 parse_ordering / c:1069 ignore_prefix /
3441    // c:1087 ignore_suffix / c:1111 restrict_range / c:1598 get_compstate
3442    // ═══════════════════════════════════════════════════════════════════
3443
3444    /// c:75 — `freecmlist(None)` is safe idempotent.
3445    #[test]
3446    fn freecmlist_none_idempotent() {
3447        for _ in 0..5 {
3448            freecmlist(None);
3449        }
3450    }
3451
3452    /// c:101 — `freecmatcher(None)` is safe idempotent.
3453    #[test]
3454    fn freecmatcher_none_idempotent() {
3455        for _ in 0..5 {
3456            freecmatcher(None);
3457        }
3458    }
3459
3460    /// c:124 — `freecpattern(None)` is safe idempotent.
3461    #[test]
3462    fn freecpattern_none_idempotent() {
3463        for _ in 0..5 {
3464            freecpattern(None);
3465        }
3466    }
3467
3468    /// c:142 — `cpcmatcher(None)` returns None (type pin).
3469    #[test]
3470    fn cpcmatcher_none_returns_none_type_pin() {
3471        let r: Option<Box<Cmatcher>> = cpcmatcher(None);
3472        assert!(r.is_none());
3473    }
3474
3475    /// c:203 — `cpcpattern(None)` returns None (type pin).
3476    #[test]
3477    fn cpcpattern_none_returns_none_type_pin() {
3478        let r: Option<Box<Cpattern>> = cpcpattern(None);
3479        assert!(r.is_none());
3480    }
3481
3482    /// c:294 — `parse_cmatcher` is deterministic for empty input.
3483    #[test]
3484    fn parse_cmatcher_empty_is_deterministic() {
3485        let _g = crate::test_util::global_state_lock();
3486        let _g2 = zle_test_setup();
3487        for _ in 0..3 {
3488            assert!(parse_cmatcher("test", "").is_none());
3489        }
3490    }
3491
3492    /// c:791 — `parse_ordering(empty, _)` returns i32 (type pin).
3493    #[test]
3494    fn parse_ordering_returns_i32_type() {
3495        let mut flags: Option<i32> = None;
3496        let _: i32 = parse_ordering("name", &mut flags);
3497    }
3498
3499    /// c:1069 — `ignore_prefix(0)` is safe.
3500    #[test]
3501    fn ignore_prefix_zero_no_panic() {
3502        let _g = crate::test_util::global_state_lock();
3503        let _g2 = zle_test_setup();
3504        ignore_prefix(0);
3505    }
3506
3507    /// c:1087 — `ignore_suffix(0)` is safe.
3508    #[test]
3509    fn ignore_suffix_zero_no_panic() {
3510        let _g = crate::test_util::global_state_lock();
3511        let _g2 = zle_test_setup();
3512        ignore_suffix(0);
3513    }
3514
3515    /// c:1111 — `restrict_range(0, 0)` is safe (zero-width).
3516    #[test]
3517    fn restrict_range_zero_zero_no_panic_pin() {
3518        let _g = crate::test_util::global_state_lock();
3519        let _g2 = zle_test_setup();
3520        restrict_range(0, 0);
3521    }
3522
3523    /// c:1598 — `get_compstate(null)` returns Option<usize> type.
3524    #[test]
3525    fn get_compstate_null_returns_option_type() {
3526        let _g = crate::test_util::global_state_lock();
3527        let _: Option<usize> = get_compstate(std::ptr::null_mut());
3528    }
3529
3530    // ═══════════════════════════════════════════════════════════════════
3531    // Additional C-parity tests for Src/Zle/complete.c
3532    // c:75 freecmlist / c:294 parse_cmatcher / c:791 parse_ordering /
3533    // c:853 bin_compadd / c:942 prefix_injector / c:953 compadd_trace /
3534    // c:1069 ignore_prefix / c:1087 ignore_suffix / c:1111 restrict_range /
3535    // c:1393 bin_compset
3536    // ═══════════════════════════════════════════════════════════════════
3537
3538    /// c:75 — `freecmlist(None)` is idempotent (alt 10-call).
3539    #[test]
3540    fn freecmlist_none_idempotent_10_call() {
3541        for _ in 0..10 {
3542            freecmlist(None);
3543        }
3544    }
3545
3546    /// c:1069 — `ignore_prefix(positive)` is safe.
3547    #[test]
3548    fn ignore_prefix_positive_no_panic() {
3549        let _g = crate::test_util::global_state_lock();
3550        let _g2 = zle_test_setup();
3551        for n in [1i32, 10, 100] {
3552            ignore_prefix(n);
3553        }
3554    }
3555
3556    /// c:1087 — `ignore_suffix(positive)` is safe.
3557    #[test]
3558    fn ignore_suffix_positive_no_panic() {
3559        let _g = crate::test_util::global_state_lock();
3560        let _g2 = zle_test_setup();
3561        for n in [1i32, 10, 100] {
3562            ignore_suffix(n);
3563        }
3564    }
3565
3566    /// c:1069 — `ignore_prefix(0)` is idempotent across repeated calls.
3567    #[test]
3568    fn ignore_prefix_idempotent() {
3569        let _g = crate::test_util::global_state_lock();
3570        let _g2 = zle_test_setup();
3571        for _ in 0..10 {
3572            ignore_prefix(0);
3573        }
3574    }
3575
3576    /// c:1111 — `restrict_range` is safe for various endpoints.
3577    #[test]
3578    fn restrict_range_various_endpoints_no_panic() {
3579        let _g = crate::test_util::global_state_lock();
3580        let _g2 = zle_test_setup();
3581        for &(b, e) in &[(0, 0), (1, 10), (-1, -1), (i32::MAX, i32::MAX)] {
3582            restrict_range(b, e);
3583        }
3584    }
3585
3586    /// c:791 — `parse_ordering` is deterministic for empty name.
3587    #[test]
3588    fn parse_ordering_empty_deterministic() {
3589        let mut flags1: Option<i32> = None;
3590        let first = parse_ordering("", &mut flags1);
3591        let mut flags2: Option<i32> = None;
3592        let second = parse_ordering("", &mut flags2);
3593        assert_eq!(first, second, "parse_ordering('') must be deterministic");
3594    }
3595
3596    /// c:853 — `bin_compadd` returns i32 (compile-time pin).
3597    #[test]
3598    fn bin_compadd_returns_i32_type() {
3599        let _g = crate::test_util::global_state_lock();
3600        let _g2 = zle_test_setup();
3601        let ops = crate::ported::zsh_h::options {
3602            ind: [0u8; crate::ported::zsh_h::MAX_OPS],
3603            args: Vec::new(),
3604            argscount: 0,
3605            argsalloc: 0,
3606        };
3607        let _: i32 = bin_compadd("compadd", &[], &ops, 0);
3608    }
3609
3610    /// c:1393 — `bin_compset` returns i32 (compile-time pin).
3611    #[test]
3612    fn bin_compset_returns_i32_type() {
3613        let _g = crate::test_util::global_state_lock();
3614        let _g2 = zle_test_setup();
3615        let ops = crate::ported::zsh_h::options {
3616            ind: [0u8; crate::ported::zsh_h::MAX_OPS],
3617            args: Vec::new(),
3618            argscount: 0,
3619            argsalloc: 0,
3620        };
3621        let _: i32 = bin_compset("compset", &[], &ops, 0);
3622    }
3623
3624    /// c:294 — `parse_cmatcher` is deterministic for various inputs.
3625    #[test]
3626    fn parse_cmatcher_various_inputs_deterministic() {
3627        let _g = crate::test_util::global_state_lock();
3628        let _g2 = zle_test_setup();
3629        for s in ["", "x", "m:{a-z}={A-Z}", "garbage"] {
3630            let a = parse_cmatcher("test", s).is_some();
3631            let b = parse_cmatcher("test", s).is_some();
3632            assert_eq!(a, b, "parse_cmatcher({:?}) must be deterministic", s);
3633        }
3634    }
3635
3636    /// c:942 — `set_compadd_prefix_injector` round-trips with clear.
3637    #[test]
3638    fn compadd_prefix_injector_round_trip() {
3639        let _g = crate::test_util::global_state_lock();
3640        let _g2 = zle_test_setup();
3641        let _: Option<String> = set_compadd_prefix_injector("test_prefix_xyz");
3642        clear_compadd_prefix_injector();
3643    }
3644
3645    /// c:953 — `set_compadd_trace` is idempotent (toggle on/off).
3646    #[test]
3647    fn set_compadd_trace_toggle_safe() {
3648        let _g = crate::test_util::global_state_lock();
3649        let _g2 = zle_test_setup();
3650        for active in [true, false, true, true, false] {
3651            set_compadd_trace(active);
3652        }
3653    }
3654}