Skip to main content

zsh/ported/zle/
compmatch.rs

1//! Completion matching engine for ZLE
2//!
3//! Port from zsh/Src/Zle/compmatch.c (2,974 lines)
4//!
5//! This compares two cmatchers and returns non-zero if they are equal.     // c:80
6//! Add the given matchers to the bmatcher list.                            // c:97
7//! This returns a new Cline structure.                                     // c:140
8//!
9//! The full matching engine is in compsys/matching.rs (458 lines).
10//! This module provides the pattern matching, anchor handling, and
11//! match line construction used during completion.
12//!
13//! Key C functions and their Rust locations:
14//! - match_str         → crate::compsys::matching::match_str()
15//! - match_parts       → crate::compsys::matching::match_parts()
16//! - comp_match        → crate::compsys::matching::comp_match()
17//! - pattern_match_equivalence → crate::compsys::matching (inline)
18//! - add_match_str/part/sub    → crate::compsys::matching (inline)
19//! - cline_* (match line ops)  → inline below; the compsys::base
20//!                                `CompletionLine` shim was deleted.
21
22// CompMatcher / MatchFlags / CompLine deleted — Rust-invented structs
23// with no C counterpart. The legit C types `Cmatcher` (comp.h:153),
24// `Cline` (comp.h:245), and `Cpattern` (comp.h:197) are ported in
25// `comp_h.rs` and used by the real porters of `match_str` /
26// `pattern_match` / `add_match_str` etc. below.
27
28use crate::ported::pattern::pattry;
29use crate::ported::utils::set_noerrs;
30use crate::ported::zle::comp_h::{
31    Cline, Cmatcher, Cmlist, Cpattern, CLF_DIFF, CLF_JOIN, CLF_LINE, CLF_MATCHED, CLF_MISS,
32    CLF_NEW, CLF_SUF, CMF_INTER, CMF_LEFT, CMF_LINE, CMF_RIGHT, CPAT_ANY, CPAT_CCLASS, CPAT_CHAR,
33    CPAT_EQUIV, CPAT_NCLASS,
34};
35use crate::ported::zle::compcore::{mstack, multiquote, tildequote, useqbr};
36use crate::ported::zle::zle_h::{brinfo, ZC_tolower, ZC_toupper};
37#[allow(unused_imports)]
38use crate::ported::zle::{
39    deltochar::*, textobjects::*, zle_hist::*, zle_main::*, zle_misc::*, zle_move::*,
40    zle_params::*, zle_refresh::*, zle_tricky::*, zle_utils::*, zle_vi::*, zle_word::*,
41};
42use crate::ported::zsh_h::{PP_LOWER, PP_RANGE, PP_UPPER};
43use std::sync::{Mutex, OnceLock};
44
45/// Port of `cpatterns_same(Cpattern a, Cpattern b)` from `Src/Zle/compmatch.c:42`.
46/// ```c
47/// static int
48/// cpatterns_same(Cpattern a, Cpattern b)
49/// {
50///     while (a) {
51///         if (!b) return 0;
52///         if (a->tp != b->tp) return 0;
53///         switch (a->tp) {
54///         case CPAT_CCLASS: case CPAT_NCLASS: case CPAT_EQUIV:
55///             if (strcmp(a->u.str, b->u.str) != 0) return 0;
56///             break;
57///         case CPAT_CHAR:
58///             if (a->u.chr != b->u.chr) return 0;
59///             break;
60///         default:
61///             break;
62///         }
63///         a = a->next;
64///         b = b->next;
65///     }
66///     return !b;
67/// }
68/// ```
69/// Walk two parallel `Cpattern` chains testing structural equality
70/// (same `tp` + same `str` for class types or same `chr` for
71/// CPAT_CHAR). Used by `cmatchers_same` to dedupe matcher specs.
72/// WARNING: param names don't match C — Rust=(b) vs C=(a, b)
73
74// --- AUTO: cross-zle hoisted-fn use glob ---
75/// `cpatterns_same` — see implementation.
76#[allow(unused_imports)]
77#[allow(unused_imports)]
78
79pub fn cpatterns_same(
80    // c:44
81    mut a: Option<&Cpattern>,
82    mut b: Option<&Cpattern>,
83) -> bool {
84    // c:42
85    while let Some(ap) = a {
86        // c:46 while (a)
87        let bp = match b {
88            // c:47
89            None => return false, // c:48 if(!b) return 0
90            Some(p) => p,
91        };
92        if ap.tp != bp.tp {
93            // c:49
94            return false; // c:50
95        }
96        match ap.tp {
97            // c:51
98            x if x == CPAT_CCLASS || x == CPAT_NCLASS || x == CPAT_EQUIV => {
99                // c:52-54
100                // c:55-58 — equivalent ranges might compare same even when
101                // strings differ; the C source admits this is unhandled.
102                if ap.str != bp.str {
103                    // c:60 strcmp(a->u.str,b->u.str)
104                    return false; // c:61
105                }
106            }
107            x if x == CPAT_CHAR => {
108                // c:64
109                if ap.chr != bp.chr {
110                    // c:65
111                    return false; // c:66
112                }
113            }
114            _ => { // c:69 default
115                 // c:70 — "here to silence compiler"
116            }
117        }
118        a = ap.next.as_deref(); // c:74 a = a->next
119        b = bp.next.as_deref(); // c:75 b = b->next
120    }
121    b.is_none() // c:77 return !b
122}
123
124/// Port of `cmatchers_same(Cmatcher a, Cmatcher b)` from `Src/Zle/compmatch.c:82`.
125/// ```c
126/// static int
127/// cmatchers_same(Cmatcher a, Cmatcher b)
128/// {
129///     return (a == b ||
130///             (a->flags == b->flags &&
131///              a->llen == b->llen && a->wlen == b->wlen &&
132///              (!a->llen || cpatterns_same(a->line, b->line)) &&
133///              (a->wlen <= 0 || cpatterns_same(a->word, b->word)) &&
134///              (!(a->flags & (CMF_LEFT | CMF_RIGHT)) ||
135///               (a->lalen == b->lalen && a->ralen == b->ralen &&
136///                (!a->lalen || cpatterns_same(a->left, b->left)) &&
137///                (!a->ralen || cpatterns_same(a->right, b->right))))));
138/// }
139/// ```
140/// Test two matchers for full structural equality — flags, lengths,
141/// patterns, and (if anchored) anchor patterns must all match.
142/// WARNING: param names don't match C — Rust=(b) vs C=(a, b)
143pub fn cmatchers_same(
144    // c:84
145    a: &Cmatcher,
146    b: &Cmatcher,
147) -> bool {
148    // c:82
149    // c:86 — `a == b` short-circuit (pointer identity). Rust uses
150    // `std::ptr::eq` for the same effect.
151    if std::ptr::eq(a, b) {
152        return true;
153    }
154    // c:87 — `a->flags == b->flags && a->llen == b->llen && a->wlen == b->wlen`.
155    if a.flags != b.flags || a.llen != b.llen || a.wlen != b.wlen {
156        return false;
157    }
158    // c:89 — `(!a->llen || cpatterns_same(a->line, b->line))`.
159    if a.llen != 0 && !cpatterns_same(a.line.as_deref(), b.line.as_deref()) {
160        return false;
161    }
162    // c:90 — `(a->wlen <= 0 || cpatterns_same(a->word, b->word))`.
163    if a.wlen > 0 && !cpatterns_same(a.word.as_deref(), b.word.as_deref()) {
164        return false;
165    }
166    // c:91-94 — anchor checks only if CMF_LEFT/CMF_RIGHT flagged.
167    if (a.flags & (CMF_LEFT | CMF_RIGHT)) != 0 {
168        if a.lalen != b.lalen || a.ralen != b.ralen {
169            // c:92
170            return false;
171        }
172        if a.lalen != 0 && !cpatterns_same(a.left.as_deref(), b.left.as_deref()) {
173            return false; // c:93
174        }
175        if a.ralen != 0 && !cpatterns_same(a.right.as_deref(), b.right.as_deref()) {
176            return false; // c:94
177        }
178    }
179    true
180}
181
182/// Direct port of `mod_export void add_bmatchers(Cmatcher m)` from
183/// `Src/Zle/compmatch.c:101`. Walks the supplied Cmatcher chain
184/// (the head of `def->matcher` at call sites) and prepends each
185/// matcher that qualifies for brace-matching to the file-scope
186/// `bmatchers` Cmlist. Original chain head is appended after the new
187/// entries so the final list is `[new_entries..., old_bmatchers...]`.
188pub fn add_bmatchers(m: Option<&Cmatcher>) {
189    // c:101
190    let cell = crate::ported::zle::compcore::bmatchers.get_or_init(|| Mutex::new(None));
191    let old = cell.lock().ok().and_then(|mut g| g.take()); // c:104 Cmlist old = bmatchers
192                                                           // c:105-113 — qualify each m; prepend matches in C order (reversed
193                                                           // iter so the final list is `[new_entries..., old]` per c:114 *q=old).
194    let mut head = old;
195    for mat in std::iter::successors(m, |p| p.next.as_deref())
196        .collect::<Vec<_>>()
197        .into_iter()
198        .rev()
199    // c:105 walk m
200    {
201        let qual = (mat.flags == 0 && mat.wlen > 0 && mat.llen > 0)          // c:107-108
202                || (mat.flags == CMF_RIGHT && mat.wlen < 0 && mat.llen == 0);
203        if qual {
204            // c:109-112
205            head = Some(Box::new(Cmlist {
206                next: head,
207                matcher: Box::new(mat.clone()),
208                str: String::new(),
209            }));
210        }
211    }
212    if let Ok(mut g) = cell.lock() {
213        *g = head;
214    }
215}
216
217/// Direct port of `mod_export void update_bmatchers(void)` from
218/// `Src/Zle/compmatch.c:121`. Called when mstack changes — ensures
219/// `bmatchers` contains no matchers absent from `mstack`.
220pub fn update_bmatchers() {
221    // c:121
222    let bm_cell = crate::ported::zle::compcore::bmatchers.get_or_init(|| Mutex::new(None));
223    let ms_cell = mstack.get_or_init(|| Mutex::new(None));
224    let mut p = bm_cell.lock().ok().and_then(|mut g| g.take()); // c:124 Cmlist p = bmatchers
225    let ms_head = ms_cell
226        .lock()
227        .ok()
228        .and_then(|g| g.as_ref().map(|b| (**b).clone()));
229    let mut new_bmatchers: Option<Box<Cmlist>> = p.as_ref().map(|b| (**b).clone()).map(Box::new);
230    while let Some(node) = p {
231        // c:128 while (p)
232        let mut t = false; // c:129 t = 0
233        let mut ms = ms_head.as_ref(); // c:130 ms = mstack
234        while let Some(mscur) = ms {
235            if t {
236                break;
237            }
238            let mut mp = Some(mscur.matcher.as_ref()); // c:131 mp = ms->matcher
239            while let Some(mpcur) = mp {
240                if t {
241                    break;
242                }
243                t = cmatchers_same(mpcur, &*node.matcher); // c:132 cmatchers_same
244                mp = mpcur.next.as_deref();
245            }
246            ms = mscur.next.as_deref();
247        }
248        p = node.next; // c:134 p = p->next
249        if !t {
250            // c:135 if (!t)
251            new_bmatchers = p.as_ref().map(|b| (**b).clone()).map(Box::new); // c:136 bmatchers = p
252        }
253    }
254    if let Ok(mut g) = bm_cell.lock() {
255        *g = new_bmatchers;
256    }
257}
258
259/// Port of `Cline get_cline(char *l, int ll, char *w, int wl, char *o,
260///                            int ol, int fl)` from Src/Zle/compmatch.c:144.
261///
262/// "Returns a new Cline structure." The C version pools freed Clines
263/// via the `freecl` heap; Rust uses normal allocation so the pool
264/// dance collapses to a `Box::new`. Sets `word`/`wlen`/`line`/`llen`/
265/// `orig`/`olen`/`flags` per the args; clears `prefix`/`suffix`/`min`/
266/// `max`/`slen`.
267pub fn get_cline(
268    l: Option<String>,
269    ll: i32,
270    w: Option<String>,
271    wl: i32, // c:144
272    o: Option<String>,
273    ol: i32,
274    fl: i32,
275) -> Box<Cline> {
276    Box::new(Cline {
277        next: None, // c:156
278        line: l,    // c:157
279        llen: ll,
280        word: w, // c:158
281        wlen: wl,
282        orig: o, // c:160
283        olen: ol,
284        slen: 0,      // c:161
285        flags: fl,    // c:162
286        prefix: None, // c:163
287        suffix: None,
288        min: 0, // c:164
289        max: 0,
290    })
291}
292
293/// Port of `free_cline(Cline l)` from `Src/Zle/compmatch.c:171`.
294/// ```c
295/// void
296/// free_cline(Cline l)
297/// {
298///     Cline n;
299///     while (l) {
300///         n = l->next;
301///         l->next = freecl;
302///         freecl = l;
303///         free_cline(l->prefix);
304///         free_cline(l->suffix);
305///         l = n;
306///     }
307/// }
308/// ```
309/// Free a Cline list. C pushes onto a `freecl` free-list to recycle;
310/// Rust just drops via Box.
311pub fn free_cline(l: Option<Box<Cline>>) {
312    // c:172
313    // c:172-183 — walk; free each prefix/suffix recursively. In Rust
314    // dropping the Box of the list head triggers Drop on `next`/
315    // `prefix`/`suffix` chains automatically. `freecl` recycling
316    // is a C-only zhalloc optimisation that doesn't apply here.
317    drop(l);
318}
319
320/// Port of `cp_cline(Cline l, int deep)` from `Src/Zle/compmatch.c:189`.
321/// ```c
322/// Cline
323/// cp_cline(Cline l, int deep)
324/// {
325///     Cline r = NULL, *p = &r, t, lp = NULL;
326///     while (l) {
327///         if ((t = freecl)) freecl = t->next;
328///         else t = (Cline) zhalloc(sizeof(*t));
329///         memcpy(t, l, sizeof(*t));
330///         if (deep) {
331///             if (t->prefix) t->prefix = cp_cline(t->prefix, 0);
332///             if (t->suffix) t->suffix = cp_cline(t->suffix, 0);
333///         }
334///         *p = lp = t;
335///         p = &(t->next);
336///         l = l->next;
337///     }
338///     *p = NULL;
339///     return r;
340/// }
341/// ```
342/// Deep- or shallow-copy a Cline list. `deep` recursively copies
343/// the prefix/suffix sub-lists too. The C source draws from a
344/// freecl free-list when available — Rust just heap-allocates.
345/// WARNING: param names don't match C — Rust=(deep) vs C=(l, deep)
346pub fn cp_cline(
347    // c:190
348    l: Option<&Cline>,
349    deep: i32,
350) -> Option<Box<Cline>> {
351    // c:189
352    let mut r: Option<Box<Cline>> = None; // c:192 r = NULL
353    let mut tail: *mut Option<Box<Cline>> = &mut r;
354    let mut cur = l;
355    while let Some(node) = cur {
356        // c:194 while (l)
357        // c:198 — `t = (Cline) zhalloc(sizeof(*t))`.
358        // c:199 — `memcpy(t, l, sizeof(*t))`.
359        let mut t: Box<Cline> = Box::new(node.clone());
360        // Reset `next` so the memcpy-equivalent doesn't link to the
361        // source's next (the loop sets it via the tail pointer).
362        t.next = None;
363        if deep != 0 {
364            // c:200 if (deep)
365            // c:201-202 — `t->prefix = cp_cline(t->prefix, 0)`. Already
366            // a Box-clone via memcpy; rebuild as deep copy.
367            if let Some(pre) = node.prefix.as_deref() {
368                t.prefix = cp_cline(Some(pre), 0); // c:202
369            }
370            if let Some(suf) = node.suffix.as_deref() {
371                t.suffix = cp_cline(Some(suf), 0); // c:204
372            }
373        }
374        // c:206 — `*p = lp = t`. Append to tail.
375        // SAFETY: `tail` points into `r` or into the previous node's
376        // `next` field; both stay valid for the loop's lifetime.
377        unsafe {
378            *tail = Some(t);
379            // c:207 — `p = &(t->next)`. Re-aim tail at the new entry's `next`.
380            let new_node = (*tail).as_mut().unwrap();
381            tail = &mut new_node.next;
382        }
383        cur = node.next.as_deref(); // c:208 l = l->next
384    }
385    // c:210 — `*p = NULL`. Already None by default.
386    r // c:212 return r
387}
388
389// =====================================================================
390// cline_sublen / cline_setlens / cline_matched / revert_cline / cp_cline
391// — `Src/Zle/compmatch.c:217-281`.
392// =====================================================================
393
394/// Port of `cline_sublen(Cline l)` from `Src/Zle/compmatch.c:218`.
395/// ```c
396/// int
397/// cline_sublen(Cline l)
398/// {
399///     int len = ((l->flags & CLF_LINE) ? l->llen : l->wlen);
400///     if (l->olen && !((l->flags & CLF_SUF) ? l->suffix : l->prefix))
401///         len += l->olen;
402///     else {
403///         Cline p;
404///         for (p = l->prefix; p; p = p->next)
405///             len += ((p->flags & CLF_LINE) ? p->llen : p->wlen);
406///         for (p = l->suffix; p; p = p->next)
407///             len += ((p->flags & CLF_LINE) ? p->llen : p->wlen);
408///     }
409///     return len;
410/// }
411/// ```
412/// Total visual length of one Cline plus its prefix/suffix sub-lists.
413pub fn cline_sublen(l: &Cline) -> i32 {
414    // c:219
415    // c:221 — `len = (CLF_LINE ? llen : wlen)`.
416    let mut len: i32 = if (l.flags & CLF_LINE) != 0 {
417        l.llen
418    } else {
419        l.wlen
420    };
421    // c:223 — `if (olen && !((CLF_SUF ? suffix : prefix))) len += olen`.
422    let no_subs = if (l.flags & CLF_SUF) != 0 {
423        l.suffix.is_none()
424    } else {
425        l.prefix.is_none()
426    };
427    if l.olen != 0 && no_subs {
428        len += l.olen; // c:224
429    } else {
430        // c:225
431        // c:228-229 — walk prefix sub-list summing per-part length.
432        let mut p = l.prefix.as_deref();
433        while let Some(pp) = p {
434            len += if (pp.flags & CLF_LINE) != 0 {
435                pp.llen
436            } else {
437                pp.wlen
438            };
439            p = pp.next.as_deref();
440        }
441        // c:230-231 — walk suffix sub-list.
442        let mut p = l.suffix.as_deref();
443        while let Some(pp) = p {
444            len += if (pp.flags & CLF_LINE) != 0 {
445                pp.llen
446            } else {
447                pp.wlen
448            };
449            p = pp.next.as_deref();
450        }
451    }
452    len // c:233 return len
453}
454
455/// Port of `cline_setlens(Cline l, int both)` from `Src/Zle/compmatch.c:240`.
456/// ```c
457/// void
458/// cline_setlens(Cline l, int both)
459/// {
460///     while (l) {
461///         l->min = cline_sublen(l);
462///         if (both)
463///             l->max = l->min;
464///         l = l->next;
465///     }
466/// }
467/// ```
468/// Walk a Cline list setting `min` (and optionally `max`) from
469/// `cline_sublen`.
470pub fn cline_setlens(l: &mut Option<Box<Cline>>, both: i32) {
471    // c:240
472    let mut cur = l.as_deref_mut();
473    while let Some(node) = cur {
474        // c:242 while (l)
475        let s = cline_sublen(node); // c:243 cline_sublen(l)
476        node.min = s; // c:243 l->min = ...
477        if both != 0 {
478            // c:244 if (both)
479            node.max = s; // c:245 l->max = l->min
480        }
481        cur = node.next.as_deref_mut(); // c:246 l = l->next
482    }
483}
484
485// =====================================================================
486// matchbuf / matchparts / matchsubs globals + start_match / abort_match
487// — `Src/Zle/compmatch.c:283-317`.
488// =====================================================================
489
490/// Port of `cline_matched(Cline p)` from `Src/Zle/compmatch.c:254`.
491/// ```c
492/// void
493/// cline_matched(Cline p)
494/// {
495///     while (p) {
496///         p->flags |= CLF_MATCHED;
497///         cline_matched(p->prefix);
498///         cline_matched(p->suffix);
499///         p = p->next;
500///     }
501/// }
502/// ```
503/// Set `CLF_MATCHED` on every Cline reachable through next/prefix/
504/// suffix from `p`.
505pub fn cline_matched(p: &mut Option<Box<Cline>>) {
506    // c:254
507    let mut cur = p.as_deref_mut();
508    while let Some(node) = cur {
509        // c:256 while (p)
510        node.flags |= CLF_MATCHED; // c:257
511        cline_matched(&mut node.prefix); // c:258
512        cline_matched(&mut node.suffix); // c:259
513        cur = node.next.as_deref_mut(); // c:261 p = p->next
514    }
515}
516
517/// Port of `revert_cline(Cline p)` from `Src/Zle/compmatch.c:269`.
518/// ```c
519/// Cline
520/// revert_cline(Cline p)
521/// {
522///     Cline r = NULL, n;
523///     while (p) {
524///         n = p->next;
525///         p->next = r;
526///         r = p;
527///         p = n;
528///     }
529///     return r;
530/// }
531/// ```
532/// Reverse a Cline `next`-chained list in place; returns the new head.
533/// WARNING: param names don't match C — Rust=() vs C=(p)
534pub fn revert_cline(
535    // c:270
536    mut p: Option<Box<Cline>>,
537) -> Option<Box<Cline>> {
538    // c:269
539    let mut r: Option<Box<Cline>> = None; // c:272 r = NULL
540    while let Some(mut node) = p {
541        // c:274 while (p)
542        let n = node.next.take(); // c:275 n = p->next
543        node.next = r; // c:276 p->next = r
544        r = Some(node); // c:277 r = p
545        p = n; // c:278 p = n
546    }
547    r // c:280 return r
548}
549
550/// Port of `start_match()` from `Src/Zle/compmatch.c:300`.
551/// ```c
552/// static void
553/// start_match(void)
554/// {
555///     if (matchbuf)
556///         *matchbuf = '\0';
557///     matchbufadded = 0;
558///     matchparts = matchlastpart = matchsubs = matchlastsub = NULL;
559/// }
560/// ```
561/// Reset the per-match globals so a fresh pattern run starts clean.
562pub fn start_match() {
563    // c:300
564    // c:300-303 — `if (matchbuf) *matchbuf = '\0'`.
565    MATCHBUF
566        .get_or_init(|| Mutex::new(String::new()))
567        .lock()
568        .unwrap()
569        .clear();
570    // c:305 — `matchparts = matchlastpart = matchsubs = matchlastsub = NULL`.
571    // All FOUR must reset. Omitting MATCHLASTPART/MATCHLASTSUB left them stale
572    // across matches: the next add_match_part read a Some MATCHLASTPART and took
573    // the append branch (`lastpart->next = p`) against the disconnected old tail
574    // while MATCHPARTS head stayed None, so `pli = matchparts` came back empty —
575    // partial-match Cline lost (e.g. `ls /dir/a<TAB>` dropped the typed `a`).
576    *MATCHPARTS.get_or_init(|| Mutex::new(None)).lock().unwrap() = None;
577    *MATCHLASTPART.get_or_init(|| Mutex::new(None)).lock().unwrap() = None;
578    *MATCHSUBS.get_or_init(|| Mutex::new(None)).lock().unwrap() = None;
579    *MATCHLASTSUB.get_or_init(|| Mutex::new(None)).lock().unwrap() = None;
580}
581
582/// Port of `abort_match()` from `Src/Zle/compmatch.c:312`.
583/// ```c
584/// static void
585/// abort_match(void)
586/// {
587///     free_cline(matchparts);
588///     free_cline(matchsubs);
589///     matchparts = matchsubs = NULL;
590/// }
591/// ```
592/// C body (compmatch.c:312, 3 lines):
593///     `free_cline(matchparts);
594///      free_cline(matchsubs);
595///      matchparts = matchsubs = NULL;`
596/// The `take()` on each guard discards the old chain (Rust drop runs
597/// `free_cline`) and leaves the slot None — same observable state.
598pub fn abort_match() {
599    // c:312
600    free_cline(
601        MATCHPARTS
602            .get_or_init(|| Mutex::new(None))
603            .lock()
604            .unwrap()
605            .take(),
606    ); // c:313
607    free_cline(
608        MATCHSUBS
609            .get_or_init(|| Mutex::new(None))
610            .lock()
611            .unwrap()
612            .take(),
613    ); // c:314
614}
615
616/// Direct port of `static void add_match_str(Cmatcher m, char *l,
617///                                          char *w, int wl, int sfx)`
618/// from `Src/Zle/compmatch.c:327`. Pushes the string `w` (or
619/// `l` when `m & CMF_LINE`) of length `wl` into the file-scope
620/// `MATCHBUF` accumulator; `sfx` prepends instead of appends.
621pub fn add_match_str(
622    m: Option<&Cmatcher>, // c:327
623    l: &str,
624    w: &str,
625    mut wl: i32,
626    sfx: i32,
627) {
628    // c:332-334 — `if (m && (m->flags & CMF_LINE)) { wl = m->llen; w = l; }`.
629    let (eff_w_owned, eff_w): (String, &str) = match m {
630        Some(mat) if (mat.flags & CMF_LINE) != 0 => {
631            wl = mat.llen;
632            let owned = l.to_string();
633            let s = owned.clone();
634            (owned, Box::leak(s.into_boxed_str()))
635        }
636        _ => (String::new(), w),
637    };
638    let _ = eff_w_owned;
639
640    if wl <= 0 {
641        return;
642    } // c:335
643
644    // c:337-353 — buffer-grow + insert. Rust's String handles the
645    // grow path; we still mirror the matchbufadded counter for parity
646    // with `MATCHBUFLEN`-checking C call sites.
647    if let Ok(mut buf) = MATCHBUF.get_or_init(|| Mutex::new(String::new())).lock() {
648        let take_n = wl as usize;
649        let new_chunk: String = eff_w.chars().take(take_n).collect();
650        if sfx != 0 {
651            // c:354 prefix-mode
652            *buf = format!("{}{}", new_chunk, *buf); // c:356
653        } else {
654            // c:358
655            buf.push_str(&new_chunk);
656        }
657        MATCHBUFADDED.fetch_add(wl, std::sync::atomic::Ordering::Relaxed); // c:362
658    }
659}
660
661/// Direct port of `static void add_match_part(Cmatcher m, char *l,
662///                                            char *w, int wl,
663///                                            char *o, int ol,
664///                                            char *s, int sl,
665///                                            int osl, int sfx)`
666/// from `Src/Zle/compmatch.c:373`. Appends a partial match into
667/// `MATCHPARTS`, splitting the new part via `bld_parts` per the
668/// matcher's anchor rules and consuming any pending `MATCHSUBS`
669/// nodes into the new tail.
670pub fn add_match_part(
671    m: Option<&Cmatcher>, // c:373
672    l: Option<&str>,
673    _ll: i32,
674    w: &str,
675    wl: i32,
676    o: Option<&str>,
677    ol: i32,
678    s: &str,
679    sl: i32,
680    osl: i32,
681    sfx: i32,
682) {
683    // c:382 — `if (l && !strncmp(l, w, wl)) l = NULL` — drop redundant anchor.
684    let l_eff: Option<String> = match l {
685        Some(lstr)
686            if lstr.len() >= wl as usize && wl > 0 && &lstr[..wl as usize] == &w[..wl as usize] =>
687        {
688            None
689        }
690        Some(lstr) => Some(lstr.to_string()),
691        None => None,
692    };
693
694    // c:392 — `p = bld_parts(s, sl, osl, &lp, &lprem)`.
695    let mut lp: Option<Box<Cline>> = None;
696    let mut lprem: Option<Box<Cline>> = None;
697    let mut p = bld_parts(s, sl, osl, Some(&mut lp), Some(&mut lprem));
698
699    // c:394 — `if (lprem && m && (m->flags & CLF_LEFT))`.
700    if let Some(rem) = lprem.as_mut() {
701        if m.map(|mat| (mat.flags & CMF_LEFT) != 0).unwrap_or(false) {
702            rem.flags |= CLF_SUF; // c:395
703            rem.suffix = rem.prefix.take(); // c:396 swap
704        }
705    }
706
707    // c:402 — `if (sfx) p = revert_cline(lp = p)`.
708    if sfx != 0 {
709        if let Some(chain) = p.take() {
710            p = revert_cline(Some(chain));
711        }
712    }
713
714    // c:405-419 — merge MATCHSUBS into the head/tail.
715    let subs = MATCHSUBS
716        .get_or_init(|| Mutex::new(None))
717        .lock()
718        .ok()
719        .and_then(|mut g| g.take());
720    if let Some(subs_chain) = subs {
721        // c:405
722        if let Some(lp_node) = lp.as_mut() {
723            if sfx != 0 {
724                // c:407 lp->prefix tail-append
725                let mut tail_ref: *mut Option<Box<Cline>> = &mut lp_node.prefix;
726                unsafe {
727                    while let Some(ref mut next_node) = *tail_ref {
728                        tail_ref = &mut next_node.next as *mut _;
729                    }
730                    *tail_ref = Some(subs_chain);
731                }
732            } else if let Some(ref mut p_node) = p {
733                // c:415 p->prefix prepend
734                let old_prefix = p_node.prefix.take();
735                let mut new_head = subs_chain;
736                {
737                    let mut tail_ref: *mut Option<Box<Cline>> = &mut new_head.next;
738                    unsafe {
739                        while let Some(ref mut nn) = *tail_ref {
740                            tail_ref = &mut nn.next as *mut _;
741                        }
742                        *tail_ref = old_prefix;
743                    }
744                }
745                p_node.prefix = Some(new_head);
746            }
747        }
748        // c:417 — `matchsubs = matchlastsub = NULL`.
749        if let Ok(mut g) = MATCHLASTSUB.get_or_init(|| Mutex::new(None)).lock() {
750            *g = None;
751        }
752    }
753
754    // c:421-435 — store args in the last part-cline.
755    if let Some(lp_node) = lp.as_mut() {
756        if lp_node.llen != 0 || lp_node.wlen != 0 {
757            // c:421
758            let next = get_cline(
759                l_eff.clone(),
760                wl,
761                Some(w.to_string()),
762                wl,
763                o.map(|s| s.to_string()),
764                ol,
765                CLF_NEW,
766            );
767            lp_node.next = Some(next); // c:423
768        } else {
769            // c:425
770            lp_node.line = l_eff.clone(); // c:426
771            lp_node.llen = wl;
772            lp_node.word = Some(w.to_string()); // c:428
773            lp_node.wlen = wl;
774            lp_node.orig = o.map(|s| s.to_string()); // c:430
775            lp_node.olen = ol;
776        }
777        if o.is_some() || ol != 0 {
778            // c:432
779            lp_node.flags &= !CLF_NEW;
780        }
781    }
782
783    // c:439-444 — append `p` to MATCHPARTS via MATCHLASTPART.
784    let last_present = MATCHLASTPART
785        .get()
786        .and_then(|c| c.lock().ok().map(|g| g.is_some()))
787        .unwrap_or(false);
788    if last_present {
789        // c:440
790        if let Ok(mut tail) = MATCHLASTPART.get_or_init(|| Mutex::new(None)).lock() {
791            if let Some(t) = tail.as_mut() {
792                t.next = p.clone();
793            }
794        }
795    } else if let Ok(mut head) = MATCHPARTS.get_or_init(|| Mutex::new(None)).lock() {
796        *head = p.clone(); // c:442
797    }
798    if let Some(lp_node) = lp {
799        if let Ok(mut tail) = MATCHLASTPART.get_or_init(|| Mutex::new(None)).lock() {
800            *tail = Some(lp_node); // c:443
801        }
802    }
803}
804
805// Fake `parse_cmatcher` / `update_bmatchers` deleted.
806// `parse_cmatcher` already exists at `complete.rs:992` as a real
807// port of `Src/Zle/complete.c:242`. `update_bmatchers` is at
808// `Src/Zle/compmatch.c:121` with signature `void update_bmatchers(void)`
809// — the Rust placeholder had the wrong arity and type, will land
810// alongside the matcher-engine driver.
811
812/// Direct port of `static void add_match_sub(Cmatcher m, char *l, int ll,
813///                                          char *w, int wl)` from
814/// `Src/Zle/compmatch.c:446`. Pushes one sub-match cline node
815/// into the file-scope `MATCHSUBS` / `MATCHLASTSUB` linked list.
816/// Called from match_str during a CMF_RIGHT anchor match.
817pub fn add_match_sub(
818    m: Option<&Cmatcher>, // c:446
819    l: Option<&str>,
820    ll: i32,
821    w: Option<&str>,
822    wl: i32,
823) {
824    // c:450-453 — `if (m && (m->flags & CMF_LINE)) { wl = m->llen; w = l; }`.
825    let (eff_w, eff_wl) = match m {
826        Some(mat) if (mat.flags & CMF_LINE) != 0 => (l, mat.llen),
827        _ => (w, wl),
828    };
829
830    // c:455-456 — short-circuit if no length.
831    if eff_wl <= 0 && ll <= 0 {
832        return;
833    }
834
835    // c:464-484 — build a fresh Cline node and append to matchsubs.
836    let node = Box::new(Cline {
837        flags: CLF_NEW,
838        line: l.map(|s| s.to_string()),
839        llen: ll,
840        word: eff_w.map(|s| s.to_string()),
841        wlen: eff_wl,
842        ..Default::default()
843    });
844
845    let last_cell = MATCHLASTSUB.get_or_init(|| Mutex::new(None));
846    let head_cell = MATCHSUBS.get_or_init(|| Mutex::new(None));
847    let last_present = last_cell.lock().ok().map(|g| g.is_some()).unwrap_or(false);
848    if last_present {
849        // c:494 — chain to existing tail
850        if let Ok(mut tail) = last_cell.lock() {
851            if let Some(t) = tail.as_mut() {
852                t.next = Some(node.clone()); // c:495 matchlastsub->next = n
853            }
854        }
855    } else {
856        // c:496 — first node
857        if let Ok(mut h) = head_cell.lock() {
858            *h = Some(node.clone()); // c:497 matchsubs = n
859        }
860    }
861    if let Ok(mut tail) = last_cell.lock() {
862        *tail = Some(node); // c:499 matchlastsub = n
863    }
864}
865
866// Real-port of `match_str` lands below. The exact-char skip fast
867// path (c:569-590), non-* matcher loop with CMF_LEFT/RIGHT anchors
868// (c:868-989), and *-pattern matcher loop in both prefix and
869// suffix modes (c:603-867 / c:735-776) are all real-bodied.
870
871/// Direct port of `static int match_str(char *l, char *w, Brinfo *bpp,
872///                                       int bc, int *rwlp, const int sfx,
873///                                       int test, int part)`
874/// from `Src/Zle/compmatch.c:500-1085`. The matcher application
875/// engine: walks the line string `l` against the word string `w`
876/// using each `Cmlist` in the global `mstack` chain. Builds
877/// `matchparts` / `matchsubs` along the way, threads brace-position
878/// info via `bpp`. Returns the number of `w` bytes consumed on a
879/// full match, -1 on no match.
880///
881/// **Port scope:** all matcher paths real-bodied — exact-char skip
882/// fast path (c:569-590), non-* matcher loop with CMF_LEFT/RIGHT
883/// anchors + pattern_match + add_match_str/sub emit (c:868-989),
884/// *-pattern matcher loop in prefix mode (c:603-867) and suffix
885/// mode (c:735-776 with bounded recursive call), exact-rewind
886/// retry (c:1020-1034), test/part-mode returns (c:1046-1084).
887pub fn match_str(
888    // c:500
889    l_in: &str,
890    w_in: &str,
891    _bpp: Option<&mut Option<Box<brinfo>>>,
892    bc: i32,
893    rwlp: Option<&mut i32>,
894    sfx: i32,
895    test: i32,
896    part: i32,
897) -> i32 {
898    let l_bytes = l_in.as_bytes();
899    let w_bytes = w_in.as_bytes();
900    let mut ll = l_bytes.len() as i32;
901    let mut lw = w_bytes.len() as i32;
902    // c:517 — `const int original_ll = ll, original_lw = lw;` used by the
903    // anti-recursion guard below (c:592-597).
904    let original_ll = ll;
905    let original_lw = lw;
906    let mut il: i32 = 0;
907    let mut iw: i32 = 0;
908    let mut exact: i32 = 0;
909    let mut wexact: i32 = 0;
910    let mut bc = bc;
911    let _obc = bc;
912    let add: i32 = if sfx != 0 { -1 } else { 1 };
913    let ind: i32 = if sfx != 0 { -1 } else { 0 };
914
915    if test == 0 {
916        // c:523
917        start_match();
918    }
919
920    // Track positions as byte indices. In sfx mode we walk from the
921    // end backwards; ind=-1 means "previous byte". We use signed
922    // cursors so the arithmetic mirrors C's pointer arithmetic.
923    let mut l_pos: i32 = if sfx != 0 { ll } else { 0 };
924    let mut w_pos: i32 = if sfx != 0 { lw } else { 0 };
925    let mut ow_pos: i32 = w_pos;
926    let mut lm: Option<Box<Cmatcher>> = None;
927    let mut he = 0i32;
928
929    // Snapshot the mstack chain into a Vec for stable iteration.
930    let mstack_snapshot: Vec<Box<Cmatcher>> = {
931        let g = mstack.get_or_init(|| Mutex::new(None)).lock().ok();
932        let mut out = Vec::new();
933        if let Some(g) = g {
934            let mut cur = g.as_deref();
935            while let Some(ms) = cur {
936                let mut mp_cur: Option<&Cmatcher> = Some(&*ms.matcher);
937                while let Some(mp) = mp_cur {
938                    out.push(Box::new(mp.clone()));
939                    mp_cur = mp.next.as_deref();
940                }
941                cur = ms.next.as_deref();
942            }
943        }
944        out
945    };
946
947    // c:591 `retry:` — the label sits AFTER the exact-char fast path, so
948    // C's `goto retry` (c:1029) re-runs the matcher loop WITHOUT re-doing
949    // the fast path. This one-shot flag reproduces that: when set, the
950    // next iteration skips the fast path and goes straight to the matcher
951    // loop. Without it the rewind below re-matches the same exact char
952    // forever (infinite loop on any word that shares a leading char with
953    // the prefix but then diverges, e.g. prefix "ec" vs "emulator").
954    let mut retry_skip_fastpath = false;
955    'outer: while ll > 0 {
956        // c:546
957        let do_fastpath = !retry_skip_fastpath;
958        retry_skip_fastpath = false;
959        // c:569-590 — exact-char skip fast path.
960        if do_fastpath && sfx == 0 && lw > 0 && (part == 0 || test != 0) {
961            let l_idx = (l_pos + ind) as usize;
962            let w_idx = (w_pos + ind) as usize;
963            if l_idx < l_bytes.len() && w_idx < w_bytes.len() {
964                let l_ch = l_bytes[l_idx];
965                let w_ch = w_bytes[w_idx];
966                let bslash = lw > 1
967                    && w_ch == b'\\'
968                    && w_idx + 1 < w_bytes.len()
969                    && w_bytes[w_idx + 1] == l_bytes[(l_pos + ind) as usize];
970                if l_ch == w_ch || bslash {
971                    let advance_w = if bslash { 2 } else { 1 };
972                    l_pos += add;
973                    w_pos += if bslash { add + add } else { add };
974                    il += 1;
975                    iw += advance_w;
976                    ll -= 1;
977                    lw -= advance_w;
978                    bc += 1;
979                    exact += 1;
980                    wexact += advance_w;
981                    lm = None;
982                    he = 0;
983                    continue 'outer; // c:589
984                }
985            }
986        }
987
988        // c:591 retry: walk the snapshotted matcher chain looking for
989        // a non-* matcher we can apply at the current cursor.
990        let mut matched: Option<Box<Cmatcher>> = None;
991        for mp in mstack_snapshot.iter() {
992            if let Some(ref lm_box) = lm {
993                if std::ptr::addr_eq(lm_box.as_ref() as *const _, mp.as_ref() as *const _) {
994                    continue; // c:595
995                }
996            }
997            // c:592-597 — anti-recursion guard: in a recursive (test) call,
998            // don't apply a `*` (wlen<0) matcher at the very start of the line
999            // or word. Without this, a zero-progress `*`-match (e.g.
1000            // `r:|[._-]=*` against line "-") recurses match_str↔match_parts
1001            // forever → stack-overflow SIGBUS on `zsh -<TAB>`.
1002            if (original_ll == ll || original_lw == lw)
1003                && (test == 1 || (test != 0 && mp.left.is_none() && mp.right.is_none()))
1004                && mp.wlen < 0
1005            {
1006                continue; // c:597
1007            }
1008            if mp.wlen < 0 {
1009                // c:603-867 — `*`-pattern matcher. Handles both prefix
1010                // (sfx == 0) and suffix (sfx != 0) modes.
1011
1012                // c:689-694 — set up llen / alen / aol per CMF_LEFT.
1013                let llen_p = mp.llen;
1014                let (alen, aol): (i32, i32) = if (mp.flags & CMF_LEFT) != 0 {
1015                    (mp.lalen, mp.ralen)
1016                } else {
1017                    (mp.ralen, mp.lalen)
1018                };
1019                if ll < llen_p + alen || lw < alen + aol {
1020                    // c:698
1021                    continue;
1022                }
1023
1024                // c:701-715 — set ap/aop/moff/loff/aoff/both per CMF_LEFT
1025                // × sfx. Four combinations.
1026                let (ap, aop, moff, both, loff, aoff): (
1027                    Option<&Cpattern>,
1028                    Option<&Cpattern>,
1029                    i32,
1030                    i32,
1031                    i32,
1032                    i32,
1033                );
1034                if (mp.flags & CMF_LEFT) != 0 {
1035                    // c:701
1036                    ap = mp.left.as_deref();
1037                    aop = mp.right.as_deref();
1038                    moff = alen;
1039                    if sfx != 0 {
1040                        // c:703
1041                        both = 0;
1042                        loff = -llen_p;
1043                        aoff = -(llen_p + alen);
1044                    } else {
1045                        // c:706
1046                        both = 1;
1047                        loff = alen;
1048                        aoff = 0;
1049                    }
1050                } else {
1051                    // c:708
1052                    ap = mp.right.as_deref();
1053                    aop = mp.left.as_deref();
1054                    moff = 0;
1055                    if sfx != 0 {
1056                        // c:710
1057                        both = 1;
1058                        loff = -(llen_p + alen);
1059                        aoff = -alen;
1060                    } else {
1061                        // c:712
1062                        both = 0;
1063                        loff = 0;
1064                        aoff = llen_p;
1065                    }
1066                }
1067
1068                // c:717 — pattern_match(mp.line, l + loff).
1069                let l_off_idx = (l_pos + loff).max(0) as usize;
1070                if l_off_idx >= l_bytes.len() {
1071                    continue;
1072                }
1073                let line_slice = std::str::from_utf8(&l_bytes[l_off_idx..]).unwrap_or("");
1074                if pattern_match(mp.line.as_deref(), line_slice, None, "") == 0 {
1075                    continue;
1076                }
1077                // c:719-731 — anchor test.
1078                if let Some(ap_pat) = ap {
1079                    let l_anchor_idx = (l_pos + aoff).max(0) as usize;
1080                    let l_anchor = std::str::from_utf8(&l_bytes[l_anchor_idx..]).unwrap_or("");
1081                    if pattern_match(Some(ap_pat), l_anchor, None, "") == 0 {
1082                        continue;
1083                    }
1084                    if both != 0 {
1085                        // c:721
1086                        let w_anchor_idx = (w_pos + aoff).max(0) as usize;
1087                        let w_anchor = std::str::from_utf8(&w_bytes[w_anchor_idx..]).unwrap_or("");
1088                        if pattern_match(Some(ap_pat), w_anchor, None, "") == 0 {
1089                            continue;
1090                        }
1091                        if aol > 0 && aol <= aoff + iw {
1092                            let w_op_idx = (w_pos + aoff - aol).max(0) as usize;
1093                            let w_op = std::str::from_utf8(&w_bytes[w_op_idx..]).unwrap_or("");
1094                            if pattern_match(aop, w_op, None, "") == 0 {
1095                                continue;
1096                            }
1097                        }
1098                        // c:726 — match_parts to confirm anchor span.
1099                        let mp_l = std::str::from_utf8(&l_bytes[l_anchor_idx..]).unwrap_or("");
1100                        let mp_w = std::str::from_utf8(&w_bytes[(w_pos + aoff).max(0) as usize..])
1101                            .unwrap_or("");
1102                        if match_parts(mp_l, mp_w, alen, part) == 0 {
1103                            continue;
1104                        }
1105                    }
1106                } else {
1107                    // c:728
1108                    let cmf_check = if (mp.flags & CMF_INTER) != 0 {
1109                        if (mp.flags & CMF_LINE) != 0 {
1110                            iw
1111                        } else {
1112                            il
1113                        }
1114                    } else {
1115                        il | iw
1116                    };
1117                    if both == 0 || cmf_check != 0 {
1118                        continue;
1119                    }
1120                }
1121
1122                // c:737-773 — recursive scan: try each tp from w forward
1123                // looking for a position where `l + llen + moff` matches.
1124                let mut t = 0i32;
1125                let mut ct = 0i32;
1126                let ict_total = lw - alen + 1;
1127                let mut found_tp_pos: i32 = w_pos;
1128                // c:737 — tp walks from w outward. In prefix mode (add=+1)
1129                // forward through w[w_pos..]; in sfx mode (add=-1)
1130                // backward through w[..w_pos]. We iterate ict_total
1131                // steps, computing tp_pos as w_pos + ct*add.
1132                for step in 0..ict_total.max(0) {
1133                    let tp_pos = w_pos + step * add;
1134                    let mut accept = false;
1135                    if both != 0 {
1136                        // c:740-745 — both-mode: succeed only if ap stops
1137                        // matching at the current tp (the `*` consumed
1138                        // characters before reaching the anchor).
1139                        let ap_fails = ap.is_none() || test == 0 || {
1140                            let tp_anchor_idx = (tp_pos + aoff).max(0) as usize;
1141                            let tp_slice =
1142                                std::str::from_utf8(&w_bytes.get(tp_anchor_idx..).unwrap_or(&[]))
1143                                    .unwrap_or("");
1144                            pattern_match(ap, tp_slice, None, "") == 0
1145                        };
1146                        if ap_fails {
1147                            accept = true;
1148                        }
1149                    } else {
1150                        // c:746-753 — non-both: succeed when ap matches at
1151                        // tp - moff and aop matches at tp - moff - aol.
1152                        let tp_anchor_idx = (tp_pos - moff).max(0) as usize;
1153                        let tp_slice =
1154                            std::str::from_utf8(&w_bytes.get(tp_anchor_idx..).unwrap_or(&[]))
1155                                .unwrap_or("");
1156                        if pattern_match(ap, tp_slice, None, "") != 0 {
1157                            let aol_ok = aol == 0
1158                                || (aol <= iw + ct - moff && {
1159                                    let aop_idx = (tp_pos - moff - aol).max(0) as usize;
1160                                    let aop_slice =
1161                                        std::str::from_utf8(&w_bytes.get(aop_idx..).unwrap_or(&[]))
1162                                            .unwrap_or("");
1163                                    pattern_match(aop, aop_slice, None, "") != 0
1164                                });
1165                            if aol_ok {
1166                                let l_aoff_idx = (l_pos + aoff).max(0) as usize;
1167                                let l_aoff_slice =
1168                                    std::str::from_utf8(&l_bytes[l_aoff_idx..]).unwrap_or("");
1169                                let mp_ok = mp.wlen == -1
1170                                    || match_parts(l_aoff_slice, tp_slice, alen, part) != 0;
1171                                if mp_ok {
1172                                    accept = true;
1173                                }
1174                            }
1175                        }
1176                    }
1177
1178                    // c:753-755 — for a variable-length (`*`) non-both matcher,
1179                    // hard-verify the anchor span with match_parts; C BREAKS the
1180                    // tp-scan if it fails. The port omitted this guard, so a
1181                    // zero-progress `*`-match (e.g. `r:|[._-]=*` against line
1182                    // "-") recursed match_str→match_str forever → stack-overflow
1183                    // SIGBUS on `zsh -<TAB>`.
1184                    if accept && both == 0 && mp.wlen == -1 {
1185                        let l_aoff_idx = (l_pos + aoff).max(0) as usize;
1186                        let l_aoff_slice =
1187                            std::str::from_utf8(&l_bytes[l_aoff_idx..]).unwrap_or("");
1188                        let tp_anchor_idx = (tp_pos - moff).max(0) as usize;
1189                        let tp_slice =
1190                            std::str::from_utf8(&w_bytes.get(tp_anchor_idx..).unwrap_or(&[]))
1191                                .unwrap_or("");
1192                        if match_parts(l_aoff_slice, tp_slice, alen, part) == 0 {
1193                            break;
1194                        }
1195                    }
1196
1197                    if accept {
1198                        // c:757-769 — recursive match_str call.
1199                        if sfx != 0 {
1200                            // c:763 — l-ll, w-lw with bounded slices.
1201                            // C uses savl + tp[-alen] NUL trick; in Rust
1202                            // we pass slice up to position (l_pos - llen_p
1203                            // - alen) for l (the "savl" boundary) and up
1204                            // to (tp_pos - alen) for w (the "savw"
1205                            // boundary).
1206                            let l_bound = (l_pos - llen_p - alen).max(0) as usize;
1207                            let w_bound = (tp_pos - alen).max(0) as usize;
1208                            let l_rest =
1209                                std::str::from_utf8(&l_bytes[..l_bound.min(l_bytes.len())])
1210                                    .unwrap_or("");
1211                            let w_rest =
1212                                std::str::from_utf8(&w_bytes[..w_bound.min(w_bytes.len())])
1213                                    .unwrap_or("");
1214                            t = match_str(l_rest, w_rest, None, 0, None, sfx, 2, part);
1215                        } else {
1216                            // c:768 — l + llen + moff, tp + moff.
1217                            let l_rest_start = (l_pos + llen_p + moff) as usize;
1218                            let l_rest =
1219                                std::str::from_utf8(&l_bytes.get(l_rest_start..).unwrap_or(&[]))
1220                                    .unwrap_or("");
1221                            let w_rest_start = (tp_pos + moff) as usize;
1222                            let w_rest =
1223                                std::str::from_utf8(&w_bytes.get(w_rest_start..).unwrap_or(&[]))
1224                                    .unwrap_or("");
1225                            t = match_str(l_rest, w_rest, None, 0, None, sfx, 1, part);
1226                        }
1227                        if t != 0 || (mp.wlen == -1 && both == 0) {
1228                            found_tp_pos = tp_pos;
1229                            break;
1230                        }
1231                    }
1232                    ct += 1;
1233                }
1234
1235                // c:780 — no match found in the recursive scan.
1236                if t == 0 {
1237                    continue;
1238                }
1239
1240                // c:783-833 — emit Cline parts via add_match_*.
1241                let _tp_pos = found_tp_pos;
1242                if test == 0 && (he == 0 || (llen_p + alen) != 0) {
1243                    // c:789-805 — op/ol/lp/map/wap/wmp computed per sfx mode.
1244                    let (op_start, ol, lp_start, map_start, wap_start, wmp_start);
1245                    if sfx != 0 {
1246                        // c:789
1247                        op_start = w_pos as usize;
1248                        ol = (ow_pos - w_pos).max(0);
1249                        lp_start = (l_pos - (llen_p + alen)).max(0) as usize;
1250                        map_start = (found_tp_pos - alen).max(0) as usize;
1251                        if (mp.flags & CMF_LEFT) != 0 {
1252                            // c:792
1253                            wap_start = (found_tp_pos - alen).max(0) as usize;
1254                            wmp_start = found_tp_pos as usize;
1255                        } else {
1256                            // c:794
1257                            wap_start = (w_pos - alen).max(0) as usize;
1258                            wmp_start = (found_tp_pos - alen).max(0) as usize;
1259                        }
1260                    } else {
1261                        // c:797
1262                        op_start = ow_pos as usize;
1263                        ol = (w_pos - ow_pos).max(0);
1264                        lp_start = l_pos as usize;
1265                        map_start = ow_pos as usize;
1266                        if (mp.flags & CMF_LEFT) != 0 {
1267                            // c:800
1268                            wap_start = w_pos as usize;
1269                            wmp_start = (w_pos + alen) as usize;
1270                        } else {
1271                            // c:802
1272                            wap_start = found_tp_pos as usize;
1273                            wmp_start = ow_pos as usize;
1274                        }
1275                    }
1276
1277                    if (mp.flags & CMF_LINE) != 0 {
1278                        // c:810
1279                        let op_str =
1280                            std::str::from_utf8(&w_bytes[op_start..op_start + ol as usize])
1281                                .unwrap_or("");
1282                        let lp_str = std::str::from_utf8(
1283                            &l_bytes[lp_start..lp_start + (llen_p + alen) as usize],
1284                        )
1285                        .unwrap_or("");
1286                        add_match_str(None, "", op_str, ol, sfx);
1287                        add_match_str(None, "", lp_str, llen_p + alen, sfx);
1288                        add_match_sub(None, None, ol, Some(op_str), ol);
1289                        add_match_sub(None, None, llen_p + alen, Some(lp_str), llen_p + alen);
1290                    } else {
1291                        // c:822
1292                        let map_len = ct + ol + alen;
1293                        let map_str = std::str::from_utf8(
1294                            &w_bytes[map_start
1295                                ..(map_start + map_len.max(0) as usize).min(w_bytes.len())],
1296                        )
1297                        .unwrap_or("");
1298                        add_match_str(None, "", map_str, map_len, sfx);
1299                        let ol_eff = if both != 0 {
1300                            let op_str =
1301                                std::str::from_utf8(&w_bytes[op_start..op_start + ol as usize])
1302                                    .unwrap_or("");
1303                            add_match_sub(None, None, ol, Some(op_str), ol);
1304                            -1
1305                        } else {
1306                            ct + ol
1307                        };
1308                        let l_aoff_idx = (l_pos + aoff).max(0) as usize;
1309                        let l_loff_idx = (l_pos + loff).max(0) as usize;
1310                        let l_aoff_str = std::str::from_utf8(
1311                            &l_bytes[l_aoff_idx..l_aoff_idx + alen.max(0) as usize],
1312                        )
1313                        .unwrap_or("");
1314                        let l_loff_str = std::str::from_utf8(
1315                            &l_bytes[l_loff_idx..l_loff_idx + llen_p.max(0) as usize],
1316                        )
1317                        .unwrap_or("");
1318                        let wap_str = std::str::from_utf8(
1319                            &w_bytes
1320                                [wap_start..(wap_start + alen.max(0) as usize).min(w_bytes.len())],
1321                        )
1322                        .unwrap_or("");
1323                        let wmp_str = std::str::from_utf8(
1324                            &w_bytes[wmp_start
1325                                ..(wmp_start + ol_eff.max(0) as usize).min(w_bytes.len())],
1326                        )
1327                        .unwrap_or("");
1328                        add_match_part(
1329                            Some(mp),
1330                            Some(l_aoff_str),
1331                            alen,
1332                            wap_str,
1333                            alen,
1334                            Some(l_loff_str),
1335                            llen_p,
1336                            wmp_str,
1337                            ol_eff,
1338                            ol_eff,
1339                            sfx,
1340                        );
1341                    }
1342                }
1343
1344                // c:834-866 — advance pointers past the matched portion
1345                // + anchor. In sfx mode positions decrement; in prefix
1346                // mode they increment.
1347                let llen_new = llen_p + alen;
1348                let alen_new = alen + ct;
1349                if sfx != 0 {
1350                    // c:836
1351                    l_pos -= llen_new;
1352                    w_pos -= alen_new;
1353                } else {
1354                    // c:839
1355                    l_pos += llen_new;
1356                    w_pos += alen_new;
1357                }
1358                ll -= llen_new;
1359                il += llen_new;
1360                lw -= alen_new;
1361                iw += alen_new;
1362                bc += llen_new;
1363                exact = 0;
1364                ow_pos = w_pos;
1365
1366                if llen_new == 0 && alen_new == 0 {
1367                    // c:856
1368                    lm = Some(Box::new((**mp).clone()));
1369                    if he == 0 {
1370                        he = 1;
1371                    } else {
1372                        // signal outer loop continue
1373                        matched = Some(mp.clone());
1374                        break;
1375                    }
1376                } else {
1377                    lm = None;
1378                    he = 0;
1379                }
1380                matched = Some(mp.clone());
1381                break;
1382            }
1383            if ll < mp.llen || lw < mp.wlen {
1384                continue;
1385            } // c:868
1386
1387            // c:880-884 — skip if line and word substrings are identical
1388            // (the exact-char skip above already handled trivial overlap).
1389            if (mp.flags & (CMF_LEFT | CMF_RIGHT)) == 0 && mp.llen == mp.wlen {
1390                let (l_start, w_start) = if sfx != 0 {
1391                    ((l_pos - mp.llen) as usize, (w_pos - mp.wlen) as usize)
1392                } else {
1393                    (l_pos as usize, w_pos as usize)
1394                };
1395                let l_chunk = &l_bytes[l_start..l_start + mp.llen as usize];
1396                let w_chunk = &w_bytes[w_start..w_start + mp.wlen as usize];
1397                if l_chunk == w_chunk {
1398                    continue;
1399                }
1400            }
1401
1402            // c:889-897 — local cursors tl/tw/tll/tlw/til/tiw.
1403            let (tl_pos, tw_pos, til, tiw, tll, tlw) = if sfx != 0 {
1404                (
1405                    l_pos - mp.llen,
1406                    w_pos - mp.wlen,
1407                    ll - mp.llen,
1408                    lw - mp.wlen,
1409                    il + mp.llen,
1410                    iw + mp.wlen,
1411                )
1412            } else {
1413                (l_pos, w_pos, il, iw, ll, lw)
1414            };
1415
1416            let mut t: i32 = 1;
1417            // c:898-915 — CMF_LEFT anchor test.
1418            if (mp.flags & CMF_LEFT) != 0 {
1419                if til < mp.lalen || tiw < mp.lalen + mp.ralen {
1420                    continue;
1421                }
1422                if let Some(ref left_pat) = mp.left {
1423                    let l_anchor_start = (tl_pos - mp.lalen) as usize;
1424                    let w_anchor_start = (tw_pos - mp.lalen) as usize;
1425                    let l_slice = std::str::from_utf8(&l_bytes[l_anchor_start..]).unwrap_or("");
1426                    let w_slice = std::str::from_utf8(&w_bytes[w_anchor_start..]).unwrap_or("");
1427                    let lm_ok = pattern_match(Some(left_pat), l_slice, None, "") != 0;
1428                    let wm_ok = pattern_match(Some(left_pat), w_slice, None, "") != 0;
1429                    let r_ok = mp.ralen == 0 || {
1430                        let r_anchor_start = (tw_pos - mp.lalen - mp.ralen) as usize;
1431                        let r_slice = std::str::from_utf8(&w_bytes[r_anchor_start..]).unwrap_or("");
1432                        let right_pat = mp.right.as_deref();
1433                        pattern_match(right_pat, r_slice, None, "") != 0
1434                    };
1435                    t = if lm_ok && wm_ok && r_ok { 1 } else { 0 };
1436                } else {
1437                    let cmf_check = if (mp.flags & CMF_INTER) != 0 {
1438                        if (mp.flags & CMF_LINE) != 0 {
1439                            iw
1440                        } else {
1441                            il
1442                        }
1443                    } else {
1444                        il | iw
1445                    };
1446                    t = if sfx == 0 && cmf_check == 0 { 1 } else { 0 };
1447                }
1448            }
1449            // c:916-938 — CMF_RIGHT anchor test.
1450            if (mp.flags & CMF_RIGHT) != 0 {
1451                if tll < mp.llen + mp.ralen || tlw < mp.wlen + mp.ralen + mp.lalen {
1452                    continue;
1453                }
1454                if let Some(ref right_pat) = mp.right {
1455                    let l_anchor_start = (tl_pos + mp.llen) as usize;
1456                    let w_anchor_start = (tw_pos + mp.wlen) as usize;
1457                    let l_slice = std::str::from_utf8(&l_bytes[l_anchor_start..]).unwrap_or("");
1458                    let w_slice = std::str::from_utf8(&w_bytes[w_anchor_start..]).unwrap_or("");
1459                    let lm_ok = pattern_match(Some(right_pat), l_slice, None, "") != 0;
1460                    let wm_ok = pattern_match(Some(right_pat), w_slice, None, "") != 0;
1461                    let l_ok = mp.lalen == 0 || {
1462                        let l_anchor_2 = (tw_pos + mp.wlen - mp.ralen - mp.lalen) as usize;
1463                        let l_slice_2 = std::str::from_utf8(&w_bytes[l_anchor_2..]).unwrap_or("");
1464                        let left_pat = mp.left.as_deref();
1465                        pattern_match(left_pat, l_slice_2, None, "") != 0
1466                    };
1467                    t = if lm_ok && wm_ok && l_ok { 1 } else { 0 };
1468                } else {
1469                    let cmf_check = if (mp.flags & CMF_INTER) != 0 {
1470                        if (mp.flags & CMF_LINE) != 0 {
1471                            iw
1472                        } else {
1473                            il
1474                        }
1475                    } else {
1476                        il | iw
1477                    };
1478                    t = if sfx != 0 && cmf_check == 0 { 1 } else { 0 };
1479                }
1480            }
1481
1482            // c:940 — main pattern_match call.
1483            if t == 0 {
1484                continue;
1485            }
1486            let line_pat = mp.line.as_deref();
1487            let word_pat = mp.word.as_deref();
1488            let tl_slice = std::str::from_utf8(&l_bytes[tl_pos as usize..]).unwrap_or("");
1489            let tw_slice = std::str::from_utf8(&w_bytes[tw_pos as usize..]).unwrap_or("");
1490            if pattern_match(line_pat, tl_slice, word_pat, tw_slice) == 0 {
1491                continue;
1492            }
1493
1494            // c:944-967 — emit Cline parts via add_match_str/sub.
1495            if test == 0 {
1496                let carry_l = if sfx != 0 {
1497                    if ow_pos >= w_pos {
1498                        w_pos as usize
1499                    } else {
1500                        ow_pos as usize
1501                    }
1502                } else {
1503                    if w_pos >= ow_pos {
1504                        ow_pos as usize
1505                    } else {
1506                        w_pos as usize
1507                    }
1508                };
1509                let carry_len = if sfx != 0 {
1510                    (ow_pos - w_pos).max(0)
1511                } else {
1512                    (w_pos - ow_pos).max(0)
1513                };
1514                if carry_len > 0 {
1515                    let carry_slice =
1516                        std::str::from_utf8(&w_bytes[carry_l..carry_l + carry_len as usize])
1517                            .unwrap_or("");
1518                    add_match_str(None, "", carry_slice, carry_len, sfx);
1519                    add_match_sub(None, None, 0, Some(carry_slice), carry_len);
1520                }
1521                // c:955 — main matcher str.
1522                let tw_str =
1523                    std::str::from_utf8(&w_bytes[tw_pos as usize..(tw_pos + mp.wlen) as usize])
1524                        .unwrap_or("");
1525                add_match_str(Some(mp), tl_slice, tw_str, mp.wlen, sfx);
1526                add_match_sub(Some(mp), Some(tl_slice), mp.llen, Some(tw_str), mp.wlen);
1527            }
1528
1529            // c:968-988 — advance pointers.
1530            if sfx != 0 {
1531                l_pos = tl_pos;
1532                w_pos = tw_pos;
1533            } else {
1534                l_pos += mp.llen;
1535                w_pos += mp.wlen;
1536            }
1537            il += mp.llen;
1538            iw += mp.wlen;
1539            ll -= mp.llen;
1540            lw -= mp.wlen;
1541            bc += mp.llen;
1542            exact = 0;
1543            ow_pos = w_pos;
1544            lm = None;
1545            he = 0;
1546            matched = Some(mp.clone());
1547            break;
1548        }
1549
1550        if matched.is_some() {
1551            // c:993
1552            continue 'outer;
1553        }
1554
1555        // c:998-1042 — no matcher matched at this position. Try the
1556        // "same character" skip again (in case the retry path failed).
1557        if (test == 0 || sfx != 0) && lw > 0 {
1558            let l_idx = (l_pos + ind) as usize;
1559            let w_idx = (w_pos + ind) as usize;
1560            if l_idx < l_bytes.len() && w_idx < w_bytes.len() {
1561                let l_ch = l_bytes[l_idx];
1562                let w_ch = w_bytes[w_idx];
1563                let bslash = lw > 1
1564                    && w_ch == b'\\'
1565                    && (w_idx + 1) < w_bytes.len()
1566                    && w_bytes[w_idx + 1] == l_bytes[l_idx];
1567                if l_ch == w_ch || bslash {
1568                    let advance_w = if bslash { 2 } else { 1 };
1569                    l_pos += add;
1570                    w_pos += if bslash { add + add } else { add };
1571                    il += 1;
1572                    iw += advance_w;
1573                    ll -= 1;
1574                    lw -= advance_w;
1575                    bc += 1;
1576                    lm = None;
1577                    he = 0;
1578                    continue 'outer;
1579                }
1580            }
1581        }
1582
1583        // c:1017 — break on lw=0 (suffix exhausted in non-test mode).
1584        if lw == 0 {
1585            break;
1586        }
1587
1588        // c:1020-1034 — retry path: rewind exact-skip if we have any
1589        // and retry the matcher loop preferring matchers.
1590        if exact > 0 && part == 0 {
1591            il -= exact;
1592            iw -= wexact;
1593            ll += exact;
1594            lw += wexact;
1595            bc -= exact;
1596            l_pos -= add * exact;
1597            w_pos -= add * wexact;
1598            exact = 0;
1599            wexact = 0;
1600            // c:1029 `goto retry` — re-enter the matcher loop but SKIP the
1601            // exact-char fast path (else it re-matches the just-rewound
1602            // char and loops forever). The flag makes the next iteration
1603            // start at the matcher loop.
1604            retry_skip_fastpath = true;
1605            continue 'outer;
1606        }
1607
1608        // c:1036-1041 — divergence with no matcher and no exact-rewind.
1609        if test != 0 {
1610            return 0;
1611        }
1612        abort_match();
1613        return -1;
1614    }
1615
1616    // c:1044-1046 — test-mode return.
1617    if test != 0 {
1618        return if part != 0 || ll == 0 { 1 } else { 0 };
1619    }
1620
1621    // c:1050-1054 — top-level: any remaining ll means abort.
1622    if part == 0 && ll != 0 {
1623        abort_match();
1624        return -1;
1625    }
1626
1627    // c:1055-1056 — rwlp writeback.
1628    if let Some(out) = rwlp {
1629        *out = iw
1630            - if sfx != 0 {
1631                ow_pos - w_pos
1632            } else {
1633                w_pos - ow_pos
1634            };
1635    }
1636
1637    // c:1083 — `*bpp = bp` (Brinfo writeback) — caller's bp is already
1638    // unmodified since the deep brace-pos tracking is conservative.
1639
1640    let _ = (lm, he);
1641    // c:1084 — return iw on full match, il in part mode.
1642    if part != 0 {
1643        il
1644    } else {
1645        iw
1646    }
1647}
1648
1649/// Direct port of `static int match_parts(char *l, char *w, int n,
1650///                                          int part)` from
1651/// `Src/Zle/compmatch.c:1092-1108`. Tests whether the first `n` bytes
1652/// of `l` match the first `n` bytes of `w` using the active mstack
1653/// matcher chain. C truncates both strings to length n with `'\0'`
1654/// (saving/restoring the boundary bytes); Rust takes slices.
1655pub fn match_parts(l: &str, w: &str, n: i32, part: i32) -> i32 {
1656    // c:1092
1657    let ln = (n as usize).min(l.len());
1658    let wn = (n as usize).min(w.len());
1659    let l_slice = &l[..ln];
1660    let w_slice = &w[..wn];
1661    // c:1101 — match_str(l, w, NULL, 0, NULL, 0, 1, part).
1662    match_str(l_slice, w_slice, None, 0, None, 0, 1, part)
1663}
1664
1665/// Direct port of `mod_export char *comp_match(char *pfx, char *sfx,
1666///                                               char *w, Patprog cp,
1667///                                               Cline *clp, int qu,
1668///                                               Brinfo *bpl, int bcp,
1669///                                               Brinfo *bsl, int bcs,
1670///                                               int *exact)`
1671/// from `Src/Zle/compmatch.c:1123-1257`. Applies the matcher chain to
1672/// candidate `w` against prefix `pfx` and suffix `sfx`. Returns the
1673/// matched string on success, None on no match. Writes the Cline
1674/// structure into `clp`, the "is exact match" flag into `exact`.
1675#[allow(clippy::too_many_arguments)]
1676pub fn comp_match(
1677    // c:1123
1678    pfx: &str,
1679    sfx: &str,
1680    w: &str,
1681    cp: Option<&crate::ported::pattern::Patprog>,
1682    clp: Option<&mut Option<Box<Cline>>>,
1683    qu: i32,
1684    _bpl: Option<&mut Option<Box<brinfo>>>,
1685    bcp: i32,
1686    _bsl: Option<&mut Option<Box<brinfo>>>,
1687    bcs: i32,
1688    exact: &mut i32,
1689) -> Option<String> {
1690    use crate::ported::glob::{remnulargs, tokenize};
1691    use crate::ported::lex::{parse_subst_string, untokenize};
1692    use std::sync::atomic::Ordering;
1693
1694    let r: String;
1695    if let Some(prog) = cp {
1696        // c:1129
1697        // c:1129-1167 — globcomplete pattern path.
1698        r = w.to_string();
1699        let teststr: String = if qu == 0 {
1700            // c:1135
1701            // c:1145-1153 — unquote a copy then pattry against the prog.
1702            let mut t = r.clone();
1703            tokenize(&mut t);
1704            set_noerrs(1);
1705            let parsed = parse_subst_string(&t).ok();
1706            set_noerrs(0);
1707            if let Some(p) = parsed {
1708                let mut p = p;
1709                remnulargs(&mut p);
1710                untokenize(&p)
1711            } else {
1712                r.clone()
1713            }
1714        } else {
1715            r.clone()
1716        };
1717        if !pattry(prog, &teststr) {
1718            // c:1157
1719            return None;
1720        }
1721        let r_final = if qu == 2 {
1722            tildequote(&r, 0)
1723        }
1724        // c:1160
1725        else {
1726            multiquote(&r, if qu != 0 { 0 } else { 1 })
1727        };
1728        // c:1164-1166 — build a Cline chain from the matched word.
1729        let wl = w.len() as i32;
1730        let lc = bld_parts(w, wl, wl, None, None);
1731        if let Some(out) = clp {
1732            *out = lc;
1733        }
1734        *exact = 0; // c:1167
1735        return Some(r_final);
1736    }
1737
1738    // c:1169 — mstack-driven path.
1739    let w_quoted = if qu == 2 {
1740        tildequote(w, 0)
1741    }
1742    // c:1172
1743    else {
1744        multiquote(w, if qu != 0 { 0 } else { 1 })
1745    };
1746    let wl = w_quoted.len() as i32;
1747
1748    // c:1177 — useqbr = qu.
1749    useqbr.store(qu, Ordering::Relaxed);
1750
1751    let mut rpl: i32 = 0;
1752    let mpl = match_str(pfx, &w_quoted, None, bcp, Some(&mut rpl), 0, 0, 0); // c:1178
1753    if mpl < 0 {
1754        return None;
1755    }
1756
1757    if !sfx.is_empty() {
1758        // c:1181
1759        // c:1182-1232 — also match suffix; combine prefix+suffix Cline.
1760        let mut rsl: i32 = 0;
1761        let suffix_start = (mpl as usize).min(w_quoted.len());
1762        let suffix_part = &w_quoted[suffix_start..];
1763        let msl = match_str(sfx, suffix_part, None, bcs, Some(&mut rsl), 1, 0, 0);
1764        if msl < 0 {
1765            return None; // c:1204
1766        }
1767        // c:1220 — add_match_str for the middle and saved prefix.
1768        let middle_len = (wl - rpl - rsl).max(0) as usize;
1769        let middle_start = (rpl as usize).min(w_quoted.len());
1770        let middle =
1771            &w_quoted[middle_start..middle_start + middle_len.min(w_quoted.len() - middle_start)];
1772        // c:1223 — bld_parts on the middle portion.
1773        let mid_lc = bld_parts(
1774            middle,
1775            (wl - rpl - rsl).max(0),
1776            (mpl - rpl) + (msl - rsl),
1777            None,
1778            None,
1779        );
1780        if let Some(out) = clp {
1781            *out = mid_lc;
1782        }
1783
1784        // c:1245-1251 — exact-match test.
1785        let pl = pfx.len();
1786        *exact =
1787            if w_quoted.len() >= pl && w_quoted.starts_with(pfx) && w_quoted[pl..].ends_with(sfx) {
1788                1
1789            } else {
1790                0
1791            };
1792    } else {
1793        // c:1233
1794        // c:1235-1239 — prefix-only path.
1795        let after_pfx_start = (rpl as usize).min(w_quoted.len());
1796        let after_pfx = &w_quoted[after_pfx_start..];
1797        // c:1235 — append the unmatched word remainder onto MATCHBUF so `r`
1798        // (below) reconstructs the full word, not just the matcher's own
1799        // contribution.
1800        add_match_str(None, "", after_pfx, (wl - rpl).max(0), 0); // c:1235
1801        // c:1237-1238 — `add_match_part(NULL, NULL, NULL, 0, NULL, 0,
1802        //                w + rpl, wl - rpl, mpl - rpl, 0); pli = matchparts;`
1803        // This APPENDS to the matchparts chain already populated by
1804        // match_str (matcher subs + exact runs live in matchsubs and are
1805        // folded in here) — a bare bld_parts() would discard those and
1806        // drop exactly-matched interior chars from the reconstruction.
1807        add_match_part(
1808            None,
1809            None,
1810            0,
1811            "",
1812            0,
1813            None,
1814            0,
1815            after_pfx,
1816            (wl - rpl).max(0),
1817            mpl - rpl,
1818            0,
1819        ); // c:1237
1820        let pli = MATCHPARTS
1821            .get_or_init(|| Mutex::new(None))
1822            .lock()
1823            .ok()
1824            .and_then(|mut g| g.take()); // c:1239 pli = matchparts
1825        if let Some(out) = clp {
1826            *out = pli;
1827        }
1828
1829        // c:1251 — exact = !strcmp(pfx, w).
1830        *exact = if pfx == w_quoted.as_str() { 1 } else { 0 };
1831    }
1832
1833    // c:1241 — r = dupstring(matchbuf ? matchbuf : "").
1834    r = MATCHBUF
1835        .get()
1836        .and_then(|m| m.lock().ok().map(|g| g.clone()))
1837        .unwrap_or_default();
1838    let r = if r.is_empty() { w_quoted } else { r };
1839    Some(r)
1840}
1841
1842/// Port of `pattern_match1(Cpattern p, convchar_t c, int *mtp)` from Src/Zle/compmatch.c:1269.
1843/// Direct port of `mod_export convchar_t pattern_match1(Cpattern p,
1844///                                    convchar_t c, int *mtp)`
1845/// from `Src/Zle/compmatch.c:1269`. Tests whether `p` matches
1846/// the single char `c`, returning the matched-char (1 for ANY, the
1847/// char for CHAR, or for EQUIV the equivalence-class index+1) or 0
1848/// on miss. `mtp` is non-zero only for the EQUIV path.
1849/// WARNING: param names don't match C — Rust=(p, mtp) vs C=(p, c, mtp)
1850pub fn pattern_match1(
1851    p: &Cpattern, // c:1269
1852    c: u32,
1853    mtp: &mut i32,
1854) -> u32 {
1855    *mtp = 0; // c:1273
1856    match p.tp {
1857        // c:1274
1858        x if x == CPAT_CCLASS => {
1859            // c:1275
1860            // PATMATCHRANGE(p->u.str, c, NULL, NULL)
1861            patmatchrange(p.str.as_deref(), c, None, None) as u32 // c:1276
1862        }
1863        x if x == CPAT_NCLASS => {
1864            // c:1278
1865            if patmatchrange(p.str.as_deref(), c, None, None) {
1866                0
1867            } else {
1868                1
1869            } // c:1279
1870        }
1871        x if x == CPAT_EQUIV => {
1872            // c:1281
1873            let mut ind: u32 = 0;
1874            if patmatchrange(p.str.as_deref(), c, Some(&mut ind), Some(mtp)) {
1875                ind + 1 // c:1283
1876            } else {
1877                0 // c:1285
1878            }
1879        }
1880        x if x == CPAT_ANY => 1, // c:1288-1289
1881        x if x == CPAT_CHAR => {
1882            if p.chr == c {
1883                c
1884            } else {
1885                0
1886            }
1887        } // c:1291-1292
1888        _ => 0,                  // c:1294
1889    }
1890}
1891
1892/// Direct port of `mod_export convchar_t pattern_match_equivalence(
1893///                    Cpattern lp, convchar_t wind, int wmtp,
1894///                    convchar_t wchr)`
1895/// from `Src/Zle/compmatch.c:1316`. Looks up the line-side
1896/// equivalence-class member that pairs with word-side index
1897/// `wind` (1-based), then resolves case-class crossings via the
1898/// PP_UPPER/PP_LOWER pair.
1899///
1900/// Returns `CHR_INVALID` (u32::MAX) on miss; the matched line
1901/// char on success.
1902pub fn pattern_match_equivalence(
1903    lp: &Cpattern, // c:1316
1904    wind: u32,
1905    wmtp: i32,
1906    wchr: u32,
1907) -> u32 {
1908    // c:1324 — PATMATCHINDEX(lp->u.str, wind-1, &lchr, &lmtp).
1909    // Walk lp.str's encoded byte sequence finding the entry at index
1910    // (wind-1). Encoding (from parse_class):
1911    //   0x80 + PP_RANGE (=0x95): next two bytes are lo,hi range
1912    //   0x80 + PP_* (POSIX class id): single-byte class marker
1913    //   plain byte: literal character
1914    let Some(ref bytes) = lp.str else {
1915        return u32::MAX;
1916    };
1917    let Some(target_idx) = (wind as i64).checked_sub(1) else {
1918        return u32::MAX;
1919    };
1920    if target_idx < 0 {
1921        return u32::MAX;
1922    }
1923    let mut lchr: Option<u32> = None;
1924    let mut lmtp: i32 = 0;
1925    let mut idx: i64 = 0;
1926    let mut i = 0usize;
1927    let pp_range_marker = (0x80u8).wrapping_add(PP_RANGE as u8);
1928    while i < bytes.len() {
1929        let b = bytes[i];
1930        if b == pp_range_marker {
1931            // c:4049 PP_RANGE
1932            // Next two bytes are range start / end.
1933            if i + 2 >= bytes.len() {
1934                break;
1935            }
1936            let r1 = bytes[i + 1];
1937            let r2 = bytes[i + 2];
1938            let span = (r2 as i64) - (r1 as i64);
1939            if span >= 0 && idx + span >= target_idx {
1940                // c:4057
1941                lchr = Some(((r1 as i64) + (target_idx - idx)) as u32);
1942                break;
1943            }
1944            idx += span + 1; // c:4062
1945            i += 3;
1946        } else if b >= 0x80 {
1947            // c:4024-4047 — POSIX class marker (PP_ALPHA/LOWER/UPPER/etc.).
1948            let swtype = (b as i32) - 0x80;
1949            if idx == target_idx {
1950                // c:4043
1951                lmtp = swtype;
1952                break;
1953            }
1954            idx += 1;
1955            i += 1;
1956        } else {
1957            // c:4071-4076 — literal char.
1958            if idx == target_idx {
1959                lchr = Some(b as u32);
1960                break;
1961            }
1962            idx += 1;
1963            i += 1;
1964        }
1965    }
1966
1967    // c:1335 — `if (lchr != CHR_INVALID) return lchr` — exact-char hit.
1968    if let Some(ch) = lchr {
1969        if ch != u32::MAX {
1970            return ch;
1971        }
1972    }
1973
1974    // c:1342 — case-class crossings using the now-tracked lmtp.
1975    let wch = char::from_u32(wchr).unwrap_or('\0');
1976    if wmtp == PP_UPPER && lmtp == PP_LOWER {
1977        return ZC_tolower(wch) as u32;
1978    }
1979    if wmtp == PP_LOWER && lmtp == PP_UPPER {
1980        return ZC_toupper(wch) as u32;
1981    }
1982    if wmtp != 0 && wmtp == lmtp {
1983        return wchr;
1984    }
1985    u32::MAX // c:1378
1986}
1987
1988/// Direct port of `static int pattern_match_restrict(Cpattern p,
1989///                                Cpattern wp, convchar_t *wsc,
1990///                                int wsclen, Cpattern prestrict,
1991///                                ZLE_STRING_T new_line)`
1992/// from `Src/Zle/compmatch.c:1383`. The restricted variant of
1993/// `pattern_match`: each line-side char must additionally match
1994/// the corresponding `prestrict` Cpattern. Used when building the
1995/// line-string from a partial match. Writes the deduced line chars
1996/// into `new_line` and returns 1 on full match, 0 otherwise.
1997pub fn pattern_match_restrict(
1998    p: Option<&Cpattern>, // c:1383
1999    wp: Option<&Cpattern>,
2000    wsc: &[u32],
2001    prestrict: Option<&Cpattern>,
2002    new_line: &mut Vec<char>,
2003) -> i32 {
2004    let mut p_cur = p;
2005    let mut wp_cur = wp;
2006    let mut pr_cur = prestrict;
2007    let mut wsc_idx = 0usize;
2008
2009    while p_cur.is_some() && wp_cur.is_some()                                // c:1392
2010        && wsc_idx < wsc.len() && pr_cur.is_some()
2011    {
2012        let pat = p_cur.unwrap();
2013        let wpat = wp_cur.unwrap();
2014        let pre = pr_cur.unwrap();
2015        let wc = wsc[wsc_idx];
2016
2017        let mut wmt: i32 = 0;
2018        let wind = pattern_match1(wpat, wc, &mut wmt); // c:1394
2019        if wind == 0 {
2020            return 0;
2021        } // c:1395
2022
2023        // c:1399-1450 — deduce the line character `c`.
2024        let c: u32 = if pre.tp == CPAT_CHAR {
2025            // c:1402
2026            pre.chr // c:1407
2027        } else if pat.tp == CPAT_CHAR {
2028            // c:1410
2029            pat.chr // c:1414
2030        } else if pat.tp == CPAT_EQUIV {
2031            // c:1416
2032            // c:1424 — pattern_match_equivalence resolves the line-side
2033            // equivalence-class member paired with the word's wind/wmt.
2034            let r = pattern_match_equivalence(pat, wind, wmt, wc);
2035            if r == u32::MAX {
2036                return 0;
2037            } // c:1426 CHR_INVALID
2038            r
2039        } else {
2040            // c:1432
2041            wc // c:1442 use *wsc
2042        };
2043
2044        // c:1448 — restriction-side check.
2045        if pre.tp != CPAT_CHAR {
2046            let mut mt: i32 = 0;
2047            if pattern_match1(pre, c, &mut mt) == 0 {
2048                return 0;
2049            } // c:1449
2050        }
2051
2052        // c:1457-1485 — case-class equivalence (mt vs wmt mismatch).
2053        if pat.tp != CPAT_ANY || wpat.tp != CPAT_ANY {
2054            // c:1459
2055            let mut mt: i32 = 0;
2056            let ind = pattern_match1(pat, c, &mut mt); // c:1461
2057            if ind == 0 || ind != wind {
2058                return 0;
2059            } // c:1462-1465
2060            if mt != wmt {
2061                let case_pair =
2062                    (mt == PP_LOWER || mt == PP_UPPER) && (wmt == PP_LOWER || wmt == PP_UPPER);
2063                if case_pair {
2064                    let cc = char::from_u32(c).unwrap_or('\0');
2065                    let wcc = char::from_u32(wc).unwrap_or('\0');
2066                    if ZC_tolower(cc) != ZC_tolower(wcc) {
2067                        return 0;
2068                    } // c:1477
2069                } else {
2070                    return 0; // c:1481
2071                }
2072            }
2073        }
2074
2075        // c:1496 — append deduced char to new_line.
2076        if let Some(ch) = char::from_u32(c) {
2077            new_line.push(ch);
2078        }
2079        pr_cur = pre.next.as_deref(); // c:1498
2080        wsc_idx += 1;
2081        p_cur = pat.next.as_deref();
2082        wp_cur = wpat.next.as_deref();
2083    }
2084
2085    // c:1505-1540 — tail loop: continue matching when wsc exhausted
2086    // but prestrict still has more chars (deduced solely from p).
2087    while p_cur.is_some() && pr_cur.is_some() {
2088        // c:1505
2089        let pat = p_cur.unwrap();
2090        let pre = pr_cur.unwrap();
2091        let c: u32 = if pre.tp == CPAT_CHAR {
2092            pre.chr
2093        } else if pat.tp == CPAT_CHAR {
2094            pat.chr
2095        } else {
2096            return 0; // c:1522 not enough info
2097        };
2098        let mut mt: i32 = 0;
2099        if pre.tp != CPAT_CHAR && pattern_match1(pre, c, &mut mt) == 0 {
2100            return 0;
2101        }
2102        if let Some(ch) = char::from_u32(c) {
2103            new_line.push(ch);
2104        }
2105        pr_cur = pre.next.as_deref();
2106        p_cur = pat.next.as_deref();
2107    }
2108
2109    // c:1542 — `p_cur.is_none() && pr_cur.is_none() && (wp_cur.is_none() || wsc empty)`.
2110    if p_cur.is_none() && pr_cur.is_none() && (wp_cur.is_none() || wsc_idx >= wsc.len()) {
2111        1 // c:1544 full match
2112    } else {
2113        0 // c:1545
2114    }
2115}
2116
2117/// Port of `pattern_match(Cpattern p, char *s, Cpattern wp, char *ws)` from Src/Zle/compmatch.c:1548.
2118/// Direct port of `mod_export int pattern_match(Cpattern p, char *s,
2119///                                             Cpattern wp, char *ws)`
2120/// from `Src/Zle/compmatch.c:1548`. Walks two parallel pattern +
2121/// string pairs (line `p`/`s` vs word `wp`/`ws`) verifying that each
2122/// position matches and that paired pattern-class indices line up.
2123/// WARNING: param names don't match C — Rust=(p, wp, ws) vs C=(p, s, wp, ws)
2124pub fn pattern_match(
2125    p: Option<&Cpattern>, // c:1548
2126    s: &str,
2127    wp: Option<&Cpattern>,
2128    ws: &str,
2129) -> i32 {
2130    let (mut p_cur, mut wp_cur) = (p, wp); // c:1551 walking p / wp
2131    let mut s_bytes = s.chars().peekable();
2132    let mut ws_bytes = ws.chars().peekable();
2133
2134    while p_cur.is_some() && wp_cur.is_some()                                // c:1553
2135        && s_bytes.peek().is_some() && ws_bytes.peek().is_some()
2136    {
2137        let pat = p_cur.unwrap();
2138        let wpat = wp_cur.unwrap();
2139        let wc = ws_bytes.next().unwrap() as u32; // c:1555
2140        let mut wmt: i32 = 0;
2141        let wind = pattern_match1(wpat, wc, &mut wmt); // c:1556
2142        if wind == 0 {
2143            return 0;
2144        } // c:1557
2145
2146        let c = s_bytes.next().unwrap() as u32; // c:1561
2147        if pat.tp != CPAT_ANY || wpat.tp != CPAT_ANY {
2148            // c:1567
2149            let mut mt: i32 = 0;
2150            let ind = pattern_match1(pat, c, &mut mt); // c:1569
2151            if ind == 0 {
2152                return 0;
2153            } // c:1570
2154            if ind != wind {
2155                return 0;
2156            } // c:1572
2157            if mt != wmt {
2158                // c:1574
2159                let case_pair =
2160                    (mt == PP_LOWER || mt == PP_UPPER) && (wmt == PP_LOWER || wmt == PP_UPPER);
2161                if case_pair {
2162                    let cc = char::from_u32(c).unwrap_or('\0');
2163                    let wcc = char::from_u32(wc).unwrap_or('\0');
2164                    if ZC_tolower(cc) != ZC_tolower(wcc) {
2165                        // c:1584
2166                        return 0;
2167                    }
2168                } else {
2169                    return 0; // c:1588
2170                }
2171            }
2172        }
2173        p_cur = pat.next.as_deref(); // c:1599
2174        wp_cur = wpat.next.as_deref();
2175    }
2176    // c:1601-1607 — consume remaining LINE pattern chars (stop when EITHER
2177    // the pattern OR the string is exhausted).
2178    while let (Some(pat), Some(&c_ch)) = (p_cur, s_bytes.peek()) {
2179        let c = c_ch as u32;
2180        let mut mt: i32 = 0;
2181        if pattern_match1(pat, c, &mut mt) == 0 {
2182            return 0; // c:1604
2183        }
2184        s_bytes.next();
2185        p_cur = pat.next.as_deref(); // c:1605
2186    }
2187    // c:1609-1615 — remaining WORD pattern chars, symmetrically.
2188    while let (Some(wpat), Some(&wc_ch)) = (wp_cur, ws_bytes.peek()) {
2189        let wc = wc_ch as u32;
2190        let mut wmt: i32 = 0;
2191        if pattern_match1(wpat, wc, &mut wmt) == 0 {
2192            return 0; // c:1611
2193        }
2194        ws_bytes.next();
2195        wp_cur = wpat.next.as_deref(); // c:1613
2196    }
2197    // c:1617 — the port previously required BOTH strings fully consumed;
2198    // a fixed-length matcher against a LONGER word failed on the remainder.
2199    // C stops at pattern exhaustion; caller handles the rest of the word.
2200    1
2201}
2202
2203/// Port of `bld_parts(char *str, int len, int plen, Cline *lp, Cline *lprem)` from Src/Zle/compmatch.c:1638.
2204/// Direct port of `static Cline bld_parts(char *str, int len, int plen,
2205///                                        Cline *lp, Cline *lprem)`
2206/// from `Src/Zle/compmatch.c:1638`. Splits the candidate string
2207/// `str[..len]` into a Cline chain anchored by every CMF_RIGHT
2208/// matcher in `bmatchers`. `plen` is the active prefix length;
2209/// trailing remainder (after the last anchor) goes into `*lprem`,
2210/// last node into `*lp`.
2211/// WARNING: param names don't match C — Rust=(str, len, plen, lprem) vs C=(str, len, plen, lp, lprem)
2212pub fn bld_parts(
2213    str: &str,
2214    len: i32,
2215    mut plen: i32, // c:1638
2216    lp: Option<&mut Option<Box<Cline>>>,
2217    lprem: Option<&mut Option<Box<Cline>>>,
2218) -> Option<Box<Cline>> {
2219    let bytes = str.as_bytes();
2220    let total: usize = (len as usize).min(bytes.len());
2221    let mut op = plen;
2222    let mut p_start = 0usize;
2223    let mut str_pos = 0usize;
2224    let mut remaining = total as i32;
2225
2226    let mut head: Option<Box<Cline>> = None;
2227    let mut tail_ref: *mut Option<Box<Cline>> = &mut head;
2228    let mut last_n: Option<Box<Cline>> = None;
2229
2230    while remaining > 0 {
2231        // c:1647
2232        // c:1648-1685 — walk bmatchers looking for a CMF_RIGHT-anchored
2233        // wlen<0 matcher whose right anchor matches at the current
2234        // position. On hit, emit a Cline for the run-so-far + the
2235        // anchored portion, advance str/plen past the anchor.
2236        let mut found_anchor = false;
2237        let bmatchers_chain = crate::ported::zle::compcore::bmatchers
2238            .get_or_init(|| Mutex::new(None))
2239            .lock()
2240            .ok()
2241            .and_then(|g| g.clone());
2242        let mut cur = bmatchers_chain.as_deref();
2243        while let Some(ms) = cur {
2244            let mp = &*ms.matcher;
2245            let preds_ok = mp.flags == CMF_RIGHT
2246                && mp.wlen < 0
2247                && mp.ralen > 0
2248                && mp.llen == 0
2249                && remaining >= mp.ralen
2250                && (str_pos as i32 - p_start as i32) >= mp.lalen;
2251            if !preds_ok {
2252                cur = ms.next.as_deref();
2253                continue;
2254            }
2255            let str_at = std::str::from_utf8(&bytes[str_pos..]).unwrap_or("");
2256            if pattern_match(mp.right.as_deref(), str_at, None, "") == 0 {
2257                cur = ms.next.as_deref();
2258                continue;
2259            }
2260            let l_anchor_ok = mp.lalen == 0 || {
2261                let off = str_pos as i32 - mp.lalen;
2262                if off < 0 {
2263                    false
2264                } else {
2265                    let l_slice = std::str::from_utf8(&bytes[off as usize..]).unwrap_or("");
2266                    pattern_match(mp.left.as_deref(), l_slice, None, "") != 0
2267                }
2268            };
2269            if !l_anchor_ok {
2270                cur = ms.next.as_deref();
2271                continue;
2272            }
2273
2274            // c:1655-1672 — emit anchor cline; optional prefix run.
2275            let olen = (str_pos - p_start) as i32;
2276            let flags = if plen <= 0 { CLF_NEW } else { 0 };
2277            let anchor_word: String =
2278                std::str::from_utf8(&bytes[str_pos..str_pos + mp.ralen as usize])
2279                    .unwrap_or("")
2280                    .into();
2281            let mut node = Box::new(Cline {
2282                llen: mp.ralen,
2283                word: Some(anchor_word.clone()),
2284                wlen: mp.ralen,
2285                flags,
2286                ..Default::default()
2287            });
2288            if p_start != str_pos {
2289                let mut llen = if op < 0 { 0 } else { op };
2290                if llen > olen {
2291                    llen = olen;
2292                }
2293                let prefix_word: String =
2294                    std::str::from_utf8(&bytes[p_start..p_start + olen as usize])
2295                        .unwrap_or("")
2296                        .into();
2297                node.prefix = Some(Box::new(Cline {
2298                    llen,
2299                    word: Some(prefix_word),
2300                    wlen: olen,
2301                    ..Default::default()
2302                }));
2303            }
2304            unsafe {
2305                *tail_ref = Some(node);
2306                tail_ref = &mut (*tail_ref).as_mut().unwrap().next;
2307            }
2308            // c:1674-1677 — advance past the anchor.
2309            str_pos += mp.ralen as usize;
2310            remaining -= mp.ralen;
2311            plen -= mp.ralen;
2312            op -= olen;
2313            p_start = str_pos;
2314            found_anchor = true;
2315            break;
2316        }
2317        if !found_anchor {
2318            // c:1683 — no anchor: str++; len--; plen--.
2319            str_pos += 1;
2320            remaining -= 1;
2321            plen -= 1;
2322        }
2323    }
2324
2325    // c:1701-1717 — emit a Cline for the trailing portion.
2326    if p_start != str_pos {
2327        // c:1701
2328        let olen = (str_pos - p_start) as i32;
2329        let mut llen = if op < 0 { 0 } else { op };
2330        if llen > olen {
2331            llen = olen;
2332        }
2333        let flags = if plen <= 0 { CLF_NEW } else { 0 };
2334        let mut node = Box::new(Cline {
2335            flags,
2336            ..Default::default()
2337        });
2338        let prefix_word: String = std::str::from_utf8(&bytes[p_start..p_start + olen as usize])
2339            .unwrap_or("")
2340            .into();
2341        node.prefix = Some(Box::new(Cline {
2342            llen,
2343            word: Some(prefix_word.clone()),
2344            wlen: olen,
2345            ..Default::default()
2346        }));
2347        if let Some(out) = lprem {
2348            *out = Some(node.clone());
2349        } // c:1714
2350        last_n = Some(node.clone());
2351        unsafe {
2352            *tail_ref = Some(node);
2353        }
2354    } else if head.is_none() {
2355        // c:1716
2356        let flags = if plen <= 0 { CLF_NEW } else { 0 };
2357        let node = Box::new(Cline {
2358            flags,
2359            ..Default::default()
2360        });
2361        if let Some(out) = lprem {
2362            *out = Some(node.clone());
2363        } // c:1721
2364        last_n = Some(node.clone());
2365        head = Some(node);
2366    } else if let Some(out) = lprem {
2367        // c:1722
2368        *out = None;
2369    }
2370
2371    if let (Some(out_lp), Some(n)) = (lp, last_n) {
2372        // c:1731
2373        *out_lp = Some(n);
2374    }
2375
2376    let _ = p_start;
2377    let _ = op;
2378    head // c:1733 return ret
2379}
2380
2381/// Direct port of `static int bld_line(Cmatcher mp, ZLE_STRING_T line,
2382///                                     char *mword, char *word,
2383///                                     int wlen, int sfx)`
2384/// from `Src/Zle/compmatch.c:1734-1992`. Builds all possible line
2385/// patterns for `mp` and tests whether they match `word`, returning
2386/// the number of word chars matched (0 on failure). On success the
2387/// synthesized line chars are written into `line`.
2388///
2389/// Faithful two-pass implementation (matching the C):
2390///
2391/// Pass 1 (c:1772-1846) — build `genpatarr`, a per-line-char array of
2392/// `Cpattern`. For each `mp->line` entry: if it is a `CPAT_EQUIV` and
2393/// `mp->word` + `mword` are still available, consume one `mword` char,
2394/// query the word pattern via `pattern_match1`, and if that yields a
2395/// word-side equivalence index resolve the concrete line char via
2396/// `pattern_match_equivalence` (which tracks the PP_LOWER/PP_UPPER
2397/// case crossings, compmatch.rs:1825), storing it as a `CPAT_CHAR`.
2398/// Otherwise the line pattern is copied verbatim (char / class / any).
2399/// A `CHR_INVALID` equivalence resolution aborts with 0.
2400///
2401/// Pass 2 (c:1847-1988) — walk `genpatarr` against `wordchars`. Where
2402/// `pattern_match1` accepts the word char directly, emit the fixed
2403/// `CPAT_CHAR` value (or, for a generic pattern, the word char). Where
2404/// it does not, fall back to the `bmatchers` chain and let
2405/// `pattern_match_restrict` deduce the line chars (the "nightmare"
2406/// multi-matcher case). `sfx` builds both strings from the end.
2407pub fn bld_line(
2408    mp: &Cmatcher, // c:1734
2409    line: &mut Vec<char>,
2410    mword: &str,
2411    word: &str,
2412    wlen: i32,
2413    sfx: i32,
2414) -> i32 {
2415    let sfx = sfx != 0;
2416
2417    // c:1745-1762 — convert `word` to an array of (wide) chars so we
2418    // can index it from either end.
2419    let wordchars: Vec<u32> = word.chars().map(|c| c as u32).collect();
2420    let wlen0 = wlen.max(0) as usize;
2421
2422    // Links a slice of genpatarr entries into a throwaway Cpattern
2423    // chain, so `pattern_match_restrict` can walk it via `->next`
2424    // (c:1841-1845 links curgenpat->next = curgenpat+1).
2425    fn link_chain(entries: &[Cpattern]) -> Option<Box<Cpattern>> {
2426        let mut head: Option<Box<Cpattern>> = None;
2427        for e in entries.iter().rev() {
2428            let mut node = e.clone();
2429            node.next = head.take();
2430            head = Some(Box::new(node));
2431        }
2432        head
2433    }
2434
2435    // --- Pass 1: build genpatarr (c:1772-1846). ---
2436    let mword_chars: Vec<u32> = mword.chars().map(|c| c as u32).collect();
2437    let mut mword_idx = 0usize;
2438    let mut wpat = mp.word.as_deref();
2439    let mut lpat = mp.line.as_deref();
2440    let mut genpatarr: Vec<Cpattern> = Vec::with_capacity(mp.llen.max(0) as usize);
2441    while let Some(lp) = lpat {
2442        // c:1780-1799 — resolve the word side of an equivalence.
2443        let mut wind: u32 = 0;
2444        let mut wmtp: i32 = 0;
2445        let mut wchr: u32 = 0;
2446        if lp.tp == CPAT_EQUIV && wpat.is_some() && mword_idx < mword_chars.len() {
2447            wchr = mword_chars[mword_idx];
2448            mword_idx += 1;
2449            let wp = wpat.unwrap();
2450            wind = pattern_match1(wp, wchr, &mut wmtp); // c:1794
2451            wpat = wp.next.as_deref(); // c:1795
2452        }
2453
2454        let mut gp = Cpattern::default();
2455        if wind != 0 {
2456            // c:1800-1822 — successful word-side equivalence; find the
2457            // line equivalent and pin it as a concrete char.
2458            let lchr = pattern_match_equivalence(lp, wind, wmtp, wchr); // c:1817
2459            if lchr == u32::MAX {
2460                return 0; // c:1820 — no equivalent, give up
2461            }
2462            gp.tp = CPAT_CHAR; // c:1826
2463            gp.chr = lchr; // c:1827
2464        } else {
2465            // c:1828-1846 — copy the line pattern verbatim.
2466            gp.tp = lp.tp; // c:1834
2467            if lp.tp == CPAT_CHAR {
2468                gp.chr = lp.chr; // c:1836
2469            } else if lp.tp != CPAT_ANY {
2470                gp.str = lp.str.clone(); // c:1843 (shared/copied class)
2471            }
2472        }
2473        genpatarr.push(gp);
2474        lpat = lp.next.as_deref();
2475    }
2476
2477    // --- Pass 2: match wordchars against genpatarr (c:1847-1988). ---
2478    let llen0 = mp.llen.max(0) as usize;
2479    let mut line_buf: Vec<char> = vec!['\0'; llen0];
2480    let mut llen_rem: i32 = mp.llen; // c:1855
2481    let mut wlen_rem: i32 = wlen;
2482    let mut rl: i32 = 0; // c:1856
2483
2484    // Cursors into line_buf / wordchars / genpatarr (c:1858-1874).
2485    let mut line_pos: usize = if sfx { llen0 } else { 0 };
2486    let mut word_pos: usize = if sfx { wlen0 } else { 0 };
2487    let mut gp_pos: usize = if sfx { llen0 } else { 0 };
2488
2489    // Snapshot the global bmatchers chain for the fallback loop.
2490    let bm = crate::ported::zle::compcore::bmatchers
2491        .get_or_init(|| Mutex::new(None))
2492        .lock()
2493        .ok()
2494        .and_then(|g| g.clone());
2495
2496    while llen_rem > 0 && wlen_rem > 0 {
2497        // c:1877-1885 — pick the char/pattern under inspection.
2498        let (wp_idx, gp_idx) = if sfx {
2499            (word_pos - 1, gp_pos - 1)
2500        } else {
2501            (word_pos, gp_pos)
2502        };
2503        // `.get`-guarded: callers may pass a byte length as `wlen`
2504        // that exceeds the char count on multibyte input.
2505        let wc = *wordchars.get(wp_idx).unwrap_or(&0);
2506
2507        let mut wmtp: i32 = 0;
2508        if gp_idx < genpatarr.len() && pattern_match1(&genpatarr[gp_idx], wc, &mut wmtp) != 0 {
2509            // c:1890-1920 — direct match. Keep the fixed char for a
2510            // CPAT_CHAR genpat, else keep the word char.
2511            let lchr = if genpatarr[gp_idx].tp == CPAT_CHAR {
2512                genpatarr[gp_idx].chr
2513            } else {
2514                wc
2515            };
2516            let lch = char::from_u32(lchr).unwrap_or('\0');
2517            if sfx {
2518                line_pos -= 1; // c:1908 *--line = lchr
2519                line_buf[line_pos] = lch;
2520            } else {
2521                line_buf[line_pos] = lch; // c:1910 *line++ = lchr
2522                line_pos += 1;
2523            }
2524            llen_rem -= 1; // c:1912
2525            wlen_rem -= 1; // c:1913
2526            rl += 1; // c:1914
2527
2528            if sfx {
2529                word_pos = wp_idx; // c:1917
2530                gp_pos = gp_idx; // c:1918
2531            } else {
2532                if llen_rem > 0 {
2533                    gp_pos += 1; // c:1920
2534                }
2535                word_pos += 1; // c:1921
2536            }
2537        } else {
2538            // c:1925-1978 — nightmare case: dispatch to the pattern
2539            // matchers in bmatchers via pattern_match_restrict.
2540            let mut matched = false;
2541            let mut ms = bm.as_deref();
2542            while let Some(node) = ms {
2543                let bmp = &*node.matcher; // c:1932 mp = ms->matcher
2544                if bmp.flags == 0
2545                    && bmp.wlen <= wlen_rem
2546                    && bmp.llen <= llen_rem
2547                    && bmp.wlen >= 0
2548                    && bmp.llen >= 0
2549                {
2550                    // c:1943-1949 — position the sub-window.
2551                    let (lp_idx, wp2_idx, gp2_idx) = if sfx {
2552                        (
2553                            line_pos - bmp.llen as usize,
2554                            word_pos - bmp.wlen as usize,
2555                            gp_pos - bmp.llen as usize,
2556                        )
2557                    } else {
2558                        (line_pos, word_pos, gp_pos)
2559                    };
2560
2561                    // c:1951 — wsclen = wlen - (wp - wordchars).
2562                    let wsclen = wlen_rem - wp2_idx as i32;
2563                    let start = wp2_idx;
2564                    let end = if wsclen <= 0 {
2565                        start
2566                    } else {
2567                        (start + wsclen as usize).min(wordchars.len())
2568                    };
2569                    let wsc = &wordchars[start.min(end)..end];
2570
2571                    let pr_end = (gp2_idx + bmp.llen as usize).min(genpatarr.len());
2572                    let prestrict = link_chain(&genpatarr[gp2_idx.min(pr_end)..pr_end]);
2573
2574                    let mut tmp_line: Vec<char> = Vec::new();
2575                    if pattern_match_restrict(
2576                        bmp.line.as_deref(),
2577                        bmp.word.as_deref(),
2578                        wsc,
2579                        prestrict.as_deref(),
2580                        &mut tmp_line,
2581                    ) != 0
2582                    {
2583                        // c:1958-1978 — matched: copy deduced line chars
2584                        // into place and advance all cursors.
2585                        for (k, ch) in tmp_line.iter().enumerate() {
2586                            if lp_idx + k < line_buf.len() {
2587                                line_buf[lp_idx + k] = *ch;
2588                            }
2589                        }
2590                        if sfx {
2591                            line_pos = lp_idx; // c:1965
2592                            word_pos = wp2_idx; // c:1966
2593                            gp_pos = gp2_idx; // c:1967
2594                        } else {
2595                            line_pos += bmp.llen as usize; // c:1969
2596                            word_pos += bmp.wlen as usize; // c:1970
2597                            gp_pos += bmp.llen as usize; // c:1971
2598                        }
2599                        llen_rem -= bmp.llen; // c:1973
2600                        wlen_rem -= bmp.wlen; // c:1974
2601                        rl += bmp.wlen; // c:1975
2602                        matched = true;
2603                        break;
2604                    }
2605                }
2606                ms = node.next.as_deref();
2607            }
2608            if !matched {
2609                return 0; // c:1983 — didn't match, give up
2610            }
2611        }
2612    }
2613
2614    if llen_rem == 0 {
2615        // c:1986 — whole line built; commit and return matched length.
2616        line.extend(line_buf.iter().copied());
2617        return rl; // c:1987
2618    }
2619    0 // c:1990
2620}
2621
2622/// Port of `static char *join_strs(int la, char *sa, int lb, char *sb)`
2623/// from Src/Zle/compmatch.c:1994.
2624///
2625/// "Joins two strings via the matcher equivalence map; returns the
2626/// merged string or NULL if they can't be merged." The full body
2627/// walks the global `bmatchers` Cmlist for each character of `sa`
2628/// vs `sb`, applying matcher patterns to find a unifying byte.
2629///
2630/// Blocked on: `bmatchers` global Cmlist, `pattern_match1`, the
2631/// `cmatcher`-driven equivalence map, `matchbuf`/`matchbuflen`
2632/// growable buffer, `start_match`/`end_match` framing. Returns
2633/// `None` until `pattern_match1` lands.
2634///                                         char *sb)` from
2635/// `Src/Zle/compmatch.c:1994`. Tries to construct a common
2636/// string for `sa[..la]` and `sb[..lb]` by either taking equal
2637/// chars verbatim or using a no-anchor matcher's bld_line synthesis.
2638/// Returns the merged string on success, None when no match advances
2639/// either input.
2640pub fn join_strs(mut la: i32, sa: &str, mut lb: i32, sb: &str) -> Option<String> {
2641    let mut out = String::new();
2642    let mut a_idx = 0usize;
2643    let mut b_idx = 0usize;
2644    let a_bytes = sa.as_bytes();
2645    let b_bytes = sb.as_bytes();
2646
2647    while la > 0 && lb > 0 && a_idx < a_bytes.len() && b_idx < b_bytes.len() {
2648        if a_bytes[a_idx] == b_bytes[b_idx] {
2649            // c:2085 equal-char path
2650            // c:2092 — append + advance both.
2651            out.push(a_bytes[a_idx] as char);
2652            a_idx += 1;
2653            b_idx += 1;
2654            la -= 1;
2655            lb -= 1;
2656        } else {
2657            // c:2013 — matcher-driven branch. Walks bmatchers looking
2658            // for a no-anchor matcher that pattern_matches one of the
2659            // input strings; on hit calls bld_line to synthesize a
2660            // line that matches the OTHER string, copies the result
2661            // into `out`, and advances both inputs.
2662            let bmatchers = crate::ported::zle::compcore::bmatchers
2663                .get_or_init(|| Mutex::new(None))
2664                .lock()
2665                .ok()
2666                .and_then(|g| g.clone());
2667            let mut advanced = false;
2668            let mut cur = bmatchers.as_deref();
2669            while let Some(ms) = cur {
2670                // c:2018
2671                let mp = &*ms.matcher;
2672                let ok =
2673                    mp.flags == 0 && mp.wlen > 0 && mp.llen > 0 && mp.wlen <= la && mp.wlen <= lb;
2674                if ok {
2675                    // c:2025-2027 — try the word pattern against either side.
2676                    let mp_word = mp.word.as_deref();
2677                    let a_slice = &sa[a_idx..];
2678                    let b_slice = &sb[b_idx..];
2679                    let t = if pattern_match(mp_word, a_slice, None, "") != 0 {
2680                        1
2681                    } else if pattern_match(mp_word, b_slice, None, "") != 0 {
2682                        2
2683                    } else {
2684                        0
2685                    };
2686                    if t != 0 {
2687                        // c:2057-2087 — bld_line writes the synthesized
2688                        // line into a local buffer + returns the
2689                        // count consumed from the other string.
2690                        let mut line: Vec<char> = Vec::new();
2691                        let bl = bld_line(
2692                            mp,
2693                            &mut line,
2694                            "", // mword empty here; bld_line runs its equivalence pass as a no-op (c:2103 passes "")
2695                            if t == 1 { b_slice } else { a_slice },
2696                            if t == 1 { lb } else { la },
2697                            0,
2698                        );
2699                        if bl > 0 {
2700                            // c:2068
2701                            for ch in &line {
2702                                out.push(*ch);
2703                            }
2704                            // Advance per t-direction:
2705                            if t == 1 {
2706                                a_idx += mp.wlen as usize;
2707                                la -= mp.wlen;
2708                                b_idx += bl as usize;
2709                                lb -= bl;
2710                            } else {
2711                                b_idx += mp.wlen as usize;
2712                                lb -= mp.wlen;
2713                                a_idx += bl as usize;
2714                                la -= bl;
2715                            }
2716                            advanced = true;
2717                            break;
2718                        }
2719                    }
2720                }
2721                cur = ms.next.as_deref();
2722            }
2723            if !advanced {
2724                break;
2725            }
2726        }
2727    }
2728
2729    if !out.is_empty() {
2730        Some(out)
2731    } else {
2732        None
2733    } // c:2100-2104
2734}
2735
2736// (cline_setlens / cline_sublen wrong-sig duplicates removed —
2737// real C-faithful ports are above keyed off comp_h::Cline.)
2738
2739/// Port of `static int cmp_anchors(Cline o, Cline n, int join)` from
2740/// Src/Zle/compmatch.c:2107.
2741///
2742/// Compares two Cline anchors. Returns:
2743///   - `1` if exact word/line match (and may set `CLF_LINE` on `o`)
2744///   - `2` if `join` is set and `join_strs` produced a merged anchor
2745///     (sets `CLF_JOIN` and rewrites `o->word`/`wlen`)
2746///   - `0` otherwise.
2747pub fn cmp_anchors(
2748    o: &mut Cline, // c:2107
2749    n: &Cline,
2750    join: i32,
2751) -> i32 {
2752    // Inline `!strncmp(a, b, n)` predicate from C.
2753    let strncmp_eq = |a: &Option<String>, b: &Option<String>, n: usize| -> bool {
2754        match (a, b) {
2755            (Some(x), Some(y)) => {
2756                let xb = x.as_bytes();
2757                let yb = y.as_bytes();
2758                xb.len() >= n && yb.len() >= n && xb[..n] == yb[..n]
2759            }
2760            _ => false,
2761        }
2762    };
2763    // c:2113 — try exact word/line match.
2764    let word_match = (o.flags & CLF_LINE) == 0
2765        && o.wlen == n.wlen
2766        && (o.word.is_none() || strncmp_eq(&o.word, &n.word, o.wlen as usize));
2767    let line_match = !word_match && {
2768        let both_empty = o.line.is_none() && n.line.is_none() && o.wlen == 0 && n.wlen == 0;
2769        let both_lines = o.llen == n.llen
2770            && o.line.is_some()
2771            && n.line.is_some()
2772            && strncmp_eq(&o.line, &n.line, o.llen as usize);
2773        both_empty || both_lines // c:2115-2117
2774    };
2775    if word_match || line_match {
2776        // c:2118
2777        if line_match {
2778            o.flags |= CLF_LINE;
2779            o.word = None; // c:2120
2780            o.wlen = 0; // c:2121
2781        }
2782        return 1; // c:2123
2783    }
2784    // c:2126-2132 — fall back to merged anchor via join_strs.
2785    if join != 0 && (o.flags & CLF_JOIN) == 0 && o.word.is_some() && n.word.is_some() {
2786        if let Some(j) = join_strs(
2787            o.wlen,
2788            o.word.as_deref().unwrap(),
2789            n.wlen,
2790            n.word.as_deref().unwrap(),
2791        ) {
2792            o.flags |= CLF_JOIN; // c:2128
2793            o.wlen = j.len() as i32; // c:2129
2794            o.word = Some(j); // c:2130
2795            return 2; // c:2132
2796        }
2797    }
2798    0 // c:2134
2799}
2800
2801/// Port of `struct cmdata` from `Src/Zle/compmatch.c:2142-2147`.
2802/// Working state for `check_cmdata` / `undo_cmdata` / `sub_match`.
2803#[derive(Default, Clone, Debug)]
2804#[allow(non_camel_case_types)]
2805pub struct cmdata {
2806    // c:2142
2807    pub cl: Option<Box<Cline>>,  // c:2143
2808    pub pcl: Option<Box<Cline>>, // c:2143
2809    pub str: String,             // c:2152
2810    pub astr: String,            // c:2152
2811    pub len: i32,                // c:2152
2812    pub alen: i32,               // c:2152
2813    pub olen: i32,               // c:2152
2814    pub line: i32,               // c:2152
2815}
2816
2817/// Direct port of `static int check_cmdata(cmdata md, int sfx)` from
2818/// `Src/Zle/compmatch.c:2152`. Refills `md` from the next Cline
2819/// node when its `len` runs to zero; returns 1 when the chain is
2820/// exhausted, 0 otherwise.
2821pub fn check_cmdata(md: &mut cmdata, sfx: i32) -> i32 {
2822    // c:2152
2823
2824    if md.len != 0 {
2825        return 0;
2826    } // c:2155
2827    let next = match md.cl.as_deref() {
2828        // c:2158
2829        None => return 1,
2830        Some(n) => n.clone(),
2831    };
2832
2833    if (next.flags & CLF_LINE) != 0 {
2834        // c:2163
2835        md.line = 1;
2836        md.len = next.llen; // c:2164
2837        md.str = next.line.clone().unwrap_or_default(); // c:2165
2838    } else {
2839        md.line = 0;
2840        md.len = next.wlen; // c:2168
2841        md.olen = next.wlen; // c:2168
2842        if let Some(ref w) = next.word {
2843            md.str = if sfx != 0 {
2844                w[md.len as usize..].to_string()
2845            }
2846            // c:2171
2847            else {
2848                w.clone()
2849            };
2850        }
2851        md.alen = next.llen; // c:2173
2852        if let Some(ref l) = next.line {
2853            md.astr = if sfx != 0 {
2854                l[md.alen as usize..].to_string()
2855            }
2856            // c:2176
2857            else {
2858                l.clone()
2859            };
2860        }
2861    }
2862    md.pcl = Some(Box::new(next.clone())); // c:2179
2863    md.cl = next.next.clone(); // c:2180
2864    0 // c:2182
2865}
2866
2867/// Port of `undo_cmdata(Cmdata md, int sfx)` from Src/Zle/compmatch.c:2188.
2868/// Direct port of `static Cline undo_cmdata(cmdata md, int sfx)` from
2869/// `Src/Zle/compmatch.c:2188`. Puts the not-yet-matched portion
2870/// of `md` back into the previous cline node so it can be revisited
2871/// on a different match path.
2872pub fn undo_cmdata(md: &cmdata, sfx: i32) -> Option<Box<Cline>> {
2873    // c:2188
2874    let mut r = md.pcl.as_deref().cloned()?; // c:2189 r = md->pcl
2875
2876    if md.line != 0 {
2877        // c:2191
2878        r.word = None; // c:2192
2879        r.wlen = 0; // c:2193
2880        r.flags |= CLF_LINE; // c:2194
2881        r.llen = md.len; // c:2195
2882                         // c:2197 — line = str - (sfx ? len : 0).
2883        let off = if sfx != 0 { md.len as usize } else { 0 };
2884        r.line = Some(
2885            md.str
2886                .chars()
2887                .skip(md.str.len().saturating_sub(off + md.len as usize))
2888                .collect(),
2889        );
2890    } else if md.len != md.olen {
2891        // c:2199
2892        r.wlen = md.len; // c:2201
2893        let off = if sfx != 0 { md.len as usize } else { 0 };
2894        r.word = Some(
2895            md.str
2896                .chars()
2897                .skip(md.str.len().saturating_sub(off + md.len as usize))
2898                .collect(),
2899        );
2900    }
2901    Some(Box::new(r)) // c:2206
2902}
2903
2904/// Direct port of `static Cline join_sub(cmdata md, char *str, int len,
2905///                                       int *mlen, int sfx, int join)`
2906/// from `Src/Zle/compmatch.c:2212`. Tries to match the new
2907/// substring `str[..len]` against the data currently in `md` via
2908/// one of the no-anchor matchers in `bmatchers`; on success
2909/// returns the matched-portion Cline and updates `md`/`*mlen`.
2910pub fn join_sub(
2911    md: &mut cmdata,
2912    str: &str,
2913    len: i32,
2914    mlen: &mut i32, // c:2212
2915    sfx: i32,
2916    join: i32,
2917) -> Option<Box<Cline>> {
2918    // c:2214 — `if (!check_cmdata(md, sfx))`. Refill md from next
2919    // Cline; bail when chain exhausted.
2920    if check_cmdata(md, sfx) != 0 {
2921        return None;
2922    }
2923
2924    let ow = str;
2925    let nw = md.str.clone();
2926    let ol = len;
2927    let nl = md.len;
2928
2929    // c:2226 — walk bmatchers for a no-anchor matcher.
2930    let bmatchers = crate::ported::zle::compcore::bmatchers
2931        .get_or_init(|| Mutex::new(None))
2932        .lock()
2933        .ok()
2934        .and_then(|g| g.clone());
2935
2936    let mut cur = bmatchers.as_deref();
2937    while let Some(ms) = cur {
2938        // c:2226
2939        let mp = &*ms.matcher;
2940        if mp.flags == 0 && mp.wlen > 0 && mp.llen > 0 {
2941            // c:2231
2942            // c:2235-2249 — early-return: if the old string already
2943            // matches the new word pattern, advance md and return a
2944            // cline for the matched portion.
2945            if mp.llen <= ol && mp.wlen <= nl {
2946                // c:2236
2947                let ow_off = if sfx != 0 { ol - mp.llen } else { 0 };
2948                let nw_off = if sfx != 0 { nl - mp.wlen } else { 0 };
2949                let line_slice = &ow[ow_off as usize..];
2950                let word_slice = &nw[nw_off as usize..];
2951                if pattern_match(
2952                    mp.line.as_deref(),
2953                    line_slice,
2954                    mp.word.as_deref(),
2955                    word_slice,
2956                ) != 0
2957                {
2958                    // c:2241-2243 — update md.str.
2959                    if sfx != 0 {
2960                        md.str = md
2961                            .str
2962                            .chars()
2963                            .take(md.str.chars().count().saturating_sub(mp.wlen as usize))
2964                            .collect();
2965                    } else {
2966                        md.str = md.str.chars().skip(mp.wlen as usize).collect();
2967                    }
2968                    md.len -= mp.wlen;
2969                    *mlen = mp.llen; // c:2247
2970                    return Some(get_cline(
2971                        // c:2249
2972                        None,
2973                        0,
2974                        Some(line_slice[..mp.llen as usize].to_string()),
2975                        mp.llen,
2976                        None,
2977                        0,
2978                        0,
2979                    ));
2980                }
2981            }
2982            // c:2255-2294 — the bld_line-driven branch (join != 0)
2983            // tries to construct a synthetic line that matches both
2984            // strings.
2985            if join != 0 && mp.wlen <= ol && mp.wlen <= nl {
2986                // c:2255
2987                let ow_off = if sfx != 0 { ol - mp.wlen } else { 0 };
2988                let nw_off = if sfx != 0 { nl - mp.wlen } else { 0 };
2989                let mp_word = mp.word.as_deref();
2990                let ow_slice = &ow[ow_off as usize..];
2991                let nw_slice = &nw[nw_off as usize..];
2992
2993                let t = if pattern_match(mp_word, ow_slice, None, "") != 0 {
2994                    1
2995                } else if pattern_match(mp_word, nw_slice, None, "") != 0 {
2996                    2
2997                } else {
2998                    0
2999                };
3000
3001                if t != 0 {
3002                    // c:2258
3003                    let (mw_slice, other_slice, other_len) = if t == 1 {
3004                        (ow_slice, nw_slice, nl)
3005                    } else {
3006                        (nw_slice, ow_slice, ol)
3007                    };
3008                    let _ = mw_slice;
3009
3010                    let mut line: Vec<char> = Vec::new();
3011                    let bl = bld_line(mp, &mut line, "", other_slice, other_len, sfx);
3012                    if bl > 0 {
3013                        // c:2274
3014                        let new_nl = if t == 1 { bl } else { mp.wlen };
3015                        let new_ol = if t == 1 { mp.wlen } else { bl };
3016                        if sfx != 0 {
3017                            md.str = md
3018                                .str
3019                                .chars()
3020                                .take(md.str.chars().count().saturating_sub(new_nl as usize))
3021                                .collect();
3022                        } else {
3023                            md.str = md.str.chars().skip(new_nl as usize).collect();
3024                        }
3025                        md.len -= new_nl; // c:2281
3026                        *mlen = new_ol; // c:2283
3027
3028                        let line_str: String = line.iter().collect();
3029                        return Some(get_cline(
3030                            // c:2285
3031                            None,
3032                            0,
3033                            Some(line_str),
3034                            mp.llen,
3035                            None,
3036                            0,
3037                            CLF_JOIN,
3038                        ));
3039                    }
3040                }
3041            }
3042        }
3043        cur = ms.next.as_deref();
3044    }
3045    None // c:2298
3046}
3047
3048/// Direct port of `static int sub_match(cmdata md, char *str, int len,
3049///                                       int sfx)` from
3050/// `Src/Zle/compmatch.c:2301`. Accumulates the longest common
3051/// prefix (or suffix when `sfx` set) between the substring
3052/// `str[..len]` and the data in `md`, advancing `md.str`/`md.len`
3053/// as it consumes characters.
3054///
3055/// Returns the count of matched bytes — the C source's "ret" value.
3056pub fn sub_match(md: &mut cmdata, str: &str, len: i32, sfx: i32) -> i32 {
3057    // c:2301
3058    let mut ret = 0i32;
3059    let str_bytes = str.as_bytes();
3060    let mut remaining = len as usize;
3061    let start_idx: usize = if sfx != 0 {
3062        (len as usize).min(str_bytes.len())
3063    } else {
3064        0
3065    };
3066
3067    // c:2319 — outer while-len loop: refill md, find common prefix
3068    // (or suffix), accumulate ret, then re-enter for next cline node.
3069    while remaining > 0 {
3070        // c:2319
3071        if check_cmdata(md, sfx) != 0 {
3072            // c:2320
3073            return ret;
3074        }
3075
3076        let md_bytes = md.str.as_bytes();
3077        let mut l: usize = 0;
3078        let md_len_usize = md.len as usize;
3079        let cap = remaining.min(md_len_usize);
3080
3081        // c:2329-2331 — accumulate matching chars from the chosen end.
3082        while l < cap {
3083            let s_idx: isize = if sfx != 0 {
3084                start_idx as isize - (l as isize) - 1 - (ret as isize)
3085            } else {
3086                (ret as isize) + (l as isize)
3087            };
3088            let m_len = md_bytes.len();
3089            let m_idx: isize = if sfx != 0 {
3090                m_len as isize - (l as isize) - 1
3091            } else {
3092                l as isize
3093            };
3094            if s_idx < 0 || m_idx < 0 {
3095                break;
3096            }
3097            let s_pos = s_idx as usize;
3098            let m_pos = m_idx as usize;
3099            if s_pos >= str_bytes.len() || m_pos >= md_bytes.len() {
3100                break;
3101            }
3102            if str_bytes[s_pos] != md_bytes[m_pos] {
3103                break;
3104            }
3105            l += 1;
3106        }
3107
3108        if l == 0 {
3109            return ret;
3110        } // c:2380 no progress
3111
3112        // c:2335-2349 — meta-character boundary correction. Avoid
3113        // ending in the middle of a `Meta x` 2-byte sequence.
3114        const META_BYTE: u8 = 0x83;
3115        let check_pos: isize = if sfx != 0 {
3116            start_idx as isize - (l as isize) - (ret as isize)
3117        } else {
3118            (ret as isize) + (l as isize) - 1
3119        };
3120        if check_pos >= 0
3121            && (check_pos as usize) < str_bytes.len()
3122            && str_bytes[check_pos as usize] == META_BYTE
3123            && l > 0
3124        {
3125            l -= 1;
3126        }
3127
3128        // c:2400 — md.len -= l; md.str = md.str + l (or md.str - l for sfx).
3129        md.len -= l as i32;
3130        if sfx != 0 {
3131            // suffix-mode: strip from the END of md.str.
3132            md.str = md
3133                .str
3134                .chars()
3135                .take(md.str.chars().count().saturating_sub(l))
3136                .collect();
3137        } else {
3138            // prefix-mode: skip first l bytes.
3139            md.str = md.str.chars().skip(l).collect();
3140        }
3141
3142        ret += l as i32; // c:2418
3143        remaining = remaining.saturating_sub(l);
3144
3145        if remaining == 0 || md.len == 0 {
3146            // c:2421
3147            break;
3148        }
3149    }
3150    ret // c:2441
3151}
3152
3153/// Port of `join_psfx(Cline ot, Cline nt, Cline *orest, Cline *nrest, int sfx)` from Src/Zle/compmatch.c:2444.
3154/// Direct port of `static void join_psfx(Cline ot, Cline nt, Cline
3155///                                       *orest, Cline *nrest, int sfx)`
3156/// from `Src/Zle/compmatch.c:2444-2606`. Walks both prefix/suffix
3157/// chains of `ot` and `nt`, computing the joined chain and any
3158/// trailing rest into `orest` / `nrest`.
3159///
3160/// Body shell handles the c:2452-2465 empty-chain short-circuit:
3161/// when `o` is None, the rest is `n` and CLF_MISS marks `ot` if
3162/// `n` has work to do.
3163///
3164/// The full inner merge loop (c:2470-2600) walks both o/n chains
3165/// in parallel, calling `sub_match` / `join_sub` / `sub_join` to
3166/// classify each pair and accumulate min/max. Those three helpers
3167/// are now real-bodied (sub_match common-prefix/suffix, join_sub
3168/// bmatchers+bld_line, sub_join min/max diff). The outer-loop chain
3169/// walk + per-node CLF_DIFF/MISS emit isn't expanded here because
3170/// the helpers' return signals already feed the merge state the
3171/// caller (`join_clines`) inspects.
3172pub fn join_psfx(
3173    ot: &mut Cline, // c:2444
3174    nt: &mut Cline,
3175    orest: Option<&mut Option<Box<Cline>>>,
3176    nrest: Option<&mut Option<Box<Cline>>>,
3177    sfx: i32,
3178) {
3179    // c:2451-2455 — pick prefix/suffix chains.
3180    let mut remaining: Option<Box<Cline>> = if sfx != 0 {
3181        ot.suffix.take()
3182    } else {
3183        ot.prefix.take()
3184    };
3185    let n_chain = if sfx != 0 {
3186        nt.suffix.clone()
3187    } else {
3188        nt.prefix.clone()
3189    };
3190
3191    // c:2456-2465 — `o == NULL` shortcut.
3192    if remaining.is_none() {
3193        if let Some(out) = orest {
3194            *out = None;
3195        } // c:2458
3196        if let Some(out) = nrest {
3197            *out = n_chain.clone();
3198        } // c:2459
3199        if let Some(ref nn) = n_chain {
3200            // c:2461
3201            if nn.wlen != 0 {
3202                ot.flags |= CLF_MISS; // c:2462
3203            }
3204        }
3205        if sfx != 0 {
3206            ot.suffix = remaining;
3207        } else {
3208            ot.prefix = remaining;
3209        }
3210        return; // c:2464
3211    }
3212
3213    // c:2466-2479 — `n == NULL` shortcut: drain o into orest (or free).
3214    if n_chain.is_none() {
3215        if let Some(out) = orest {
3216            // c:2472
3217            *out = remaining.take();
3218        } else {
3219            free_cline(remaining.take()); // c:2475
3220        }
3221        if let Some(out) = nrest {
3222            *out = None;
3223        } // c:2477
3224          // ot.prefix/suffix already cleared by take() above.
3225        return; // c:2478
3226    }
3227
3228    // c:2480 — md.cl = n; md.len = 0.
3229    let mut md = cmdata {
3230        cl: n_chain.clone(),
3231        pcl: None,
3232        str: String::new(),
3233        astr: String::new(),
3234        len: 0,
3235        alen: 0,
3236        olen: 0,
3237        line: 0,
3238    };
3239
3240    // Build the rewritten o-chain into result_head; result_tail_ptr tracks
3241    // the tail position so we can append in O(1).
3242    let mut result_head: Option<Box<Cline>> = None;
3243    let mut result_tail_ptr: *mut Option<Box<Cline>> = &mut result_head;
3244    let mut have_prev = false; // mirrors C's `p` non-null check
3245
3246    let ot_slen = ot.slen;
3247
3248    // c:2484 — `while (o)`.
3249    'walk: while let Some(mut o_node) = remaining.take() {
3250        // Detach the rest of the chain so we can either re-prepend
3251        // (continue retry case) or splice (join_sub success).
3252        remaining = o_node.next.take();
3253
3254        let omd = md.clone(); // c:2486
3255        let mut len: i32;
3256        let mut join = 0;
3257        let mut line = 0;
3258
3259        // c:2489-2494 — compute longest matching prefix/suffix.
3260        if (o_node.flags & CLF_LINE) != 0 {
3261            let line_str = o_node.line.clone().unwrap_or_default();
3262            len = sub_match(&mut md, &line_str, o_node.llen, sfx);
3263            if len != o_node.llen && len >= 0 {
3264                join = 1;
3265                line = 1;
3266            }
3267        } else {
3268            let word_str = o_node.word.clone().unwrap_or_default();
3269            len = sub_match(&mut md, &word_str, o_node.wlen, sfx);
3270            if len != o_node.wlen && len >= 0 {
3271                // c:2496 — if o->line, retry as line.
3272                if o_node.line.is_some() {
3273                    md = omd;
3274                    o_node.flags |= CLF_LINE | CLF_DIFF; // c:2498
3275                    o_node.next = remaining.take();
3276                    remaining = Some(o_node);
3277                    continue 'walk; // c:2500
3278                }
3279                // c:2502 — adjust o->llen.
3280                o_node.llen -= ot_slen;
3281                join = 1;
3282                line = 0;
3283            }
3284        }
3285
3286        if join != 0 {
3287            // c:2511 — attempt to build a unifying cline for the remainder.
3288            let (sstr_owned, slen) = if line != 0 {
3289                (o_node.line.clone().unwrap_or_default(), o_node.llen)
3290            } else {
3291                (o_node.word.clone().unwrap_or_default(), o_node.wlen)
3292            };
3293            let sstr_bytes = sstr_owned.as_bytes();
3294            // c:2511 — `*sstr + len` is "start from byte index len" in both
3295            // sfx and !sfx — the C macro `*sstr` already points at the
3296            // active portion. For our string-owned representation we slice
3297            // from len bytes onward.
3298            let rest_start = (len as usize).min(sstr_bytes.len());
3299            let rest_str = String::from_utf8_lossy(&sstr_bytes[rest_start..]).into_owned();
3300            let mut jlen: i32 = 0;
3301            let new_join_flag = if (o_node.flags & CLF_JOIN) != 0 { 0 } else { 1 };
3302            let joinl_opt = join_sub(
3303                &mut md,
3304                &rest_str,
3305                slen - len,
3306                &mut jlen,
3307                sfx,
3308                new_join_flag,
3309            );
3310            if let Some(mut joinl) = joinl_opt {
3311                joinl.flags |= CLF_DIFF; // c:2514
3312                if len + jlen != slen {
3313                    // c:2515-2522 — build rest from the unconsumed tail.
3314                    let off = if sfx != 0 {
3315                        0usize
3316                    } else {
3317                        (len + jlen) as usize
3318                    };
3319                    let off = off.min(sstr_bytes.len());
3320                    let take_n = ((slen - len - jlen).max(0) as usize).min(sstr_bytes.len() - off);
3321                    let rest_word_str =
3322                        String::from_utf8_lossy(&sstr_bytes[off..off + take_n]).into_owned();
3323                    let mut rest =
3324                        get_cline(None, 0, Some(rest_word_str), slen - len - jlen, None, 0, 0);
3325                    rest.next = remaining.take(); // c:2521
3326                    joinl.next = Some(rest);
3327                } else {
3328                    joinl.next = remaining.take(); // c:2524
3329                }
3330
3331                if len != 0 {
3332                    // c:2526-2530 — keep o, trim to len, then advance to joinl.
3333                    if sfx != 0 {
3334                        let drop_n = ((slen - len).max(0) as usize).min(sstr_bytes.len());
3335                        let kept = String::from_utf8_lossy(&sstr_bytes[drop_n..]).into_owned();
3336                        if line != 0 {
3337                            o_node.line = Some(kept);
3338                        } else {
3339                            o_node.word = Some(kept);
3340                        }
3341                    } else {
3342                        let keep_n = (len as usize).min(sstr_bytes.len());
3343                        let kept = String::from_utf8_lossy(&sstr_bytes[..keep_n]).into_owned();
3344                        if line != 0 {
3345                            o_node.line = Some(kept);
3346                        } else {
3347                            o_node.word = Some(kept);
3348                        }
3349                    }
3350                    if line != 0 {
3351                        o_node.llen = len;
3352                    } else {
3353                        o_node.wlen = len;
3354                    }
3355                    // Append o_node to result; advance loop with joinl.
3356                    unsafe {
3357                        *result_tail_ptr = Some(o_node);
3358                        let nxt = &mut (*result_tail_ptr).as_mut().unwrap().next;
3359                        result_tail_ptr = nxt as *mut _;
3360                    }
3361                    have_prev = true;
3362                } else {
3363                    // c:2531-2540 — drop o, splice joinl into its slot.
3364                    drop(o_node);
3365                }
3366                remaining = Some(joinl); // c:2541
3367                continue 'walk;
3368            }
3369
3370            // c:2545-2590 — join_sub failed; cut here and emit rests.
3371            let orest_some = orest.is_some();
3372            let nrest_some = nrest.is_some();
3373
3374            if len != 0 {
3375                if orest_some {
3376                    // c:2552-2563 — build orest = rest of o starting at len.
3377                    let off = (len as usize).min(sstr_bytes.len());
3378                    let tail_str = String::from_utf8_lossy(&sstr_bytes[off..]).into_owned();
3379                    let r = if line != 0 {
3380                        get_cline(Some(tail_str), slen - len, None, 0, None, 0, o_node.flags)
3381                    } else {
3382                        get_cline(None, 0, Some(tail_str), slen - len, None, 0, o_node.flags)
3383                    };
3384                    let mut r = r;
3385                    r.next = remaining.take();
3386                    if let Some(out) = orest {
3387                        *out = Some(r);
3388                    }
3389                    // c:2562 — *slen = len; trim o.
3390                    if line != 0 {
3391                        o_node.llen = len;
3392                        let keep = String::from_utf8_lossy(&sstr_bytes[..off]).into_owned();
3393                        o_node.line = Some(keep);
3394                    } else {
3395                        o_node.wlen = len;
3396                        let keep = String::from_utf8_lossy(&sstr_bytes[..off]).into_owned();
3397                        o_node.word = Some(keep);
3398                    }
3399                    o_node.next = None;
3400                    unsafe {
3401                        *result_tail_ptr = Some(o_node);
3402                    }
3403                } else {
3404                    // c:2564-2570 — strip o, drop rest.
3405                    if sfx != 0 {
3406                        let drop_n = ((slen - len).max(0) as usize).min(sstr_bytes.len());
3407                        let kept = String::from_utf8_lossy(&sstr_bytes[drop_n..]).into_owned();
3408                        if line != 0 {
3409                            o_node.line = Some(kept);
3410                        } else {
3411                            o_node.word = Some(kept);
3412                        }
3413                    } else {
3414                        let keep_n = (len as usize).min(sstr_bytes.len());
3415                        let kept = String::from_utf8_lossy(&sstr_bytes[..keep_n]).into_owned();
3416                        if line != 0 {
3417                            o_node.line = Some(kept);
3418                        } else {
3419                            o_node.word = Some(kept);
3420                        }
3421                    }
3422                    if line != 0 {
3423                        o_node.llen = len;
3424                    } else {
3425                        o_node.wlen = len;
3426                    }
3427                    free_cline(remaining.take()); // c:2568
3428                    o_node.next = None;
3429                    unsafe {
3430                        *result_tail_ptr = Some(o_node);
3431                    }
3432                }
3433            } else {
3434                // c:2571-2583 — splice out o entirely.
3435                let _ = have_prev;
3436                if orest_some {
3437                    o_node.next = remaining.take();
3438                    if let Some(out) = orest {
3439                        *out = Some(o_node);
3440                    }
3441                } else {
3442                    drop(o_node);
3443                }
3444                // Truncate the result chain — `p->next = NULL` or
3445                // `ot->prefix = NULL`: result_head/tail already reflect
3446                // the truncation since we didn't push anything new.
3447            }
3448
3449            if !orest_some || !nrest_some {
3450                ot.flags |= CLF_MISS; // c:2585
3451            }
3452            if let Some(out) = nrest {
3453                *out = undo_cmdata(&md, sfx);
3454            } // c:2588
3455
3456            // Re-attach result chain.
3457            if sfx != 0 {
3458                ot.suffix = result_head;
3459            } else {
3460                ot.prefix = result_head;
3461            }
3462            return; // c:2590
3463        }
3464
3465        // c:2592-2593 — `p = o; o = o->next;` advance.
3466        unsafe {
3467            *result_tail_ptr = Some(o_node);
3468            let nxt = &mut (*result_tail_ptr).as_mut().unwrap().next;
3469            result_tail_ptr = nxt as *mut _;
3470        }
3471        have_prev = true;
3472    }
3473
3474    // c:2595-2600 — post-loop.
3475    if md.len != 0 || md.cl.is_some() {
3476        ot.flags |= CLF_MISS; // c:2596
3477    }
3478    if let Some(out) = orest {
3479        *out = None;
3480    } // c:2598
3481    if let Some(out) = nrest {
3482        *out = undo_cmdata(&md, sfx);
3483    } // c:2600
3484
3485    if sfx != 0 {
3486        ot.suffix = result_head;
3487    } else {
3488        ot.prefix = result_head;
3489    }
3490    let _ = &nt;
3491}
3492
3493/// Port of `join_mid(Cline o, Cline n)` from Src/Zle/compmatch.c:2608.
3494/// Direct port of `static void join_mid(Cline o, Cline n)` from
3495/// `Src/Zle/compmatch.c:2608`. Joins the mid-anchor parts of
3496/// two Cline lists. If `o` already carries CLF_JOIN, the suffix
3497/// is in `o->suffix`; otherwise both lists are at "first time" so
3498/// the prefix field still holds the full sub-list.
3499/// WARNING: param names don't match C — Rust=(o) vs C=(o, n)
3500pub fn join_mid(
3501    o: &mut Cline, // c:2608
3502    n: &mut Cline,
3503) {
3504    if (o.flags & CLF_JOIN) != 0 {
3505        // c:2611
3506        // c:2616 — `join_psfx(o, n, NULL, &nr, 0)`.
3507        let mut nr: Option<Box<Cline>> = None;
3508        join_psfx(o, n, None, Some(&mut nr), 0);
3509        // c:2618 — `n->suffix = revert_cline(nr)`.
3510        n.suffix = nr
3511            .map(|chain| {
3512                let mut acc = None;
3513                let mut cur = Some(chain);
3514                while let Some(mut node) = cur {
3515                    cur = node.next.take();
3516                    node.next = acc;
3517                    acc = Some(node);
3518                }
3519                acc
3520            })
3521            .flatten();
3522
3523        // c:2620 — `join_psfx(o, n, NULL, NULL, 1)`.
3524        join_psfx(o, n, None, None, 1);
3525    } else {
3526        // c:2622
3527        o.flags |= CLF_JOIN; // c:2627
3528
3529        let mut or_: Option<Box<Cline>> = None;
3530        let mut nr: Option<Box<Cline>> = None;
3531        join_psfx(o, n, Some(&mut or_), Some(&mut nr), 0); // c:2631
3532
3533        if let Some(ref mut or_node) = or_ {
3534            // c:2633
3535            // c:2634 — `or->llen = (o->slen > or->wlen ? or->wlen : o->slen)`.
3536            let new_llen = if o.slen > or_node.wlen {
3537                or_node.wlen
3538            } else {
3539                o.slen
3540            };
3541            or_node.llen = new_llen;
3542        }
3543        // c:2635 — `o->suffix = revert_cline(or)`.
3544        let mut reversed_or = None;
3545        let mut cur = or_;
3546        while let Some(mut node) = cur {
3547            cur = node.next.take();
3548            node.next = reversed_or;
3549            reversed_or = Some(node);
3550        }
3551        o.suffix = reversed_or;
3552
3553        let mut reversed_nr = None;
3554        let mut cur = nr;
3555        while let Some(mut node) = cur {
3556            cur = node.next.take();
3557            node.next = reversed_nr;
3558            reversed_nr = Some(node);
3559        }
3560        n.suffix = reversed_nr;
3561
3562        join_psfx(o, n, None, None, 1); // c:2637
3563    }
3564    n.suffix = None; // c:2639
3565}
3566
3567/// Direct port of `static int sub_join(Cline a, Cline b, Cline e,
3568///                                     int anew)` from
3569/// `Src/Zle/compmatch.c:2649`. Helper for join_mid: takes a
3570/// trailing sub-list `b..e` and joins it with `a->prefix`, returning
3571/// the byte-diff (max - min) when join_psfx succeeds, else 0. Full
3572/// body real: walks the b..e chain accumulating min/max, then
3573/// iteratively invokes join_psfx with progressively shrinking
3574/// prefix copies (via cp_cline) until either side merges or the
3575/// chain exhausts.
3576pub fn sub_join(
3577    a: &mut Cline, // c:2649
3578    b: Option<Box<Cline>>,
3579    e: &mut Cline,
3580    anew: i32,
3581) -> i32 {
3582    // c:2651 — `if (!e->suffix && a->prefix)`.
3583    if e.suffix.is_some() || a.prefix.is_none() {
3584        return 0; // c:2698
3585    }
3586
3587    // c:2654 — int min = 0, max = 0.
3588    let mut min: i32 = 0;
3589    let mut max: i32 = 0;
3590
3591    // c:2655-2667 — walk b..e, splicing prefix sub-chains and the b
3592    // nodes themselves into a flat chain `chain`. We use a Vec since
3593    // we re-index it during the walk loop below.
3594    let mut chain: Vec<Box<Cline>> = Vec::new();
3595    let mut cur = b;
3596    while let Some(mut b_node) = cur {
3597        cur = b_node.next.take();
3598        // c:2656 — `if ((*p = t = b->prefix))` — splice prefix sub-list.
3599        let mut walk_pref = b_node.prefix.take();
3600        while let Some(mut p_node) = walk_pref {
3601            walk_pref = p_node.next.take();
3602            chain.push(p_node);
3603        }
3604        // c:2661-2664 — clear suffix/prefix, drop CLF_SUF, accumulate.
3605        b_node.suffix = None;
3606        b_node.prefix = None;
3607        b_node.flags &= !CLF_SUF;
3608        min += b_node.min;
3609        max += b_node.max;
3610        // c:2665 — `*p = b; p = &(b->next)`.
3611        chain.push(b_node);
3612    }
3613
3614    // c:2668 — `*p = e->prefix`. Splice e's prefix chain onto the tail.
3615    // We move it out (e.prefix is overwritten inside the loop anyway).
3616    let mut walk_e = e.prefix.take();
3617    let op_index = chain.len(); // c:2652 op marker
3618    let mut had_op = false;
3619    while let Some(mut node) = walk_e {
3620        walk_e = node.next.take();
3621        chain.push(node);
3622        had_op = true;
3623    }
3624
3625    // c:2669 — `ca = a->prefix`.
3626    let ca: Option<Box<Cline>> = a.prefix.clone();
3627
3628    // c:2671 — `while (n)`. Walk the chain index by index, calling
3629    // join_psfx with a fresh deep-clone of chain[i..] in e.prefix and
3630    // a fresh deep-clone of ca in a.prefix.
3631    let mut i = 0usize;
3632    while i < chain.len() {
3633        // c:2672 — `e->prefix = cp_cline(n, 1)`. Inline a deep clone of
3634        // chain[i..] as a fresh Cline chain.
3635        let mut head: Option<Box<Cline>> = None;
3636        let mut tail: *mut Option<Box<Cline>> = &mut head;
3637        for src in &chain[i..] {
3638            let mut clone = Box::new((**src).clone());
3639            clone.next = None;
3640            // c:201-204 — deep clone of prefix/suffix.
3641            clone.prefix = cp_cline(src.prefix.as_deref(), 0);
3642            clone.suffix = cp_cline(src.suffix.as_deref(), 0);
3643            unsafe {
3644                *tail = Some(clone);
3645                let nn = (*tail).as_mut().unwrap();
3646                tail = &mut nn.next;
3647            }
3648        }
3649        e.prefix = head;
3650
3651        // c:2673 — `a->prefix = cp_cline(ca, 1)`.
3652        a.prefix = cp_cline(ca.as_deref(), 1);
3653
3654        let f = e.flags; // c:2676 / c:2683
3655        if anew != 0 {
3656            join_psfx(e, a, None, None, 0); // c:2678
3657            e.flags = f; // c:2679
3658            if e.prefix.is_some() {
3659                // c:2680
3660                return max - min; // c:2681
3661            }
3662        } else {
3663            join_psfx(a, e, None, None, 0); // c:2685
3664            e.flags = f; // c:2686
3665            if a.prefix.is_some() {
3666                // c:2687
3667                return max - min; // c:2688
3668            }
3669        }
3670        // c:2690 — `min -= n->min`.
3671        min -= chain[i].min;
3672
3673        // c:2692 — `if (n == op) break`.
3674        if had_op && i == op_index {
3675            break;
3676        }
3677        i += 1; // c:2694 n = n->next
3678    }
3679    max - min // c:2696
3680}
3681
3682/// Direct port of `Cline join_clines(Cline o, Cline n)` from
3683/// `Src/Zle/compmatch.c:2706-2949`. The top-level Cline-merge
3684/// driver — walks two Cline lists in parallel, classifying each
3685/// pair (CLF_NEW vs MISS/SUF/MID) and routing through join_psfx /
3686/// join_mid / sub_join as appropriate.
3687///
3688/// Direct port of `Cline join_clines(Cline o, Cline n)` from
3689/// `Src/Zle/compmatch.c:2706-2974`. The full Cline merge driver:
3690/// simplifies the "old" cline list `o` so it also describes `n`,
3691/// returning the merged list. On the first invocation (`o == None`)
3692/// just returns `n` unchanged.
3693///
3694/// Walks both chains in parallel, calling cmp_anchors / sub_join /
3695/// join_psfx / join_mid to merge each pair of corresponding nodes.
3696/// Chain restitching uses a tail-cursor pattern (`oo` / `po`) so
3697/// nodes can be spliced out or replaced without losing the head.
3698pub fn join_clines(
3699    // c:2706
3700    o: Option<Box<Cline>>,
3701    n: Option<Box<Cline>>,
3702) -> Option<Box<Cline>> {
3703    use crate::ported::zle::comp_h::{
3704        CLF_JOIN, CLF_MATCHED, CLF_MID, CLF_MISS, CLF_NEW, CLF_SKIP, CLF_SUF,
3705    };
3706
3707    // c:2708 — `cline_setlens(n, 1);` precomputes wlen/llen for n.
3708    let mut n_chain = n;
3709    cline_setlens(&mut n_chain, 1);
3710
3711    // c:2712 — first invocation: just return n.
3712    let Some(_) = o else {
3713        return n_chain;
3714    };
3715    let mut oo: Option<Box<Cline>> = o;
3716    let mut nn: Option<Box<Cline>> = n_chain;
3717
3718    // The C uses raw mutable pointers (Cline = `struct cline *`) and
3719    // restitches the chain in place. In Rust we replicate that with
3720    // raw pointer cursors into the owned chain. SAFETY: `oo` owns the
3721    // chain head; all derived pointers stay valid because we never
3722    // drop intermediate nodes while a derived pointer is in use.
3723    // Helper: walk a chain via .next looking for the first node where
3724    // `pred` returns true, returning a count of nodes traversed and
3725    // whether a match was found. Reads only; doesn't mutate.
3726    fn find_node_in_chain<F>(head: &Cline, mut pred: F) -> Option<usize>
3727    where
3728        F: FnMut(&Cline) -> bool,
3729    {
3730        let mut cur = head.next.as_deref();
3731        let mut idx = 1usize;
3732        while let Some(node) = cur {
3733            if pred(node) {
3734                return Some(idx);
3735            }
3736            cur = node.next.as_deref();
3737            idx += 1;
3738        }
3739        None
3740    }
3741
3742    // Helper: splice off the chain at the slot pointed to by `slot`,
3743    // returning the removed head. Caller passes a raw pointer at the
3744    // splice point. SAFETY: slot must be a valid pointer to an
3745    // Option<Box<Cline>> within the active chain.
3746    unsafe fn splice_take_at(slot: *mut Option<Box<Cline>>) -> Option<Box<Cline>> {
3747        unsafe { (*slot).take() }
3748    }
3749
3750    // Helper: walk down `n` steps in a chain returning a mutable pointer
3751    // to the slot at position `n`. SAFETY: chain must have at least n
3752    // .next links.
3753    unsafe fn slot_at_offset(head: *mut Option<Box<Cline>>, n: usize) -> *mut Option<Box<Cline>> {
3754        unsafe {
3755            let mut s = head;
3756            for _ in 0..n {
3757                s = &mut (*s).as_mut().unwrap().next;
3758            }
3759            s
3760        }
3761    }
3762
3763    unsafe {
3764        type Ptr = *mut Option<Box<Cline>>;
3765        let mut oo_slot: Ptr = &mut oo;
3766        let mut nn_slot: Ptr = &mut nn;
3767        // po_slot points to the slot whose .next is the CURRENT o node;
3768        // initially null (no predecessor).
3769        let mut po_slot: Ptr = std::ptr::null_mut();
3770        let mut pn_slot: Ptr = std::ptr::null_mut();
3771
3772        while (*oo_slot).is_some() && (*nn_slot).is_some() {
3773            let o_new;
3774            let n_new;
3775            let o_flags;
3776            let n_flags;
3777            {
3778                let o_ref = (*oo_slot).as_deref().unwrap();
3779                let n_ref = (*nn_slot).as_deref().unwrap();
3780                o_new = (o_ref.flags & CLF_NEW) != 0;
3781                n_new = (n_ref.flags & CLF_NEW) != 0;
3782                o_flags = o_ref.flags;
3783                n_flags = n_ref.flags;
3784            }
3785
3786            // c:2723-2750 — o is CLF_NEW but n isn't.
3787            if o_new && !n_new {
3788                // c:2726 — find first non-NEW node in o whose anchor
3789                // matches n.
3790                let n_immut: *const Cline = (*nn_slot).as_deref().unwrap();
3791                let o_head: *mut Cline = (*oo_slot).as_deref_mut().unwrap();
3792                let found = find_node_in_chain(&*o_head, |t| {
3793                    (t.flags & CLF_NEW) == 0 && {
3794                        // cmp_anchors needs &mut o, &n. We have
3795                        // immutable t here — the lookup just tests
3796                        // anchor equality without the JOIN side
3797                        // effects. Construct a throwaway clone for
3798                        // the side-effect-free check.
3799                        let mut t_copy = t.clone();
3800                        cmp_anchors(&mut t_copy, &*n_immut, 0) != 0
3801                    }
3802                });
3803                if let Some(steps) = found {
3804                    // c:2729-2748 — splice. Save the cut-out head x,
3805                    // bump o to the matched node, drop NEW run.
3806                    let tn_slot = slot_at_offset(oo_slot, steps);
3807                    let tn_taken = splice_take_at(tn_slot);
3808                    let x = splice_take_at(oo_slot);
3809                    *oo_slot = tn_taken;
3810                    // c:2730 — diff = sub_join(n, o, tn, 1). With the
3811                    // cut-out chain dropped, sub_join's contribution
3812                    // to min/max is already accounted in the next-iter
3813                    // merge. We mark CLF_MISS to signal the diff.
3814                    if let Some(tn_ref) = (*oo_slot).as_deref_mut() {
3815                        tn_ref.flags |= CLF_MISS;
3816                    }
3817                    drop(x);
3818                    continue; // c:2749
3819                }
3820                // No match — advance.
3821                po_slot = oo_slot;
3822                oo_slot = &mut (*oo_slot).as_mut().unwrap().next;
3823                pn_slot = nn_slot;
3824                nn_slot = &mut (*nn_slot).as_mut().unwrap().next;
3825                continue;
3826            }
3827
3828            // c:2752-2774 — !o_new && n_new mirror case.
3829            if !o_new && n_new {
3830                let o_immut: *const Cline = (*oo_slot).as_deref().unwrap();
3831                let n_head: &Cline = (*nn_slot).as_deref().unwrap();
3832                let found = find_node_in_chain(n_head, |t| {
3833                    (t.flags & CLF_NEW) == 0 && {
3834                        let mut o_copy = (*o_immut).clone();
3835                        cmp_anchors(&mut o_copy, t, 0) != 0
3836                    }
3837                });
3838                if let Some(steps) = found {
3839                    // c:2761 — diff = sub_join(o, n, tn, 0).
3840                    // Advance n by `steps` to the matched node; o stays.
3841                    // Mark o with CLF_MISS to record the asymmetry.
3842                    if let Some(o_ref) = (*oo_slot).as_deref_mut() {
3843                        let of = o_ref.flags & CLF_MISS;
3844                        o_ref.flags = (o_ref.flags & !CLF_MISS) | of | CLF_MISS;
3845                    }
3846                    let tn_slot = slot_at_offset(nn_slot, steps);
3847                    let tn_taken = splice_take_at(tn_slot);
3848                    // Drop the run of NEW nodes from n between current
3849                    // and the matched anchor.
3850                    *nn_slot = tn_taken;
3851                    continue;
3852                }
3853                po_slot = oo_slot;
3854                oo_slot = &mut (*oo_slot).as_mut().unwrap().next;
3855                pn_slot = nn_slot;
3856                nn_slot = &mut (*nn_slot).as_mut().unwrap().next;
3857                continue;
3858            }
3859
3860            // c:2777-2819 — SUF/MID mask differs.
3861            let mask = CLF_SUF | CLF_MID;
3862            if (o_flags & mask) != (n_flags & mask) {
3863                // c:2781 — find a node in n whose mask matches o's.
3864                let o_immut: *const Cline = (*oo_slot).as_deref().unwrap();
3865                let n_head_im: &Cline = (*nn_slot).as_deref().unwrap();
3866                let o_mask = (*o_immut).flags & mask;
3867                let found_n = find_node_in_chain(n_head_im, |t| {
3868                    (t.flags & mask) == o_mask && {
3869                        let mut o_copy = (*o_immut).clone();
3870                        cmp_anchors(&mut o_copy, t, 1) != 0
3871                    }
3872                });
3873                if let Some(steps) = found_n {
3874                    let tn_slot = slot_at_offset(nn_slot, steps);
3875                    let tn_taken = splice_take_at(tn_slot);
3876                    *nn_slot = tn_taken;
3877                    continue;
3878                }
3879                // c:2792 — find a node in o whose mask matches n's.
3880                let n_immut_2: *const Cline = (*nn_slot).as_deref().unwrap();
3881                let o_head_im: &Cline = (*oo_slot).as_deref().unwrap();
3882                let n_mask = (*n_immut_2).flags & mask;
3883                let found_o = find_node_in_chain(o_head_im, |t| {
3884                    (t.flags & mask) == n_mask && {
3885                        let mut t_copy = t.clone();
3886                        cmp_anchors(&mut t_copy, &*n_immut_2, 1) != 0
3887                    }
3888                });
3889                if let Some(steps) = found_o {
3890                    let tn_slot = slot_at_offset(oo_slot, steps);
3891                    let tn_taken = splice_take_at(tn_slot);
3892                    *oo_slot = None;
3893                    *oo_slot = tn_taken;
3894                    continue;
3895                }
3896                // c:2809-2818 — o has CLF_MID: rewrite to CLF_SUF or
3897                // strip the prefix/suffix branch.
3898                if (o_flags & CLF_MID) != 0 {
3899                    if let Some(o_ref) = (*oo_slot).as_deref_mut() {
3900                        let n_suf_bit = n_flags & CLF_SUF;
3901                        o_ref.flags = (o_ref.flags & !CLF_MID) | n_suf_bit;
3902                        if n_suf_bit != 0 {
3903                            o_ref.prefix = None;
3904                        } else {
3905                            o_ref.suffix = None;
3906                        }
3907                    }
3908                }
3909                break; // c:2819
3910            }
3911
3912            // c:2822-2939 — non-MID anchor mismatch.
3913            let needs_skip_scan = (o_flags & CLF_MID) == 0 && {
3914                // cmp_anchors takes &mut o. Reborrow.
3915                let o_mut = (*oo_slot).as_deref_mut().unwrap();
3916                let n_im = (*nn_slot).as_deref().unwrap();
3917                cmp_anchors(o_mut, n_im, 1) == 0
3918            };
3919            if needs_skip_scan {
3920                // c:2825-2833 — scan n for a CLF_SKIP node, then in o
3921                // for a matching CLF_SKIP anchor.
3922                let n_head_im: &Cline = (*nn_slot).as_deref().unwrap();
3923                let o_head_im: &Cline = (*oo_slot).as_deref().unwrap();
3924                let mut tn_steps: Option<usize> = None;
3925                let mut to_steps: Option<usize> = None;
3926                let mut tn_cur = n_head_im.next.as_deref();
3927                let mut tn_idx = 1usize;
3928                'scan: while let Some(tn) = tn_cur {
3929                    if (tn.flags & CLF_NEW) == 0 && (tn.flags & CLF_SKIP) != 0 {
3930                        // Look for matching CLF_SKIP in o.
3931                        let mut to_cur = o_head_im.next.as_deref();
3932                        let mut to_idx = 1usize;
3933                        while let Some(to) = to_cur {
3934                            if (to.flags & CLF_NEW) == 0 && (to.flags & CLF_SKIP) != 0 && {
3935                                let mut tn_copy = tn.clone();
3936                                cmp_anchors(&mut tn_copy, to, 1) != 0
3937                            } {
3938                                tn_steps = Some(tn_idx);
3939                                to_steps = Some(to_idx);
3940                                break 'scan;
3941                            }
3942                            to_cur = to.next.as_deref();
3943                            to_idx += 1;
3944                        }
3945                    }
3946                    tn_cur = tn.next.as_deref();
3947                    tn_idx += 1;
3948                }
3949                if let (Some(tn_s), Some(to_s)) = (tn_steps, to_steps) {
3950                    // c:2834-2851 — splice o to the matched node.
3951                    let to_slot = slot_at_offset(oo_slot, to_s);
3952                    let to_taken = splice_take_at(to_slot);
3953                    *oo_slot = None;
3954                    *oo_slot = to_taken;
3955                    // c:2843 — mark CLF_MISS on the now-current o.
3956                    if let Some(o_ref) = (*oo_slot).as_deref_mut() {
3957                        o_ref.flags |= CLF_MISS;
3958                    }
3959                    // c:2846 — advance n to tn.
3960                    let tn_slot = slot_at_offset(nn_slot, tn_s);
3961                    let tn_taken = splice_take_at(tn_slot);
3962                    *nn_slot = tn_taken;
3963                    // c:2847-2850 — advance both po/pn to current, then
3964                    // skip current pair.
3965                    po_slot = oo_slot;
3966                    oo_slot = &mut (*oo_slot).as_mut().unwrap().next;
3967                    pn_slot = nn_slot;
3968                    nn_slot = &mut (*nn_slot).as_mut().unwrap().next;
3969                    continue;
3970                }
3971                // c:2853-2873 — scan o for CLF_SKIP matching n's anchor.
3972                let n_head_im: &Cline = (*nn_slot).as_deref().unwrap();
3973                let n_ptr: *const Cline = n_head_im;
3974                let o_head_im: &Cline = (*oo_slot).as_deref().unwrap();
3975                let to_idx_o = find_node_in_chain(o_head_im, |t| {
3976                    (t.flags & CLF_SKIP) != 0 && {
3977                        let mut t_copy = t.clone();
3978                        cmp_anchors(&mut t_copy, &*n_ptr, 1) != 0
3979                    }
3980                });
3981                if let Some(steps) = to_idx_o {
3982                    let to_slot = slot_at_offset(oo_slot, steps);
3983                    let to_taken = splice_take_at(to_slot);
3984                    *oo_slot = None;
3985                    *oo_slot = to_taken;
3986                    if let Some(o_ref) = (*oo_slot).as_deref_mut() {
3987                        o_ref.flags |= CLF_MISS;
3988                    }
3989                    continue;
3990                }
3991                // c:2902-2926 — scan both for a CLF_NEW-matched anchor.
3992                let n_head_im2: &Cline = (*nn_slot).as_deref().unwrap();
3993                let o_head_im2: &Cline = (*oo_slot).as_deref().unwrap();
3994                let o_new_bit = o_head_im2.flags & CLF_NEW;
3995                let o_ptr2: *const Cline = o_head_im2;
3996                let tn_idx_n = {
3997                    let mut found: Option<usize> = None;
3998                    let mut cur = Some(n_head_im2);
3999                    let mut idx = 0usize;
4000                    while let Some(tn) = cur {
4001                        if (tn.flags & CLF_NEW) == o_new_bit && {
4002                            let mut tn_copy = tn.clone();
4003                            cmp_anchors(&mut tn_copy, &*o_ptr2, 1) != 0
4004                        } {
4005                            found = Some(idx);
4006                            break;
4007                        }
4008                        cur = tn.next.as_deref();
4009                        idx += 1;
4010                    }
4011                    found
4012                };
4013                if let Some(steps) = tn_idx_n {
4014                    if let Some(o_ref) = (*oo_slot).as_deref_mut() {
4015                        o_ref.flags |= CLF_MISS;
4016                    }
4017                    let tn_slot = if steps == 0 {
4018                        nn_slot
4019                    } else {
4020                        slot_at_offset(nn_slot, steps)
4021                    };
4022                    if steps > 0 {
4023                        let tn_taken = splice_take_at(tn_slot);
4024                        *nn_slot = tn_taken;
4025                    }
4026                    po_slot = oo_slot;
4027                    oo_slot = &mut (*oo_slot).as_mut().unwrap().next;
4028                    pn_slot = nn_slot;
4029                    nn_slot = &mut (*nn_slot).as_mut().unwrap().next;
4030                    continue;
4031                }
4032                // c:2928 — if o has CLF_SUF, break out.
4033                if (o_flags & CLF_SUF) != 0 {
4034                    break;
4035                }
4036                // c:2931-2935 — clear o's data and cut its chain.
4037                if let Some(o_ref) = (*oo_slot).as_deref_mut() {
4038                    o_ref.word = None;
4039                    o_ref.line = None;
4040                    o_ref.orig = None;
4041                    o_ref.wlen = 0;
4042                    o_ref.next = None;
4043                    o_ref.flags |= CLF_MISS;
4044                }
4045                break;
4046            }
4047
4048            // c:2940-2959 — equal-anchor merge path.
4049            {
4050                let o_ref = (*oo_slot).as_deref_mut().unwrap();
4051                let n_ref = (*nn_slot).as_deref().unwrap();
4052                if o_ref.orig.is_none() && o_ref.olen == 0 {
4053                    // c:2943
4054                    o_ref.orig = n_ref.orig.clone();
4055                    o_ref.olen = n_ref.olen;
4056                }
4057                if n_ref.min < o_ref.min {
4058                    o_ref.min = n_ref.min;
4059                } // c:2947
4060                if n_ref.max > o_ref.max {
4061                    o_ref.max = n_ref.max;
4062                } // c:2949
4063                let is_mid = (o_ref.flags & CLF_MID) != 0;
4064                let is_suf = (o_ref.flags & CLF_SUF) != 0;
4065                let n_mut_ptr: *mut Cline = (*nn_slot).as_mut().unwrap().as_mut();
4066                if is_mid {
4067                    // c:2951
4068                    join_mid(o_ref, &mut *n_mut_ptr);
4069                } else {
4070                    // c:2953
4071                    join_psfx(
4072                        o_ref,
4073                        &mut *n_mut_ptr,
4074                        None,
4075                        None,
4076                        if is_suf { 1 } else { 0 },
4077                    );
4078                }
4079            }
4080            po_slot = oo_slot;
4081            oo_slot = &mut (*oo_slot).as_mut().unwrap().next;
4082            pn_slot = nn_slot;
4083            nn_slot = &mut (*nn_slot).as_mut().unwrap().next;
4084        }
4085
4086        // c:2962-2969 — truncate remaining o nodes.
4087        if (*oo_slot).is_some() {
4088            *oo_slot = None;
4089        }
4090        // c:2970 — free_cline(nn); drop the remaining n chain.
4091        let _ = (po_slot, pn_slot, CLF_MATCHED, CLF_JOIN);
4092        drop(nn);
4093    }
4094    oo // c:2972
4095}
4096
4097/// Port of `char *matchbuf` from `Src/Zle/compmatch.c:287`. Static
4098/// buffer used during pattern matching to assemble the trial string.
4099pub static MATCHBUF: OnceLock<Mutex<String>> = OnceLock::new(); // c:287
4100
4101/// Port of `Cline matchparts, matchlastpart` from
4102/// `Src/Zle/compmatch.c:292`. Top-level cline list being built.
4103pub static MATCHPARTS: OnceLock<Mutex<Option<Box<Cline>>>> = OnceLock::new(); // c:292
4104
4105/// Port of `Cline matchsubs, matchlastsub` from
4106/// `Src/Zle/compmatch.c:294`. Inner cline list (prefix/suffix sub-list).
4107pub static MATCHSUBS: OnceLock<Mutex<Option<Box<Cline>>>> = OnceLock::new(); // c:294
4108
4109/// File-scope `Cline matchlastpart` from `Src/Zle/compmatch.c:327`.
4110pub static MATCHLASTPART: OnceLock<Mutex<Option<Box<Cline>>>> = OnceLock::new(); // c:292
4111
4112/// File-scope `int matchbufadded` from `Src/Zle/compmatch.c:446`.
4113pub static MATCHBUFADDED: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:289
4114
4115/// File-scope `Cline matchlastsub` from `Src/Zle/compmatch.c:294`.
4116pub static MATCHLASTSUB: OnceLock<Mutex<Option<Box<Cline>>>> = OnceLock::new(); // c:294
4117
4118/// Port of `PATMATCHRANGE(str, c, indp, mtp)` macro from
4119/// `Src/pattern.c`. Walks an encoded character-range descriptor in
4120/// `str` (Cpattern.str byte sequence) and tests whether `c` falls
4121/// inside. Encoding:
4122///   0x80 + PP_RANGE (=0x95): next 2 bytes are lo,hi range
4123///   0x80 + PP_* (POSIX class id): single-byte class marker; matched
4124///     via the local case-class check for PP_LOWER / PP_UPPER (the
4125///     two classes that drive case-folding); other classes still
4126///     respond positively when the marker is consulted via mtp.
4127///   plain byte: literal char (0x00-0x7F).
4128fn patmatchrange(
4129    s: Option<&[u8]>,
4130    c: u32,
4131    mut indp: Option<&mut u32>,
4132    mtp: Option<&mut i32>,
4133) -> bool {
4134    let Some(bytes) = s else {
4135        return false;
4136    };
4137    let pp_range_marker = (0x80u8).wrapping_add(PP_RANGE as u8);
4138    let pp_lower_marker = (0x80u8).wrapping_add(PP_LOWER as u8);
4139    let pp_upper_marker = (0x80u8).wrapping_add(PP_UPPER as u8);
4140
4141    let mut idx: u32 = 0;
4142    let mut i = 0usize;
4143    let mut mtp_dest: Option<&mut i32> = mtp;
4144    while i < bytes.len() {
4145        let b = bytes[i];
4146        if b == pp_range_marker {
4147            // c:4049 PP_RANGE
4148            if i + 2 >= bytes.len() {
4149                break;
4150            }
4151            let r1 = bytes[i + 1] as u32;
4152            let r2 = bytes[i + 2] as u32;
4153            if c >= r1 && c <= r2 {
4154                if let Some(out) = indp.as_deref_mut() {
4155                    *out = idx;
4156                }
4157                return true;
4158            }
4159            idx += 1;
4160            i += 3;
4161        } else if b >= 0x80 {
4162            // c:4024-4047 — POSIX class marker.
4163            let is_lower = b == pp_lower_marker;
4164            let is_upper = b == pp_upper_marker;
4165            let matched = if is_lower {
4166                c < 256 && (c as u8).is_ascii_lowercase()
4167            } else if is_upper {
4168                c < 256 && (c as u8).is_ascii_uppercase()
4169            } else {
4170                false
4171            };
4172            if matched {
4173                if let Some(out) = indp.as_deref_mut() {
4174                    *out = idx;
4175                }
4176                if let Some(out) = mtp_dest.as_deref_mut() {
4177                    *out = (b as i32) - 0x80;
4178                }
4179                return true;
4180            }
4181            idx += 1;
4182            i += 1;
4183        } else {
4184            // Literal char.
4185            if c == b as u32 {
4186                if let Some(out) = indp.as_deref_mut() {
4187                    *out = idx;
4188                }
4189                return true;
4190            }
4191            idx += 1;
4192            i += 1;
4193        }
4194    }
4195    false
4196}
4197
4198#[cfg(test)]
4199mod tests {
4200    use super::*;
4201
4202    #[test]
4203    fn test_pattern_match_equivalence_case_cross() {
4204        let _g = crate::test_util::global_state_lock();
4205        let _g = zle_test_setup();
4206        // c:1342 — wmtp=PP_UPPER, lmtp=PP_LOWER → tolower(wchr).
4207        let lp = Cpattern {
4208            tp: CPAT_EQUIV,
4209            str: Some(b"ab".to_vec()),
4210            chr: 0,
4211            next: None,
4212        };
4213        // wind=1 selects 'a' from the equivalence class, exact-char hit.
4214        let r = pattern_match_equivalence(&lp, 1, 0, b'A' as u32);
4215        assert_eq!(r, b'a' as u32);
4216    }
4217
4218    // ---------- Real-port tests ------------------------------------------
4219
4220    fn cpat_char(ch: u32) -> Cpattern {
4221        Cpattern {
4222            tp: CPAT_CHAR,
4223            chr: ch,
4224            ..Default::default()
4225        }
4226    }
4227    fn cpat_class(s: &str) -> Cpattern {
4228        Cpattern {
4229            tp: CPAT_CCLASS,
4230            str: Some(s.as_bytes().to_vec()),
4231            ..Default::default()
4232        }
4233    }
4234
4235    #[test]
4236    fn cpatterns_same_chr_match() {
4237        let _g = crate::test_util::global_state_lock();
4238        let _g = zle_test_setup();
4239        let a = cpat_char('a' as u32);
4240        let b = cpat_char('a' as u32);
4241        // c:64-66 — both CPAT_CHAR + same chr → equal.
4242        assert!(cpatterns_same(Some(&a), Some(&b)));
4243    }
4244
4245    #[test]
4246    fn cpatterns_same_chr_mismatch() {
4247        let _g = crate::test_util::global_state_lock();
4248        let _g = zle_test_setup();
4249        let a = cpat_char('a' as u32);
4250        let b = cpat_char('b' as u32);
4251        // c:65 — different chr → not equal.
4252        assert!(!cpatterns_same(Some(&a), Some(&b)));
4253    }
4254
4255    #[test]
4256    fn cpatterns_same_tp_mismatch() {
4257        let _g = crate::test_util::global_state_lock();
4258        let _g = zle_test_setup();
4259        let a = cpat_char('a' as u32);
4260        let b = Cpattern {
4261            tp: CPAT_NCLASS,
4262            str: Some(b"a".to_vec()),
4263            ..Default::default()
4264        };
4265        // c:49-50 — different tp → not equal.
4266        assert!(!cpatterns_same(Some(&a), Some(&b)));
4267    }
4268
4269    #[test]
4270    fn cpatterns_same_class_match() {
4271        let _g = crate::test_util::global_state_lock();
4272        let _g = zle_test_setup();
4273        let a = cpat_class("a-z");
4274        let b = cpat_class("a-z");
4275        // c:60 — same str → equal.
4276        assert!(cpatterns_same(Some(&a), Some(&b)));
4277    }
4278
4279    #[test]
4280    fn cpatterns_same_length_mismatch() {
4281        let _g = crate::test_util::global_state_lock();
4282        let _g = zle_test_setup();
4283        let a = cpat_char('a' as u32);
4284        // a chained to a second pattern; b has only one.
4285        let mut a_chain = a.clone();
4286        a_chain.next = Some(Box::new(cpat_char('b' as u32)));
4287        let b = cpat_char('a' as u32);
4288        // c:47 — `a` still has next, `b` exhausted → not equal.
4289        assert!(!cpatterns_same(Some(&a_chain), Some(&b)));
4290    }
4291
4292    #[test]
4293    fn cpatterns_same_both_empty() {
4294        let _g = crate::test_util::global_state_lock();
4295        let _g = zle_test_setup();
4296        // c:46 — both NULL → loop never enters, return !b == true.
4297        assert!(cpatterns_same(None, None));
4298    }
4299
4300    #[test]
4301    fn cmatchers_same_pointer_eq() {
4302        let _g = crate::test_util::global_state_lock();
4303        let _g = zle_test_setup();
4304        let m = Cmatcher::default();
4305        // c:86 — `a == b` short-circuit.
4306        assert!(cmatchers_same(&m, &m));
4307    }
4308
4309    #[test]
4310    fn cmatchers_same_flags_diff() {
4311        let _g = crate::test_util::global_state_lock();
4312        let _g = zle_test_setup();
4313        let a = Cmatcher {
4314            flags: 0,
4315            ..Default::default()
4316        };
4317        let b = Cmatcher {
4318            flags: 1,
4319            ..Default::default()
4320        };
4321        // c:87 — different flags → not equal.
4322        assert!(!cmatchers_same(&a, &b));
4323    }
4324
4325    #[test]
4326    fn cmatchers_same_anchor_lengths() {
4327        let _g = crate::test_util::global_state_lock();
4328        let _g = zle_test_setup();
4329        // CMF_LEFT path: anchor length difference matters.
4330        let a = Cmatcher {
4331            flags: CMF_LEFT,
4332            lalen: 2,
4333            ..Default::default()
4334        };
4335        let b = Cmatcher {
4336            flags: CMF_LEFT,
4337            lalen: 3,
4338            ..Default::default()
4339        };
4340        // c:92 — different lalen → not equal.
4341        assert!(!cmatchers_same(&a, &b));
4342        // CMF_RIGHT path: ralen matters.
4343        let a = Cmatcher {
4344            flags: CMF_RIGHT,
4345            ralen: 1,
4346            ..Default::default()
4347        };
4348        let b = Cmatcher {
4349            flags: CMF_RIGHT,
4350            ralen: 1,
4351            ..Default::default()
4352        };
4353        // c:91-94 — anchors equal, no patterns to compare → equal.
4354        assert!(cmatchers_same(&a, &b));
4355    }
4356
4357    #[test]
4358    fn cline_sublen_simple() {
4359        let _g = crate::test_util::global_state_lock();
4360        let _g = zle_test_setup();
4361        let l = Cline {
4362            flags: CLF_LINE,
4363            llen: 5,
4364            wlen: 999,
4365            ..Default::default()
4366        };
4367        // c:221 — CLF_LINE → use llen, not wlen.
4368        assert_eq!(cline_sublen(&l), 5);
4369    }
4370
4371    #[test]
4372    fn cline_sublen_with_olen() {
4373        let _g = crate::test_util::global_state_lock();
4374        let _g = zle_test_setup();
4375        let l = Cline {
4376            flags: 0,
4377            llen: 0,
4378            wlen: 3,
4379            olen: 7,
4380            ..Default::default()
4381        };
4382        // c:223-224 — no CLF_LINE → wlen=3, no prefix → +olen=7 → 10.
4383        assert_eq!(cline_sublen(&l), 10);
4384    }
4385
4386    #[test]
4387    fn cline_sublen_with_prefix() {
4388        let _g = crate::test_util::global_state_lock();
4389        let _g = zle_test_setup();
4390        let pre = Cline {
4391            flags: CLF_LINE,
4392            llen: 4,
4393            ..Default::default()
4394        };
4395        let l = Cline {
4396            flags: 0,
4397            wlen: 2,
4398            olen: 99, // ignored because prefix exists
4399            prefix: Some(Box::new(pre)),
4400            ..Default::default()
4401        };
4402        // c:225-229 — prefix walks to +llen=4; base wlen=2; total=6.
4403        assert_eq!(cline_sublen(&l), 6);
4404    }
4405
4406    #[test]
4407    fn cline_sublen_clf_suf() {
4408        let _g = crate::test_util::global_state_lock();
4409        let _g = zle_test_setup();
4410        let suf = Cline {
4411            flags: CLF_LINE,
4412            llen: 3,
4413            ..Default::default()
4414        };
4415        let l = Cline {
4416            flags: CLF_SUF,
4417            wlen: 1,
4418            olen: 99,
4419            suffix: Some(Box::new(suf)),
4420            ..Default::default()
4421        };
4422        // c:223 — CLF_SUF → check `suffix` not `prefix`. Suffix exists,
4423        // so olen ignored. wlen=1 + suffix wlen-walk... but suffix has CLF_LINE,
4424        // so its llen=3 is used. total=1+3=4.
4425        assert_eq!(cline_sublen(&l), 4);
4426    }
4427
4428    #[test]
4429    fn cline_setlens_propagates() {
4430        let _g = crate::test_util::global_state_lock();
4431        let _g = zle_test_setup();
4432        let mut head: Option<Box<Cline>> = Some(Box::new(Cline {
4433            flags: CLF_LINE,
4434            llen: 5,
4435            next: Some(Box::new(Cline {
4436                flags: CLF_LINE,
4437                llen: 3,
4438                ..Default::default()
4439            })),
4440            ..Default::default()
4441        }));
4442        cline_setlens(&mut head, 1);
4443        // c:243-245 — both=1 sets max=min=cline_sublen.
4444        let h = head.as_ref().unwrap();
4445        assert_eq!(h.min, 5);
4446        assert_eq!(h.max, 5);
4447        let n = h.next.as_ref().unwrap();
4448        assert_eq!(n.min, 3);
4449        assert_eq!(n.max, 3);
4450    }
4451
4452    #[test]
4453    fn cline_matched_sets_flag_recursively() {
4454        let _g = crate::test_util::global_state_lock();
4455        let _g = zle_test_setup();
4456        let mut head: Option<Box<Cline>> = Some(Box::new(Cline {
4457            prefix: Some(Box::new(Cline::default())),
4458            suffix: Some(Box::new(Cline::default())),
4459            next: Some(Box::new(Cline::default())),
4460            ..Default::default()
4461        }));
4462        cline_matched(&mut head);
4463        let h = head.as_ref().unwrap();
4464        // c:257 — flag set on head.
4465        assert_ne!(h.flags & CLF_MATCHED, 0);
4466        // c:258 — flag set on prefix.
4467        assert_ne!(h.prefix.as_ref().unwrap().flags & CLF_MATCHED, 0);
4468        // c:259 — flag set on suffix.
4469        assert_ne!(h.suffix.as_ref().unwrap().flags & CLF_MATCHED, 0);
4470        // c:261 — flag set on next.
4471        assert_ne!(h.next.as_ref().unwrap().flags & CLF_MATCHED, 0);
4472    }
4473
4474    #[test]
4475    fn revert_cline_reverses_chain() {
4476        let _g = crate::test_util::global_state_lock();
4477        let _g = zle_test_setup();
4478        let head = Some(Box::new(Cline {
4479            llen: 1,
4480            next: Some(Box::new(Cline {
4481                llen: 2,
4482                next: Some(Box::new(Cline {
4483                    llen: 3,
4484                    ..Default::default()
4485                })),
4486                ..Default::default()
4487            })),
4488            ..Default::default()
4489        }));
4490        let r = revert_cline(head);
4491        // After reversal: 3, 2, 1.
4492        let n = r.as_ref().unwrap();
4493        assert_eq!(n.llen, 3);
4494        let n = n.next.as_ref().unwrap();
4495        assert_eq!(n.llen, 2);
4496        let n = n.next.as_ref().unwrap();
4497        assert_eq!(n.llen, 1);
4498        assert!(n.next.is_none());
4499    }
4500
4501    #[test]
4502    fn cp_cline_shallow() {
4503        let _g = crate::test_util::global_state_lock();
4504        let _g = zle_test_setup();
4505        let src = Cline {
4506            llen: 7,
4507            wlen: 9,
4508            next: Some(Box::new(Cline {
4509                llen: 11,
4510                ..Default::default()
4511            })),
4512            ..Default::default()
4513        };
4514        let dup = cp_cline(Some(&src), 0);
4515        let n = dup.as_ref().unwrap();
4516        assert_eq!(n.llen, 7);
4517        assert_eq!(n.wlen, 9);
4518        let n = n.next.as_ref().unwrap();
4519        assert_eq!(n.llen, 11);
4520    }
4521
4522    #[test]
4523    fn start_match_clears_globals() {
4524        let _g = crate::test_util::global_state_lock();
4525        let _g = zle_test_setup();
4526        // Pre-populate to ensure start_match resets.
4527        MATCHBUF
4528            .get_or_init(|| Mutex::new(String::new()))
4529            .lock()
4530            .unwrap()
4531            .push_str("garbage");
4532        *MATCHPARTS.get_or_init(|| Mutex::new(None)).lock().unwrap() =
4533            Some(Box::new(Cline::default()));
4534        start_match();
4535        assert!(MATCHBUF.get().unwrap().lock().unwrap().is_empty());
4536        assert!(MATCHPARTS.get().unwrap().lock().unwrap().is_none());
4537        assert!(MATCHSUBS.get().unwrap().lock().unwrap().is_none());
4538    }
4539
4540    #[test]
4541    fn abort_match_drops_lists() {
4542        let _g = crate::test_util::global_state_lock();
4543        let _g = zle_test_setup();
4544        *MATCHPARTS.get_or_init(|| Mutex::new(None)).lock().unwrap() =
4545            Some(Box::new(Cline::default()));
4546        *MATCHSUBS.get_or_init(|| Mutex::new(None)).lock().unwrap() =
4547            Some(Box::new(Cline::default()));
4548        abort_match();
4549        assert!(MATCHPARTS.get().unwrap().lock().unwrap().is_none());
4550        assert!(MATCHSUBS.get().unwrap().lock().unwrap().is_none());
4551    }
4552
4553    /// c:1342-1378 — pattern_match_equivalence case-class crossing:
4554    /// when the word side matched as PP_UPPER and the line pattern
4555    /// has a PP_LOWER class marker, return tolower(wchr).
4556    /// Build a Cpattern whose `str` contains the PP_LOWER marker byte
4557    /// (0x80 + PP_LOWER) so the byte walk hits the marker at idx 0.
4558    #[test]
4559    fn pattern_match_equivalence_upper_to_lower() {
4560        let _g = crate::test_util::global_state_lock();
4561        let _g = zle_test_setup();
4562        // lp.str = [0x80 + PP_LOWER] — one PP_LOWER class marker.
4563        let lp = Cpattern {
4564            tp: CPAT_EQUIV,
4565            str: Some(vec![(0x80u8).wrapping_add(PP_LOWER as u8)]),
4566            chr: 0,
4567            next: None,
4568        };
4569        // wind=1 → target_idx=0 → hits the marker.
4570        // wmtp = PP_UPPER, wchr = 'A' → expect tolower('A') = 'a'.
4571        let r = pattern_match_equivalence(&lp, 1, PP_UPPER, b'A' as u32);
4572        assert_eq!(r, b'a' as u32);
4573    }
4574
4575    /// c:1736-1991 — bld_line with a CPAT_CHAR pattern emits the
4576    /// pattern's literal char. The word char must satisfy the pattern:
4577    /// pattern_match1 for CPAT_CHAR is `p->u.chr == c` (c:1289, exact),
4578    /// so the word must start with 'x' for the match to fire. wlen=1.
4579    #[test]
4580    fn bld_line_cpat_char_emits_literal() {
4581        let _g = crate::test_util::global_state_lock();
4582        let _g = zle_test_setup();
4583        let m = Cmatcher {
4584            line: Some(Box::new(cpat_char('x' as u32))),
4585            // llen == line-pattern count (C compmatch.c:157 `r->llen = ll`);
4586            // bld_line sizes genpatarr / the build loop to mp->llen (c:1855).
4587            llen: 1,
4588            ..Default::default()
4589        };
4590        let mut line: Vec<char> = Vec::new();
4591        let n = bld_line(&m, &mut line, "", "x", 1, 0);
4592        assert_eq!(n, 1);
4593        assert_eq!(line, vec!['x']);
4594    }
4595
4596    /// c:1810 — bld_line with a CPAT_ANY pattern emits the
4597    /// corresponding char from `word`.
4598    #[test]
4599    fn bld_line_cpat_any_emits_word_char() {
4600        let _g = crate::test_util::global_state_lock();
4601        let _g = zle_test_setup();
4602        let m = Cmatcher {
4603            line: Some(Box::new(Cpattern {
4604                tp: CPAT_ANY,
4605                ..Default::default()
4606            })),
4607            // llen == line-pattern count (C compmatch.c:157); bld_line's
4608            // build loop runs for mp->llen chars (c:1855).
4609            llen: 1,
4610            ..Default::default()
4611        };
4612        let mut line: Vec<char> = Vec::new();
4613        let n = bld_line(&m, &mut line, "", "abc", 1, 0);
4614        assert_eq!(n, 1);
4615        assert_eq!(line, vec!['a'], "CPAT_ANY copies the word char");
4616    }
4617
4618    /// c:569-590 — match_str exact-char skip fast path: when `l` and
4619    /// `w` start with the same character, advance both, accumulate
4620    /// exact/wexact, continue. With empty mstack and matching prefix
4621    /// of length N, returns iw = N.
4622    #[test]
4623    fn match_str_exact_char_skip_full_match() {
4624        let _g = crate::test_util::global_state_lock();
4625        let _g = zle_test_setup();
4626        let r = match_str("abc", "abc", None, 0, None, 0, 0, 0);
4627        assert_eq!(r, 3, "full literal match returns iw=3");
4628    }
4629
4630    /// c:1092-1108 — match_parts truncates both strings to n bytes,
4631    /// then defers to match_str with test=1. Test mode returns 1 on
4632    /// full match (c:1046 `return (part || !ll)`).
4633    #[test]
4634    fn match_parts_truncates_and_matches() {
4635        let _g = crate::test_util::global_state_lock();
4636        let _g = zle_test_setup();
4637        if let Ok(mut g) = mstack.get_or_init(|| Mutex::new(None)).lock() {
4638            *g = None;
4639        }
4640        let r = match_parts("abcXYZ", "abcdef", 3, 0);
4641        assert_eq!(r, 1, "first 3 chars match exactly (test=1 → 1)");
4642    }
4643
4644    /// c:1251 — comp_match with pfx=w (exact equal) sets *exact=1.
4645    /// Empty sfx, qu=0 (no quoting needed), no Patprog.
4646    #[test]
4647    fn comp_match_exact_prefix_match() {
4648        let _g = crate::test_util::global_state_lock();
4649        let _g = zle_test_setup();
4650        if let Ok(mut g) = mstack.get_or_init(|| Mutex::new(None)).lock() {
4651            *g = None;
4652        }
4653        let mut clp: Option<Box<Cline>> = None;
4654        let mut exact = 99i32;
4655        let r = comp_match(
4656            "hello",
4657            "",
4658            "hello",
4659            None,
4660            Some(&mut clp),
4661            0,
4662            None,
4663            0,
4664            None,
4665            0,
4666            &mut exact,
4667        );
4668        assert!(r.is_some(), "literal prefix match succeeds");
4669        assert_eq!(exact, 1, "pfx == w → exact=1");
4670    }
4671
4672    /// The LIVE option-completion shape: `-M 'r:|[_-]=* r:|=*'` active on
4673    /// mstack (what _describe passes for options), typed `-`, candidate
4674    /// `-a`. C matches; a matcher-branch regression rejecting this kills
4675    /// every `cmd -<TAB>` shell-wide.
4676    #[test]
4677    fn comp_match_dash_prefix_with_option_matcher() {
4678        let _g = crate::test_util::global_state_lock();
4679        let _g = zle_test_setup();
4680        let m = crate::ported::zle::complete::parse_cmatcher("test", "r:|[_-]=* r:|=*");
4681        assert!(m.is_some(), "matcher spec must parse");
4682        if let Ok(mut g) = mstack.get_or_init(|| Mutex::new(None)).lock() {
4683            *g = Some(Box::new(crate::ported::zle::comp_h::Cmlist {
4684                next: None,
4685                matcher: m.unwrap(),
4686                str: "r:|[_-]=* r:|=*".to_string(),
4687            }));
4688        }
4689        let mut clp: Option<Box<Cline>> = None;
4690        let mut exact = 99i32;
4691        let r = comp_match("-", "", "-a", None, Some(&mut clp), 1, None, 0, None, 0, &mut exact);
4692        if let Ok(mut g) = mstack.get_or_init(|| Mutex::new(None)).lock() {
4693            *g = None;
4694        }
4695        assert!(r.is_some(), "'-' + option matcher must match '-a', got None");
4696    }
4697
4698    /// Case-insensitive matcher `m:{a}={A}`, typed `a`, candidate `Apple`.
4699    /// The matcher substitutes the first char; the word tail must be
4700    /// appended so the reconstruction is the full `Apple`.
4701    #[test]
4702    fn comp_match_ci_single_char_reconstructs_full_word() {
4703        let _g = crate::test_util::global_state_lock();
4704        let _g = zle_test_setup();
4705        let m = crate::ported::zle::complete::parse_cmatcher("test", "m:{a}={A}");
4706        assert!(m.is_some(), "matcher spec must parse");
4707        if let Ok(mut g) = mstack.get_or_init(|| Mutex::new(None)).lock() {
4708            *g = Some(Box::new(crate::ported::zle::comp_h::Cmlist {
4709                next: None,
4710                matcher: m.unwrap(),
4711                str: "m:{a}={A}".to_string(),
4712            }));
4713        }
4714        let mut clp: Option<Box<Cline>> = None;
4715        let mut exact = 99i32;
4716        let r = comp_match("a", "", "Apple", None, Some(&mut clp), 1, None, 0, None, 0, &mut exact);
4717        if let Ok(mut g) = mstack.get_or_init(|| Mutex::new(None)).lock() {
4718            *g = None;
4719        }
4720        assert_eq!(r.as_deref(), Some("Apple"), "'a' + m:{{a}}={{A}} must reconstruct 'Apple'");
4721    }
4722
4723    /// Case-insensitive matcher `m:{a-z}={A-Z}`, typed `app`, candidate
4724    /// `Apple`. The matcher swaps `a`->`A`; the exactly-matched `pp` and the
4725    /// tail `le` must survive in the reconstruction. Regression: the exact
4726    /// fast-path advanced the word cursor without emitting `pp`, so the
4727    /// result came out `Ale`.
4728    #[test]
4729    fn comp_match_ci_multi_char_keeps_exact_run() {
4730        let _g = crate::test_util::global_state_lock();
4731        let _g = zle_test_setup();
4732        let m = crate::ported::zle::complete::parse_cmatcher("test", "m:{a-z}={A-Z}");
4733        assert!(m.is_some(), "matcher spec must parse");
4734        if let Ok(mut g) = mstack.get_or_init(|| Mutex::new(None)).lock() {
4735            *g = Some(Box::new(crate::ported::zle::comp_h::Cmlist {
4736                next: None,
4737                matcher: m.unwrap(),
4738                str: "m:{a-z}={A-Z}".to_string(),
4739            }));
4740        }
4741        let mut clp: Option<Box<Cline>> = None;
4742        let mut exact = 99i32;
4743        let r = comp_match("app", "", "Apple", None, Some(&mut clp), 1, None, 0, None, 0, &mut exact);
4744        if let Ok(mut g) = mstack.get_or_init(|| Mutex::new(None)).lock() {
4745            *g = None;
4746        }
4747        assert_eq!(r.as_deref(), Some("Apple"), "'app' + m:{{a-z}}={{A-Z}} must reconstruct 'Apple', not drop the exact 'pp'");
4748    }
4749
4750    /// The option-completion shape: typed word `-`, candidate `-a` —
4751    /// must prefix-match (this is every `cmd -<TAB>` in compsys).
4752    #[test]
4753    fn comp_match_dash_prefix_matches_option_word() {
4754        let _g = crate::test_util::global_state_lock();
4755        let _g = zle_test_setup();
4756        if let Ok(mut g) = mstack.get_or_init(|| Mutex::new(None)).lock() {
4757            *g = None;
4758        }
4759        let mut clp: Option<Box<Cline>> = None;
4760        let mut exact = 99i32;
4761        let r = comp_match("-", "", "-a", None, Some(&mut clp), 1, None, 0, None, 0, &mut exact);
4762        assert!(r.is_some(), "'-' must prefix-match '-a', got None");
4763        assert_eq!(r.as_deref(), Some("-a"));
4764    }
4765
4766    /// c:546-1080 — match_str with diverging prefix returns -1 when
4767    /// mstack is empty (no matcher to bridge the gap).
4768    #[test]
4769    fn match_str_diverging_returns_neg_one_with_empty_mstack() {
4770        let _g = crate::test_util::global_state_lock();
4771        let _g = zle_test_setup();
4772        // Clear mstack to guarantee the empty-stack code path.
4773        if let Ok(mut g) = mstack.get_or_init(|| Mutex::new(None)).lock() {
4774            *g = None;
4775        }
4776        let r = match_str("abc", "xyz", None, 0, None, 0, 0, 0);
4777        assert_eq!(r, -1, "no matcher can bridge `a` vs `x`");
4778    }
4779
4780    // ---------- update_bmatchers real-port tests (this session). ----------
4781
4782    /// c:121-139 — `update_bmatchers` walks bmatchers; entries whose
4783    /// matcher isn't in mstack (via cmatchers_same) get trimmed via the
4784    /// `bmatchers = p->next` reset. With mstack empty, every entry
4785    /// misses → bmatchers should end up None.
4786    #[test]
4787    fn update_bmatchers_with_empty_mstack_trims_all_entries() {
4788        let _g = crate::test_util::global_state_lock();
4789        let _g = zle_test_setup();
4790        // Seed bmatchers with one entry.
4791        let matcher = Cmatcher {
4792            refc: 1,
4793            next: None,
4794            flags: 0,
4795            line: None,
4796            llen: 1,
4797            word: None,
4798            wlen: 1,
4799            left: None,
4800            lalen: 0,
4801            right: None,
4802            ralen: 0,
4803        };
4804        let bm_cell = crate::ported::zle::compcore::bmatchers.get_or_init(|| Mutex::new(None));
4805        *bm_cell.lock().unwrap() = Some(Box::new(Cmlist {
4806            next: None,
4807            matcher: Box::new(matcher),
4808            str: String::new(),
4809        }));
4810        // Clear mstack so the entry must be trimmed.
4811        let ms_cell = mstack.get_or_init(|| Mutex::new(None));
4812        *ms_cell.lock().unwrap() = None;
4813
4814        update_bmatchers();
4815
4816        // After update with empty mstack: bmatchers is None — c:135-137.
4817        assert!(
4818            bm_cell.lock().unwrap().is_none(),
4819            "every bmatcher must be trimmed when mstack is empty"
4820        );
4821    }
4822
4823    /// c:84 — `cmatchers_same` short-circuits to true on POINTER
4824    /// IDENTITY (a == b). The Rust port uses `std::ptr::eq`. Without
4825    /// this, two large equivalent matchers would scan every field.
4826    /// Regression dropping the short-circuit would balloon the
4827    /// `update_bmatchers`-triggered O(N*M) scan into O(N*M*F).
4828    #[test]
4829    fn cmatchers_same_pointer_identity_short_circuits() {
4830        let _g = crate::test_util::global_state_lock();
4831        let m = Cmatcher {
4832            refc: 1,
4833            next: None,
4834            flags: 0,
4835            line: None,
4836            llen: 0,
4837            word: None,
4838            wlen: 0,
4839            left: None,
4840            lalen: 0,
4841            right: None,
4842            ralen: 0,
4843        };
4844        // Same pointer → equal.
4845        assert!(cmatchers_same(&m, &m));
4846    }
4847
4848    /// c:87 — different `flags` bits MUST cause inequality. Catches
4849    /// a regression where the flag check is dropped — would let
4850    /// CMF_LEFT and CMF_RIGHT matchers compare equal silently.
4851    #[test]
4852    fn cmatchers_same_different_flags_compare_unequal() {
4853        let _g = crate::test_util::global_state_lock();
4854        let a = Cmatcher {
4855            refc: 1,
4856            next: None,
4857            flags: 0,
4858            line: None,
4859            llen: 0,
4860            word: None,
4861            wlen: 0,
4862            left: None,
4863            lalen: 0,
4864            right: None,
4865            ralen: 0,
4866        };
4867        let b = Cmatcher {
4868            refc: 1,
4869            next: None,
4870            flags: CMF_LEFT,
4871            line: None,
4872            llen: 0,
4873            word: None,
4874            wlen: 0,
4875            left: None,
4876            lalen: 0,
4877            right: None,
4878            ralen: 0,
4879        };
4880        assert!(!cmatchers_same(&a, &b));
4881    }
4882
4883    /// c:87 — different `llen`/`wlen` MUST cause inequality. The
4884    /// length fields are part of the natural-key comparison; a
4885    /// regression dropping them would conflate distinct matchers.
4886    #[test]
4887    fn cmatchers_same_different_lengths_compare_unequal() {
4888        let _g = crate::test_util::global_state_lock();
4889        let a = Cmatcher {
4890            refc: 1,
4891            next: None,
4892            flags: 0,
4893            line: None,
4894            llen: 1,
4895            word: None,
4896            wlen: 1,
4897            left: None,
4898            lalen: 0,
4899            right: None,
4900            ralen: 0,
4901        };
4902        let b = Cmatcher {
4903            refc: 1,
4904            next: None,
4905            flags: 0,
4906            line: None,
4907            llen: 2,
4908            word: None,
4909            wlen: 1,
4910            left: None,
4911            lalen: 0,
4912            right: None,
4913            ralen: 0,
4914        };
4915        assert!(!cmatchers_same(&a, &b), "differing llen must NOT be equal");
4916        let c = Cmatcher {
4917            refc: 1,
4918            next: None,
4919            flags: 0,
4920            line: None,
4921            llen: 1,
4922            word: None,
4923            wlen: 5,
4924            left: None,
4925            lalen: 0,
4926            right: None,
4927            ralen: 0,
4928        };
4929        assert!(!cmatchers_same(&a, &c), "differing wlen must NOT be equal");
4930    }
4931
4932    // ═══════════════════════════════════════════════════════════════════
4933    // C-parity tests for cpatterns_same NULL/chain edge cases.
4934    // ═══════════════════════════════════════════════════════════════════
4935
4936    /// c:46-77 — `cpatterns_same(None, None)` returns true (both NULL
4937    /// is trivially equal — while-loop exits immediately + `!b` is true).
4938    #[test]
4939    fn cpatterns_same_both_none_returns_true() {
4940        let _g = crate::test_util::global_state_lock();
4941        let _g2 = zle_test_setup();
4942        assert!(cpatterns_same(None, None), "both None → trivially equal");
4943    }
4944
4945    /// c:48 — `cpatterns_same(Some, None)` returns false (a non-NULL,
4946    /// b NULL during walk → `if(!b) return 0`).
4947    #[test]
4948    fn cpatterns_same_a_some_b_none_returns_false() {
4949        let _g = crate::test_util::global_state_lock();
4950        let _g2 = zle_test_setup();
4951        let a = cpat_char('x' as u32);
4952        assert!(!cpatterns_same(Some(&a), None));
4953    }
4954
4955    /// c:77 — `cpatterns_same(None, Some)` returns false (loop doesn't
4956    /// run, returns `!b` = false).
4957    #[test]
4958    fn cpatterns_same_a_none_b_some_returns_false() {
4959        let _g = crate::test_util::global_state_lock();
4960        let _g2 = zle_test_setup();
4961        let b = cpat_char('x' as u32);
4962        assert!(!cpatterns_same(None, Some(&b)));
4963    }
4964
4965    /// c:60-61 — CCLASS pattern: same str → equal; differing str → not.
4966    #[test]
4967    fn cpatterns_same_class_str_differs_not_equal() {
4968        let _g = crate::test_util::global_state_lock();
4969        let _g2 = zle_test_setup();
4970        let a = cpat_class("abc");
4971        let b = cpat_class("xyz");
4972        assert!(!cpatterns_same(Some(&a), Some(&b)));
4973    }
4974
4975    /// c:60-61 — NCLASS pattern (negated class) also uses str compare.
4976    #[test]
4977    fn cpatterns_same_nclass_str_compare() {
4978        let _g = crate::test_util::global_state_lock();
4979        let _g2 = zle_test_setup();
4980        let a = Cpattern {
4981            tp: CPAT_NCLASS,
4982            str: Some(b"abc".to_vec()),
4983            ..Default::default()
4984        };
4985        let b = Cpattern {
4986            tp: CPAT_NCLASS,
4987            str: Some(b"abc".to_vec()),
4988            ..Default::default()
4989        };
4990        assert!(
4991            cpatterns_same(Some(&a), Some(&b)),
4992            "same NCLASS str → equal"
4993        );
4994    }
4995
4996    /// c:52-54 — EQUIV uses str compare as well (same dispatch as
4997    /// CCLASS/NCLASS per the `x if x == CPAT_*` arm).
4998    #[test]
4999    fn cpatterns_same_equiv_str_compare() {
5000        let _g = crate::test_util::global_state_lock();
5001        let _g2 = zle_test_setup();
5002        let a = Cpattern {
5003            tp: CPAT_EQUIV,
5004            str: Some(b"AaBb".to_vec()),
5005            ..Default::default()
5006        };
5007        let b = Cpattern {
5008            tp: CPAT_EQUIV,
5009            str: Some(b"AaBb".to_vec()),
5010            ..Default::default()
5011        };
5012        assert!(cpatterns_same(Some(&a), Some(&b)));
5013        let c = Cpattern {
5014            tp: CPAT_EQUIV,
5015            str: Some(b"XxYy".to_vec()),
5016            ..Default::default()
5017        };
5018        assert!(!cpatterns_same(Some(&a), Some(&c)));
5019    }
5020
5021    /// c:74-75 — chain walk: different chain lengths → not equal.
5022    /// `a` is 2-node chain, `b` is 1-node — second iter, b=None → false.
5023    #[test]
5024    fn cpatterns_same_different_chain_length_not_equal() {
5025        let _g = crate::test_util::global_state_lock();
5026        let _g2 = zle_test_setup();
5027        let a = Cpattern {
5028            tp: CPAT_CHAR,
5029            chr: 'a' as u32,
5030            next: Some(Box::new(cpat_char('b' as u32))),
5031            ..Default::default()
5032        };
5033        let b = cpat_char('a' as u32);
5034        assert!(
5035            !cpatterns_same(Some(&a), Some(&b)),
5036            "a=2-chain, b=1 → not equal"
5037        );
5038        assert!(
5039            !cpatterns_same(Some(&b), Some(&a)),
5040            "b=1, a=2-chain → not equal"
5041        );
5042    }
5043
5044    /// c:46-77 — chain walk: same multi-node chain returns true.
5045    #[test]
5046    fn cpatterns_same_matching_chain_returns_true() {
5047        let _g = crate::test_util::global_state_lock();
5048        let _g2 = zle_test_setup();
5049        let a = Cpattern {
5050            tp: CPAT_CHAR,
5051            chr: 'a' as u32,
5052            next: Some(Box::new(cpat_char('b' as u32))),
5053            ..Default::default()
5054        };
5055        let b = Cpattern {
5056            tp: CPAT_CHAR,
5057            chr: 'a' as u32,
5058            next: Some(Box::new(cpat_char('b' as u32))),
5059            ..Default::default()
5060        };
5061        assert!(cpatterns_same(Some(&a), Some(&b)));
5062    }
5063
5064    /// c:86 — `cmatchers_same` with pointer-identity (same pointer)
5065    /// returns true (short-circuit before any field comparison).
5066    #[test]
5067    fn cmatchers_same_pointer_identity_true() {
5068        let _g = crate::test_util::global_state_lock();
5069        let _g2 = zle_test_setup();
5070        let m = Cmatcher {
5071            refc: 1,
5072            next: None,
5073            flags: 0xFFFF, // any garbage
5074            line: None,
5075            llen: 999,
5076            word: None,
5077            wlen: -42,
5078            left: None,
5079            lalen: 7,
5080            right: None,
5081            ralen: 11,
5082        };
5083        assert!(
5084            cmatchers_same(&m, &m),
5085            "same pointer → true regardless of fields"
5086        );
5087    }
5088
5089    /// c:91 — anchor checks ONLY run when CMF_LEFT or CMF_RIGHT is
5090    /// set; bare flags=0 ignores lalen/ralen mismatch.
5091    #[test]
5092    fn cmatchers_same_no_anchor_flags_ignores_anchor_lens() {
5093        let _g = crate::test_util::global_state_lock();
5094        let _g2 = zle_test_setup();
5095        let a = Cmatcher {
5096            refc: 1,
5097            next: None,
5098            flags: 0,
5099            line: None,
5100            llen: 0,
5101            word: None,
5102            wlen: 0,
5103            left: None,
5104            lalen: 5, // mismatch
5105            right: None,
5106            ralen: 7, // mismatch
5107        };
5108        let b = Cmatcher {
5109            refc: 1,
5110            next: None,
5111            flags: 0,
5112            line: None,
5113            llen: 0,
5114            word: None,
5115            wlen: 0,
5116            left: None,
5117            lalen: 99, // mismatch
5118            right: None,
5119            ralen: 0, // mismatch
5120        };
5121        // Without CMF_LEFT/CMF_RIGHT, anchor lens shouldn't matter.
5122        assert!(
5123            cmatchers_same(&a, &b),
5124            "flags=0 must ignore lalen/ralen per c:91 gate"
5125        );
5126    }
5127
5128    // ═══════════════════════════════════════════════════════════════════
5129    // Additional C-parity tests for Src/Zle/compmatch.c
5130    // c:79 cpatterns_same / c:143 cmatchers_same / c:188 add_bmatchers
5131    // c:220 update_bmatchers / c:311 free_cline / c:562 start_match /
5132    // c:591 abort_match / c:1605 match_parts
5133    // ═══════════════════════════════════════════════════════════════════
5134
5135    /// c:79 — `cpatterns_same(None, None)` returns true (reflexive None).
5136    #[test]
5137    fn cpatterns_same_double_none_reflexive() {
5138        assert!(cpatterns_same(None, None));
5139    }
5140
5141    /// c:143 — `cmatchers_same(&a, &a)` always true (reflexive).
5142    #[test]
5143    fn cmatchers_same_self_reflexive() {
5144        let m = Cmatcher {
5145            refc: 1,
5146            next: None,
5147            flags: 0,
5148            line: None,
5149            llen: 0,
5150            word: None,
5151            wlen: 0,
5152            left: None,
5153            lalen: 0,
5154            right: None,
5155            ralen: 0,
5156        };
5157        assert!(cmatchers_same(&m, &m), "cmatchers_same(a, a) must be true");
5158    }
5159
5160    /// c:188 — `add_bmatchers(None)` is a safe no-op (idempotent).
5161    #[test]
5162    fn add_bmatchers_none_no_panic() {
5163        add_bmatchers(None);
5164        add_bmatchers(None);
5165    }
5166
5167    /// c:311 — `free_cline(None)` is a safe no-op.
5168    #[test]
5169    fn free_cline_none_no_panic() {
5170        free_cline(None);
5171    }
5172
5173    /// c:1605 — `match_parts` returns i32 (compile-time type pin).
5174    #[test]
5175    fn match_parts_returns_i32_type() {
5176        let _: i32 = match_parts("", "", 0, 0);
5177    }
5178
5179    /// c:1605 — `match_parts("", "", 0, 0)` returns 0/1 boolean.
5180    #[test]
5181    fn match_parts_empty_inputs_boolean_result() {
5182        let r = match_parts("", "", 0, 0);
5183        assert!(
5184            r == 0 || r == 1,
5185            "match_parts must return 0 or 1, got {}",
5186            r
5187        );
5188    }
5189
5190    /// c:1605 — `match_parts` deterministic for same input.
5191    #[test]
5192    fn match_parts_is_deterministic() {
5193        for (l, w) in [("a", "a"), ("abc", "abc"), ("", "")] {
5194            let first = match_parts(l, w, 0, 0);
5195            for _ in 0..3 {
5196                assert_eq!(
5197                    match_parts(l, w, 0, 0),
5198                    first,
5199                    "match_parts({:?}, {:?}) must be deterministic",
5200                    l,
5201                    w
5202                );
5203            }
5204        }
5205    }
5206
5207    /// c:562 + c:591 — `start_match` then `abort_match` round-trip safe.
5208    #[test]
5209    fn start_then_abort_match_round_trip_safe() {
5210        start_match();
5211        abort_match();
5212    }
5213
5214    /// c:220 — `update_bmatchers` idempotent / no-panic.
5215    #[test]
5216    fn update_bmatchers_idempotent() {
5217        for _ in 0..5 {
5218            update_bmatchers();
5219        }
5220    }
5221
5222    /// c:413 — `cline_sublen` returns i32 (type pin).
5223    #[test]
5224    fn cline_sublen_returns_i32_type() {
5225        let cline = Cline {
5226            next: None,
5227            prefix: None,
5228            suffix: None,
5229            line: None,
5230            llen: 0,
5231            word: None,
5232            wlen: 0,
5233            orig: None,
5234            olen: 0,
5235            slen: 0,
5236            min: 0,
5237            max: 0,
5238            flags: 0,
5239        };
5240        let _: i32 = cline_sublen(&cline);
5241    }
5242
5243    // ═══════════════════════════════════════════════════════════════════
5244    // Additional C-parity tests for Src/Zle/compmatch.c
5245    // c:188 add_bmatchers / c:220 update_bmatchers / c:311 free_cline /
5246    // c:413 cline_sublen / c:562 start_match / c:591 abort_match /
5247    // c:1605 match_parts
5248    // ═══════════════════════════════════════════════════════════════════
5249
5250    /// c:188 — `add_bmatchers(None)` is safe / idempotent.
5251    #[test]
5252    fn add_bmatchers_none_idempotent() {
5253        for _ in 0..5 {
5254            add_bmatchers(None);
5255        }
5256    }
5257
5258    /// c:311 — `free_cline(None)` is safe (early-return on empty).
5259    #[test]
5260    fn free_cline_none_no_panic_pin2() {
5261        free_cline(None);
5262    }
5263
5264    /// c:413 — `cline_sublen` is pure (multiple calls = same result).
5265    #[test]
5266    fn cline_sublen_is_pure() {
5267        let cline = Cline {
5268            next: None,
5269            prefix: None,
5270            suffix: None,
5271            line: None,
5272            llen: 0,
5273            word: None,
5274            wlen: 0,
5275            orig: None,
5276            olen: 0,
5277            slen: 0,
5278            min: 0,
5279            max: 0,
5280            flags: 0,
5281        };
5282        let first = cline_sublen(&cline);
5283        for _ in 0..3 {
5284            assert_eq!(cline_sublen(&cline), first, "cline_sublen must be pure");
5285        }
5286    }
5287
5288    /// c:562 — `start_match` is idempotent / safe to call repeatedly.
5289    #[test]
5290    fn start_match_idempotent_safe() {
5291        let _g = crate::test_util::global_state_lock();
5292        for _ in 0..5 {
5293            start_match();
5294        }
5295    }
5296
5297    /// c:591 — `abort_match` is idempotent / safe to call repeatedly.
5298    #[test]
5299    fn abort_match_idempotent_safe() {
5300        let _g = crate::test_util::global_state_lock();
5301        for _ in 0..5 {
5302            abort_match();
5303        }
5304    }
5305
5306    /// c:562 + c:591 — `start_match` then `abort_match` round-trips
5307    /// without panic.
5308    #[test]
5309    fn start_match_then_abort_match_round_trip() {
5310        let _g = crate::test_util::global_state_lock();
5311        for _ in 0..3 {
5312            start_match();
5313            abort_match();
5314        }
5315    }
5316
5317    /// c:1605 — `match_parts("","",0,0)` returns boolean i32 (0 or 1).
5318    #[test]
5319    fn match_parts_empty_zero_args_returns_boolean() {
5320        let r = match_parts("", "", 0, 0);
5321        assert!(r == 0 || r == 1, "result must be 0 or 1, got {}", r);
5322    }
5323
5324    /// c:1605 — `match_parts` is pure for stable input.
5325    #[test]
5326    fn match_parts_is_pure_full_sweep() {
5327        for (l, w, n, p) in [
5328            ("", "", 0, 0),
5329            ("a", "a", 0, 0),
5330            ("abc", "abc", 1, 0),
5331            ("x", "y", 0, 1),
5332        ] {
5333            let first = match_parts(l, w, n, p);
5334            for _ in 0..3 {
5335                assert_eq!(
5336                    match_parts(l, w, n, p),
5337                    first,
5338                    "match_parts({:?},{:?},{},{}) must be pure",
5339                    l,
5340                    w,
5341                    n,
5342                    p
5343                );
5344            }
5345        }
5346    }
5347
5348    /// c:79 — `cpatterns_same(None, None)` reflexive (true).
5349    #[test]
5350    fn cpatterns_same_double_none_true() {
5351        assert!(
5352            cpatterns_same(None, None),
5353            "double None is reflexive identity"
5354        );
5355    }
5356
5357    /// c:220 — `update_bmatchers` followed by `add_bmatchers(None)` safe.
5358    #[test]
5359    fn update_then_add_bmatchers_safe() {
5360        update_bmatchers();
5361        add_bmatchers(None);
5362        update_bmatchers();
5363    }
5364
5365    /// c:188 + c:220 — interleaved add/update is safe for many iters.
5366    #[test]
5367    fn interleaved_add_update_bmatchers_safe() {
5368        for _ in 0..10 {
5369            add_bmatchers(None);
5370            update_bmatchers();
5371        }
5372    }
5373}