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