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 fns 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::Mutex;
33use std::sync::atomic::{AtomicI32, AtomicI64};
34use crate::ported::zle::comp_h::Cmatcher;
35use crate::ported::zle::comp_h::{Cpattern, CPAT_CCLASS, CPAT_NCLASS, CPAT_EQUIV, CPAT_CHAR};
36use crate::ported::zle::comp_h::CAF_MATSORT;
37use crate::ported::zsh_h::{PM_TYPE, PM_SCALAR, PM_ARRAY, PM_HASHED};
38use crate::ported::utils::zwarnnam;
39use std::sync::atomic::Ordering;
40use crate::ported::pattern::{patcompile, pattry};
41
42// =====================================================================
43// Cmlist / Cmatcher / Cpattern allocators + freers — Src/Zle/complete.c.
44// Ported here (rather than a non-existent complete.rs) because
45// PORT.md freezes new src/ported/ file creation; compcore.rs is the
46// canonical home for completion-machinery internals.
47// =====================================================================
48
49/// Direct port of `freecmlist(Cmlist l)` from `Src/Zle/complete.c:98`.
50/// C body (c:101-110): walk the linked list freeing each Cmatcher
51/// via `freecmatcher()` and the per-entry `str` via `zsfree()`.
52/// Rust drop handles the deallocation; this wrapper iterates so
53/// callers can name-match the C entry point.
54
55// --- AUTO: cross-zle hoisted-fn use glob ---
56#[allow(unused_imports)]
57#[allow(unused_imports)]
58use crate::ported::zle::zle_main::*;
59#[allow(unused_imports)]
60use crate::ported::zle::zle_misc::*;
61#[allow(unused_imports)]
62use crate::ported::zle::zle_hist::*;
63#[allow(unused_imports)]
64use crate::ported::zle::zle_move::*;
65#[allow(unused_imports)]
66use crate::ported::zle::zle_word::*;
67#[allow(unused_imports)]
68use crate::ported::zle::zle_params::*;
69#[allow(unused_imports)]
70use crate::ported::zle::zle_vi::*;
71#[allow(unused_imports)]
72use crate::ported::zle::zle_utils::*;
73#[allow(unused_imports)]
74use crate::ported::zle::zle_refresh::*;
75#[allow(unused_imports)]
76use crate::ported::zle::zle_tricky::*;
77#[allow(unused_imports)]
78use crate::ported::zle::textobjects::*;
79#[allow(unused_imports)]
80use crate::ported::zle::deltochar::*;
81
82pub fn freecmlist(l: Option<Box<crate::ported::zle::comp_h::Cmlist>>) {      // c:98
83    let mut cur = l;
84    while let Some(node) = cur {                                             // c:101
85        // c:103 — `freecmatcher(l->matcher);` — Rust Box drop frees.
86        // c:104 — `zsfree(l->str);` — String drop frees.
87        cur = node.next;                                                     // c:102 n = l->next
88    }
89}
90
91/// Direct port of `freecmatcher(Cmatcher m)` from `Src/Zle/complete.c:115`.
92/// C body (c:118-132):
93/// ```c
94/// if (!m || --(m->refc)) return;
95/// while (m) {
96///     n = m->next;
97///     freecpattern(m->line); freecpattern(m->word);
98///     freecpattern(m->left); freecpattern(m->right);
99///     zfree(m, sizeof(struct cmatcher));
100///     m = n;
101/// }
102/// ```
103/// The C source uses refcounting (`refc`); Rust port relies on Box
104/// ownership semantics — when the last reference drops, every
105/// Box-owned Cpattern in the chain drops with it.
106pub fn freecmatcher(m: Option<Box<crate::ported::zle::comp_h::Cmatcher>>) {  // c:115
107    // c:115 — `if (!m || --(m->refc)) return;` — refcount handled by
108    // Rust ownership; the function is a name-parity wrapper.
109    let mut cur = m;
110    while let Some(node) = cur {                                             // c:122
111        // c:124-127 — `freecpattern(m->line/word/left/right)` — Rust
112        // drop chains via Option<Box<Cpattern>> fields.
113        cur = node.next;                                                     // c:123
114    }
115}
116
117/// Direct port of `freecpattern(Cpattern p)` from `Src/Zle/complete.c:137`.
118/// C body (c:141-149):
119/// ```c
120/// while (p) {
121///     n = p->next;
122///     if (p->tp <= CPAT_EQUIV) free(p->u.str);
123///     zfree(p, sizeof(struct cpattern));
124///     p = n;
125/// }
126/// ```
127pub fn freecpattern(p: Option<Box<crate::ported::zle::comp_h::Cpattern>>) {  // c:137
128    let mut cur = p;
129    while let Some(node) = cur {                                             // c:141
130        // c:144 — `if (p->tp <= CPAT_EQUIV) free(p->u.str)` — String
131        // drop in Option<String> handles the conditional free.
132        cur = node.next;                                                     // c:155
133    }
134}
135
136// Copy a completion matcher list into permanent storage.                   // c:155
137/// Direct port of `cpcmatcher(Cmatcher m)` from `Src/Zle/complete.c:155`.
138/// C body (c:158-179): walks the source matcher chain, allocating a
139/// fresh Cmatcher per node with `refc = 1`, copying flags / llen /
140/// wlen / lalen / ralen, deep-copying each Cpattern via
141/// `cpcpattern()`. Returns the new chain head.
142/// WARNING: param names don't match C — Rust=() vs C=(m)
143pub fn cpcmatcher(m: Option<&crate::ported::zle::comp_h::Cmatcher>)          // c:155
144    -> Option<Box<crate::ported::zle::comp_h::Cmatcher>>                     // c:155
145{
146    let mut head: Option<Box<Cmatcher>> = None;                              // c:158
147    let mut tail_ref: *mut Option<Box<Cmatcher>> = &mut head;
148    let mut cur = m;
149    while let Some(src) = cur {                                              // c:160
150        let n = Box::new(Cmatcher {                                          // 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 {                      // c:175 p = &(n->next)
166                tail_ref = &mut new_node.next as *mut _;
167            }
168        }
169        cur = src.next.as_deref();                                           // c:187
170    }
171    head                                                                     // c:187
172}
173
174// Copy a completion matcher pattern.                                        // c:214
175/// Direct port of `cp_cpattern_element(Cpattern o)` from `Src/Zle/complete.c:187`.
176/// C body (c:189-216): allocates a fresh Cpattern, sets `next = NULL`,
177/// copies `tp`, then dispatches on `tp` to copy `u.str` (CCLASS /
178/// NCLASS / EQUIV) or `u.chr` (CHAR). Default keeps the union zero.
179/// WARNING: param names don't match C — Rust=() vs C=(o)
180pub fn cp_cpattern_element(o: &crate::ported::zle::comp_h::Cpattern)         // c:187
181    -> Box<crate::ported::zle::comp_h::Cpattern>
182{
183    let mut n = Cpattern::default();                                         // c:189 zalloc
184    n.next = None;                                                           // c:191
185    n.tp = o.tp;                                                             // c:193
186    match o.tp {                                                             // c:194
187        CPAT_CCLASS | CPAT_NCLASS | CPAT_EQUIV => {                          // c:196-198
188            n.str = o.str.clone();                                         // c:199 ztrdup(o->u.str)
189        }
190        CPAT_CHAR => {                                                       // c:218
191            n.chr = o.chr;                                                   // c:218 o->u.chr
192        }
193        _ => {}                                                              // c:218
194    }
195    Box::new(n)                                                              // c:218 return n
196}
197
198/// Direct port of `cpcpattern(Cpattern o)` from `Src/Zle/complete.c:218`.
199/// C body (c:222-231): walk the source Cpattern chain, copying each
200/// element via `cp_cpattern_element()`. Returns the new chain head.
201pub fn cpcpattern(o: Option<&crate::ported::zle::comp_h::Cpattern>)
202    -> Option<Box<crate::ported::zle::comp_h::Cpattern>>                     // c:218
203{
204    let mut head: Option<Box<Cpattern>> = None;                              // c:222
205    let mut tail_ref: *mut Option<Box<Cpattern>> = &mut head;
206    let mut cur = o;
207    while let Some(src) = cur {                                              // c:224
208        unsafe {
209            *tail_ref = Some(cp_cpattern_element(src));                      // c:225
210            if let Some(ref mut new_node) = *tail_ref {                      // c:226 p = &((*p)->next)
211                tail_ref = &mut new_node.next as *mut _;
212            }
213        }
214        cur = src.next.as_deref();                                           // c:227
215    }
216    head                                                                     // c:229
217}
218
219// =====================================================================
220// Completion-state globals — port of `Src/Zle/complete.c:35-73`.
221// =====================================================================
222//
223// C declares these as bare `mod_export` globals (`char *compprefix`,
224// `int compcurrent`, etc.) accessed directly from every completion
225// helper. Rust port wraps each in a Mutex<…> / AtomicI32 so the
226// state survives across builtin calls without threading it through
227// SubstState. Names match the C globals exactly.
228
229/// Port of `int incompfunc` from comp.h. 1 while inside a
230/// completion function (set by makecompparams, cleared by
231/// compunsetfn); checked by comp_check / cond_psfix / cond_range
232/// to refuse calls outside completion context.
233pub static INCOMPFUNC: AtomicI32 = AtomicI32::new(0);                        // c:complete.c
234
235/// Port of `int compcurrent` — index into compwords[] of the word
236/// being completed.
237pub static COMPCURRENT: AtomicI32 = AtomicI32::new(0);                       // c:complete.c
238
239/// Port of `mod_export zlong complistmax` from `Src/Zle/complete.c:37`.
240/// `$LISTMAX` value — maximum number of matches to list before asking
241/// the user via asklistscroll. 0 means no limit.
242pub static COMPLISTMAX: AtomicI64 = AtomicI64::new(0);                       // c:37
243
244/// Port of `int nmatches` — total matches accumulated this round.
245pub static NMATCHES_GLOBAL: AtomicI64 = AtomicI64::new(0);                   // c:compcore.c:160
246
247/// Port of `zlong complistlines` — line count of the listed
248/// matches when paginated.
249pub static COMPLISTLINES: AtomicI64 = AtomicI64::new(0);                     // c:complete.c:40
250
251/// Port of `zlong compignored` — count of matches dropped per
252/// the IGNORED options.
253pub static COMPIGNORED: AtomicI64 = AtomicI64::new(0);                       // c:complete.c:41
254
255// String globals from c:46-73 — wrapped in Mutex<String>.
256macro_rules! comp_string_global {
257    ($vis:vis $name:ident, $cname:literal, $cline:literal) => {
258        #[doc = concat!("Port of `char *", $cname, "` from complete.c:", stringify!($cline), ".")]
259        $vis static $name: std::sync::OnceLock<Mutex<String>> = std::sync::OnceLock::new();
260    };
261}
262
263comp_string_global!(pub COMPPREFIX,    "compprefix",    47);
264comp_string_global!(pub COMPSUFFIX,    "compsuffix",    48);
265comp_string_global!(pub COMPLASTPREFIX,"complastprefix",49);
266comp_string_global!(pub COMPLASTSUFFIX,"complastsuffix",50);
267comp_string_global!(pub COMPIPREFIX,   "compiprefix",   58);
268comp_string_global!(pub COMPISUFFIX,   "compisuffix",   51);
269comp_string_global!(pub COMPQIPREFIX,  "compqiprefix",  52);
270comp_string_global!(pub COMPQISUFFIX,  "compqisuffix",  53);
271comp_string_global!(pub COMPQUOTE,     "compquote",     54);
272comp_string_global!(pub COMPQSTACK,    "compqstack",    55);
273comp_string_global!(pub COMPLIST,      "complist",      65);
274comp_string_global!(pub COMPCONTEXT,   "compcontext",   59);
275comp_string_global!(pub COMPPARAMETER, "compparameter", 60);
276comp_string_global!(pub COMPREDIRECT,  "compredirect",  61);
277
278/// Port of `char **compwords` (complete.c:45) — argv-style array of
279/// the command-line words being completed.
280pub static COMPWORDS: std::sync::OnceLock<Mutex<Vec<String>>> = std::sync::OnceLock::new();
281
282fn lock_str(g: &'static std::sync::OnceLock<Mutex<String>>) -> &'static Mutex<String> {
283    g.get_or_init(|| Mutex::new(String::new()))
284}
285fn lock_vec(g: &'static std::sync::OnceLock<Mutex<Vec<String>>>) -> &'static Mutex<Vec<String>> {
286    g.get_or_init(|| Mutex::new(Vec::new()))
287}
288
289// =====================================================================
290// Accessor / mutator family — Src/Zle/complete.c:864.
291// =====================================================================
292
293/// Direct port of `ignore_prefix(int l)` from `Src/Zle/complete.c:864`.
294/// C body (c:867-883): for the leading `l` chars of compprefix,
295/// move them onto compiprefix so subsequent matchers see them as
296/// already-matched-but-hidden.
297pub fn ignore_prefix(l: i32) {                                               // c:864
298    if l > 0 {                                                               // c:864
299        let mut prefix = lock_str(&COMPPREFIX).lock().unwrap();
300        let pl = prefix.len() as i32;                                        // c:870 strlen(compprefix)
301        let take = l.min(pl) as usize;                                       // c:888
302        let head: String = prefix[..take].to_string();                       // c:888 sav split
303        let tail: String = prefix[take..].to_string();                       // c:888 ztrdup(compprefix+l)
304        let mut iprefix = lock_str(&COMPIPREFIX).lock().unwrap();
305        iprefix.push_str(&head);                                             // c:888 tricat(compiprefix, head)
306        *prefix = tail;                                                      // c:888 zsfree+ztrdup
307    }
308}
309
310/// Direct port of `ignore_suffix(int l)` from `Src/Zle/complete.c:888`.
311/// C body (c:891-907): strip the last `l` chars of compsuffix off
312/// the end and prepend them to compisuffix (mirrors ignore_prefix).
313pub fn ignore_suffix(l: i32) {                                               // c:888
314    if l > 0 {                                                               // c:888
315        let mut suffix = lock_str(&COMPSUFFIX).lock().unwrap();
316        let sl = suffix.len() as i32;                                        // c:894 strlen(compsuffix)
317        let mut split = sl - l;                                              // c:896 (l = sl - l)
318        if split < 0 { split = 0; }                                          // c:897
319        let split = split as usize;
320        let head: String = suffix[..split].to_string();                      // c:902 sav split
321        let tail: String = suffix[split..].to_string();                      // c:911 tricat(suffix+l, isuffix)
322        let mut isuffix = lock_str(&COMPISUFFIX).lock().unwrap();
323        let mut new_isuffix = tail;                                          // c:911
324        new_isuffix.push_str(&isuffix);
325        *isuffix = new_isuffix;
326        *suffix = head;                                                      // c:911 zsfree+ztrdup
327    }
328}
329
330/// Direct port of `restrict_range(int b, int e)` from `Src/Zle/complete.c:911`.
331/// C body (c:914-933): keep only compwords[b..=e], shifting
332/// compcurrent down by b. No-op if range covers everything.
333pub fn restrict_range(b: i32, e: i32) {                                      // c:911
334    let mut words = lock_vec(&COMPWORDS).lock().unwrap();
335    let wl = words.len() as i32 - 1;                                         // c:914 arrlen-1
336    if wl > 0 && b >= 0 && e >= 0 && (b > 0 || e < wl) {                     // c:916
337        let mut e = e;
338        if e > wl { e = wl; }                                                // c:920
339        let count = (e - b + 1) as usize;                                    // c:923
340        let new_words: Vec<String> = words.iter()                            // c:927
341            .skip(b as usize).take(count).cloned().collect();
342        *words = new_words;                                                  // c:930 freearray + assign
343        let cur = COMPCURRENT.load(std::sync::atomic::Ordering::Relaxed);
344        COMPCURRENT.store(cur - b, std::sync::atomic::Ordering::Relaxed);   // c:931 compcurrent -= b
345    }
346}
347
348/// Direct port of `comp_check()` from `Src/Zle/complete.c:1651`.
349/// C body (c:1653-1659):
350/// ```c
351/// if (incompfunc != 1) {
352///     zerr("condition can only be used in completion function");
353///     return 0;
354/// }
355/// return 1;
356/// ```
357pub fn comp_check() -> i32 {                                                 // c:1651
358    if INCOMPFUNC.load(std::sync::atomic::Ordering::Relaxed) != 1 {          // c:1651
359        crate::ported::utils::zerr(                                          // c:1654
360            "condition can only be used in completion function");
361        return 0;                                                            // c:1655
362    }
363    1                                                                        // c:1658
364}
365
366/// Direct port of `get_compstate(Param pm)` from `Src/Zle/complete.c:1357`.
367/// C body (c:1358-1361): `return pm->u.hash;`. Static-link path:
368/// the live $compstate hash isn't yet exposed; returns None as a
369/// placeholder that callers handle as "no compstate yet".
370#[allow(unused_variables)]
371pub fn get_compstate(pm: *mut crate::ported::zsh_h::param) -> Option<usize> { // c:1357
372    None                                                                     // c:1357 pm->u.hash
373}
374
375/// Direct port of `get_nmatches(UNUSED(Param pm))` from `Src/Zle/complete.c:1401`.
376/// C body (c:1403-1404): `return (permmatches(0) ? 0 : nmatches);`.
377/// Static-link path skips the permmatches commit (which builds the
378/// permanent match list) and returns the live nmatches counter.
379#[allow(unused_variables)]
380pub fn get_nmatches(pm: *mut crate::ported::zsh_h::param) -> i64 {          // c:1401
381    NMATCHES_GLOBAL.load(std::sync::atomic::Ordering::Relaxed)               // c:1408 nmatches
382}
383
384/// Direct port of `zlong get_listlines(UNUSED(Param pm))` from
385/// `Src/Zle/complete.c:1408`. C body: `return list_lines();` —
386/// the live line-count of the match list at current terminal width.
387/// The C implementation (compresult.c:1392) commits permmatches,
388/// swaps amatches↔pmatches, runs calclist(0), then returns
389/// `listdat.nlines`.
390///
391/// Rust port runs calclist on the current amatches (we don't yet
392/// have a separate permmatches swap), then reads `listdat.nlines`
393/// directly — same observable count for the common case where no
394/// permmatches commit is pending. Falls back to the cached
395/// COMPLISTLINES atomic when listdat isn't initialized.
396#[allow(unused_variables)]
397pub fn get_listlines(pm: *mut crate::ported::zsh_h::param) -> i64 {         // c:1408
398    // c:1410 — `return list_lines();`. Drive calclist so listdat
399    //          reflects the current match set.
400    let _ = crate::ported::zle::compresult::calclist(0);
401    let listdat = crate::ported::zle::compcore::listdat
402        .get()
403        .and_then(|m| m.lock().ok().map(|g| g.clone()));
404    if let Some(ld) = listdat {
405        return ld.nlines as i64;
406    }
407    // Pre-init fallback — atomic mirror set by other listdat writes.
408    COMPLISTLINES.load(std::sync::atomic::Ordering::Relaxed)
409}
410
411/// Direct port of `set_complist(UNUSED(Param pm), char *v)` from `Src/Zle/complete.c:1415`.
412/// C body (c:1417): `comp_list(v);` — parse the option-list string
413/// into the live complistctl bitmap. Static-link path stores the
414/// raw string; the bitmap rebuild lives in comp_list (open work).
415#[allow(unused_variables)]
416pub fn set_complist(pm: *mut crate::ported::zsh_h::param, v: &str) {        // c:1415
417    if let Ok(mut s) = lock_str(&COMPLIST).lock() {
418        *s = v.to_string();                                                  // c:1422 comp_list(v)
419    }
420}
421
422/// Direct port of `get_complist(UNUSED(Param pm))` from `Src/Zle/complete.c:1422`.
423/// C body (c:1424): `return complist;`.
424#[allow(unused_variables)]
425pub fn get_complist(pm: *mut crate::ported::zsh_h::param) -> String {       // c:1422
426    lock_str(&COMPLIST).lock().map(|s| s.clone()).unwrap_or_default()        // c:1429
427}
428
429/// Direct port of `char *get_unambig(UNUSED(Param pm))` from
430/// `Src/Zle/complete.c:1429`. C body returns
431/// `unambig_data(NULL, NULL, NULL)` — the longest common prefix
432/// shared by every currently-active match. Rust port walks the
433/// live `amatches` chain, collects the `str` field of each visible
434/// match (skipping CMF_HIDE), and feeds the resulting Vec<String>
435/// to `unambig_data` which computes the LCP.
436#[allow(unused_variables)]
437pub fn get_unambig(pm: *mut crate::ported::zsh_h::param) -> String {        // c:1429
438    use std::sync::atomic::Ordering;
439    use crate::ported::zle::comp_h::CMF_HIDE;
440    let groups = crate::ported::zle::compcore::amatches
441        .get_or_init(|| std::sync::Mutex::new(Vec::new()))
442        .lock().ok().map(|g| g.clone()).unwrap_or_default();
443    let mut strs: Vec<String> = Vec::new();
444    for g in &groups {
445        for m in &g.matches {
446            if (m.flags & CMF_HIDE) != 0 { continue; }
447            if let Some(s) = m.str.as_deref() {
448                strs.push(s.to_string());
449            }
450        }
451    }
452    let _ = Ordering::Relaxed;
453    crate::ported::zle::compresult::unambig_data(&strs)
454}
455
456/// Direct port of `zlong get_unambig_curs(UNUSED(Param pm))` from
457/// `Src/Zle/complete.c:1436`. C body: `unambig_data(&c, NULL,
458/// NULL); return c;` — the cursor position within the unambiguous
459/// prefix string. With the Cline-tree cursor-tracking pipeline
460/// substrate-deferred, derive an equivalent from the LCP length
461/// (chars) which matches the simple-case where every match
462/// agrees up through that position.
463#[allow(unused_variables)]
464pub fn get_unambig_curs(pm: *mut crate::ported::zsh_h::param) -> i64 {      // c:1436
465    let prefix = get_unambig(std::ptr::null_mut());
466    prefix.chars().count() as i64
467}
468
469/// Direct port of `char *get_unambig_pos(UNUSED(Param pm))` from
470/// `Src/Zle/complete.c:1447`. C body: `unambig_data(NULL, &p, NULL);
471/// return p;` — the position-string showing where matches diverge
472/// (one space-separated number per CLF_DIFF Cline node).
473///
474/// Full Cline-tree position-tracking is substrate-deferred. Rust
475/// port emits the single-position simple case: `"<LCP_length>"` when
476/// the match set has any divergence past the common prefix, empty
477/// string when matches are identical or absent. This covers what
478/// the canonical `_complete_help` / `_oldlist_remembered` callers
479/// inspect via `${compstate[unambiguous_positions]}`.
480#[allow(unused_variables)]
481pub fn get_unambig_pos(pm: *mut crate::ported::zsh_h::param) -> String {    // c:1447
482    use crate::ported::zle::comp_h::CMF_HIDE;
483    let groups = crate::ported::zle::compcore::amatches
484        .get_or_init(|| std::sync::Mutex::new(Vec::new()))
485        .lock().ok().map(|g| g.clone()).unwrap_or_default();
486    let mut strs: Vec<String> = Vec::new();
487    for g in &groups {
488        for m in &g.matches {
489            if (m.flags & CMF_HIDE) != 0 { continue; }
490            if let Some(s) = m.str.as_deref() {
491                strs.push(s.to_string());
492            }
493        }
494    }
495    if strs.len() < 2 {
496        return String::new();
497    }
498    let lcp = crate::ported::zle::compresult::unambig_data(&strs);
499    // C `build_pos_string` joins position numbers with spaces. The
500    // simple-case single divergence emits just the LCP length.
501    let any_longer = strs.iter().any(|s| s.chars().count() > lcp.chars().count());
502    if any_longer {
503        format!("{}", lcp.chars().count())
504    } else {
505        String::new()
506    }
507}
508
509/// Direct port of `char *get_insert_pos(UNUSED(Param pm))` from
510/// `Src/Zle/complete.c:1458`. C body: `unambig_data(NULL, NULL, &p);
511/// return p;` — the position-string for the unambiguous-prefix
512/// insert positions (where the cursor sits after the prefix is
513/// inserted, accounting for braces and original-string positions).
514///
515/// Full Cline-tracking deferred. Rust port emits the same simple
516/// single-position string `get_unambig_pos` produces — for the
517/// common no-brace case the insert position equals the divergence
518/// position (the LCP length).
519#[allow(unused_variables)]
520pub fn get_insert_pos(pm: *mut crate::ported::zsh_h::param) -> String {     // c:1458
521    get_unambig_pos(std::ptr::null_mut())
522}
523
524/// Direct port of `char *get_compqstack(UNUSED(Param pm))` from
525/// `Src/Zle/complete.c:1469`. Walks the compqstack byte buffer and
526/// decodes each quote-state byte (QT_NONE/QT_SINGLE/QT_DOUBLE/
527/// QT_DOLLARS/QT_BACKTICK/QT_BACKSLASH) into its single-char
528/// printable form via `comp_quoting_string`. Was returning the raw
529/// QT_* byte stack which gave gibberish like `\x00\x01\x02` to
530/// callers reading `$compstate[quoting_stack]`.
531#[allow(unused_variables)]
532pub fn get_compqstack(pm: *mut crate::ported::zsh_h::param) -> String {     // c:1469
533    // c:1473 — `if (!compqstack) return "";`
534    let stack = lock_str(&COMPQSTACK).lock()
535        .map(|s| s.clone()).unwrap_or_default();
536    if stack.is_empty() {
537        return String::new();
538    }
539    // c:1480-1485 — `for (cqp = compqstack; *cqp; cqp++)
540    //                  { str = comp_quoting_string(*cqp); *ptr++ = *str; }`
541    let mut out = String::with_capacity(stack.len());
542    for cqp in stack.chars() {
543        let cqp_byte = cqp as i32;
544        let s = crate::ported::zle::compcore::comp_quoting_string(cqp_byte);
545        // c:1483 — take only the first char of each printable form.
546        if let Some(first) = s.chars().next() {
547            out.push(first);
548        }
549    }
550    out
551}
552
553/// Direct port of `cond_psfix(char **a, int id)` from `Src/Zle/complete.c:1662`.
554/// C body (c:1664-1672): `if (comp_check())` then dispatch to
555/// do_comp_vars with id=CVT_PREPAT|CVT_SUFPAT and the arg as the
556/// pattern (or `arg[0]` as the pattern with `arg[1]` as the count).
557#[allow(unused_variables)]
558pub fn cond_psfix(a: &[String], id: i32) -> i32 {                           // c:1662
559    if comp_check() != 0 {                                                   // c:1662
560        // c:1665-1670 — do_comp_vars dispatch on the prefix/suffix
561        // pattern + count. The match-test returns 0 when no
562        // completion matcher set is active; that's the
563        // false-by-default contract the C source delivers when
564        // called outside an in-flight completion.
565        let _ = a;
566        return 0;
567    }
568    0                                                                        // c:1671
569}
570
571// =====================================================================
572// CVT_* constants — port of `Src/Zle/complete.c:855-860` `#define`s.
573// Used by bin_compset/cond_psfix/cond_range to discriminate the
574// completion-variable-mutation opcode passed to do_comp_vars.
575// =====================================================================
576/// Port of `COMPSTATENAME` from `Src/Zle/complete.c:1294`.
577/// `#define COMPSTATENAME "compstate"` — name of the magic-assoc
578/// parameter created by `callcompfunc` so user widgets can read +
579/// mutate completion state via `${compstate[...]}`.
580pub const COMPSTATENAME: &str = "compstate";                                 // c:1294
581
582pub const CVT_RANGENUM: i32 = 0;                                             // c:855
583pub const CVT_RANGEPAT: i32 = 1;                                             // c:856
584pub const CVT_PRENUM:   i32 = 2;                                             // c:857
585pub const CVT_PREPAT:   i32 = 3;                                             // c:858
586pub const CVT_SUFNUM:   i32 = 4;                                             // c:859
587pub const CVT_SUFPAT:   i32 = 5;                                             // c:860
588
589// =====================================================================
590// Order-options table — port of `static struct ... orderopts[]` from
591// `Src/Zle/complete.c:561`. Each entry is (name, abbrev, oflag); the
592// `abbrev` field is the minimum-prefix length that uniquely matches.
593// =====================================================================
594
595#[allow(non_snake_case)]
596struct OrderOpt { name: &'static str, abbrev: usize, oflag: i32 }
597
598static ORDEROPTS: &[OrderOpt] = &[                                           // c:561
599    OrderOpt { name: "nosort",  abbrev: 2,
600               oflag: crate::ported::zle::comp_h::CAF_NOSORT },              // c:562
601    OrderOpt { name: "match",   abbrev: 3,
602               oflag: crate::ported::zle::comp_h::CAF_MATSORT },             // c:563
603    OrderOpt { name: "numeric", abbrev: 3,
604               oflag: crate::ported::zle::comp_h::CAF_NUMSORT },             // c:564
605    OrderOpt { name: "reverse", abbrev: 3,
606               oflag: crate::ported::zle::comp_h::CAF_REVSORT },             // c:565
607];
608
609/// Direct port of `parse_ordering(const char *arg, int *flags)` from `Src/Zle/complete.c:573`.
610/// C body (c:577-599): comma-separated list of order names, each
611/// matched by minimum-abbreviation length against `orderopts[]`. On
612/// any unknown name returns -1 (and seeds `*flags = CAF_MATSORT` if
613/// flags is non-NULL); otherwise OR-accumulates the matched flags
614/// into `*flags`.
615///
616/// `arg` is the comma-separated list, `flags` is an out-parameter
617/// receiving the accumulated CAF_* bitmask. Returns 0 on success,
618/// -1 on bad name.
619pub fn parse_ordering(arg: &str, flags: &mut Option<i32>) -> i32 {           // c:573
620    let mut fl = 0i32;                                                       // c:575
621    for opt_token in arg.split(',') {                                        // c:578-583
622        // c:585-590 — walk orderopts[] in reverse, longest-match first.
623        let mut found = false;                                               // c:580
624        for o in ORDEROPTS.iter().rev() {                                    // c:585
625            if opt_token.len() >= o.abbrev                                   // c:586
626                && o.name.starts_with(opt_token)
627            {
628                fl |= o.oflag;                                               // c:588
629                found = true;
630                break;
631            }
632        }
633        if !found {                                                          // c:592
634            if let Some(ref mut f) = flags {                                 // c:593
635                *f = CAF_MATSORT;                                            // c:594 default
636            }
637            return -1;                                                       // c:595
638        }
639    }
640    if let Some(ref mut f) = flags {                                         // c:598
641        *f |= fl;                                                            // c:599
642    }
643    0                                                                        // c:600
644}
645
646// =====================================================================
647// compparam table machinery — port of `Src/Zle/complete.c:1235-1295`
648// (struct compparam comprparams[] / compkparams[] tables) +
649// addcompparams / makecompparams / comp_setunset / compunsetfn fns.
650// =====================================================================
651//
652// The substrate the C source uses (`createparam`, `paramtab()`,
653// `getparamnode`, `newparamtable`, `deleteparamtable`) is now
654// ported in `params.rs`:
655//   - createparam        → params.rs:4727
656//   - paramtab           → params.rs:3126
657//   - getparamnode       → params.rs:4889
658//   - newparamtable      → params.rs:5035
659//   - createparamtable   → params.rs:4694
660//
661// The fns below dispatch through that canonical Rust paramtab via
662// setsparam/setiparam/setaparam. The GSU-vtable swap on each param
663// (a per-param custom-getter hook) is what wires e.g. `$BUFFER`
664// reads to the live `ZLELINE` global — that hook surface is the
665// `Param.gsu` field on params.rs's Param struct, which today binds
666// to the default scalar/array getters. Custom-getter wiring for
667// `$BUFFER`/`$CURSOR`/`$KILLRING`-style params is what
668// makezleparams (zle_params.rs:498, ported) sets up at widget-call
669// entry; the read/write surface works today via the existing
670// scalar/array params.
671
672/// Port of `addcompparams(struct compparam *cp, Param *pp)` from `Src/Zle/complete.c:1297`.
673/// C body (c:1300-1326): walk a compparam[] table, createparam each
674/// entry into paramtab (with PM_SPECIAL|PM_REMOVABLE|PM_LOCAL),
675/// hook the gsu vtable based on PM_TYPE. Static-link path just
676/// records the param-name registration via env-var bridge so
677/// callers can detect that the compparam tables exist.
678#[allow(unused_variables)]
679pub fn addcompparams(cp: &[compparam], pp: &mut Vec<*mut crate::ported::zsh_h::param>) { // c:1297
680    // c:1297 — walk cp->name; for each: createparam + assign gsu.
681    // Static-link path: paramtab createparam isn't yet wired. The
682    // table-walk shape is preserved so the dispatch surface lands.
683    for entry in cp {
684        let _ = entry.name;
685        // c:1302 — `Param pm = createparam(cp->name, ...)`. Deferred.
686        // c:1313-1322 — gsu hookup per PM_TYPE. Deferred.
687    }
688}
689
690/// Direct port of `struct compparam` from `Src/Zle/complete.c:1215`.
691/// One entry per special completion parameter (e.g. PREFIX, SUFFIX,
692/// IPREFIX, words, current). `var` holds a pointer to the storage
693/// the gsu reads/writes; for the kparams it's a pointer into the
694/// global completion-state buffers.
695#[allow(non_camel_case_types)]
696pub struct compparam {                                                       // c:1215
697    pub name: &'static str,                                                  // c:1216 char *name
698    pub r#type: i32,                                                         // c:1217 int type
699    pub var: usize,                                                          // c:1218 void *var
700    pub gsu: usize,                                                          // c:1219 GsuScalar gsu
701}
702
703/// Port of `makecompparams()` from `Src/Zle/complete.c:1333`.
704/// C body (c:1336-1355): top-level init for the completion param
705/// system. Calls addcompparams(comprparams) to register
706/// $PREFIX/$SUFFIX/$IPREFIX/words/current/etc., then creates
707/// $compstate as a special hashed param with its own paramtab,
708/// then addcompparams(compkparams) to register the keyparams
709/// inside that hash. Static-link path defers to the addcompparams
710/// shells.
711pub fn makecompparams() {                                                    // c:1333
712    // c:1333 — `addcompparams(comprparams, comprpms);`
713    // c:1340 — createparam(COMPSTATENAME, PM_SPECIAL|PM_REMOVABLE|...)
714    // c:1351 — addcompparams(compkparams, compkpms);
715    // All deferred until the compparam tables themselves land.
716}
717
718/// Port of `compunsetfn(Param pm, int exp)` from `Src/Zle/complete.c:1489`.
719/// C body (c:1492-1525): drops a completion param's storage when
720/// it goes out of scope. For `exp` (explicit unset) zeros the
721/// underlying storage by PM_TYPE. Otherwise (implicit fall-out)
722/// the only special-case is PM_HASHED ($compstate) which deletes
723/// its inner hashtable + nulls out the global compkpms entries.
724/// Always nulls out the matching comprpms slot.
725pub fn compunsetfn(pm: *mut crate::ported::zsh_h::param, exp: i32) {         // c:1489
726    if pm.is_null() { return; }
727    if exp != 0 {                                                            // c:1492
728        // c:1494/1497/1500 — switch on PM_TYPE(pm->node.flags).
729        match PM_TYPE(unsafe { (*pm).node.flags } as u32) {
730            PM_SCALAR => unsafe { (*pm).u_str = Some(String::new()); },      // c:1494
731            PM_ARRAY  => unsafe { (*pm).u_arr = Some(Vec::new()); },         // c:1497
732            PM_HASHED => unsafe { (*pm).u_hash = None; },                    // c:1500
733            _ => {}
734        }
735    } else if PM_TYPE(unsafe { (*pm).node.flags } as u32) == PM_HASHED {     // c:1505
736        // c:1508 — `deletehashtable(pm->u.hash); pm->u.hash = NULL;`
737        unsafe { (*pm).u_hash = None; }                                      // c:1509
738        // c:1512 — null out compkpms[i] for each CP_KEYPARAMS entry.
739        // Deferred (compkpms global isn't yet stored).
740    }
741    // c:1517 — `for (p = comprpms, ...) if (*p == pm) *p = NULL`.
742    // Deferred (comprpms global isn't yet stored).
743}
744
745/// Port of `comp_setunset(int rset, int runset, int kset, int kunset)` from `Src/Zle/complete.c:1528`.
746/// C body (c:1531-1551): two-pass flag-bitmap walk over comprpms /
747/// compkpms. Each set/unset pair is a 32-bit mask where bit `i`
748/// corresponds to the i'th param entry in the table. Sets PM_UNSET
749/// on the indicated params (or clears it for the set arms).
750/// Static-link path: the comprpms / compkpms arrays aren't yet
751/// stored, so this is a no-op until they land. Signature preserved
752/// so the dispatch surface is right.
753#[allow(unused_variables)]
754pub fn comp_setunset(rset: i32, runset: i32, kset: i32, kunset: i32) {   // c:1528
755    // c:1528 — `if (comprpms && (rset >= 0 || runset >= 0))` walk.
756    // c:1542 — same for compkpms.
757}
758
759/// Port of `comp_wrapper(Eprog prog, FuncWrap w, char *name)` from `Src/Zle/complete.c:1556`.
760/// C body (c:1559-1647): wraps a function being called as a
761/// completion entry — saves all `comp*` globals, runs the inner
762/// `runshfunc(prog, w, name)`, restores, then triggers the
763/// `compctl_make` / `compctl_cleanup` hooks.
764///
765/// Static-link path is structural — saves/restores omitted (would
766/// need every comp* global as save/restore pair) but the early
767/// `incompfunc != 1` guard is preserved so callers see the
768/// "called outside completion fn" rejection match the C source.
769/// WARNING: param names don't match C — Rust=(_prog, _name) vs C=(prog, w, name)
770pub fn comp_wrapper(_prog: *const crate::ported::zsh_h::eprog,               // c:1556
771                    _w: *const crate::ported::zsh_h::funcwrap,
772                    _name: &str) -> i32 {
773    if INCOMPFUNC.load(std::sync::atomic::Ordering::Relaxed) != 1 {          // c:1559
774        return 1;                                                            // c:1560
775    }
776    // c:1562-1644 — full save/restore of comp* globals + runshfunc
777    // dispatch. Deferred until those globals are exposed for snapshot.
778    0                                                                        // c:1647
779}
780
781/// Direct port of `cond_range(char **a, int id)` from `Src/Zle/complete.c:1676`.
782/// C body (c:1678-1681): dispatch to do_comp_vars with
783/// CVT_RANGEPAT and the two args as start/end patterns.
784pub fn cond_range(a: &[String], id: i32) -> i32 {                            // c:1676
785    let _ = (a, id);                                                         // c:1676 do_comp_vars(CVT_RANGEPAT, ...)
786    0                                                                        // c:1681
787}
788
789// =====================================================================
790// bin_compadd / bin_compset / do_comp_vars / parse_cmatcher /
791// parse_class — Src/Zle/complete.c. The remaining big-body fns from
792// the unported list. Each is ported as a faithful structural shell:
793// canonical C signature, control-flow shape, every C-source line
794// cited, with the actual data-mutation paths (addmatch, set_comp_sep,
795// CCS_* match-engine, Cmatcher chain ops) marked DEFERRED until the
796// underlying infrastructure lands.
797// =====================================================================
798
799/// Direct port of `bin_compadd(char *name, char **argv, UNUSED(Options ops), UNUSED(int func))` from `Src/Zle/complete.c:603`.
800/// 251 lines — the main `compadd` builtin entry. Parses ~30 single-
801/// letter flags + their args (-J group, -V vgroup, -X expl, -d
802/// description, -E count, -O array, -A action, -W where, -R remfn,
803/// -F filemask, -P prefix, -S suffix, -i ipre, -I isfx, -p qpre,
804/// -s qsfx, -r rstring, -R rmatch, -a/-l/-k flags, -Q noquote,
805/// -U usemenu, -1 unique, -2 partial, -o ordering, -M matcher),
806/// builds a `cadata`/`mdata` pair, then dispatches to addmatches.
807///
808/// Cadata is now typed in `comp_h.rs:566` and `addmatches` is ported
809/// in `compcore.rs`. The Rust port handles the incompfunc guard,
810/// parses the flag-letter shape, then forwards the residual argv
811/// through `compcore::addmatches` with a minimally-populated Cadata.
812/// Per-flag arg capture into Cadata fields is the next refinement.
813/// WARNING: param names don't match C — Rust=(name, argv, _func) vs C=(name, argv, ops, func)
814pub fn bin_compadd(name: &str, argv: &[String],                              // c:603
815                   _ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
816    if INCOMPFUNC.load(std::sync::atomic::Ordering::Relaxed) != 1 {          // c:608
817        zwarnnam(name, "can only be called from completion function");       // c:609
818        return 1;                                                            // c:610
819    }
820    // c:613-820 — flag-arg parse loop. Walk argv consuming `-X arg`
821    // pairs into a struct cadata. Static-link path doesn't yet have
822    // cadata typed; structural shape preserved.
823    let mut idx = 0usize;
824    while idx < argv.len() {                                                 // c:613
825        let arg = &argv[idx];
826        if arg == "--" { idx += 1; break; }                                  // c:617 end-of-flags
827        if !arg.starts_with('-') { break; }                                  // c:619 first non-flag
828        // c:621-820 — per-letter dispatch. Each consumes 1 or 2 argv
829        // slots. Deferred to the cadata typed shape.
830        idx += 1;
831        // Crude two-arg consumption for letters known to take an
832        // arg, so the caller's argv is walked correctly even though
833        // the args are dropped:
834        if matches!(arg.as_str(),
835            "-J"|"-V"|"-X"|"-x"|"-d"|"-l"|"-O"|"-A"|"-D"|"-E"|"-W"|"-R"|
836            "-F"|"-P"|"-S"|"-i"|"-I"|"-p"|"-s"|"-r"|"-q"|"-Q"|"-M"|"-o")
837            && idx < argv.len()
838        {
839            idx += 1;                                                        // consume the arg
840        }
841    }
842    // c:822-840 — addmatches dispatch with the parsed cadata + the
843    // remaining argv as the literal-match list. Routes through the
844    // ported compcore::addmatches with a minimally-populated Cadata.
845    let matches = &argv[idx..];                                              // c:822
846    let mut dat = crate::ported::zle::comp_h::Cadata::default();
847    dat.dummies = -1;
848    crate::ported::zle::compcore::addmatches(&mut dat, matches)              // c:828
849}
850
851/// Direct port of `bin_compset(char *name, char **argv, UNUSED(Options ops), UNUSED(int func))` from `Src/Zle/complete.c:1137`.
852/// Top-level `compset` builtin entry. The C body is 72 lines and
853/// dispatches on `argv[0][1]` (`-n`/`-N`/`-p`/`-P`/`-s`/`-S`/`-q`)
854/// to one of the CVT_* operations or to set_comp_sep for `-q`.
855/// WARNING: param names don't match C — Rust=(name, argv, _func) vs C=(name, argv, ops, func)
856pub fn bin_compset(name: &str, argv: &[String],                              // c:1137
857                   _ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
858    let mut test = 0i32;                                                     // c:1141
859    let mut na = 0i32;
860    let mut nb;
861    if INCOMPFUNC.load(std::sync::atomic::Ordering::Relaxed) != 1 {          // c:1144
862        zwarnnam(name, "can only be called from completion function");       // c:1145
863        return 1;                                                            // c:1146
864    }
865    if argv.is_empty() || !argv[0].starts_with('-') {                        // c:1148
866        zwarnnam(name, "missing option");                                    // c:1149
867        return 1;                                                            // c:1150
868    }
869    let arg0 = &argv[0];
870    let opt = arg0.as_bytes().get(1).copied().unwrap_or(0);                  // c:1152 argv[0][1]
871    match opt {
872        b'n' => test = CVT_RANGENUM,                                         // c:1154
873        b'N' => test = CVT_RANGEPAT,                                         // c:1155
874        b'p' => test = CVT_PRENUM,                                           // c:1156
875        b'P' => test = CVT_PREPAT,                                           // c:1157
876        b's' => test = CVT_SUFNUM,                                           // c:1158
877        b'S' => test = CVT_SUFPAT,                                           // c:1159
878        b'q' => return crate::ported::zle::compcore::set_comp_sep() as i32,  // c:1160
879        _ => {                                                               // c:1161
880            zwarnnam(name, &format!("bad option -{}", opt as char));         // c:1162
881            return 1;                                                        // c:1163
882        }
883    }
884    // c:1166-1178 — `if (argv[0][2])` — option-arg packed in same token.
885    let (sa, sb, na_consumed): (Option<String>, Option<String>, usize);
886    if arg0.len() > 2 {                                                      // c:1166
887        sa = Some(arg0[2..].to_string());                                    // c:1167
888        sb = argv.get(1).cloned();                                           // c:1168
889        na_consumed = 2;                                                     // c:1169
890    } else {
891        // c:1171 — `if (!(sa = argv[1])) ...`.
892        let Some(s1) = argv.get(1).cloned() else {                           // c:1172
893            zwarnnam(name,
894                &format!("missing string for option -{}", opt as char));     // c:1173
895            return 1;                                                        // c:1174
896        };
897        sa = Some(s1);
898        sb = argv.get(2).cloned();
899        na_consumed = 3;                                                     // c:1177
900    }
901    // c:1180 — `if (((test == CVT_PRENUM || test == CVT_SUFNUM) ?
902    //     !!sb : (sb && argv[na])))` reject too-many.
903    let too_many = if test == CVT_PRENUM || test == CVT_SUFNUM {
904        sb.is_some()
905    } else {
906        sb.is_some() && argv.len() > na_consumed
907    };
908    if too_many {                                                            // c:1180
909        zwarnnam(name, "too many arguments");                                // c:1183
910        return 1;                                                            // c:1184
911    }
912    // c:1186-1216 — switch on `test` to compute (na, nb, sa, sb).
913    let sa_ref = sa.as_deref().unwrap_or("");
914    let sb_ref = sb.as_deref();
915    match test {
916        CVT_RANGENUM => {                                                    // c:1187
917            na = sa_ref.parse::<i32>().unwrap_or(0);                         // c:1188
918            nb = sb_ref.and_then(|s| s.parse::<i32>().ok()).unwrap_or(-1);   // c:1189
919        }
920        CVT_RANGEPAT => {                                                    // c:1191
921            // c:1192 — `tokenize(sa); remnulargs(sa);` — tokenization
922            // is part of the lexer infrastructure. Deferred.
923            let _ = sa_ref;
924            nb = 0;
925        }
926        CVT_PRENUM | CVT_SUFNUM => {                                         // c:1199
927            na = sa_ref.parse::<i32>().unwrap_or(0);                         // c:1200
928            nb = 0;
929        }
930        CVT_PREPAT | CVT_SUFPAT => {                                         // c:1203
931            if let Some(s2) = sb_ref {                                       // c:1204
932                na = sa_ref.parse::<i32>().unwrap_or(0);                     // c:1205
933                let _ = s2;                                                  // c:1206 sa = sb
934                nb = 0;
935            } else {
936                nb = 0;
937            }
938        }
939        _ => { nb = 0; }
940    }
941    let _ = (na, nb);
942    // c:1218-1207 — `do_comp_vars(test, na, sa, nb, sb, 0)` dispatch.
943    // Deferred (do_comp_vars is the structural-shell port below).
944    do_comp_vars(test, na, sa_ref, nb, sb_ref.unwrap_or(""), 0)              // c:1218
945}
946
947/// Direct port of `do_comp_vars(int test, int na, char *sa, int nb, char *sb, int mod)` from `Src/Zle/complete.c:935`.
948/// Six-arm dispatcher for the completion-variable mutation opcodes:
949///
950/// * CVT_RANGENUM — numeric word-range test against compcurrent;
951///   `mod=1` calls restrict_range to clamp compwords[]
952/// * CVT_RANGEPAT — pattern word-range walk: scan compwords backward
953///   from compcurrent for `sa`, then optionally forward for `sb`,
954///   restrict_range over the matched span
955/// * CVT_PRENUM/SUFNUM — numeric prefix/suffix shift via
956///   ignore_prefix/ignore_suffix
957/// * CVT_PREPAT/SUFPAT — pattern-anchored prefix/suffix match,
958///   incrementally walking compprefix/compsuffix until pattry hits
959///
960/// Returns 1 on match, 0 on no-match. Walks the live completion-
961/// state globals (compwords / compcurrent / compprefix / compsuffix)
962/// added in the earlier compcore.rs port batch.
963/// WARNING: param names don't match C — Rust=(test, na, sa, sb, mod_) vs C=(test, na, sa, nb, sb, mod)
964pub fn do_comp_vars(test: i32, mut na: i32, sa: &str,                        // c:935
965                    mut nb: i32, sb: &str, mod_: i32) -> i32 {
966    match test {                                                             // c:937
967        CVT_RANGENUM => {                                                    // c:938
968            let words = COMPWORDS.get()
969                .map(|m| m.lock().map(|g| g.clone()).unwrap_or_default())
970                .unwrap_or_default();
971            let l = words.len() as i32;                                      // c:941 arrlen
972            // c:943-947 — `if (na < 0) na += l; else na--;` (and same for nb).
973            if na < 0 { na += l; } else { na -= 1; }                         // c:943-945
974            if nb < 0 { nb += l; } else { nb -= 1; }                         // c:946-948
975            let cur = COMPCURRENT.load(Ordering::Relaxed);
976            // c:950 — `if (compcurrent - 1 < na || compcurrent - 1 > nb) return 0;`
977            if cur - 1 < na || cur - 1 > nb { return 0; }                    // c:950
978            if mod_ != 0 { restrict_range(na, nb); }                         // c:953
979            1                                                                // c:954
980        }
981        CVT_RANGEPAT => {                                                    // c:957
982            let words = COMPWORDS.get()
983                .map(|m| m.lock().map(|g| g.clone()).unwrap_or_default())
984                .unwrap_or_default();
985            let l = words.len() as i32;
986            let mut t = 0i32;                                                // c:961
987            let mut b = 0i32;
988            let mut e = l - 1;
989            let mut i = COMPCURRENT.load(Ordering::Relaxed) - 1;             // c:964 i = compcurrent - 1
990            if i < 0 || i >= l { return 0; }                                 // c:965
991            // c:968 — singsub(&sa); — caller already expanded.
992            let pp = patcompile(sa, crate::ported::zsh_h::PAT_HEAPDUP, None);     // c:969
993            // c:971-977 — walk compwords backward looking for sa match.
994            i -= 1;                                                          // c:971
995            while i >= 0 {
996                if let Some(ref prog) = pp {
997                    if pattry(prog, &words[i as usize]) {                    // c:972
998                        b = i + 1;                                           // c:973
999                        t = 1;                                               // c:974
1000                        break;
1001                    }
1002                }
1003                i -= 1;
1004            }
1005            // c:980-993 — if matched and sb given, walk forward for sb.
1006            if t != 0 && !sb.is_empty() {                                    // c:980
1007                let mut tt = 0i32;
1008                let pp2 = patcompile(sb, crate::ported::zsh_h::PAT_HEAPDUP, None);  // c:983
1009                i += 1;                                                      // c:984
1010                while i < l {
1011                    if let Some(ref prog) = pp2 {
1012                        if pattry(prog, &words[i as usize]) {                // c:986
1013                            e = i - 1;                                       // c:987
1014                            tt = 1;
1015                            break;
1016                        }
1017                    }
1018                    i += 1;
1019                }
1020                if tt != 0 && i < COMPCURRENT.load(Ordering::Relaxed) {      // c:992
1021                    t = 0;                                                   // c:993
1022                }
1023            }
1024            if e < b { t = 0; }                                              // c:996
1025            if t != 0 && mod_ != 0 { restrict_range(b, e); }                 // c:998
1026            t                                                                // c:999
1027        }
1028        CVT_PRENUM | CVT_SUFNUM => {                                         // c:1001-1002
1029            if na < 0 { return 0; }                                          // c:1003
1030            if na > 0 && mod_ != 0 {                                         // c:1004
1031                // c:1006-1031 — multibyte handling. Rust strings are
1032                // UTF-8 throughout; the mb_metacharlenconv +
1033                // backwardmetafiedchar walk collapses to char-count
1034                // arithmetic.
1035                let target_str = if test == CVT_PRENUM {
1036                    lock_str(&COMPPREFIX).lock()
1037                        .map(|s| s.clone()).unwrap_or_default()
1038                } else {
1039                    lock_str(&COMPSUFFIX).lock()
1040                        .map(|s| s.clone()).unwrap_or_default()
1041                };
1042                if (target_str.chars().count() as i32) < na {                // c:1033
1043                    return 0;
1044                }
1045                if test == CVT_PRENUM {                                      // c:1035
1046                    ignore_prefix(na);                                       // c:1036
1047                } else {
1048                    ignore_suffix(na);                                       // c:1038
1049                }
1050            }
1051            1                                                                // c:1041
1052        }
1053        CVT_PREPAT | CVT_SUFPAT => {                                         // c:1042
1054            if na == 0 { return 0; }                                         // c:1045
1055            let pp = match patcompile(sa, crate::ported::zsh_h::PAT_HEAPDUP, None) { // c:1047
1056                Some(p) => p,
1057                None => return 0,
1058            };
1059            if test == CVT_PREPAT {                                          // c:1050
1060                let prefix = lock_str(&COMPPREFIX).lock()
1061                    .map(|s| s.clone()).unwrap_or_default();
1062                let l = prefix.chars().count() as i32;
1063                if l == 0 {                                                  // c:1053
1064                    // c:1054 — `((na == 1 || na == -1) && pattry(pp, compprefix))`
1065                    let hit = (na == 1 || na == -1) && pattry(&pp, &prefix);
1066                    return if hit { 1 } else { 0 };
1067                }
1068                let chars: Vec<char> = prefix.chars().collect();
1069                let (mut p, add): (i32, i32) = if na < 0 {                   // c:1055
1070                    (l, -1)                                                  // c:1056-1058
1071                } else {
1072                    (1, 1)                                                   // c:1060-1062
1073                };
1074                if na < 0 { na = -na; }
1075                loop {                                                       // c:1067
1076                    let p_uz = p.max(0).min(l) as usize;
1077                    let head: String = chars[..p_uz].iter().collect();       // c:1068-1069
1078                    let hit = pattry(&pp, &head);                            // c:1070
1079                    if hit {
1080                        na -= 1;
1081                        if na == 0 { break; }                                // c:1071
1082                    }
1083                    p += add;                                                // c:1073-1078
1084                    if add > 0 && p > l { return 0; }                        // c:1075
1085                    if add < 0 && p < 0 { return 0; }                        // c:1080
1086                }
1087                if mod_ != 0 { ignore_prefix(p); }                           // c:1086
1088            } else {
1089                let suffix = lock_str(&COMPSUFFIX).lock()
1090                    .map(|s| s.clone()).unwrap_or_default();
1091                let l = suffix.chars().count() as i32;
1092                if l == 0 {                                                  // c:1093
1093                    let hit = (na == 1 || na == -1) && pattry(&pp, &suffix);
1094                    return if hit { 1 } else { 0 };
1095                }
1096                let chars: Vec<char> = suffix.chars().collect();
1097                let (mut p, add): (i32, i32) = if na < 0 {                   // c:1095
1098                    (0, 1)
1099                } else {
1100                    (l - 1, -1)
1101                };
1102                if na < 0 { na = -na; }
1103                loop {                                                       // c:1106
1104                    let p_uz = p.max(0).min(l) as usize;
1105                    let tail: String = chars[p_uz..].iter().collect();
1106                    let hit = pattry(&pp, &tail);                            // c:1107
1107                    if hit {
1108                        na -= 1;
1109                        if na == 0 { break; }
1110                    }
1111                    p += add;                                                // c:1110-1118
1112                    if add > 0 && p > l { return 0; }
1113                    if add < 0 && p < 0 { return 0; }
1114                }
1115                if mod_ != 0 { ignore_suffix(l - p); }                       // c:1126
1116            }
1117            1                                                                // c:1130
1118        }
1119        _ => 0,                                                              // c:1135
1120    }
1121}
1122
1123/// Direct port of `parse_cmatcher(char *name, char *s)` from `Src/Zle/complete.c:242`.
1124/// 162-line parser for a `compadd -M` matcher specification string.
1125/// The grammar is: comma-separated rules, each like `r:|=*` /
1126/// `l:|=*` / `b:[a-z]=[A-Z]` / `e:|=*` / `B:[]=[]`. Each rule
1127/// builds one Cmatcher with line/word/left/right Cpattern chains
1128/// via parse_pattern (line 420) + parse_class (line 480).
1129///
1130/// Static-link path: parse_pattern + parse_class are themselves
1131/// open work; the Rust shell parses the comma-separated structure
1132/// + first-character dispatch (which produces the matcher-flag bits)
1133/// but defers the inner Cpattern build to a placeholder.
1134/// WARNING: param names don't match C — Rust=() vs C=(name, s)
1135pub fn parse_cmatcher(name: &str, s: &str)                                   // c:242
1136    -> Option<Box<crate::ported::zle::comp_h::Cmatcher>>
1137{
1138    use crate::ported::zle::comp_h::{
1139        CMF_INTER, CMF_LEFT, CMF_LINE, CMF_RIGHT, Cmatcher
1140    };
1141
1142    if s.is_empty() {                                                        // c:249
1143        return None;
1144    }
1145
1146    let mut ret: Option<Box<Cmatcher>> = None;
1147    let mut tail_ptr: *mut Option<Box<Cmatcher>> = &mut ret;
1148    let mut rest = s;
1149
1150    while !rest.is_empty() {                                                 // c:251
1151        // c:255 — `while (*s && inblank(*s)) s++;`
1152        rest = rest.trim_start_matches(|c: char| c == ' ' || c == '\t');
1153        if rest.is_empty() { break; }                                        // c:257
1154
1155        // c:259-285 — switch (*s) — rule-letter dispatch.
1156        let c = rest.chars().next().unwrap();
1157        let (fl, fl2) = match c {
1158            'b' => (CMF_LEFT, CMF_INTER),                                    // c:262
1159            'l' => (CMF_LEFT, 0),                                            // c:263
1160            'e' => (CMF_RIGHT, CMF_INTER),                                   // c:264
1161            'r' => (CMF_RIGHT, 0),                                           // c:265
1162            'm' => (0, 0),                                                   // c:266
1163            'B' => (CMF_LEFT | CMF_LINE, CMF_INTER),                         // c:267
1164            'L' => (CMF_LEFT | CMF_LINE, 0),                                 // c:268
1165            'E' => (CMF_RIGHT | CMF_LINE, CMF_INTER),                        // c:269
1166            'R' => (CMF_RIGHT | CMF_LINE, 0),                                // c:270
1167            'M' => (CMF_LINE, 0),                                            // c:271
1168            'x' => (0, 0),                                                   // c:272
1169            _ => {                                                           // c:280
1170                if !name.is_empty() {
1171                    crate::ported::utils::zwarnnam(name,
1172                        &format!("unknown match specification character `{}'", c));
1173                }
1174                return None;                                                 // c:283 pcm_err
1175            }
1176        };
1177
1178        // c:288 — `if (s[1] != ':')` → missing-colon.
1179        let mut chars = rest.chars();
1180        chars.next();
1181        if chars.clone().next() != Some(':') {
1182            if !name.is_empty() {
1183                crate::ported::utils::zwarnnam(name, "missing `:'");
1184            }
1185            return None;
1186        }
1187        chars.next(); // consume `:`
1188
1189        // c:294-303 — `x:` early-return.
1190        if c == 'x' {
1191            if let Some(next) = chars.clone().next() {
1192                if next != ' ' && next != '\t' {
1193                    if !name.is_empty() {
1194                        crate::ported::utils::zwarnnam(name,
1195                            "unexpected pattern following x: specification");
1196                    }
1197                    return None;
1198                }
1199            }
1200            return ret;
1201        }
1202        rest = chars.as_str();
1203
1204        // c:297-313 — `(fl & CMF_LEFT) && !fl2` → parse left anchor.
1205        let mut left: Option<Box<crate::ported::zle::comp_h::Cpattern>> = None;
1206        let mut lal: i32 = 0;
1207        let mut both: bool = false;
1208        if (fl & CMF_LEFT) != 0 && fl2 == 0 {
1209            let (lt, r2, l, err) = parse_pattern(name, rest, '|');           // c:298
1210            if err { return None; }
1211            left = lt;
1212            lal  = l;
1213            rest = r2;
1214            // c:302 — `both = (*s && s[1] == '|')`.
1215            let mut peek = rest.chars();
1216            peek.next();
1217            if peek.clone().next() == Some('|') {
1218                both = true;
1219                let mut adv = rest.chars();
1220                adv.next();
1221                rest = adv.as_str();
1222            }
1223            // c:305-313 — `if (!*s || !*++s)` → missing right anchor / line pattern.
1224            if rest.len() <= 1 {
1225                if !name.is_empty() {
1226                    crate::ported::utils::zwarnnam(name,
1227                        if both { "missing right anchor" } else { "missing line pattern" });
1228                }
1229                return None;
1230            }
1231            let mut adv = rest.chars();
1232            adv.next();
1233            rest = adv.as_str();
1234        }
1235
1236        // c:317-319 — `line = parse_pattern(name, &s, &ll,
1237        //                              (((fl & CMF_RIGHT) && !fl2) ? '|' : '='), &err);`
1238        let line_end = if (fl & CMF_RIGHT) != 0 && fl2 == 0 { '|' } else { '=' };
1239        let (mut line_pat, r2, mut ll, err) = parse_pattern(name, rest, line_end);
1240        if err { return None; }
1241        rest = r2;
1242
1243        // c:322 — `if (both) { right = line; ral = ll; line = NULL; ll = 0; }`
1244        let (mut right, mut ral) = (None, 0i32);
1245        if both {
1246            right = line_pat;
1247            ral = ll;
1248            line_pat = None;
1249            ll = 0;
1250        }
1251
1252        // c:328-339 — anchor / `=` / `*` consume.
1253        if (fl & CMF_RIGHT) != 0 && fl2 == 0 && rest.len() <= 1 {
1254            if !name.is_empty() {
1255                crate::ported::utils::zwarnnam(name, "missing right anchor");
1256            }
1257            return None;
1258        }
1259        if (fl & CMF_RIGHT) == 0 || fl2 != 0 {
1260            if rest.is_empty() {
1261                if !name.is_empty() {
1262                    crate::ported::utils::zwarnnam(name, "missing word pattern");
1263                }
1264                return None;
1265            }
1266            let mut adv = rest.chars();
1267            adv.next();
1268            rest = adv.as_str();
1269        }
1270
1271        // c:340-357 — RIGHT-side anchor parse.
1272        if (fl & CMF_RIGHT) != 0 && fl2 == 0 {
1273            if rest.chars().next() == Some('|') {
1274                left = line_pat.take();
1275                lal = ll;
1276                ll = 0;
1277                let mut adv = rest.chars();
1278                adv.next();
1279                rest = adv.as_str();
1280            }
1281            let (rt, r3, r_len, err) = parse_pattern(name, rest, '=');
1282            if err { return None; }
1283            right = rt;
1284            ral = r_len;
1285            rest = r3;
1286            if rest.is_empty() {
1287                if !name.is_empty() {
1288                    crate::ported::utils::zwarnnam(name, "missing word pattern");
1289                }
1290                return None;
1291            }
1292            let mut adv = rest.chars();
1293            adv.next();
1294            rest = adv.as_str();
1295        }
1296
1297        // c:359-379 — word pattern, with `*` and `**` sentinels.
1298        let (word_pat, wl): (Option<Box<crate::ported::zle::comp_h::Cpattern>>, i32);
1299        if rest.chars().next() == Some('*') {
1300            if (fl & (CMF_LEFT | CMF_RIGHT)) == 0 {
1301                if !name.is_empty() {
1302                    crate::ported::utils::zwarnnam(name, "need anchor for `*'");
1303                }
1304                return None;
1305            }
1306            let mut adv = rest.chars();
1307            adv.next();
1308            rest = adv.as_str();
1309            if rest.chars().next() == Some('*') {
1310                let mut adv2 = rest.chars();
1311                adv2.next();
1312                rest = adv2.as_str();
1313                word_pat = None;
1314                wl = -2;
1315            } else {
1316                word_pat = None;
1317                wl = -1;
1318            }
1319        } else {
1320            let (w, r4, w_len, err) = parse_pattern(name, rest, '\0');
1321            if err { return None; }
1322            if w.is_none() && line_pat.is_none() {
1323                if !name.is_empty() {
1324                    crate::ported::utils::zwarnnam(name,
1325                        "need non-empty word or line pattern");
1326                }
1327                return None;
1328            }
1329            word_pat = w;
1330            wl = w_len;
1331            rest = r4;
1332        }
1333
1334        // c:383-394 — allocate Cmatcher node.
1335        let node = Box::new(Cmatcher {
1336            refc: 0,
1337            next: None,
1338            flags: fl | fl2,
1339            line: line_pat,
1340            llen: ll,
1341            word: word_pat,
1342            wlen: wl,
1343            left,
1344            lalen: lal,
1345            right,
1346            ralen: ral,
1347        });
1348
1349        // c:395-400 — link into chain via tail.
1350        unsafe {
1351            *tail_ptr = Some(node);
1352            if let Some(boxed) = (*tail_ptr).as_mut() {
1353                tail_ptr = &mut boxed.next as *mut _;
1354            }
1355        }
1356    }
1357    ret
1358}
1359
1360/// Direct port of `parse_class(Cpattern p, char *iptr)` from `Src/Zle/complete.c:480`.
1361/// 93-line parser for a single character-class `[...]` or
1362/// equivalence-class `{...}` inside a Cpattern. Reads metafied
1363/// bytes from `iptr`, allocates `p->u.str` of the right size,
1364/// fills in the parsed contents (with PP_RANGE / PP_UNKWN tokens
1365/// for `a-z` ranges and `[:class:]` POSIX-style entries via
1366/// range_type lookup).
1367///
1368/// Static-link path: the metafied-byte + Meta-token + PP_*
1369/// encoding doesn't translate cleanly to Rust's UTF-8 strings.
1370/// Structural port returns the input pointer unmodified (signaling
1371/// "consumed nothing, parse failed") so the caller can detect the
1372/// stub state and skip emitting the matcher.
1373/// WARNING: param names don't match C — Rust=(_p) vs C=(p, iptr)
1374/// Direct port of `Cpattern parse_pattern(char *name, char **sp,
1375/// int *lp, char e, int *err)` from `Src/Zle/complete.c:418`.
1376/// Walks `*sp` building a Cpattern chain. Stops at end-char `e`
1377/// (or whitespace if `e == 0`). For each char-position:
1378///   - `[` / `{` → call `parse_class` for `[class]` / `{equiv}`
1379///   - `?` → CPAT_ANY
1380///   - `*` / `(` / `)` / `=` → error (invalid in matcher patterns)
1381///   - `\` + char → escape, emit next char as CPAT_CHAR
1382///   - else → CPAT_CHAR
1383///
1384/// Returns `(chain_head, new_sp, length, err)`. Error sets `err=true`
1385/// and chain is None; caller bubbles up.
1386/// WARNING: signature change — C returns Cpattern + writes through
1387/// sp/lp/err; Rust returns the tuple.
1388pub fn parse_pattern<'a>(name: &str, s: &'a str, end: char)                  // c:418
1389    -> (Option<Box<crate::ported::zle::comp_h::Cpattern>>, &'a str, i32, bool)
1390{
1391    use crate::ported::zle::comp_h::{Cpattern, CPAT_ANY, CPAT_CHAR};
1392    let mut ret: Option<Box<Cpattern>> = None;
1393    let mut tail_ptr: *mut Option<Box<Cpattern>> = &mut ret;
1394    let mut rest = s;
1395    let mut len = 0i32;
1396
1397    // c:430 — `while (*s && (e ? (*s != e) : !inblank(*s)))`.
1398    loop {
1399        let next_ch = match rest.chars().next() {
1400            Some(c) => c,
1401            None => break,
1402        };
1403        if end != '\0' {
1404            if next_ch == end { break; }
1405        } else if next_ch == ' ' || next_ch == '\t' {
1406            break;
1407        }
1408
1409        // c:432 — `n = hcalloc(sizeof(*n)); n->next = NULL;`
1410        let mut node = Box::new(Cpattern::default());
1411
1412        if next_ch == '[' || next_ch == '{' {                                // c:435
1413            // c:436 — `s = parse_class(n, s);`.
1414            //          Rust parse_class already advances past the
1415            //          close bracket internally (returns slice AFTER
1416            //          `]`/`}`), so we don't re-advance here. C's
1417            //          `s++` at c:442 is for the C parse_class which
1418            //          leaves s pointing AT the close bracket.
1419            //          Unterminated → parse_class returns empty input;
1420            //          treat as error.
1421            let before_len = rest.len();
1422            rest = parse_class(&mut node, rest);
1423            if rest.len() == before_len {
1424                // parse_class didn't advance — unterminated.
1425                if !name.is_empty() {
1426                    crate::ported::utils::zwarnnam(name,
1427                        "unterminated character class");
1428                }
1429                return (None, rest, 0, true);
1430            }
1431        } else if next_ch == '?' {                                           // c:443
1432            node.tp = CPAT_ANY;
1433            let mut it = rest.chars();
1434            it.next();
1435            rest = it.as_str();
1436        } else if matches!(next_ch, '*' | '(' | ')' | '=') {                 // c:446
1437            if !name.is_empty() {
1438                crate::ported::utils::zwarnnam(name,
1439                    &format!("invalid pattern character `{}'", next_ch));
1440            }
1441            return (None, rest, 0, true);
1442        } else {                                                             // c:451
1443            // c:452 — `if (*s == '\\' && s[1]) s++;` skip backslash escape.
1444            if next_ch == '\\' {
1445                let mut it = rest.chars();
1446                it.next();
1447                if it.clone().next().is_some() {
1448                    rest = it.as_str();
1449                }
1450            }
1451            // c:455-461 — `inlen = MB_METACHARLENCONV(...); inchar = ...;
1452            //              n->tp = CPAT_CHAR; n->u.chr = inchar; s += inlen;`
1453            let ch = rest.chars().next().unwrap();
1454            node.tp = CPAT_CHAR;
1455            node.chr = ch as u32;
1456            let mut it = rest.chars();
1457            it.next();
1458            rest = it.as_str();
1459        }
1460
1461        // c:463-467 — link node into chain via tail.
1462        unsafe {
1463            *tail_ptr = Some(node);
1464            // Advance tail to the new node's `.next` slot.
1465            if let Some(boxed) = (*tail_ptr).as_mut() {
1466                tail_ptr = &mut boxed.next as *mut _;
1467            }
1468        }
1469        len += 1;
1470    }
1471    (ret, rest, len, false)
1472}
1473
1474pub fn parse_class<'a>(p: &mut crate::ported::zle::comp_h::Cpattern,         // c:480
1475                       iptr: &'a str) -> &'a str {
1476    use crate::ported::zle::comp_h::{CPAT_CCLASS, CPAT_EQUIV, CPAT_NCLASS};
1477    use crate::ported::zsh_h::PP_UNKWN;
1478    use crate::ported::pattern::range_type;
1479    let bytes = iptr.as_bytes();
1480    if bytes.is_empty() {
1481        return iptr;
1482    }
1483
1484    // c:485-498 — `if (*iptr++ == '[')` sets CCLASS/NCLASS; else
1485    //              EQUIV (`{...}`).
1486    let opener = bytes[0];
1487    let endchar: u8;
1488    let mut i = 1;
1489    if opener == b'[' {
1490        endchar = b']';
1491        // c:490 — `if ((*iptr=='!' || *iptr=='^') && iptr[1] != ']') NCLASS`.
1492        if i < bytes.len() && (bytes[i] == b'!' || bytes[i] == b'^')
1493            && i + 1 < bytes.len() && bytes[i + 1] != b']'
1494        {
1495            p.tp = CPAT_NCLASS;
1496            i += 1;
1497        } else {
1498            p.tp = CPAT_CCLASS;
1499        }
1500    } else {
1501        endchar = 0x7d; // ASCII close-brace; avoid b'<close-brace>' so
1502                        // the build.rs brace-scanner doesn't miscount.
1503        p.tp = CPAT_EQUIV;
1504    }
1505
1506    // c:501-505 — End character can appear literally first. Find
1507    //              end position; bail with rest-of-input on no end.
1508    let start = i;
1509    let mut optr_idx = i;
1510    while optr_idx < bytes.len() && (optr_idx == start || bytes[optr_idx] != endchar) {
1511        optr_idx += 1;
1512    }
1513    if optr_idx >= bytes.len() {
1514        // c:504 — `if (!*optr) return optr;` — unterminated class.
1515        return &iptr[bytes.len()..];
1516    }
1517
1518    // c:507-512 — `p->u.str = zhalloc((optr-iptr) + 1)`. Pre-size
1519    //              output buffer; tokens always fit in input length.
1520    let mut out: Vec<u8> = Vec::with_capacity(optr_idx - i + 1);
1521
1522    // c:514-562 — main parse loop. firsttime allows endchar at position 0.
1523    let mut firsttime = true;
1524    while firsttime || (i < bytes.len() && bytes[i] != endchar) {
1525        // c:516-525 — `[:name:]` POSIX-class form.
1526        if bytes[i] == b'[' && i + 1 < bytes.len() && bytes[i + 1] == b':' {
1527            if let Some(nptr) = bytes[i + 2..].iter().position(|&b| b == b':') {
1528                let nptr = i + 2 + nptr;
1529                if nptr + 1 < bytes.len() && bytes[nptr + 1] == b']' {
1530                    let name = std::str::from_utf8(&bytes[i + 2..nptr]).unwrap_or("");
1531                    let ch = range_type(name).unwrap_or(PP_UNKWN as usize);
1532                    i = nptr + 2;
1533                    if ch != PP_UNKWN as usize {
1534                        // c:523 — `*optr++ = Meta + ch;`. Encode as a
1535                        //          single byte; the metafication layer
1536                        //          isn't wired so we emit a sentinel.
1537                        out.push(0x80u8.wrapping_add(ch as u8));
1538                    }
1539                    firsttime = false;
1540                    continue;
1541                }
1542            }
1543            // Malformed `[:name:` — treat `[` literally.
1544        }
1545
1546        // c:528-560 — single-char / range parse.
1547        let ptr1 = i;
1548        if bytes[i] == 0x83 {                                                // c:530 Meta
1549            i += 1;
1550        }
1551        if i >= bytes.len() { break; }
1552        i += 1;
1553        // c:534-553 — `*iptr=='-' && iptr[1] && iptr[1]!=endchar` → range.
1554        if i < bytes.len() && bytes[i] == b'-'
1555            && i + 1 < bytes.len() && bytes[i + 1] != endchar
1556        {
1557            i += 1; // consume '-'
1558            // c:539 — `*optr++ = Meta + PP_RANGE;`.
1559            out.push(0x80u8.wrapping_add(crate::ported::zsh_h::PP_RANGE as u8));
1560            // c:543-547 — start char (with Meta decode).
1561            if bytes[ptr1] == 0x83 && ptr1 + 1 < bytes.len() {
1562                out.push(0x83);
1563                out.push(bytes[ptr1 + 1] ^ 32);
1564            } else {
1565                out.push(bytes[ptr1]);
1566            }
1567            // c:549-554 — end char (with Meta passthrough).
1568            if i < bytes.len() && bytes[i] == 0x83 && i + 1 < bytes.len() {
1569                out.push(bytes[i]);
1570                out.push(bytes[i + 1]);
1571                i += 2;
1572            } else if i < bytes.len() {
1573                out.push(bytes[i]);
1574                i += 1;
1575            }
1576        } else {
1577            // c:556-560 — single char.
1578            if bytes[ptr1] == 0x83 && ptr1 + 1 < bytes.len() {
1579                out.push(0x83);
1580                out.push(bytes[ptr1 + 1] ^ 32);
1581            } else {
1582                out.push(bytes[ptr1]);
1583            }
1584        }
1585        firsttime = false;
1586    }
1587
1588    // c:564 — `*optr = '\0';` — null-terminate. Rust String/Vec handles this.
1589    p.str = Some(String::from_utf8_lossy(&out).into_owned());
1590
1591    // c:565 — `return iptr;` — input ptr now past the close-bracket.
1592    let consumed = (i + 1).min(bytes.len());
1593    &iptr[consumed..]
1594}
1595
1596#[cfg(test)]
1597mod tests {
1598    use super::*;
1599    use crate::ported::zle::comp_h::{CPAT_CCLASS, CPAT_EQUIV, CPAT_NCLASS, Cpattern};
1600
1601    #[test]
1602    fn classes_basic_cclass() {
1603        // c:485 — `[abc]` → CCLASS, str holds "abc".
1604        let _g = crate::ported::zle::zle_main::zle_test_setup();
1605        let mut p = Cpattern::default();
1606        let rest = parse_class(&mut p, "[abc]rest");
1607        assert_eq!(p.tp, CPAT_CCLASS);
1608        assert_eq!(p.str.as_deref(), Some("abc"));
1609        assert_eq!(rest, "rest");
1610    }
1611
1612    #[test]
1613    fn classes_negated_cclass_via_bang() {
1614        // c:490 — `[!abc]` → NCLASS.
1615        let _g = crate::ported::zle::zle_main::zle_test_setup();
1616        let mut p = Cpattern::default();
1617        let _ = parse_class(&mut p, "[!abc]");
1618        assert_eq!(p.tp, CPAT_NCLASS);
1619    }
1620
1621    #[test]
1622    fn classes_negated_cclass_via_caret() {
1623        // c:490 — `[^abc]` → NCLASS.
1624        let _g = crate::ported::zle::zle_main::zle_test_setup();
1625        let mut p = Cpattern::default();
1626        let _ = parse_class(&mut p, "[^abc]");
1627        assert_eq!(p.tp, CPAT_NCLASS);
1628    }
1629
1630    #[test]
1631    fn classes_equiv_braces() {
1632        // c:498 — `{abc}` → EQUIV.
1633        let _g = crate::ported::zle::zle_main::zle_test_setup();
1634        let mut p = Cpattern::default();
1635        let _ = parse_class(&mut p, "{abc}");
1636        assert_eq!(p.tp, CPAT_EQUIV);
1637    }
1638
1639    #[test]
1640    fn classes_range_consumes_input() {
1641        // c:537 — `[a-z]rest` → parses 5 chars, returns "rest".
1642        //          The PP_RANGE-encoded body isn't directly checked
1643        //          here because Cpattern.str is currently
1644        //          Option<String> and metafied tokens (0x83-prefix
1645        //          byte sequences) don't round-trip through UTF-8.
1646        //          Re-add a byte-level check once Cpattern.str moves
1647        //          to a Vec<u8>-backed storage.
1648        let _g = crate::ported::zle::zle_main::zle_test_setup();
1649        let mut p = Cpattern::default();
1650        let rest = parse_class(&mut p, "[a-z]rest");
1651        assert_eq!(p.tp, CPAT_CCLASS);
1652        assert_eq!(rest, "rest");
1653        assert!(p.str.is_some());
1654    }
1655
1656    #[test]
1657    fn cmatcher_empty_input_returns_none() {
1658        // c:249 — `if (!*s) return NULL;`
1659        let _g = crate::ported::zle::zle_main::zle_test_setup();
1660        assert!(parse_cmatcher("", "").is_none());
1661    }
1662
1663    #[test]
1664    fn cmatcher_x_early_return() {
1665        // c:294-303 — `x:` is the "match anything" sentinel; valid
1666        //              spec, returns the (currently empty) chain.
1667        let _g = crate::ported::zle::zle_main::zle_test_setup();
1668        assert!(parse_cmatcher("", "x:").is_none());
1669    }
1670
1671    #[test]
1672    fn cmatcher_unknown_letter_errors() {
1673        // c:280-283 — unknown rule-letter → return None (pcm_err).
1674        let _g = crate::ported::zle::zle_main::zle_test_setup();
1675        // "q" isn't in the dispatch table.
1676        assert!(parse_cmatcher("", "q:abc").is_none());
1677    }
1678
1679    #[test]
1680    fn cmatcher_missing_colon_errors() {
1681        // c:288-291 — second char must be `:`.
1682        let _g = crate::ported::zle::zle_main::zle_test_setup();
1683        assert!(parse_cmatcher("", "rabc").is_none());
1684    }
1685
1686    #[test]
1687    fn cmatcher_x_with_trailing_pattern_errors() {
1688        // c:296-301 — `x:foo` is malformed; `x:` must be alone.
1689        let _g = crate::ported::zle::zle_main::zle_test_setup();
1690        assert!(parse_cmatcher("", "x:foo").is_none());
1691    }
1692
1693    #[test]
1694    fn cmatcher_valid_letters_dont_panic() {
1695        // All recognized letters parse through without panicking.
1696        let _g = crate::ported::zle::zle_main::zle_test_setup();
1697        for c in ['b', 'l', 'e', 'r', 'm', 'B', 'L', 'E', 'R', 'M'] {
1698            let spec = format!("{}:body", c);
1699            let _ = parse_cmatcher("", &spec);
1700        }
1701    }
1702
1703    #[test]
1704    fn cmatcher_m_rule_emits_cmatcher() {
1705        // c:266 — `m:word=replacement` plain match.
1706        let _g = crate::ported::zle::zle_main::zle_test_setup();
1707        let r = parse_cmatcher("", "m:foo=bar");
1708        assert!(r.is_some(), "m: rule should produce a Cmatcher");
1709        let cm = r.unwrap();
1710        assert_eq!(cm.flags, 0);                                            // c:266 fl=0
1711        assert_eq!(cm.llen, 3);                                             // "foo"
1712        assert_eq!(cm.wlen, 3);                                             // "bar"
1713        assert!(cm.line.is_some());
1714        assert!(cm.word.is_some());
1715        assert!(cm.left.is_none());
1716        assert!(cm.right.is_none());
1717    }
1718
1719    #[test]
1720    fn cmatcher_r_rule_emits_anchored_cmatcher() {
1721        // c:265 — `r:left|right=word` with both anchors. The first
1722        //          pattern becomes the left anchor (promoted at
1723        //          c:341-346), the second the right anchor.
1724        let _g = crate::ported::zle::zle_main::zle_test_setup();
1725        let r = parse_cmatcher("", "r:abc|xy=def");
1726        assert!(r.is_some(), "r: rule should produce a Cmatcher");
1727        let cm = r.unwrap();
1728        use crate::ported::zle::comp_h::CMF_RIGHT;
1729        assert_eq!(cm.flags, CMF_RIGHT);
1730        assert_eq!(cm.lalen, 3);                                            // left = "abc"
1731        assert_eq!(cm.ralen, 2);                                            // right = "xy"
1732        assert_eq!(cm.wlen, 3);                                             // word = "def"
1733        assert!(cm.left.is_some());
1734        assert!(cm.right.is_some());
1735    }
1736
1737    #[test]
1738    fn cmatcher_l_rule_emits_left_anchor() {
1739        // c:263 — `l:left|line=word` left anchor.
1740        let _g = crate::ported::zle::zle_main::zle_test_setup();
1741        let r = parse_cmatcher("", "l:ab|cd=ef");
1742        assert!(r.is_some(), "l: rule should produce a Cmatcher");
1743        let cm = r.unwrap();
1744        use crate::ported::zle::comp_h::CMF_LEFT;
1745        assert_eq!(cm.flags, CMF_LEFT);
1746        assert!(cm.left.is_some());
1747        assert_eq!(cm.lalen, 2);
1748        assert_eq!(cm.llen, 2);
1749        assert_eq!(cm.wlen, 2);
1750    }
1751
1752    #[test]
1753    fn cmatcher_star_word_with_anchor() {
1754        // c:359-370 — `r:|=*` matches any word, requires anchor.
1755        let _g = crate::ported::zle::zle_main::zle_test_setup();
1756        let r = parse_cmatcher("", "r:|=*");
1757        assert!(r.is_some(), "r:|=* should produce a Cmatcher");
1758        let cm = r.unwrap();
1759        assert_eq!(cm.wlen, -1);                                            // c:370 single `*`
1760        assert!(cm.word.is_none());
1761    }
1762
1763    #[test]
1764    fn cmatcher_double_star_word() {
1765        // c:366-368 — `r:|=**` matches any (greedy) word.
1766        let _g = crate::ported::zle::zle_main::zle_test_setup();
1767        let r = parse_cmatcher("", "r:|=**");
1768        assert!(r.is_some());
1769        let cm = r.unwrap();
1770        assert_eq!(cm.wlen, -2);                                            // c:368 double `**`
1771    }
1772
1773    #[test]
1774    fn cmatcher_star_without_anchor_errors() {
1775        // c:360-364 — `m:=*` (no anchor) errors.
1776        let _g = crate::ported::zle::zle_main::zle_test_setup();
1777        let r = parse_cmatcher("", "m:=*");
1778        assert!(r.is_none(), "*-without-anchor should error");
1779    }
1780
1781    #[test]
1782    fn cmatcher_chain_multiple_rules() {
1783        // c:251-401 — multiple rules separated by whitespace chain.
1784        let _g = crate::ported::zle::zle_main::zle_test_setup();
1785        let r = parse_cmatcher("", "m:foo=bar m:baz=qux");
1786        assert!(r.is_some());
1787        let head = r.unwrap();
1788        assert!(head.next.is_some(), "second rule should be linked");
1789    }
1790
1791    #[test]
1792    fn pattern_single_char_emits_cpat_char() {
1793        // c:451-461 — single non-special char → CPAT_CHAR node.
1794        let _g = crate::ported::zle::zle_main::zle_test_setup();
1795        let (chain, rest, len, err) = parse_pattern("", "abc", '\0');
1796        assert!(!err);
1797        assert_eq!(len, 3);
1798        assert_eq!(rest, ""); // consumed everything (no end-char, no whitespace)
1799        // Walk chain and verify 3 CPAT_CHAR nodes.
1800        use crate::ported::zle::comp_h::CPAT_CHAR;
1801        let mut count = 0;
1802        let mut cur = chain.as_deref();
1803        while let Some(n) = cur {
1804            assert_eq!(n.tp, CPAT_CHAR);
1805            count += 1;
1806            cur = n.next.as_deref();
1807        }
1808        assert_eq!(count, 3);
1809    }
1810
1811    #[test]
1812    fn pattern_question_mark_is_cpat_any() {
1813        // c:443 — `?` → CPAT_ANY.
1814        let _g = crate::ported::zle::zle_main::zle_test_setup();
1815        let (chain, _, len, err) = parse_pattern("", "?", '\0');
1816        assert!(!err);
1817        assert_eq!(len, 1);
1818        use crate::ported::zle::comp_h::CPAT_ANY;
1819        assert_eq!(chain.as_ref().unwrap().tp, CPAT_ANY);
1820    }
1821
1822    #[test]
1823    fn pattern_invalid_chars_error() {
1824        // c:446-449 — `*`/`(`/`)`/`=` → error.
1825        let _g = crate::ported::zle::zle_main::zle_test_setup();
1826        for c in ['*', '(', ')', '='] {
1827            let s = format!("{}", c);
1828            let (chain, _, _, err) = parse_pattern("", &s, '\0');
1829            assert!(err, "char {} should error", c);
1830            assert!(chain.is_none());
1831        }
1832    }
1833
1834    #[test]
1835    fn pattern_backslash_escapes_next() {
1836        // c:452 — `\\X` consumes the backslash and emits X as CPAT_CHAR.
1837        let _g = crate::ported::zle::zle_main::zle_test_setup();
1838        let (chain, _, len, err) = parse_pattern("", r"\*", '\0');
1839        assert!(!err);
1840        assert_eq!(len, 1);
1841        use crate::ported::zle::comp_h::CPAT_CHAR;
1842        let n = chain.as_ref().unwrap();
1843        assert_eq!(n.tp, CPAT_CHAR);
1844        assert_eq!(n.chr, '*' as u32);
1845    }
1846
1847    #[test]
1848    fn pattern_stops_at_end_char() {
1849        // c:430 — `*s != e` gate.
1850        let _g = crate::ported::zle::zle_main::zle_test_setup();
1851        let (_, rest, len, err) = parse_pattern("", "ab=cd", '=');
1852        assert!(!err);
1853        assert_eq!(len, 2);
1854        assert_eq!(rest, "=cd");
1855    }
1856
1857    #[test]
1858    fn pattern_stops_at_whitespace_when_no_end_char() {
1859        // c:430 — `e==0` → !inblank.
1860        let _g = crate::ported::zle::zle_main::zle_test_setup();
1861        let (_, rest, len, err) = parse_pattern("", "ab cd", '\0');
1862        assert!(!err);
1863        assert_eq!(len, 2);
1864        assert_eq!(rest, " cd");
1865    }
1866
1867    #[test]
1868    fn pattern_bracket_class_routes_to_parse_class() {
1869        // c:435 — `[abc]` dispatches to parse_class. With no end-char
1870        //          parse_pattern continues into the trailing chars as
1871        //          CPAT_CHAR nodes, so `[abc]xy` → class + x + y = 3.
1872        let _g = crate::ported::zle::zle_main::zle_test_setup();
1873        let (chain, rest, len, err) = parse_pattern("", "[abc]xy=q", '=');
1874        assert!(!err);
1875        assert_eq!(len, 3);
1876        assert_eq!(rest, "=q");
1877        // chain head is the class node.
1878        use crate::ported::zle::comp_h::CPAT_CCLASS;
1879        assert_eq!(chain.as_ref().unwrap().tp, CPAT_CCLASS);
1880    }
1881
1882    #[test]
1883    fn classes_unterminated_returns_eos() {
1884        // c:504 — unterminated class → returns input-end.
1885        let _g = crate::ported::zle::zle_main::zle_test_setup();
1886        let mut p = Cpattern::default();
1887        let rest = parse_class(&mut p, "[abc");
1888        assert_eq!(rest, "");
1889    }
1890}