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