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