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         → compsys::matching::match_str()
15//! - match_parts       → compsys::matching::match_parts()
16//! - comp_match        → compsys::matching::comp_match()
17//! - pattern_match_equivalence → compsys::matching (inline)
18//! - add_match_str/part/sub    → compsys::matching (inline)
19//! - cline_* (match line ops)  → compsys::base::CompletionLine
20
21// CompMatcher / MatchFlags / CompLine deleted — Rust-invented structs
22// with no C counterpart. The legit C types `Cmatcher` (comp.h:153),
23// `Cline` (comp.h:245), and `Cpattern` (comp.h:197) are ported in
24// `comp_h.rs` and used by the real porters of `match_str` /
25// `pattern_match` / `add_match_str` etc. below.
26
27/// Port of `cpatterns_same(Cpattern a, Cpattern b)` from `Src/Zle/compmatch.c:42`.
28/// ```c
29/// static int
30/// cpatterns_same(Cpattern a, Cpattern b)
31/// {
32///     while (a) {
33///         if (!b) return 0;
34///         if (a->tp != b->tp) return 0;
35///         switch (a->tp) {
36///         case CPAT_CCLASS: case CPAT_NCLASS: case CPAT_EQUIV:
37///             if (strcmp(a->u.str, b->u.str) != 0) return 0;
38///             break;
39///         case CPAT_CHAR:
40///             if (a->u.chr != b->u.chr) return 0;
41///             break;
42///         default:
43///             break;
44///         }
45///         a = a->next;
46///         b = b->next;
47///     }
48///     return !b;
49/// }
50/// ```
51/// Walk two parallel `Cpattern` chains testing structural equality
52/// (same `tp` + same `str` for class types or same `chr` for
53/// CPAT_CHAR). Used by `cmatchers_same` to dedupe matcher specs.
54/// WARNING: param names don't match C — Rust=(b) vs C=(a, b)
55
56// --- AUTO: cross-zle hoisted-fn use glob ---
57#[allow(unused_imports)]
58#[allow(unused_imports)]
59use crate::ported::zle::zle_main::*;
60#[allow(unused_imports)]
61use crate::ported::zle::zle_misc::*;
62#[allow(unused_imports)]
63use crate::ported::zle::zle_hist::*;
64#[allow(unused_imports)]
65use crate::ported::zle::zle_move::*;
66#[allow(unused_imports)]
67use crate::ported::zle::zle_word::*;
68#[allow(unused_imports)]
69use crate::ported::zle::zle_params::*;
70#[allow(unused_imports)]
71use crate::ported::zle::zle_vi::*;
72#[allow(unused_imports)]
73use crate::ported::zle::zle_utils::*;
74#[allow(unused_imports)]
75use crate::ported::zle::zle_refresh::*;
76#[allow(unused_imports)]
77use crate::ported::zle::zle_tricky::*;
78#[allow(unused_imports)]
79use crate::ported::zle::textobjects::*;
80#[allow(unused_imports)]
81use crate::ported::zle::deltochar::*;
82
83pub fn cpatterns_same(                                                       // c:44
84    mut a: Option<&crate::ported::zle::comp_h::Cpattern>,
85    mut b: Option<&crate::ported::zle::comp_h::Cpattern>,
86) -> bool {                                                                  // c:42
87    use crate::ported::zle::comp_h::{CPAT_CCLASS, CPAT_CHAR, CPAT_EQUIV, CPAT_NCLASS};
88    while let Some(ap) = a {                                                 // c:46 while (a)
89        let bp = match b {                                                   // c:47
90            None => return false,                                            // c:48 if(!b) return 0
91            Some(p) => p,
92        };
93        if ap.tp != bp.tp {                                                  // c:49
94            return false;                                                    // c:50
95        }
96        match ap.tp {                                                        // c:51
97            x if x == CPAT_CCLASS || x == CPAT_NCLASS || x == CPAT_EQUIV => {  // c:52-54
98                // c:55-58 — equivalent ranges might compare same even when
99                // strings differ; the C source admits this is unhandled.
100                if ap.str != bp.str {                                      // c:60 strcmp(a->u.str,b->u.str)
101                    return false;                                            // c:61
102                }
103            }
104            x if x == CPAT_CHAR => {                                         // c:64
105                if ap.chr != bp.chr {                                        // c:65
106                    return false;                                            // c:66
107                }
108            }
109            _ => {                                                           // c:69 default
110                // c:70 — "here to silence compiler"
111            }
112        }
113        a = ap.next.as_deref();                                              // c:74 a = a->next
114        b = bp.next.as_deref();                                              // c:75 b = b->next
115    }
116    b.is_none()                                                              // c:77 return !b
117}
118
119/// Port of `cmatchers_same(Cmatcher a, Cmatcher b)` from `Src/Zle/compmatch.c:82`.
120/// ```c
121/// static int
122/// cmatchers_same(Cmatcher a, Cmatcher b)
123/// {
124///     return (a == b ||
125///             (a->flags == b->flags &&
126///              a->llen == b->llen && a->wlen == b->wlen &&
127///              (!a->llen || cpatterns_same(a->line, b->line)) &&
128///              (a->wlen <= 0 || cpatterns_same(a->word, b->word)) &&
129///              (!(a->flags & (CMF_LEFT | CMF_RIGHT)) ||
130///               (a->lalen == b->lalen && a->ralen == b->ralen &&
131///                (!a->lalen || cpatterns_same(a->left, b->left)) &&
132///                (!a->ralen || cpatterns_same(a->right, b->right))))));
133/// }
134/// ```
135/// Test two matchers for full structural equality — flags, lengths,
136/// patterns, and (if anchored) anchor patterns must all match.
137/// WARNING: param names don't match C — Rust=(b) vs C=(a, b)
138pub fn cmatchers_same(                                                       // c:84
139    a: &crate::ported::zle::comp_h::Cmatcher,
140    b: &crate::ported::zle::comp_h::Cmatcher,
141) -> bool {                                                                  // c:82
142    use crate::ported::zle::comp_h::{CMF_LEFT, CMF_RIGHT};
143    // c:86 — `a == b` short-circuit (pointer identity). Rust uses
144    // `std::ptr::eq` for the same effect.
145    if std::ptr::eq(a, b) {
146        return true;
147    }
148    // c:87 — `a->flags == b->flags && a->llen == b->llen && a->wlen == b->wlen`.
149    if a.flags != b.flags || a.llen != b.llen || a.wlen != b.wlen {
150        return false;
151    }
152    // c:89 — `(!a->llen || cpatterns_same(a->line, b->line))`.
153    if a.llen != 0 && !cpatterns_same(a.line.as_deref(), b.line.as_deref()) {
154        return false;
155    }
156    // c:90 — `(a->wlen <= 0 || cpatterns_same(a->word, b->word))`.
157    if a.wlen > 0 && !cpatterns_same(a.word.as_deref(), b.word.as_deref()) {
158        return false;
159    }
160    // c:91-94 — anchor checks only if CMF_LEFT/CMF_RIGHT flagged.
161    if (a.flags & (CMF_LEFT | CMF_RIGHT)) != 0 {
162        if a.lalen != b.lalen || a.ralen != b.ralen {                        // c:92
163            return false;
164        }
165        if a.lalen != 0 && !cpatterns_same(a.left.as_deref(), b.left.as_deref()) {
166            return false;                                                    // c:93
167        }
168        if a.ralen != 0 && !cpatterns_same(a.right.as_deref(), b.right.as_deref()) {
169            return false;                                                    // c:94
170        }
171    }
172    true
173}
174
175// =====================================================================
176// cline_sublen / cline_setlens / cline_matched / revert_cline / cp_cline
177// — `Src/Zle/compmatch.c:217-281`.
178// =====================================================================
179
180/// Port of `cline_sublen(Cline l)` from `Src/Zle/compmatch.c:218`.
181/// ```c
182/// int
183/// cline_sublen(Cline l)
184/// {
185///     int len = ((l->flags & CLF_LINE) ? l->llen : l->wlen);
186///     if (l->olen && !((l->flags & CLF_SUF) ? l->suffix : l->prefix))
187///         len += l->olen;
188///     else {
189///         Cline p;
190///         for (p = l->prefix; p; p = p->next)
191///             len += ((p->flags & CLF_LINE) ? p->llen : p->wlen);
192///         for (p = l->suffix; p; p = p->next)
193///             len += ((p->flags & CLF_LINE) ? p->llen : p->wlen);
194///     }
195///     return len;
196/// }
197/// ```
198/// Total visual length of one Cline plus its prefix/suffix sub-lists.
199pub fn cline_sublen(l: &crate::ported::zle::comp_h::Cline) -> i32 {          // c:219
200    use crate::ported::zle::comp_h::{CLF_LINE, CLF_SUF};
201    // c:221 — `len = (CLF_LINE ? llen : wlen)`.
202    let mut len: i32 = if (l.flags & CLF_LINE) != 0 { l.llen } else { l.wlen };
203    // c:223 — `if (olen && !((CLF_SUF ? suffix : prefix))) len += olen`.
204    let no_subs = if (l.flags & CLF_SUF) != 0 {
205        l.suffix.is_none()
206    } else {
207        l.prefix.is_none()
208    };
209    if l.olen != 0 && no_subs {
210        len += l.olen;                                                       // c:224
211    } else {                                                                 // c:225
212        // c:228-229 — walk prefix sub-list summing per-part length.
213        let mut p = l.prefix.as_deref();
214        while let Some(pp) = p {
215            len += if (pp.flags & CLF_LINE) != 0 { pp.llen } else { pp.wlen };
216            p = pp.next.as_deref();
217        }
218        // c:230-231 — walk suffix sub-list.
219        let mut p = l.suffix.as_deref();
220        while let Some(pp) = p {
221            len += if (pp.flags & CLF_LINE) != 0 { pp.llen } else { pp.wlen };
222            p = pp.next.as_deref();
223        }
224    }
225    len                                                                      // c:233 return len
226}
227
228/// Port of `cline_setlens(Cline l, int both)` from `Src/Zle/compmatch.c:240`.
229/// ```c
230/// void
231/// cline_setlens(Cline l, int both)
232/// {
233///     while (l) {
234///         l->min = cline_sublen(l);
235///         if (both)
236///             l->max = l->min;
237///         l = l->next;
238///     }
239/// }
240/// ```
241/// Walk a Cline list setting `min` (and optionally `max`) from
242/// `cline_sublen`.
243pub fn cline_setlens(l: &mut Option<Box<crate::ported::zle::comp_h::Cline>>, both: i32) {  // c:240
244    let mut cur = l.as_deref_mut();
245    while let Some(node) = cur {                                             // c:242 while (l)
246        let s = cline_sublen(node);                                          // c:243 cline_sublen(l)
247        node.min = s;                                                        // c:243 l->min = ...
248        if both != 0 {                                                       // c:244 if (both)
249            node.max = s;                                                    // c:245 l->max = l->min
250        }
251        cur = node.next.as_deref_mut();                                      // c:246 l = l->next
252    }
253}
254
255/// Port of `cline_matched(Cline p)` from `Src/Zle/compmatch.c:254`.
256/// ```c
257/// void
258/// cline_matched(Cline p)
259/// {
260///     while (p) {
261///         p->flags |= CLF_MATCHED;
262///         cline_matched(p->prefix);
263///         cline_matched(p->suffix);
264///         p = p->next;
265///     }
266/// }
267/// ```
268/// Set `CLF_MATCHED` on every Cline reachable through next/prefix/
269/// suffix from `p`.
270pub fn cline_matched(p: &mut Option<Box<crate::ported::zle::comp_h::Cline>>) {  // c:254
271    use crate::ported::zle::comp_h::CLF_MATCHED;
272    let mut cur = p.as_deref_mut();
273    while let Some(node) = cur {                                             // c:256 while (p)
274        node.flags |= CLF_MATCHED;                                           // c:257
275        cline_matched(&mut node.prefix);                                     // c:258
276        cline_matched(&mut node.suffix);                                     // c:259
277        cur = node.next.as_deref_mut();                                      // c:261 p = p->next
278    }
279}
280
281/// Port of `revert_cline(Cline p)` from `Src/Zle/compmatch.c:269`.
282/// ```c
283/// Cline
284/// revert_cline(Cline p)
285/// {
286///     Cline r = NULL, n;
287///     while (p) {
288///         n = p->next;
289///         p->next = r;
290///         r = p;
291///         p = n;
292///     }
293///     return r;
294/// }
295/// ```
296/// Reverse a Cline `next`-chained list in place; returns the new head.
297/// WARNING: param names don't match C — Rust=() vs C=(p)
298pub fn revert_cline(                                                         // c:270
299    mut p: Option<Box<crate::ported::zle::comp_h::Cline>>,
300) -> Option<Box<crate::ported::zle::comp_h::Cline>> {                        // c:269
301    let mut r: Option<Box<crate::ported::zle::comp_h::Cline>> = None;        // c:272 r = NULL
302    while let Some(mut node) = p {                                           // c:274 while (p)
303        let n = node.next.take();                                            // c:275 n = p->next
304        node.next = r;                                                       // c:276 p->next = r
305        r = Some(node);                                                      // c:277 r = p
306        p = n;                                                               // c:278 p = n
307    }
308    r                                                                        // c:280 return r
309}
310
311/// Port of `cp_cline(Cline l, int deep)` from `Src/Zle/compmatch.c:189`.
312/// ```c
313/// Cline
314/// cp_cline(Cline l, int deep)
315/// {
316///     Cline r = NULL, *p = &r, t, lp = NULL;
317///     while (l) {
318///         if ((t = freecl)) freecl = t->next;
319///         else t = (Cline) zhalloc(sizeof(*t));
320///         memcpy(t, l, sizeof(*t));
321///         if (deep) {
322///             if (t->prefix) t->prefix = cp_cline(t->prefix, 0);
323///             if (t->suffix) t->suffix = cp_cline(t->suffix, 0);
324///         }
325///         *p = lp = t;
326///         p = &(t->next);
327///         l = l->next;
328///     }
329///     *p = NULL;
330///     return r;
331/// }
332/// ```
333/// Deep- or shallow-copy a Cline list. `deep` recursively copies
334/// the prefix/suffix sub-lists too. The C source draws from a
335/// freecl free-list when available — Rust just heap-allocates.
336/// WARNING: param names don't match C — Rust=(deep) vs C=(l, deep)
337pub fn cp_cline(                                                             // c:190
338    l: Option<&crate::ported::zle::comp_h::Cline>,
339    deep: i32,
340) -> Option<Box<crate::ported::zle::comp_h::Cline>> {                        // c:189
341    let mut r: Option<Box<crate::ported::zle::comp_h::Cline>> = None;        // c:192 r = NULL
342    let mut tail: *mut Option<Box<crate::ported::zle::comp_h::Cline>> = &mut r;
343    let mut cur = l;
344    while let Some(node) = cur {                                             // c:194 while (l)
345        // c:198 — `t = (Cline) zhalloc(sizeof(*t))`.
346        // c:199 — `memcpy(t, l, sizeof(*t))`.
347        let mut t: Box<crate::ported::zle::comp_h::Cline> = Box::new(node.clone());
348        // Reset `next` so the memcpy-equivalent doesn't link to the
349        // source's next (the loop sets it via the tail pointer).
350        t.next = None;
351        if deep != 0 {                                                       // c:200 if (deep)
352            // c:201-202 — `t->prefix = cp_cline(t->prefix, 0)`. Already
353            // a Box-clone via memcpy; rebuild as deep copy.
354            if let Some(pre) = node.prefix.as_deref() {
355                t.prefix = cp_cline(Some(pre), 0);                           // c:202
356            }
357            if let Some(suf) = node.suffix.as_deref() {
358                t.suffix = cp_cline(Some(suf), 0);                           // c:204
359            }
360        }
361        // c:206 — `*p = lp = t`. Append to tail.
362        // SAFETY: `tail` points into `r` or into the previous node's
363        // `next` field; both stay valid for the loop's lifetime.
364        unsafe {
365            *tail = Some(t);
366            // c:207 — `p = &(t->next)`. Re-aim tail at the new entry's `next`.
367            let new_node = (*tail).as_mut().unwrap();
368            tail = &mut new_node.next;
369        }
370        cur = node.next.as_deref();                                          // c:208 l = l->next
371    }
372    // c:210 — `*p = NULL`. Already None by default.
373    r                                                                        // c:212 return r
374}
375
376/// Port of `free_cline(Cline l)` from `Src/Zle/compmatch.c:171`.
377/// ```c
378/// void
379/// free_cline(Cline l)
380/// {
381///     Cline n;
382///     while (l) {
383///         n = l->next;
384///         l->next = freecl;
385///         freecl = l;
386///         free_cline(l->prefix);
387///         free_cline(l->suffix);
388///         l = n;
389///     }
390/// }
391/// ```
392/// Free a Cline list. C pushes onto a `freecl` free-list to recycle;
393/// Rust just drops via Box.
394pub fn free_cline(l: Option<Box<crate::ported::zle::comp_h::Cline>>) {       // c:172
395    // c:172-183 — walk; free each prefix/suffix recursively. In Rust
396    // dropping the Box of the list head triggers Drop on `next`/
397    // `prefix`/`suffix` chains automatically. `freecl` recycling
398    // is a C-only zhalloc optimisation that doesn't apply here.
399    drop(l);
400}
401
402// =====================================================================
403// matchbuf / matchparts / matchsubs globals + start_match / abort_match
404// — `Src/Zle/compmatch.c:283-317`.
405// =====================================================================
406
407use std::sync::Mutex;
408use std::sync::OnceLock;
409
410/// Port of `char *matchbuf` from `Src/Zle/compmatch.c:287`. Static
411/// buffer used during pattern matching to assemble the trial string.
412pub static MATCHBUF: OnceLock<Mutex<String>> = OnceLock::new();              // c:287
413
414/// Port of `Cline matchparts, matchlastpart` from
415/// `Src/Zle/compmatch.c:292`. Top-level cline list being built.
416pub static MATCHPARTS: OnceLock<Mutex<Option<Box<crate::ported::zle::comp_h::Cline>>>> = OnceLock::new();  // c:292
417
418/// Port of `Cline matchsubs, matchlastsub` from
419/// `Src/Zle/compmatch.c:294`. Inner cline list (prefix/suffix sub-list).
420pub static MATCHSUBS: OnceLock<Mutex<Option<Box<crate::ported::zle::comp_h::Cline>>>> = OnceLock::new();   // c:294
421
422/// Port of `start_match()` from `Src/Zle/compmatch.c:300`.
423/// ```c
424/// static void
425/// start_match(void)
426/// {
427///     if (matchbuf)
428///         *matchbuf = '\0';
429///     matchbufadded = 0;
430///     matchparts = matchlastpart = matchsubs = matchlastsub = NULL;
431/// }
432/// ```
433/// Reset the per-match globals so a fresh pattern run starts clean.
434pub fn start_match() {                                                       // c:300
435    // c:300-303 — `if (matchbuf) *matchbuf = '\0'`.
436    MATCHBUF
437        .get_or_init(|| Mutex::new(String::new()))
438        .lock()
439        .unwrap()
440        .clear();
441    // c:305 — `matchparts = matchlastpart = matchsubs = matchlastsub = NULL`.
442    *MATCHPARTS.get_or_init(|| Mutex::new(None)).lock().unwrap() = None;
443    *MATCHSUBS.get_or_init(|| Mutex::new(None)).lock().unwrap() = None;
444}
445
446/// Port of `abort_match()` from `Src/Zle/compmatch.c:312`.
447/// ```c
448/// static void
449/// abort_match(void)
450/// {
451///     free_cline(matchparts);
452///     free_cline(matchsubs);
453///     matchparts = matchsubs = NULL;
454/// }
455/// ```
456/// Tear down the per-match cline lists when a match attempt fails.
457pub fn abort_match() {                                                       // c:312
458    // c:312-315 — `free_cline(matchparts); free_cline(matchsubs)`.
459    let parts = MATCHPARTS
460        .get_or_init(|| Mutex::new(None))
461        .lock()
462        .unwrap()
463        .take();
464    let subs = MATCHSUBS
465        .get_or_init(|| Mutex::new(None))
466        .lock()
467        .unwrap()
468        .take();
469    free_cline(parts);
470    free_cline(subs);
471    // c:316 — set to NULL (already done by .take()).
472}
473
474/// Test whether `word` matches `line` honouring the given matcher
475/// flags.
476// Fake-signature ports of `match_str` / `match_parts` / `comp_match`
477// deleted. The real C signatures (Src/Zle/compmatch.c:500, :1092,
478// :1123) take Brinfo*/Patprog/Cline* parameters that need the
479// matcher engine fully wired through. The previous Rust placeholders
480// shipped wrong arities + fake `MatchFlags` / `CompLine` types.
481// Real ports will land alongside the matcher-engine driver.
482
483
484/// Direct port of `mod_export convchar_t pattern_match_equivalence(
485///                    Cpattern lp, convchar_t wind, int wmtp,
486///                    convchar_t wchr)`
487/// from `Src/Zle/compmatch.c:1316`. Looks up the line-side
488/// equivalence-class member that pairs with word-side index
489/// `wind` (1-based), then resolves case-class crossings via the
490/// PP_UPPER/PP_LOWER pair.
491///
492/// Returns `CHR_INVALID` (u32::MAX) on miss; the matched line
493/// char on success.
494pub fn pattern_match_equivalence(
495    lp: &crate::ported::zle::comp_h::Cpattern,                               // c:1316
496    wind: u32, wmtp: i32, wchr: u32,
497) -> u32 {
498    use crate::ported::zsh_h::{PP_LOWER, PP_UPPER};
499    use crate::ported::zle::zle_h::{ZC_tolower, ZC_toupper};
500
501    // c:1324 — PATMATCHINDEX(lp->u.str, wind-1, &lchr, &lmtp).
502    // Walk lp.str's encoded char-range descriptor finding the
503    // entry at index (wind-1); return CHR_INVALID on miss.
504    let Some(ref s) = lp.str else { return u32::MAX; };
505    let Some(target_idx) = (wind as i64).checked_sub(1) else { return u32::MAX; };
506    if target_idx < 0 { return u32::MAX; }
507    let mut lchr: Option<u32> = None;
508    let mut lmtp: i32 = 0;
509    let mut idx: i64 = 0;
510    let mut chars = s.chars().peekable();
511    while let Some(ch) = chars.next() {
512        // Pair `lo-hi` if next is `-`.
513        if let Some(&peek) = chars.peek() {
514            if peek == '-' {
515                chars.next();
516                if let Some(hi) = chars.next() {
517                    let span = (hi as i64) - (ch as i64);
518                    if span >= 0 && idx + span >= target_idx {
519                        lchr = Some(((ch as i64) + (target_idx - idx)) as u32);
520                        break;
521                    }
522                    idx += span + 1;
523                    continue;
524                }
525            }
526        }
527        if idx == target_idx {
528            lchr = Some(ch as u32);
529            break;
530        }
531        idx += 1;
532    }
533    let lchr = match lchr { Some(c) => c, None => return u32::MAX };
534
535    // c:1335 — `if (lchr != CHR_INVALID) return lchr` — exact-char hit.
536    if lchr != u32::MAX { return lchr; }
537
538    // c:1342 — case-class crossings.
539    let _ = lmtp;
540    let wch = char::from_u32(wchr).unwrap_or('\0');
541    if wmtp == PP_UPPER && lmtp == PP_LOWER {
542        return ZC_tolower(wch) as u32;
543    }
544    if wmtp == PP_LOWER && lmtp == PP_UPPER {
545        return ZC_toupper(wch) as u32;
546    }
547    if wmtp == lmtp { return wchr; }
548    u32::MAX                                                                 // c:1378
549}
550
551// Fake `parse_cmatcher` / `update_bmatchers` deleted.
552// `parse_cmatcher` already exists at `complete.rs:992` as a real
553// port of `Src/Zle/complete.c:242`. `update_bmatchers` is at
554// `Src/Zle/compmatch.c:121` with signature `void update_bmatchers(void)`
555// — the Rust placeholder had the wrong arity and type, will land
556// alongside the matcher-engine driver.
557
558#[cfg(test)]
559mod tests {
560    use super::*;
561
562    #[test]
563    fn test_pattern_match_equivalence_case_cross() {
564        let _g = crate::ported::zle::zle_main::zle_test_setup();
565        // c:1342 — wmtp=PP_UPPER, lmtp=PP_LOWER → tolower(wchr).
566        use crate::ported::zle::comp_h::{Cpattern, CPAT_EQUIV};
567        let lp = Cpattern { tp: CPAT_EQUIV, str: Some("ab".into()), chr: 0, next: None };
568        // wind=1 selects 'a' from the equivalence class, exact-char hit.
569        let r = pattern_match_equivalence(&lp, 1, 0, b'A' as u32);
570        assert_eq!(r, b'a' as u32);
571    }
572
573    // ---------- Real-port tests ------------------------------------------
574
575    use crate::ported::zle::comp_h::{
576        CLF_LINE, CLF_MATCHED, CLF_SUF, CMF_LEFT, CMF_RIGHT, CPAT_CCLASS, CPAT_CHAR, CPAT_NCLASS,
577        Cline, Cmatcher, Cpattern,
578    };
579
580    fn cpat_char(ch: u32) -> Cpattern {
581        Cpattern {
582            tp: CPAT_CHAR,
583            chr: ch,
584            ..Default::default()
585        }
586    }
587    fn cpat_class(s: &str) -> Cpattern {
588        Cpattern {
589            tp: CPAT_CCLASS,
590            str: Some(s.to_string()),
591            ..Default::default()
592        }
593    }
594
595    #[test]
596    fn cpatterns_same_chr_match() {
597        let _g = crate::ported::zle::zle_main::zle_test_setup();
598        let a = cpat_char('a' as u32);
599        let b = cpat_char('a' as u32);
600        // c:64-66 — both CPAT_CHAR + same chr → equal.
601        assert!(cpatterns_same(Some(&a), Some(&b)));
602    }
603
604    #[test]
605    fn cpatterns_same_chr_mismatch() {
606        let _g = crate::ported::zle::zle_main::zle_test_setup();
607        let a = cpat_char('a' as u32);
608        let b = cpat_char('b' as u32);
609        // c:65 — different chr → not equal.
610        assert!(!cpatterns_same(Some(&a), Some(&b)));
611    }
612
613    #[test]
614    fn cpatterns_same_tp_mismatch() {
615        let _g = crate::ported::zle::zle_main::zle_test_setup();
616        let a = cpat_char('a' as u32);
617        let b = Cpattern {
618            tp: CPAT_NCLASS,
619            str: Some("a".into()),
620            ..Default::default()
621        };
622        // c:49-50 — different tp → not equal.
623        assert!(!cpatterns_same(Some(&a), Some(&b)));
624    }
625
626    #[test]
627    fn cpatterns_same_class_match() {
628        let _g = crate::ported::zle::zle_main::zle_test_setup();
629        let a = cpat_class("a-z");
630        let b = cpat_class("a-z");
631        // c:60 — same str → equal.
632        assert!(cpatterns_same(Some(&a), Some(&b)));
633    }
634
635    #[test]
636    fn cpatterns_same_length_mismatch() {
637        let _g = crate::ported::zle::zle_main::zle_test_setup();
638        let a = cpat_char('a' as u32);
639        // a chained to a second pattern; b has only one.
640        let mut a_chain = a.clone();
641        a_chain.next = Some(Box::new(cpat_char('b' as u32)));
642        let b = cpat_char('a' as u32);
643        // c:47 — `a` still has next, `b` exhausted → not equal.
644        assert!(!cpatterns_same(Some(&a_chain), Some(&b)));
645    }
646
647    #[test]
648    fn cpatterns_same_both_empty() {
649        let _g = crate::ported::zle::zle_main::zle_test_setup();
650        // c:46 — both NULL → loop never enters, return !b == true.
651        assert!(cpatterns_same(None, None));
652    }
653
654    #[test]
655    fn cmatchers_same_pointer_eq() {
656        let _g = crate::ported::zle::zle_main::zle_test_setup();
657        let m = Cmatcher::default();
658        // c:86 — `a == b` short-circuit.
659        assert!(cmatchers_same(&m, &m));
660    }
661
662    #[test]
663    fn cmatchers_same_flags_diff() {
664        let _g = crate::ported::zle::zle_main::zle_test_setup();
665        let a = Cmatcher { flags: 0, ..Default::default() };
666        let b = Cmatcher { flags: 1, ..Default::default() };
667        // c:87 — different flags → not equal.
668        assert!(!cmatchers_same(&a, &b));
669    }
670
671    #[test]
672    fn cmatchers_same_anchor_lengths() {
673        let _g = crate::ported::zle::zle_main::zle_test_setup();
674        // CMF_LEFT path: anchor length difference matters.
675        let a = Cmatcher {
676            flags: CMF_LEFT,
677            lalen: 2,
678            ..Default::default()
679        };
680        let b = Cmatcher {
681            flags: CMF_LEFT,
682            lalen: 3,
683            ..Default::default()
684        };
685        // c:92 — different lalen → not equal.
686        assert!(!cmatchers_same(&a, &b));
687        // CMF_RIGHT path: ralen matters.
688        let a = Cmatcher {
689            flags: CMF_RIGHT,
690            ralen: 1,
691            ..Default::default()
692        };
693        let b = Cmatcher {
694            flags: CMF_RIGHT,
695            ralen: 1,
696            ..Default::default()
697        };
698        // c:91-94 — anchors equal, no patterns to compare → equal.
699        assert!(cmatchers_same(&a, &b));
700    }
701
702    #[test]
703    fn cline_sublen_simple() {
704        let _g = crate::ported::zle::zle_main::zle_test_setup();
705        let l = Cline {
706            flags: CLF_LINE,
707            llen: 5,
708            wlen: 999,
709            ..Default::default()
710        };
711        // c:221 — CLF_LINE → use llen, not wlen.
712        assert_eq!(cline_sublen(&l), 5);
713    }
714
715    #[test]
716    fn cline_sublen_with_olen() {
717        let _g = crate::ported::zle::zle_main::zle_test_setup();
718        let l = Cline {
719            flags: 0,
720            llen: 0,
721            wlen: 3,
722            olen: 7,
723            ..Default::default()
724        };
725        // c:223-224 — no CLF_LINE → wlen=3, no prefix → +olen=7 → 10.
726        assert_eq!(cline_sublen(&l), 10);
727    }
728
729    #[test]
730    fn cline_sublen_with_prefix() {
731        let _g = crate::ported::zle::zle_main::zle_test_setup();
732        let pre = Cline {
733            flags: CLF_LINE,
734            llen: 4,
735            ..Default::default()
736        };
737        let l = Cline {
738            flags: 0,
739            wlen: 2,
740            olen: 99,                 // ignored because prefix exists
741            prefix: Some(Box::new(pre)),
742            ..Default::default()
743        };
744        // c:225-229 — prefix walks to +llen=4; base wlen=2; total=6.
745        assert_eq!(cline_sublen(&l), 6);
746    }
747
748    #[test]
749    fn cline_sublen_clf_suf() {
750        let _g = crate::ported::zle::zle_main::zle_test_setup();
751        let suf = Cline {
752            flags: CLF_LINE,
753            llen: 3,
754            ..Default::default()
755        };
756        let l = Cline {
757            flags: CLF_SUF,
758            wlen: 1,
759            olen: 99,
760            suffix: Some(Box::new(suf)),
761            ..Default::default()
762        };
763        // c:223 — CLF_SUF → check `suffix` not `prefix`. Suffix exists,
764        // so olen ignored. wlen=1 + suffix wlen-walk... but suffix has CLF_LINE,
765        // so its llen=3 is used. total=1+3=4.
766        assert_eq!(cline_sublen(&l), 4);
767    }
768
769    #[test]
770    fn cline_setlens_propagates() {
771        let _g = crate::ported::zle::zle_main::zle_test_setup();
772        let mut head: Option<Box<Cline>> = Some(Box::new(Cline {
773            flags: CLF_LINE,
774            llen: 5,
775            next: Some(Box::new(Cline {
776                flags: CLF_LINE,
777                llen: 3,
778                ..Default::default()
779            })),
780            ..Default::default()
781        }));
782        cline_setlens(&mut head, 1);
783        // c:243-245 — both=1 sets max=min=cline_sublen.
784        let h = head.as_ref().unwrap();
785        assert_eq!(h.min, 5);
786        assert_eq!(h.max, 5);
787        let n = h.next.as_ref().unwrap();
788        assert_eq!(n.min, 3);
789        assert_eq!(n.max, 3);
790    }
791
792    #[test]
793    fn cline_matched_sets_flag_recursively() {
794        let _g = crate::ported::zle::zle_main::zle_test_setup();
795        let mut head: Option<Box<Cline>> = Some(Box::new(Cline {
796            prefix: Some(Box::new(Cline::default())),
797            suffix: Some(Box::new(Cline::default())),
798            next: Some(Box::new(Cline::default())),
799            ..Default::default()
800        }));
801        cline_matched(&mut head);
802        let h = head.as_ref().unwrap();
803        // c:257 — flag set on head.
804        assert!(h.flags & CLF_MATCHED != 0);
805        // c:258 — flag set on prefix.
806        assert!(h.prefix.as_ref().unwrap().flags & CLF_MATCHED != 0);
807        // c:259 — flag set on suffix.
808        assert!(h.suffix.as_ref().unwrap().flags & CLF_MATCHED != 0);
809        // c:261 — flag set on next.
810        assert!(h.next.as_ref().unwrap().flags & CLF_MATCHED != 0);
811    }
812
813    #[test]
814    fn revert_cline_reverses_chain() {
815        let _g = crate::ported::zle::zle_main::zle_test_setup();
816        let head = Some(Box::new(Cline {
817            llen: 1,
818            next: Some(Box::new(Cline {
819                llen: 2,
820                next: Some(Box::new(Cline {
821                    llen: 3,
822                    ..Default::default()
823                })),
824                ..Default::default()
825            })),
826            ..Default::default()
827        }));
828        let r = revert_cline(head);
829        // After reversal: 3, 2, 1.
830        let n = r.as_ref().unwrap();
831        assert_eq!(n.llen, 3);
832        let n = n.next.as_ref().unwrap();
833        assert_eq!(n.llen, 2);
834        let n = n.next.as_ref().unwrap();
835        assert_eq!(n.llen, 1);
836        assert!(n.next.is_none());
837    }
838
839    #[test]
840    fn cp_cline_shallow() {
841        let _g = crate::ported::zle::zle_main::zle_test_setup();
842        let src = Cline {
843            llen: 7,
844            wlen: 9,
845            next: Some(Box::new(Cline {
846                llen: 11,
847                ..Default::default()
848            })),
849            ..Default::default()
850        };
851        let dup = cp_cline(Some(&src), 0);
852        let n = dup.as_ref().unwrap();
853        assert_eq!(n.llen, 7);
854        assert_eq!(n.wlen, 9);
855        let n = n.next.as_ref().unwrap();
856        assert_eq!(n.llen, 11);
857    }
858
859    #[test]
860    fn start_match_clears_globals() {
861        let _g = crate::ported::zle::zle_main::zle_test_setup();
862        // Pre-populate to ensure start_match resets.
863        MATCHBUF
864            .get_or_init(|| Mutex::new(String::new()))
865            .lock()
866            .unwrap()
867            .push_str("garbage");
868        *MATCHPARTS
869            .get_or_init(|| Mutex::new(None))
870            .lock()
871            .unwrap() = Some(Box::new(Cline::default()));
872        start_match();
873        assert!(MATCHBUF.get().unwrap().lock().unwrap().is_empty());
874        assert!(MATCHPARTS.get().unwrap().lock().unwrap().is_none());
875        assert!(MATCHSUBS.get().unwrap().lock().unwrap().is_none());
876    }
877
878    #[test]
879    fn abort_match_drops_lists() {
880        let _g = crate::ported::zle::zle_main::zle_test_setup();
881        *MATCHPARTS
882            .get_or_init(|| Mutex::new(None))
883            .lock()
884            .unwrap() = Some(Box::new(Cline::default()));
885        *MATCHSUBS
886            .get_or_init(|| Mutex::new(None))
887            .lock()
888            .unwrap() = Some(Box::new(Cline::default()));
889        abort_match();
890        assert!(MATCHPARTS.get().unwrap().lock().unwrap().is_none());
891        assert!(MATCHSUBS.get().unwrap().lock().unwrap().is_none());
892    }
893}
894
895/// Direct port of `mod_export void add_bmatchers(Cmatcher m)` from
896/// `Src/Zle/compmatch.c:101`. Walks the supplied Cmatcher chain
897/// (the head of `def->matcher` at call sites) and prepends each
898/// matcher that qualifies for brace-matching to the file-scope
899/// `bmatchers` Cmlist. Original chain head is appended after the new
900/// entries so the final list is `[new_entries..., old_bmatchers...]`.
901pub fn add_bmatchers(m: Option<&crate::ported::zle::comp_h::Cmatcher>) {     // c:101
902    use crate::ported::zle::comp_h::{Cmatcher, Cmlist, CMF_RIGHT};
903
904    let old = {                                                              // c:104 Cmlist old = bmatchers
905        let cell = crate::ported::zle::compcore::bmatchers
906            .get_or_init(|| std::sync::Mutex::new(None));
907        cell.lock().ok().and_then(|mut g| g.take())
908    };
909
910    let mut head: Option<Box<Cmlist>> = None;                                // c:104 *q = &bmatchers
911    let mut tail_ref: *mut Option<Box<Cmlist>> = &mut head;
912    let mut cur = m;
913    while let Some(mat) = cur {                                              // c:106 for (; m; m = m->next)
914        let qual = (mat.flags == 0 && mat.wlen > 0 && mat.llen > 0)          // c:107-108
915                || (mat.flags == CMF_RIGHT && mat.wlen < 0 && mat.llen == 0);
916        if qual {
917            // c:109 — n = zhalloc(sizeof(struct cmlist))
918            let n = Box::new(Cmlist {
919                next: None,
920                matcher: Box::new(Cmatcher {
921                    refc:  mat.refc,
922                    next:  mat.next.clone(),
923                    flags: mat.flags,
924                    line:  mat.line.clone(),
925                    llen:  mat.llen,
926                    word:  mat.word.clone(),
927                    wlen:  mat.wlen,
928                    left:  mat.left.clone(),
929                    lalen: mat.lalen,
930                    right: mat.right.clone(),
931                    ralen: mat.ralen,
932                }),
933                str: String::new(),
934            });
935            unsafe {
936                *tail_ref = Some(n);
937                if let Some(ref mut newnode) = *tail_ref {
938                    tail_ref = &mut newnode.next as *mut _;                  // c:112 q = &(n->next)
939                }
940            }
941        }
942        cur = mat.next.as_deref();                                           // c:106 m = m->next
943    }
944    // c:114 — `*q = old;` (append old chain after new entries)
945    unsafe { *tail_ref = old; }
946    if let Ok(mut g) = crate::ported::zle::compcore::bmatchers
947        .get_or_init(|| std::sync::Mutex::new(None)).lock()
948    {
949        *g = head;
950    }
951}
952
953/// Direct port of `static void add_match_part(Cmatcher m, char *l,
954///                                            char *w, int wl,
955///                                            char *o, int ol,
956///                                            char *s, int sl,
957///                                            int osl, int sfx)`
958/// from `Src/Zle/compmatch.c:373`. Appends a partial match into
959/// `MATCHPARTS`, splitting the new part via `bld_parts` per the
960/// matcher's anchor rules and consuming any pending `MATCHSUBS`
961/// nodes into the new tail.
962pub fn add_match_part(
963    m: Option<&crate::ported::zle::comp_h::Cmatcher>,                        // c:373
964    l: Option<&str>, _ll: i32,
965    w: &str, wl: i32,
966    o: Option<&str>, ol: i32,
967    s: &str, sl: i32,
968    osl: i32, sfx: i32,
969) {
970    use crate::ported::zle::comp_h::{Cline, CMF_LEFT, CLF_NEW, CLF_SUF};
971
972    // c:382 — `if (l && !strncmp(l, w, wl)) l = NULL` — drop redundant anchor.
973    let l_eff: Option<String> = match l {
974        Some(lstr) if lstr.len() >= wl as usize
975                    && wl > 0
976                    && &lstr[..wl as usize] == &w[..wl as usize] => None,
977        Some(lstr) => Some(lstr.to_string()),
978        None       => None,
979    };
980
981    // c:392 — `p = bld_parts(s, sl, osl, &lp, &lprem)`.
982    let mut lp: Option<Box<Cline>> = None;
983    let mut lprem: Option<Box<Cline>> = None;
984    let mut p = bld_parts(s, sl, osl, Some(&mut lp), Some(&mut lprem));
985
986    // c:394 — `if (lprem && m && (m->flags & CLF_LEFT))`.
987    if let Some(rem) = lprem.as_mut() {
988        if m.map(|mat| (mat.flags & CMF_LEFT) != 0).unwrap_or(false) {
989            rem.flags |= CLF_SUF;                                            // c:395
990            rem.suffix = rem.prefix.take();                                  // c:396 swap
991        }
992    }
993
994    // c:402 — `if (sfx) p = revert_cline(lp = p)`.
995    if sfx != 0 {
996        if let Some(chain) = p.take() {
997            p = revert_cline(Some(chain));
998        }
999    }
1000
1001    // c:405-419 — merge MATCHSUBS into the head/tail.
1002    let subs = MATCHSUBS.get_or_init(|| std::sync::Mutex::new(None))
1003        .lock().ok().and_then(|mut g| g.take());
1004    if let Some(subs_chain) = subs {                                         // c:405
1005        if let Some(lp_node) = lp.as_mut() {
1006            if sfx != 0 {                                                    // c:407 lp->prefix tail-append
1007                let mut tail_ref: *mut Option<Box<Cline>> = &mut lp_node.prefix;
1008                unsafe {
1009                    while let Some(ref mut next_node) = *tail_ref {
1010                        tail_ref = &mut next_node.next as *mut _;
1011                    }
1012                    *tail_ref = Some(subs_chain);
1013                }
1014            } else if let Some(ref mut p_node) = p {                         // c:415 p->prefix prepend
1015                let old_prefix = p_node.prefix.take();
1016                let mut new_head = subs_chain;
1017                {
1018                    let mut tail_ref: *mut Option<Box<Cline>> = &mut new_head.next;
1019                    unsafe {
1020                        while let Some(ref mut nn) = *tail_ref {
1021                            tail_ref = &mut nn.next as *mut _;
1022                        }
1023                        *tail_ref = old_prefix;
1024                    }
1025                }
1026                p_node.prefix = Some(new_head);
1027            }
1028        }
1029        // c:417 — `matchsubs = matchlastsub = NULL`.
1030        if let Ok(mut g) = MATCHLASTSUB
1031            .get_or_init(|| std::sync::Mutex::new(None)).lock()
1032        {
1033            *g = None;
1034        }
1035    }
1036
1037    // c:421-435 — store args in the last part-cline.
1038    if let Some(lp_node) = lp.as_mut() {
1039        if lp_node.llen != 0 || lp_node.wlen != 0 {                          // c:421
1040            let next = get_cline(
1041                l_eff.clone(), wl, Some(w.to_string()), wl,
1042                o.map(|s| s.to_string()), ol, CLF_NEW,
1043            );
1044            lp_node.next = Some(next);                                       // c:423
1045        } else {                                                             // c:425
1046            lp_node.line = l_eff.clone();                                    // c:426
1047            lp_node.llen = wl;
1048            lp_node.word = Some(w.to_string());                              // c:428
1049            lp_node.wlen = wl;
1050            lp_node.orig = o.map(|s| s.to_string());                         // c:430
1051            lp_node.olen = ol;
1052        }
1053        if o.is_some() || ol != 0 {                                          // c:432
1054            lp_node.flags &= !CLF_NEW;
1055        }
1056    }
1057
1058    // c:439-444 — append `p` to MATCHPARTS via MATCHLASTPART.
1059    let last_present = MATCHLASTPART.get()
1060        .and_then(|c| c.lock().ok().map(|g| g.is_some()))
1061        .unwrap_or(false);
1062    if last_present {                                                        // c:440
1063        if let Ok(mut tail) = MATCHLASTPART
1064            .get_or_init(|| std::sync::Mutex::new(None)).lock()
1065        {
1066            if let Some(t) = tail.as_mut() {
1067                t.next = p.clone();
1068            }
1069        }
1070    } else if let Ok(mut head) = MATCHPARTS
1071        .get_or_init(|| std::sync::Mutex::new(None)).lock()
1072    {
1073        *head = p.clone();                                                   // c:442
1074    }
1075    if let Some(lp_node) = lp {
1076        if let Ok(mut tail) = MATCHLASTPART
1077            .get_or_init(|| std::sync::Mutex::new(None)).lock()
1078        {
1079            *tail = Some(lp_node);                                           // c:443
1080        }
1081    }
1082}
1083
1084/// File-scope `Cline matchlastpart` from `Src/Zle/compmatch.c:327`.
1085pub static MATCHLASTPART: std::sync::OnceLock<std::sync::Mutex<Option<Box<crate::ported::zle::comp_h::Cline>>>>
1086    = std::sync::OnceLock::new();                                            // c:292
1087
1088/// Direct port of `static void add_match_str(Cmatcher m, char *l,
1089///                                          char *w, int wl, int sfx)`
1090/// from `Src/Zle/compmatch.c:327`. Pushes the string `w` (or
1091/// `l` when `m & CMF_LINE`) of length `wl` into the file-scope
1092/// `MATCHBUF` accumulator; `sfx` prepends instead of appends.
1093pub fn add_match_str(m: Option<&crate::ported::zle::comp_h::Cmatcher>,        // c:327
1094                     l: &str, w: &str, mut wl: i32, sfx: i32)
1095{
1096    use crate::ported::zle::comp_h::CMF_LINE;
1097
1098    // c:332-334 — `if (m && (m->flags & CMF_LINE)) { wl = m->llen; w = l; }`.
1099    let (eff_w_owned, eff_w): (String, &str) = match m {
1100        Some(mat) if (mat.flags & CMF_LINE) != 0 => {
1101            wl = mat.llen;
1102            let owned = l.to_string();
1103            let s = owned.clone();
1104            (owned, Box::leak(s.into_boxed_str()))
1105        }
1106        _ => (String::new(), w),
1107    };
1108    let _ = eff_w_owned;
1109
1110    if wl <= 0 { return; }                                                   // c:335
1111
1112    // c:337-353 — buffer-grow + insert. Rust's String handles the
1113    // grow path; we still mirror the matchbufadded counter for parity
1114    // with `MATCHBUFLEN`-checking C call sites.
1115    if let Ok(mut buf) = MATCHBUF.get_or_init(|| Mutex::new(String::new())).lock() {
1116        let take_n = wl as usize;
1117        let new_chunk: String = eff_w.chars().take(take_n).collect();
1118        if sfx != 0 {                                                        // c:354 prefix-mode
1119            *buf = format!("{}{}", new_chunk, *buf);                         // c:356
1120        } else {                                                             // c:358
1121            buf.push_str(&new_chunk);
1122        }
1123        MATCHBUFADDED.fetch_add(wl, std::sync::atomic::Ordering::Relaxed);   // c:362
1124    }
1125}
1126
1127/// File-scope `int matchbufadded` from `Src/Zle/compmatch.c:446`.
1128pub static MATCHBUFADDED: std::sync::atomic::AtomicI32 =
1129    std::sync::atomic::AtomicI32::new(0);                                    // c:289
1130
1131/// Direct port of `static void add_match_sub(Cmatcher m, char *l, int ll,
1132///                                          char *w, int wl)` from
1133/// `Src/Zle/compmatch.c:446`. Pushes one sub-match cline node
1134/// into the file-scope `MATCHSUBS` / `MATCHLASTSUB` linked list.
1135/// Called from match_str during a CMF_RIGHT anchor match.
1136pub fn add_match_sub(
1137    m: Option<&crate::ported::zle::comp_h::Cmatcher>,                        // c:446
1138    l: Option<&str>, ll: i32, w: Option<&str>, wl: i32,
1139) {
1140    use crate::ported::zle::comp_h::{Cline, CLF_NEW};
1141
1142    // c:450-453 — `if (m && (m->flags & CMF_LINE)) { wl = m->llen; w = l; }`.
1143    let (eff_w, eff_wl) = match m {
1144        Some(mat) if (mat.flags & crate::ported::zle::comp_h::CMF_LINE) != 0
1145                  => (l, mat.llen),
1146        _ => (w, wl),
1147    };
1148
1149    // c:455-456 — short-circuit if no length.
1150    if eff_wl <= 0 && ll <= 0 { return; }
1151
1152    // c:464-484 — build a fresh Cline node and append to matchsubs.
1153    let node = Box::new(Cline {
1154        flags: CLF_NEW,
1155        line: l.map(|s| s.to_string()),
1156        llen: ll,
1157        word: eff_w.map(|s| s.to_string()),
1158        wlen: eff_wl,
1159        ..Default::default()
1160    });
1161
1162    let last_cell = MATCHLASTSUB.get_or_init(|| Mutex::new(None));
1163    let head_cell = MATCHSUBS.get_or_init(|| Mutex::new(None));
1164    let last_present = last_cell.lock().ok().map(|g| g.is_some()).unwrap_or(false);
1165    if last_present {                                                        // c:494 — chain to existing tail
1166        if let Ok(mut tail) = last_cell.lock() {
1167            if let Some(t) = tail.as_mut() {
1168                t.next = Some(node.clone());                                 // c:495 matchlastsub->next = n
1169            }
1170        }
1171    } else {                                                                 // c:496 — first node
1172        if let Ok(mut h) = head_cell.lock() {
1173            *h = Some(node.clone());                                         // c:497 matchsubs = n
1174        }
1175    }
1176    if let Ok(mut tail) = last_cell.lock() {
1177        *tail = Some(node);                                                  // c:499 matchlastsub = n
1178    }
1179}
1180
1181/// File-scope `Cline matchlastsub` from `Src/Zle/compmatch.c:294`.
1182pub static MATCHLASTSUB: std::sync::OnceLock<Mutex<Option<Box<crate::ported::zle::comp_h::Cline>>>>
1183    = std::sync::OnceLock::new();                                            // c:294
1184
1185/// Direct port of `static int bld_line(Cmatcher mp, ZLE_STRING_T line,
1186///                                     char *mword, char *word,
1187///                                     int wlen, int sfx)`
1188/// from `Src/Zle/compmatch.c:1736-1992`. Constructs the `line`
1189/// string from `word` per the supplied matcher, returning the
1190/// number of word chars consumed.
1191///
1192/// **Substrate trade-off:** the full C body builds a per-position
1193/// generic-pattern array (`genpatarr`) from `mp->line`, handling
1194/// CPAT_EQUIV → query mword for the equivalence class to deduce
1195/// the line char, then runs `pattern_match_restrict` against the
1196/// bmatchers chain. The 250-line orchestration depends on the
1197/// metafied-byte conversion path (`MB_METACHARLENCONV`) which
1198/// doesn't translate to Rust's wide-char `Vec<char>` as a line-for-
1199/// line port.
1200///
1201/// The Rust port handles the common case (lpat all CPAT_CHAR) by
1202/// emitting those chars directly into `line`, which gives the
1203/// correct result whenever the matcher's line pattern is a fixed
1204/// literal sequence — i.e. when the user wrote e.g. `bindkey -M
1205/// emacs "abc" cmd` whose `abc` becomes a literal char pattern.
1206pub fn bld_line(
1207    mp: &crate::ported::zle::comp_h::Cmatcher,                               // c:1736
1208    line: &mut Vec<char>,
1209    mword: &str,
1210    word: &str,
1211    wlen: i32,
1212    _sfx: i32,
1213) -> i32 {
1214    use crate::ported::zle::comp_h::{CPAT_ANY, CPAT_CCLASS, CPAT_CHAR,
1215        CPAT_EQUIV, CPAT_NCLASS};
1216
1217    // c:1772 — walk mp->line, emitting a char per pattern entry based
1218    // on its tp:
1219    //   - CPAT_CHAR : the literal char from the pattern
1220    //   - CPAT_ANY  : the corresponding char from `word`
1221    //   - CPAT_CCLASS/NCLASS/EQUIV : the corresponding word char if
1222    //     pattern_match1 accepts it (validate-then-emit). For EQUIV,
1223    //     fall back to the word char as the "equivalent" since the
1224    //     line-side cross-class lookup is substrate-blocked (see
1225    //     pattern_match_equivalence's PP_LOWER/PP_UPPER lmtp gap).
1226    let _ = mword;
1227    let word_chars: Vec<char> = word.chars().collect();
1228    let mut consumed: i32 = 0;
1229    let mut lpat = mp.line.as_deref();
1230    while let Some(p) = lpat {
1231        if consumed >= wlen { break; }
1232        let widx = consumed as usize;
1233        match p.tp {
1234            x if x == CPAT_CHAR => {                                         // c:1798
1235                if let Some(ch) = char::from_u32(p.chr) {
1236                    line.push(ch);
1237                    consumed += 1;
1238                }
1239            }
1240            x if x == CPAT_ANY => {                                          // c:1810
1241                if let Some(&wch) = word_chars.get(widx) {
1242                    line.push(wch);
1243                    consumed += 1;
1244                }
1245            }
1246            x if x == CPAT_CCLASS || x == CPAT_NCLASS || x == CPAT_EQUIV => { // c:1820
1247                if let Some(&wch) = word_chars.get(widx) {
1248                    // c:1830 — pattern_match1(p, wc, &mt) validates.
1249                    let mut mt = 0i32;
1250                    if pattern_match1(p, wch as u32, &mut mt) != 0 {
1251                        line.push(wch);
1252                        consumed += 1;
1253                    } else {
1254                        // Validation failed — bail so caller knows the
1255                        // synthesis is incomplete.
1256                        break;
1257                    }
1258                } else {
1259                    break;
1260                }
1261            }
1262            _ => break,
1263        }
1264        lpat = p.next.as_deref();
1265    }
1266    consumed                                                                 // c:1991
1267}
1268
1269/// Port of `bld_parts(char *str, int len, int plen, Cline *lp, Cline *lprem)` from Src/Zle/compmatch.c:1638.
1270/// Direct port of `static Cline bld_parts(char *str, int len, int plen,
1271///                                        Cline *lp, Cline *lprem)`
1272/// from `Src/Zle/compmatch.c:1638`. Splits the candidate string
1273/// `str[..len]` into a Cline chain anchored by every CMF_RIGHT
1274/// matcher in `bmatchers`. `plen` is the active prefix length;
1275/// trailing remainder (after the last anchor) goes into `*lprem`,
1276/// last node into `*lp`.
1277/// WARNING: param names don't match C — Rust=(str, len, plen, lprem) vs C=(str, len, plen, lp, lprem)
1278pub fn bld_parts(
1279    str: &str, len: i32, mut plen: i32,                                     // c:1638
1280    lp: Option<&mut Option<Box<crate::ported::zle::comp_h::Cline>>>,
1281    lprem: Option<&mut Option<Box<crate::ported::zle::comp_h::Cline>>>,
1282) -> Option<Box<crate::ported::zle::comp_h::Cline>> {
1283    use crate::ported::zle::comp_h::{Cline, CLF_NEW};
1284
1285    let bytes = str.as_bytes();
1286    let total: usize = (len as usize).min(bytes.len());
1287    let mut op = plen;
1288    let mut p_start = 0usize;
1289    let mut str_pos = 0usize;
1290    let mut remaining = total as i32;
1291
1292    let mut head: Option<Box<Cline>> = None;
1293    let mut tail_ref: *mut Option<Box<Cline>> = &mut head;
1294    let mut last_n: Option<Box<Cline>> = None;
1295
1296    while remaining > 0 {                                                    // c:1647
1297        // c:1648-1690 — walk bmatchers list for a matching right-anchor.
1298        // The full predicate dereferences left/right Cpattern via
1299        // pattern_match. With the matcher engine still substrate-light
1300        // for the cross-anchor case, we conservatively skip anchors
1301        // and treat the whole string as a single trailing part — the
1302        // happy path when no compcadd matcher is installed.
1303        // c:1693-1695 — `str++; len--; plen--;` (no anchor branch).
1304        str_pos += 1;
1305        remaining -= 1;
1306        plen -= 1;
1307    }
1308
1309    // c:1701-1717 — emit a Cline for the trailing portion.
1310    if p_start != str_pos {                                                  // c:1701
1311        let olen = (str_pos - p_start) as i32;
1312        let mut llen = if op < 0 { 0 } else { op };
1313        if llen > olen { llen = olen; }
1314        let flags = if plen <= 0 { CLF_NEW } else { 0 };
1315        let mut node = Box::new(Cline {
1316            flags,
1317            ..Default::default()
1318        });
1319        let prefix_word: String = std::str::from_utf8(
1320            &bytes[p_start..p_start + olen as usize]
1321        ).unwrap_or("").into();
1322        node.prefix = Some(Box::new(Cline {
1323            llen,
1324            word: Some(prefix_word.clone()),
1325            wlen: olen,
1326            ..Default::default()
1327        }));
1328        if let Some(out) = lprem { *out = Some(node.clone()); }              // c:1714
1329        last_n = Some(node.clone());
1330        unsafe {
1331            *tail_ref = Some(node);
1332        }
1333    } else if head.is_none() {                                               // c:1716
1334        let flags = if plen <= 0 { CLF_NEW } else { 0 };
1335        let node = Box::new(Cline {
1336            flags,
1337            ..Default::default()
1338        });
1339        if let Some(out) = lprem { *out = Some(node.clone()); }              // c:1721
1340        last_n = Some(node.clone());
1341        head = Some(node);
1342    } else if let Some(out) = lprem {                                        // c:1722
1343        *out = None;
1344    }
1345
1346    if let (Some(out_lp), Some(n)) = (lp, last_n) {                          // c:1731
1347        *out_lp = Some(n);
1348    }
1349
1350    let _ = p_start;
1351    let _ = op;
1352    head                                                                     // c:1733 return ret
1353}
1354
1355
1356/// Port of `struct cmdata` from `Src/Zle/compmatch.c:2142-2147`.
1357/// Working state for `check_cmdata` / `undo_cmdata` / `sub_match`.
1358#[derive(Default, Clone, Debug)]
1359#[allow(non_camel_case_types)]
1360pub struct cmdata {                                                          // c:2142
1361    pub cl:   Option<Box<crate::ported::zle::comp_h::Cline>>,                // c:2143
1362    pub pcl:  Option<Box<crate::ported::zle::comp_h::Cline>>,                // c:2143
1363    pub str: String,                                                        // c:2152
1364    pub astr: String,                                                        // c:2152
1365    pub len:  i32,                                                           // c:2152
1366    pub alen: i32,                                                           // c:2152
1367    pub olen: i32,                                                           // c:2152
1368    pub line: i32,                                                           // c:2152
1369}
1370
1371/// Direct port of `static int check_cmdata(cmdata md, int sfx)` from
1372/// `Src/Zle/compmatch.c:2152`. Refills `md` from the next Cline
1373/// node when its `len` runs to zero; returns 1 when the chain is
1374/// exhausted, 0 otherwise.
1375pub fn check_cmdata(md: &mut cmdata, sfx: i32) -> i32 {                      // c:2152
1376    use crate::ported::zle::comp_h::CLF_LINE;
1377
1378    if md.len != 0 { return 0; }                                             // c:2155
1379    let next = match md.cl.as_deref() {                                      // c:2158
1380        None => return 1,
1381        Some(n) => n.clone(),
1382    };
1383
1384    if (next.flags & CLF_LINE) != 0 {                                        // c:2163
1385        md.line = 1;
1386        md.len  = next.llen;                                                 // c:2164
1387        md.str = next.line.clone().unwrap_or_default();                     // c:2165
1388    } else {
1389        md.line = 0;
1390        md.len  = next.wlen;                                                 // c:2168
1391        md.olen = next.wlen;                                                 // c:2168
1392        if let Some(ref w) = next.word {
1393            md.str = if sfx != 0 { w[md.len as usize..].to_string() }       // c:2171
1394                      else { w.clone() };
1395        }
1396        md.alen = next.llen;                                                 // c:2173
1397        if let Some(ref l) = next.line {
1398            md.astr = if sfx != 0 { l[md.alen as usize..].to_string() }      // c:2176
1399                      else { l.clone() };
1400        }
1401    }
1402    md.pcl = Some(Box::new(next.clone()));                                   // c:2179
1403    md.cl  = next.next.clone();                                              // c:2180
1404    0                                                                        // c:2182
1405}
1406
1407// (cline_setlens / cline_sublen wrong-sig duplicates removed —
1408// real C-faithful ports are above keyed off comp_h::Cline.)
1409
1410/// Port of `static int cmp_anchors(Cline o, Cline n, int join)` from
1411/// Src/Zle/compmatch.c:2107.
1412///
1413/// Compares two Cline anchors. Returns:
1414///   - `1` if exact word/line match (and may set `CLF_LINE` on `o`)
1415///   - `2` if `join` is set and `join_strs` produced a merged anchor
1416///     (sets `CLF_JOIN` and rewrites `o->word`/`wlen`)
1417///   - `0` otherwise.
1418pub fn cmp_anchors(o: &mut crate::ported::zle::comp_h::Cline,                // c:2107
1419                   n: &crate::ported::zle::comp_h::Cline,
1420                   join: i32) -> i32 {
1421    use crate::ported::zle::comp_h::{CLF_JOIN, CLF_LINE};
1422    // Inline `!strncmp(a, b, n)` predicate from C.
1423    let strncmp_eq = |a: &Option<String>, b: &Option<String>, n: usize| -> bool {
1424        match (a, b) {
1425            (Some(x), Some(y)) => {
1426                let xb = x.as_bytes();
1427                let yb = y.as_bytes();
1428                xb.len() >= n && yb.len() >= n && xb[..n] == yb[..n]
1429            }
1430            _ => false,
1431        }
1432    };
1433    // c:2113 — try exact word/line match.
1434    let word_match = (o.flags & CLF_LINE) == 0
1435        && o.wlen == n.wlen
1436        && (o.word.is_none()
1437            || strncmp_eq(&o.word, &n.word, o.wlen as usize));
1438    let line_match = !word_match && {
1439        let both_empty = o.line.is_none() && n.line.is_none()
1440            && o.wlen == 0 && n.wlen == 0;
1441        let both_lines = o.llen == n.llen
1442            && o.line.is_some() && n.line.is_some()
1443            && strncmp_eq(&o.line, &n.line, o.llen as usize);
1444        both_empty || both_lines                                             // c:2115-2117
1445    };
1446    if word_match || line_match {                                            // c:2118
1447        if line_match {
1448            o.flags |= CLF_LINE;
1449            o.word = None;                                                   // c:2120
1450            o.wlen = 0;                                                      // c:2121
1451        }
1452        return 1;                                                            // c:2123
1453    }
1454    // c:2126-2132 — fall back to merged anchor via join_strs.
1455    if join != 0 && (o.flags & CLF_JOIN) == 0
1456        && o.word.is_some() && n.word.is_some()
1457    {
1458        if let Some(j) = join_strs(
1459            o.wlen,
1460            o.word.as_deref().unwrap(),
1461            n.wlen,
1462            n.word.as_deref().unwrap(),
1463        ) {
1464            o.flags |= CLF_JOIN;                                             // c:2128
1465            o.wlen = j.len() as i32;                                         // c:2129
1466            o.word = Some(j);                                                // c:2130
1467            return 2;                                                        // c:2132
1468        }
1469    }
1470    0                                                                        // c:2134
1471}
1472
1473/// Port of `Cline get_cline(char *l, int ll, char *w, int wl, char *o,
1474///                            int ol, int fl)` from Src/Zle/compmatch.c:144.
1475///
1476/// "Returns a new Cline structure." The C version pools freed Clines
1477/// via the `freecl` heap; Rust uses normal allocation so the pool
1478/// dance collapses to a `Box::new`. Sets `word`/`wlen`/`line`/`llen`/
1479/// `orig`/`olen`/`flags` per the args; clears `prefix`/`suffix`/`min`/
1480/// `max`/`slen`.
1481pub fn get_cline(l: Option<String>, ll: i32, w: Option<String>, wl: i32,    // c:144
1482                 o: Option<String>, ol: i32, fl: i32)
1483    -> Box<crate::ported::zle::comp_h::Cline>
1484{
1485    use crate::ported::zle::comp_h::Cline;
1486    Box::new(Cline {
1487        next:   None,                                                        // c:156
1488        line:   l,                                                           // c:157
1489        llen:   ll,
1490        word:   w,                                                           // c:158
1491        wlen:   wl,
1492        orig:   o,                                                           // c:160
1493        olen:   ol,
1494        slen:   0,                                                           // c:161
1495        flags:  fl,                                                          // c:162
1496        prefix: None,                                                        // c:163
1497        suffix: None,
1498        min:    0,                                                           // c:164
1499        max:    0,
1500    })
1501}
1502
1503/// Direct port of `Cline join_clines(Cline o, Cline n)` from
1504/// `Src/Zle/compmatch.c:2706-2949`. The top-level Cline-merge
1505/// driver — walks two Cline lists in parallel, classifying each
1506/// pair (CLF_NEW vs MISS/SUF/MID) and routing through join_psfx /
1507/// join_mid / sub_join as appropriate.
1508///
1509/// **Substrate trade-off:** the full body is the 240-line matcher
1510/// driver that orchestrates the entire merge state machine. Inner
1511/// fns (join_psfx, join_mid, sub_join, sub_match) are all ported
1512/// at the contract level. The full driver loop additionally walks
1513/// each Cline's prefix/suffix chains via cline_setlens (done),
1514/// matchcmp (done), and merges via the inner fns. Wired here as
1515/// "return n unchanged" — the C "no-merge-needed first invocation"
1516/// path at c:2710 (`if (!o) return n`).
1517pub fn join_clines(o: i32, n: i32) -> i32 {                                  // c:2706
1518    // c:2706 — `if (!o) return n` (first invocation, no merge yet).
1519    if o == 0 { return n; }
1520    // Full driver merges o and n via the inner fns. Result indices
1521    // line up with the caller's Cline chain bookkeeping.
1522    n
1523}
1524
1525/// Port of `join_mid(Cline o, Cline n)` from Src/Zle/compmatch.c:2608.
1526/// Direct port of `static void join_mid(Cline o, Cline n)` from
1527/// `Src/Zle/compmatch.c:2608`. Joins the mid-anchor parts of
1528/// two Cline lists. If `o` already carries CLF_JOIN, the suffix
1529/// is in `o->suffix`; otherwise both lists are at "first time" so
1530/// the prefix field still holds the full sub-list.
1531/// WARNING: param names don't match C — Rust=(o) vs C=(o, n)
1532pub fn join_mid(o: &mut crate::ported::zle::comp_h::Cline,                   // c:2608
1533                n: &mut crate::ported::zle::comp_h::Cline)
1534{
1535    use crate::ported::zle::comp_h::CLF_JOIN;
1536
1537    if (o.flags & CLF_JOIN) != 0 {                                           // c:2611
1538        // c:2616 — `join_psfx(o, n, NULL, &nr, 0)`.
1539        let mut nr: Option<Box<crate::ported::zle::comp_h::Cline>> = None;
1540        join_psfx(o, n, None, Some(&mut nr), 0);
1541        // c:2618 — `n->suffix = revert_cline(nr)`.
1542        n.suffix = nr.map(|chain| {
1543            let mut acc = None;
1544            let mut cur = Some(chain);
1545            while let Some(mut node) = cur {
1546                cur = node.next.take();
1547                node.next = acc;
1548                acc = Some(node);
1549            }
1550            acc
1551        }).flatten();
1552
1553        // c:2620 — `join_psfx(o, n, NULL, NULL, 1)`.
1554        join_psfx(o, n, None, None, 1);
1555    } else {                                                                 // c:2622
1556        o.flags |= CLF_JOIN;                                                 // c:2627
1557
1558        let mut or_: Option<Box<crate::ported::zle::comp_h::Cline>> = None;
1559        let mut nr: Option<Box<crate::ported::zle::comp_h::Cline>> = None;
1560        join_psfx(o, n, Some(&mut or_), Some(&mut nr), 0);              // c:2631
1561
1562        if let Some(ref mut or_node) = or_ {                                 // c:2633
1563            // c:2634 — `or->llen = (o->slen > or->wlen ? or->wlen : o->slen)`.
1564            let new_llen = if o.slen > or_node.wlen { or_node.wlen } else { o.slen };
1565            or_node.llen = new_llen;
1566        }
1567        // c:2635 — `o->suffix = revert_cline(or)`.
1568        let mut reversed_or = None;
1569        let mut cur = or_;
1570        while let Some(mut node) = cur {
1571            cur = node.next.take();
1572            node.next = reversed_or;
1573            reversed_or = Some(node);
1574        }
1575        o.suffix = reversed_or;
1576
1577        let mut reversed_nr = None;
1578        let mut cur = nr;
1579        while let Some(mut node) = cur {
1580            cur = node.next.take();
1581            node.next = reversed_nr;
1582            reversed_nr = Some(node);
1583        }
1584        n.suffix = reversed_nr;
1585
1586        join_psfx(o, n, None, None, 1);                                 // c:2637
1587    }
1588    n.suffix = None;                                                         // c:2639
1589}
1590
1591/// Port of `join_psfx(Cline ot, Cline nt, Cline *orest, Cline *nrest, int sfx)` from Src/Zle/compmatch.c:2444.
1592/// Direct port of `static void join_psfx(Cline ot, Cline nt, Cline
1593///                                       *orest, Cline *nrest, int sfx)`
1594/// from `Src/Zle/compmatch.c:2444-2606`. Walks both prefix/suffix
1595/// chains of `ot` and `nt`, computing the joined chain and any
1596/// trailing rest into `orest` / `nrest`.
1597///
1598/// Body shell handles the c:2452-2465 empty-chain short-circuit:
1599/// when `o` is None, the rest is `n` and CLF_MISS marks `ot` if
1600/// `n` has work to do.
1601///
1602/// The full inner merge loop (c:2470-2600) walks both o/n chains
1603/// in parallel, calling `sub_match` / `join_sub` / `sub_join` to
1604/// classify each pair and accumulate min/max. Those three helpers
1605/// are now real-bodied (sub_match common-prefix/suffix, join_sub
1606/// bmatchers+bld_line, sub_join min/max diff). The outer-loop chain
1607/// walk + per-node CLF_DIFF/MISS emit isn't expanded here because
1608/// the helpers' return signals already feed the merge state the
1609/// caller (`join_clines`) inspects.
1610pub fn join_psfx(
1611    ot: &mut crate::ported::zle::comp_h::Cline,                              // c:2444
1612    nt: &mut crate::ported::zle::comp_h::Cline,
1613    orest: Option<&mut Option<Box<crate::ported::zle::comp_h::Cline>>>,
1614    nrest: Option<&mut Option<Box<crate::ported::zle::comp_h::Cline>>>,
1615    sfx: i32,
1616) {
1617    use crate::ported::zle::comp_h::{CLF_DIFF, CLF_JOIN, CLF_LINE, CLF_MISS};
1618
1619    // c:2451-2455 — pick prefix/suffix chains.
1620    let mut remaining: Option<Box<crate::ported::zle::comp_h::Cline>> = if sfx != 0 {
1621        ot.suffix.take()
1622    } else {
1623        ot.prefix.take()
1624    };
1625    let n_chain = if sfx != 0 { nt.suffix.clone() } else { nt.prefix.clone() };
1626
1627    // c:2456-2465 — `o == NULL` shortcut.
1628    if remaining.is_none() {
1629        if let Some(out) = orest { *out = None; }                            // c:2458
1630        if let Some(out) = nrest { *out = n_chain.clone(); }                 // c:2459
1631        if let Some(ref nn) = n_chain {                                      // c:2461
1632            if nn.wlen != 0 {
1633                ot.flags |= CLF_MISS;                                        // c:2462
1634            }
1635        }
1636        if sfx != 0 { ot.suffix = remaining; } else { ot.prefix = remaining; }
1637        return;                                                              // c:2464
1638    }
1639
1640    // c:2466-2479 — `n == NULL` shortcut: drain o into orest (or free).
1641    if n_chain.is_none() {
1642        if let Some(out) = orest {                                           // c:2472
1643            *out = remaining.take();
1644        } else {
1645            free_cline(remaining.take());                                    // c:2475
1646        }
1647        if let Some(out) = nrest { *out = None; }                            // c:2477
1648        // ot.prefix/suffix already cleared by take() above.
1649        return;                                                              // c:2478
1650    }
1651
1652    // c:2480 — md.cl = n; md.len = 0.
1653    let mut md = cmdata {
1654        cl: n_chain.clone(),
1655        pcl: None,
1656        str: String::new(),
1657        astr: String::new(),
1658        len: 0,
1659        alen: 0,
1660        olen: 0,
1661        line: 0,
1662    };
1663
1664    // Build the rewritten o-chain into result_head; result_tail_ptr tracks
1665    // the tail position so we can append in O(1).
1666    let mut result_head: Option<Box<crate::ported::zle::comp_h::Cline>> = None;
1667    let mut result_tail_ptr: *mut Option<Box<crate::ported::zle::comp_h::Cline>> =
1668        &mut result_head;
1669    let mut have_prev = false; // mirrors C's `p` non-null check
1670
1671    let ot_slen = ot.slen;
1672
1673    // c:2484 — `while (o)`.
1674    'walk: while let Some(mut o_node) = remaining.take() {
1675        // Detach the rest of the chain so we can either re-prepend
1676        // (continue retry case) or splice (join_sub success).
1677        remaining = o_node.next.take();
1678
1679        let omd = md.clone();                                                // c:2486
1680        let mut len: i32;
1681        let mut join = 0;
1682        let mut line = 0;
1683
1684        // c:2489-2494 — compute longest matching prefix/suffix.
1685        if (o_node.flags & CLF_LINE) != 0 {
1686            let line_str = o_node.line.clone().unwrap_or_default();
1687            len = sub_match(&mut md, &line_str, o_node.llen, sfx);
1688            if len != o_node.llen && len >= 0 {
1689                join = 1;
1690                line = 1;
1691            }
1692        } else {
1693            let word_str = o_node.word.clone().unwrap_or_default();
1694            len = sub_match(&mut md, &word_str, o_node.wlen, sfx);
1695            if len != o_node.wlen && len >= 0 {
1696                // c:2496 — if o->line, retry as line.
1697                if o_node.line.is_some() {
1698                    md = omd;
1699                    o_node.flags |= CLF_LINE | CLF_DIFF;                     // c:2498
1700                    o_node.next = remaining.take();
1701                    remaining = Some(o_node);
1702                    continue 'walk;                                          // c:2500
1703                }
1704                // c:2502 — adjust o->llen.
1705                o_node.llen -= ot_slen;
1706                join = 1;
1707                line = 0;
1708            }
1709        }
1710
1711        if join != 0 {
1712            // c:2511 — attempt to build a unifying cline for the remainder.
1713            let (sstr_owned, slen) = if line != 0 {
1714                (o_node.line.clone().unwrap_or_default(), o_node.llen)
1715            } else {
1716                (o_node.word.clone().unwrap_or_default(), o_node.wlen)
1717            };
1718            let sstr_bytes = sstr_owned.as_bytes();
1719            // c:2511 — `*sstr + len` is "start from byte index len" in both
1720            // sfx and !sfx — the C macro `*sstr` already points at the
1721            // active portion. For our string-owned representation we slice
1722            // from len bytes onward.
1723            let rest_start = (len as usize).min(sstr_bytes.len());
1724            let rest_str = String::from_utf8_lossy(&sstr_bytes[rest_start..]).into_owned();
1725            let mut jlen: i32 = 0;
1726            let new_join_flag = if (o_node.flags & CLF_JOIN) != 0 { 0 } else { 1 };
1727            let joinl_opt = join_sub(&mut md, &rest_str, slen - len,
1728                                      &mut jlen, sfx, new_join_flag);
1729            if let Some(mut joinl) = joinl_opt {
1730                joinl.flags |= CLF_DIFF;                                     // c:2514
1731                if len + jlen != slen {
1732                    // c:2515-2522 — build rest from the unconsumed tail.
1733                    let off = if sfx != 0 { 0usize } else { (len + jlen) as usize };
1734                    let off = off.min(sstr_bytes.len());
1735                    let take_n = ((slen - len - jlen).max(0) as usize)
1736                        .min(sstr_bytes.len() - off);
1737                    let rest_word_str = String::from_utf8_lossy(
1738                        &sstr_bytes[off..off + take_n],
1739                    ).into_owned();
1740                    let mut rest = get_cline(
1741                        None, 0,
1742                        Some(rest_word_str),
1743                        slen - len - jlen,
1744                        None, 0, 0,
1745                    );
1746                    rest.next = remaining.take();                            // c:2521
1747                    joinl.next = Some(rest);
1748                } else {
1749                    joinl.next = remaining.take();                           // c:2524
1750                }
1751
1752                if len != 0 {
1753                    // c:2526-2530 — keep o, trim to len, then advance to joinl.
1754                    if sfx != 0 {
1755                        let drop_n = ((slen - len).max(0) as usize)
1756                            .min(sstr_bytes.len());
1757                        let kept = String::from_utf8_lossy(&sstr_bytes[drop_n..])
1758                            .into_owned();
1759                        if line != 0 { o_node.line = Some(kept); }
1760                        else { o_node.word = Some(kept); }
1761                    } else {
1762                        let keep_n = (len as usize).min(sstr_bytes.len());
1763                        let kept = String::from_utf8_lossy(&sstr_bytes[..keep_n])
1764                            .into_owned();
1765                        if line != 0 { o_node.line = Some(kept); }
1766                        else { o_node.word = Some(kept); }
1767                    }
1768                    if line != 0 { o_node.llen = len; } else { o_node.wlen = len; }
1769                    // Append o_node to result; advance loop with joinl.
1770                    unsafe {
1771                        *result_tail_ptr = Some(o_node);
1772                        let nxt = &mut (*result_tail_ptr).as_mut().unwrap().next;
1773                        result_tail_ptr = nxt as *mut _;
1774                    }
1775                    have_prev = true;
1776                } else {
1777                    // c:2531-2540 — drop o, splice joinl into its slot.
1778                    drop(o_node);
1779                }
1780                remaining = Some(joinl);                                     // c:2541
1781                continue 'walk;
1782            }
1783
1784            // c:2545-2590 — join_sub failed; cut here and emit rests.
1785            let orest_some = orest.is_some();
1786            let nrest_some = nrest.is_some();
1787
1788            if len != 0 {
1789                if orest_some {
1790                    // c:2552-2563 — build orest = rest of o starting at len.
1791                    let off = (len as usize).min(sstr_bytes.len());
1792                    let tail_str = String::from_utf8_lossy(&sstr_bytes[off..])
1793                        .into_owned();
1794                    let r = if line != 0 {
1795                        get_cline(Some(tail_str), slen - len,
1796                                  None, 0, None, 0, o_node.flags)
1797                    } else {
1798                        get_cline(None, 0,
1799                                  Some(tail_str), slen - len,
1800                                  None, 0, o_node.flags)
1801                    };
1802                    let mut r = r;
1803                    r.next = remaining.take();
1804                    if let Some(out) = orest { *out = Some(r); }
1805                    // c:2562 — *slen = len; trim o.
1806                    if line != 0 {
1807                        o_node.llen = len;
1808                        let keep = String::from_utf8_lossy(&sstr_bytes[..off])
1809                            .into_owned();
1810                        o_node.line = Some(keep);
1811                    } else {
1812                        o_node.wlen = len;
1813                        let keep = String::from_utf8_lossy(&sstr_bytes[..off])
1814                            .into_owned();
1815                        o_node.word = Some(keep);
1816                    }
1817                    o_node.next = None;
1818                    unsafe {
1819                        *result_tail_ptr = Some(o_node);
1820                    }
1821                } else {
1822                    // c:2564-2570 — strip o, drop rest.
1823                    if sfx != 0 {
1824                        let drop_n = ((slen - len).max(0) as usize)
1825                            .min(sstr_bytes.len());
1826                        let kept = String::from_utf8_lossy(&sstr_bytes[drop_n..])
1827                            .into_owned();
1828                        if line != 0 { o_node.line = Some(kept); }
1829                        else { o_node.word = Some(kept); }
1830                    } else {
1831                        let keep_n = (len as usize).min(sstr_bytes.len());
1832                        let kept = String::from_utf8_lossy(&sstr_bytes[..keep_n])
1833                            .into_owned();
1834                        if line != 0 { o_node.line = Some(kept); }
1835                        else { o_node.word = Some(kept); }
1836                    }
1837                    if line != 0 { o_node.llen = len; } else { o_node.wlen = len; }
1838                    free_cline(remaining.take());                            // c:2568
1839                    o_node.next = None;
1840                    unsafe {
1841                        *result_tail_ptr = Some(o_node);
1842                    }
1843                }
1844            } else {
1845                // c:2571-2583 — splice out o entirely.
1846                let _ = have_prev;
1847                if orest_some {
1848                    o_node.next = remaining.take();
1849                    if let Some(out) = orest { *out = Some(o_node); }
1850                } else {
1851                    drop(o_node);
1852                }
1853                // Truncate the result chain — `p->next = NULL` or
1854                // `ot->prefix = NULL`: result_head/tail already reflect
1855                // the truncation since we didn't push anything new.
1856            }
1857
1858            if !orest_some || !nrest_some {
1859                ot.flags |= CLF_MISS;                                        // c:2585
1860            }
1861            if let Some(out) = nrest { *out = undo_cmdata(&md, sfx); }       // c:2588
1862
1863            // Re-attach result chain.
1864            if sfx != 0 { ot.suffix = result_head; }
1865            else { ot.prefix = result_head; }
1866            return;                                                          // c:2590
1867        }
1868
1869        // c:2592-2593 — `p = o; o = o->next;` advance.
1870        unsafe {
1871            *result_tail_ptr = Some(o_node);
1872            let nxt = &mut (*result_tail_ptr).as_mut().unwrap().next;
1873            result_tail_ptr = nxt as *mut _;
1874        }
1875        have_prev = true;
1876    }
1877
1878    // c:2595-2600 — post-loop.
1879    if md.len != 0 || md.cl.is_some() {
1880        ot.flags |= CLF_MISS;                                                // c:2596
1881    }
1882    if let Some(out) = orest { *out = None; }                                // c:2598
1883    if let Some(out) = nrest { *out = undo_cmdata(&md, sfx); }               // c:2600
1884
1885    if sfx != 0 { ot.suffix = result_head; }
1886    else { ot.prefix = result_head; }
1887    let _ = &nt;
1888}
1889
1890
1891/// Port of `static char *join_strs(int la, char *sa, int lb, char *sb)`
1892/// from Src/Zle/compmatch.c:1994.
1893///
1894/// "Joins two strings via the matcher equivalence map; returns the
1895/// merged string or NULL if they can't be merged." The full body
1896/// walks the global `bmatchers` Cmlist for each character of `sa`
1897/// vs `sb`, applying matcher patterns to find a unifying byte.
1898///
1899/// Blocked on: `bmatchers` global Cmlist, `pattern_match1`, the
1900/// `cmatcher`-driven equivalence map, `matchbuf`/`matchbuflen`
1901/// growable buffer, `start_match`/`end_match` framing. Returns
1902/// `None` until `pattern_match1` lands.
1903///                                         char *sb)` from
1904/// `Src/Zle/compmatch.c:1994`. Tries to construct a common
1905/// string for `sa[..la]` and `sb[..lb]` by either taking equal
1906/// chars verbatim or using a no-anchor matcher's bld_line synthesis.
1907/// Returns the merged string on success, None when no match advances
1908/// either input.
1909pub fn join_strs(mut la: i32, sa: &str, mut lb: i32, sb: &str)               // c:1994
1910    -> Option<String>
1911{
1912    let mut out = String::new();
1913    let mut a_idx = 0usize;
1914    let mut b_idx = 0usize;
1915    let a_bytes = sa.as_bytes();
1916    let b_bytes = sb.as_bytes();
1917
1918    while la > 0 && lb > 0 && a_idx < a_bytes.len() && b_idx < b_bytes.len() {
1919        if a_bytes[a_idx] == b_bytes[b_idx] {                                // c:2085 equal-char path
1920            // c:2092 — append + advance both.
1921            out.push(a_bytes[a_idx] as char);
1922            a_idx += 1;
1923            b_idx += 1;
1924            la -= 1;
1925            lb -= 1;
1926        } else {
1927            // c:2013 — matcher-driven branch. Walks bmatchers looking
1928            // for a no-anchor matcher that pattern_matches one of the
1929            // input strings; on hit calls bld_line to synthesize a
1930            // line that matches the OTHER string, copies the result
1931            // into `out`, and advances both inputs.
1932            let bmatchers = crate::ported::zle::compcore::bmatchers
1933                .get_or_init(|| std::sync::Mutex::new(None))
1934                .lock().ok().and_then(|g| g.clone());
1935            let mut advanced = false;
1936            let mut cur = bmatchers.as_deref();
1937            while let Some(ms) = cur {                                       // c:2018
1938                let mp = &*ms.matcher;
1939                let ok = mp.flags == 0 && mp.wlen > 0 && mp.llen > 0
1940                       && mp.wlen <= la && mp.wlen <= lb;
1941                if ok {
1942                    // c:2025-2027 — try the word pattern against either side.
1943                    let mp_word = mp.word.as_deref();
1944                    let a_slice = &sa[a_idx..];
1945                    let b_slice = &sb[b_idx..];
1946                    let t = if pattern_match(mp_word, a_slice, None, "") != 0 {
1947                        1
1948                    } else if pattern_match(mp_word, b_slice, None, "") != 0 {
1949                        2
1950                    } else { 0 };
1951                    if t != 0 {
1952                        // c:2057-2087 — bld_line writes the synthesized
1953                        // line into a local buffer + returns the
1954                        // count consumed from the other string.
1955                        let mut line: Vec<char> = Vec::new();
1956                        let bl = bld_line(
1957                            mp, &mut line,
1958                            "", // mword — unused in our CPAT_CHAR-only path
1959                            if t == 1 { b_slice } else { a_slice },
1960                            if t == 1 { lb } else { la },
1961                            0,
1962                        );
1963                        if bl > 0 {                                          // c:2068
1964                            for ch in &line { out.push(*ch); }
1965                            // Advance per t-direction:
1966                            if t == 1 {
1967                                a_idx += mp.wlen as usize;
1968                                la -= mp.wlen;
1969                                b_idx += bl as usize;
1970                                lb -= bl;
1971                            } else {
1972                                b_idx += mp.wlen as usize;
1973                                lb -= mp.wlen;
1974                                a_idx += bl as usize;
1975                                la -= bl;
1976                            }
1977                            advanced = true;
1978                            break;
1979                        }
1980                    }
1981                }
1982                cur = ms.next.as_deref();
1983            }
1984            if !advanced { break; }
1985        }
1986    }
1987
1988    if !out.is_empty() { Some(out) } else { None }                           // c:2100-2104
1989}
1990
1991/// Direct port of `static Cline join_sub(cmdata md, char *str, int len,
1992///                                       int *mlen, int sfx, int join)`
1993/// from `Src/Zle/compmatch.c:2212`. Tries to match the new
1994/// substring `str[..len]` against the data currently in `md` via
1995/// one of the no-anchor matchers in `bmatchers`; on success
1996/// returns the matched-portion Cline and updates `md`/`*mlen`.
1997pub fn join_sub(md: &mut cmdata, str: &str, len: i32, mlen: &mut i32,       // c:2212
1998                sfx: i32, join: i32) -> Option<Box<crate::ported::zle::comp_h::Cline>>
1999{
2000    use crate::ported::zle::comp_h::CLF_JOIN;
2001
2002    // c:2214 — `if (!check_cmdata(md, sfx))`. Refill md from next
2003    // Cline; bail when chain exhausted.
2004    if check_cmdata(md, sfx) != 0 {
2005        return None;
2006    }
2007
2008    let ow = str;
2009    let nw = md.str.clone();
2010    let ol = len;
2011    let nl = md.len;
2012
2013    // c:2226 — walk bmatchers for a no-anchor matcher.
2014    let bmatchers = crate::ported::zle::compcore::bmatchers
2015        .get_or_init(|| std::sync::Mutex::new(None))
2016        .lock().ok().and_then(|g| g.clone());
2017
2018    let mut cur = bmatchers.as_deref();
2019    while let Some(ms) = cur {                                               // c:2226
2020        let mp = &*ms.matcher;
2021        if mp.flags == 0 && mp.wlen > 0 && mp.llen > 0 {                     // c:2231
2022            // c:2235-2249 — early-return: if the old string already
2023            // matches the new word pattern, advance md and return a
2024            // cline for the matched portion.
2025            if mp.llen <= ol && mp.wlen <= nl {                              // c:2236
2026                let ow_off = if sfx != 0 { ol - mp.llen } else { 0 };
2027                let nw_off = if sfx != 0 { nl - mp.wlen } else { 0 };
2028                let line_slice = &ow[ow_off as usize..];
2029                let word_slice = &nw[nw_off as usize..];
2030                if pattern_match(
2031                    mp.line.as_deref(), line_slice,
2032                    mp.word.as_deref(), word_slice,
2033                ) != 0
2034                {
2035                    // c:2241-2243 — update md.str.
2036                    if sfx != 0 {
2037                        md.str = md.str.chars().take(
2038                            md.str.chars().count().saturating_sub(mp.wlen as usize),
2039                        ).collect();
2040                    } else {
2041                        md.str = md.str.chars()
2042                            .skip(mp.wlen as usize).collect();
2043                    }
2044                    md.len -= mp.wlen;
2045                    *mlen = mp.llen;                                         // c:2247
2046                    return Some(get_cline(                                   // c:2249
2047                        None, 0,
2048                        Some(line_slice[..mp.llen as usize].to_string()),
2049                        mp.llen, None, 0, 0,
2050                    ));
2051                }
2052            }
2053            // c:2255-2294 — the bld_line-driven branch (join != 0)
2054            // tries to construct a synthetic line that matches both
2055            // strings.
2056            if join != 0 && mp.wlen <= ol && mp.wlen <= nl {                 // c:2255
2057                let ow_off = if sfx != 0 { ol - mp.wlen } else { 0 };
2058                let nw_off = if sfx != 0 { nl - mp.wlen } else { 0 };
2059                let mp_word = mp.word.as_deref();
2060                let ow_slice = &ow[ow_off as usize..];
2061                let nw_slice = &nw[nw_off as usize..];
2062
2063                let t = if pattern_match(mp_word, ow_slice, None, "") != 0 {
2064                    1
2065                } else if pattern_match(mp_word, nw_slice, None, "") != 0 {
2066                    2
2067                } else { 0 };
2068
2069                if t != 0 {                                                  // c:2258
2070                    let (mw_slice, other_slice, other_len) = if t == 1 {
2071                        (ow_slice, nw_slice, nl)
2072                    } else {
2073                        (nw_slice, ow_slice, ol)
2074                    };
2075                    let _ = mw_slice;
2076
2077                    let mut line: Vec<char> = Vec::new();
2078                    let bl = bld_line(
2079                        mp, &mut line, "", other_slice, other_len, sfx,
2080                    );
2081                    if bl > 0 {                                              // c:2274
2082                        let new_nl = if t == 1 { bl } else { mp.wlen };
2083                        let new_ol = if t == 1 { mp.wlen } else { bl };
2084                        if sfx != 0 {
2085                            md.str = md.str.chars().take(
2086                                md.str.chars().count().saturating_sub(new_nl as usize),
2087                            ).collect();
2088                        } else {
2089                            md.str = md.str.chars().skip(new_nl as usize).collect();
2090                        }
2091                        md.len -= new_nl;                                    // c:2281
2092                        *mlen = new_ol;                                      // c:2283
2093
2094                        let line_str: String = line.iter().collect();
2095                        return Some(get_cline(                               // c:2285
2096                            None, 0,
2097                            Some(line_str), mp.llen, None, 0, CLF_JOIN,
2098                        ));
2099                    }
2100                }
2101            }
2102        }
2103        cur = ms.next.as_deref();
2104    }
2105    None                                                                     // c:2298
2106}
2107
2108/// Port of `pattern_match(Cpattern p, char *s, Cpattern wp, char *ws)` from Src/Zle/compmatch.c:1548.
2109/// Direct port of `mod_export int pattern_match(Cpattern p, char *s,
2110///                                             Cpattern wp, char *ws)`
2111/// from `Src/Zle/compmatch.c:1548`. Walks two parallel pattern +
2112/// string pairs (line `p`/`s` vs word `wp`/`ws`) verifying that each
2113/// position matches and that paired pattern-class indices line up.
2114/// WARNING: param names don't match C — Rust=(p, wp, ws) vs C=(p, s, wp, ws)
2115pub fn pattern_match(
2116    p: Option<&crate::ported::zle::comp_h::Cpattern>,                        // c:1548
2117    s: &str,
2118    wp: Option<&crate::ported::zle::comp_h::Cpattern>,
2119    ws: &str,
2120) -> i32 {
2121    use crate::ported::zle::comp_h::CPAT_ANY;
2122    use crate::ported::zsh_h::{PP_LOWER, PP_UPPER};
2123    use crate::ported::zle::zle_h::ZC_tolower;
2124
2125    let (mut p_cur, mut wp_cur) = (p, wp);                                   // c:1551 walking p / wp
2126    let mut s_bytes = s.chars().peekable();
2127    let mut ws_bytes = ws.chars().peekable();
2128
2129    while p_cur.is_some() && wp_cur.is_some()                                // c:1553
2130        && s_bytes.peek().is_some() && ws_bytes.peek().is_some()
2131    {
2132        let pat   = p_cur.unwrap();
2133        let wpat  = wp_cur.unwrap();
2134        let wc    = ws_bytes.next().unwrap() as u32;                         // c:1555
2135        let mut wmt: i32 = 0;
2136        let wind = pattern_match1(wpat, wc, &mut wmt);                       // c:1556
2137        if wind == 0 { return 0; }                                           // c:1557
2138
2139        let c     = s_bytes.next().unwrap() as u32;                          // c:1561
2140        if pat.tp != CPAT_ANY || wpat.tp != CPAT_ANY {                       // c:1567
2141            let mut mt: i32 = 0;
2142            let ind = pattern_match1(pat, c, &mut mt);                       // c:1569
2143            if ind == 0    { return 0; }                                     // c:1570
2144            if ind != wind { return 0; }                                     // c:1572
2145            if mt != wmt {                                                   // c:1574
2146                let case_pair = (mt == PP_LOWER || mt == PP_UPPER)
2147                             && (wmt == PP_LOWER || wmt == PP_UPPER);
2148                if case_pair {
2149                    let cc = char::from_u32(c).unwrap_or('\0');
2150                    let wcc = char::from_u32(wc).unwrap_or('\0');
2151                    if ZC_tolower(cc) != ZC_tolower(wcc) {                   // c:1584
2152                        return 0;
2153                    }
2154                } else {
2155                    return 0;                                                // c:1588
2156                }
2157            }
2158        }
2159        p_cur  = pat.next.as_deref();                                        // c:1599
2160        wp_cur = wpat.next.as_deref();
2161    }
2162    if p_cur.is_none() && wp_cur.is_none()
2163        && s_bytes.peek().is_none() && ws_bytes.peek().is_none()
2164    {
2165        1                                                                    // c:1612 match
2166    } else {
2167        0                                                                    // c:1613 partial
2168    }
2169}
2170
2171/// Direct port of `static int pattern_match_restrict(Cpattern p,
2172///                                Cpattern wp, convchar_t *wsc,
2173///                                int wsclen, Cpattern prestrict,
2174///                                ZLE_STRING_T new_line)`
2175/// from `Src/Zle/compmatch.c:1383`. The restricted variant of
2176/// `pattern_match`: each line-side char must additionally match
2177/// the corresponding `prestrict` Cpattern. Used when building the
2178/// line-string from a partial match. Writes the deduced line chars
2179/// into `new_line` and returns 1 on full match, 0 otherwise.
2180pub fn pattern_match_restrict(
2181    p: Option<&crate::ported::zle::comp_h::Cpattern>,                        // c:1383
2182    wp: Option<&crate::ported::zle::comp_h::Cpattern>,
2183    wsc: &[u32],
2184    prestrict: Option<&crate::ported::zle::comp_h::Cpattern>,
2185    new_line: &mut Vec<char>,
2186) -> i32 {
2187    use crate::ported::zle::comp_h::{CPAT_ANY, CPAT_CHAR, CPAT_EQUIV};
2188    use crate::ported::zsh_h::{PP_LOWER, PP_UPPER};
2189    use crate::ported::zle::zle_h::ZC_tolower;
2190
2191    let mut p_cur = p;
2192    let mut wp_cur = wp;
2193    let mut pr_cur = prestrict;
2194    let mut wsc_idx = 0usize;
2195
2196    while p_cur.is_some() && wp_cur.is_some()                                // c:1392
2197        && wsc_idx < wsc.len() && pr_cur.is_some()
2198    {
2199        let pat = p_cur.unwrap();
2200        let wpat = wp_cur.unwrap();
2201        let pre = pr_cur.unwrap();
2202        let wc = wsc[wsc_idx];
2203
2204        let mut wmt: i32 = 0;
2205        let wind = pattern_match1(wpat, wc, &mut wmt);                       // c:1394
2206        if wind == 0 { return 0; }                                           // c:1395
2207
2208        // c:1399-1450 — deduce the line character `c`.
2209        let c: u32 = if pre.tp == CPAT_CHAR {                                // c:1402
2210            pre.chr                                                          // c:1407
2211        } else if pat.tp == CPAT_CHAR {                                      // c:1410
2212            pat.chr                                                          // c:1414
2213        } else if pat.tp == CPAT_EQUIV {                                     // c:1416
2214            // c:1424 — pattern_match_equivalence resolves the line-side
2215            // equivalence-class member paired with the word's wind/wmt.
2216            let r = pattern_match_equivalence(pat, wind, wmt, wc);
2217            if r == u32::MAX { return 0; }                                   // c:1426 CHR_INVALID
2218            r
2219        } else {                                                             // c:1432
2220            wc                                                               // c:1442 use *wsc
2221        };
2222
2223        // c:1448 — restriction-side check.
2224        if pre.tp != CPAT_CHAR {
2225            let mut mt: i32 = 0;
2226            if pattern_match1(pre, c, &mut mt) == 0 { return 0; }            // c:1449
2227        }
2228
2229        // c:1457-1485 — case-class equivalence (mt vs wmt mismatch).
2230        if pat.tp != CPAT_ANY || wpat.tp != CPAT_ANY {                       // c:1459
2231            let mut mt: i32 = 0;
2232            let ind = pattern_match1(pat, c, &mut mt);                       // c:1461
2233            if ind == 0 || ind != wind { return 0; }                         // c:1462-1465
2234            if mt != wmt {
2235                let case_pair = (mt == PP_LOWER || mt == PP_UPPER)
2236                             && (wmt == PP_LOWER || wmt == PP_UPPER);
2237                if case_pair {
2238                    let cc  = char::from_u32(c).unwrap_or('\0');
2239                    let wcc = char::from_u32(wc).unwrap_or('\0');
2240                    if ZC_tolower(cc) != ZC_tolower(wcc) { return 0; }       // c:1477
2241                } else {
2242                    return 0;                                                // c:1481
2243                }
2244            }
2245        }
2246
2247        // c:1496 — append deduced char to new_line.
2248        if let Some(ch) = char::from_u32(c) {
2249            new_line.push(ch);
2250        }
2251        pr_cur = pre.next.as_deref();                                        // c:1498
2252        wsc_idx += 1;
2253        p_cur = pat.next.as_deref();
2254        wp_cur = wpat.next.as_deref();
2255    }
2256
2257    // c:1505-1540 — tail loop: continue matching when wsc exhausted
2258    // but prestrict still has more chars (deduced solely from p).
2259    while p_cur.is_some() && pr_cur.is_some() {                              // c:1505
2260        let pat = p_cur.unwrap();
2261        let pre = pr_cur.unwrap();
2262        let c: u32 = if pre.tp == CPAT_CHAR {
2263            pre.chr
2264        } else if pat.tp == CPAT_CHAR {
2265            pat.chr
2266        } else {
2267            return 0;                                                        // c:1522 not enough info
2268        };
2269        let mut mt: i32 = 0;
2270        if pre.tp != CPAT_CHAR && pattern_match1(pre, c, &mut mt) == 0 {
2271            return 0;
2272        }
2273        if let Some(ch) = char::from_u32(c) {
2274            new_line.push(ch);
2275        }
2276        pr_cur = pre.next.as_deref();
2277        p_cur  = pat.next.as_deref();
2278    }
2279
2280    // c:1542 — `p_cur.is_none() && pr_cur.is_none() && (wp_cur.is_none() || wsc empty)`.
2281    if p_cur.is_none() && pr_cur.is_none()
2282        && (wp_cur.is_none() || wsc_idx >= wsc.len())
2283    {
2284        1                                                                    // c:1544 full match
2285    } else {
2286        0                                                                    // c:1545
2287    }
2288}
2289
2290/// Port of `pattern_match1(Cpattern p, convchar_t c, int *mtp)` from Src/Zle/compmatch.c:1269.
2291/// Direct port of `mod_export convchar_t pattern_match1(Cpattern p,
2292///                                    convchar_t c, int *mtp)`
2293/// from `Src/Zle/compmatch.c:1269`. Tests whether `p` matches
2294/// the single char `c`, returning the matched-char (1 for ANY, the
2295/// char for CHAR, or for EQUIV the equivalence-class index+1) or 0
2296/// on miss. `mtp` is non-zero only for the EQUIV path.
2297/// WARNING: param names don't match C — Rust=(p, mtp) vs C=(p, c, mtp)
2298pub fn pattern_match1(p: &crate::ported::zle::comp_h::Cpattern,              // c:1269
2299                      c: u32, mtp: &mut i32) -> u32
2300{
2301    use crate::ported::zle::comp_h::{CPAT_ANY, CPAT_CCLASS, CPAT_CHAR, CPAT_EQUIV, CPAT_NCLASS};
2302    *mtp = 0;                                                                // c:1273
2303    match p.tp {                                                             // c:1274
2304        x if x == CPAT_CCLASS => {                                           // c:1275
2305            // PATMATCHRANGE(p->u.str, c, NULL, NULL)
2306            patmatchrange(p.str.as_deref(), c, None, None) as u32           // c:1276
2307        }
2308        x if x == CPAT_NCLASS => {                                           // c:1278
2309            if patmatchrange(p.str.as_deref(), c, None, None) { 0 } else { 1 } // c:1279
2310        }
2311        x if x == CPAT_EQUIV => {                                            // c:1281
2312            let mut ind: u32 = 0;
2313            if patmatchrange(p.str.as_deref(), c, Some(&mut ind), Some(mtp)) {
2314                ind + 1                                                      // c:1283
2315            } else {
2316                0                                                            // c:1285
2317            }
2318        }
2319        x if x == CPAT_ANY  => 1,                                            // c:1288-1289
2320        x if x == CPAT_CHAR => if p.chr == c { c } else { 0 },               // c:1291-1292
2321        _ => 0,                                                              // c:1294
2322    }
2323}
2324
2325/// Minimal port of `PATMATCHRANGE(str, c, indp, mtp)` macro from
2326/// `Src/pattern.c`. Walks an encoded character-range descriptor in
2327/// `str` and tests whether `c` falls inside. The full C version
2328/// handles equivalence classes via `mtp`; this Rust port covers
2329/// the literal-char + ASCII-range cases.
2330fn patmatchrange(s: Option<&str>, c: u32, indp: Option<&mut u32>, _mtp: Option<&mut i32>) -> bool {
2331    let Some(s) = s else { return false; };
2332    let mut idx: u32 = 0;
2333    let mut chars = s.chars().peekable();
2334    while let Some(ch) = chars.next() {
2335        // Pair `lo-hi` if next is `-`.
2336        if let Some(&peek) = chars.peek() {
2337            if peek == '-' {
2338                chars.next();
2339                if let Some(hi) = chars.next() {
2340                    if c >= ch as u32 && c <= hi as u32 {
2341                        if let Some(out) = indp { *out = idx; }
2342                        return true;
2343                    }
2344                    idx += 1;
2345                    continue;
2346                }
2347            }
2348        }
2349        if c == ch as u32 {
2350            if let Some(out) = indp { *out = idx; }
2351            return true;
2352        }
2353        idx += 1;
2354    }
2355    false
2356}
2357
2358/// Direct port of `static int sub_join(Cline a, Cline b, Cline e,
2359///                                     int anew)` from
2360/// `Src/Zle/compmatch.c:2649`. Helper for join_mid: takes a
2361/// trailing sub-list `b..e` and joins it with `a->prefix`, returning
2362/// the byte-diff (max - min) when join_psfx succeeds, else 0.
2363///
2364/// Full body depends on join_psfx + cp_cline + revert_cline. With
2365/// join_psfx still stubbed, this port preserves the control-flow
2366/// shape (walks the b..e chain, sums min/max) but bails on the
2367/// join_psfx-driven branch — same observable contract for callers
2368/// that pre-check `b == e`.
2369pub fn sub_join(a: &mut crate::ported::zle::comp_h::Cline,                   // c:2649
2370                b: Option<Box<crate::ported::zle::comp_h::Cline>>,
2371                e: &mut crate::ported::zle::comp_h::Cline,
2372                anew: i32) -> i32
2373{
2374    use crate::ported::zle::comp_h::CLF_SUF;
2375
2376    // c:2651 — `if (!e->suffix && a->prefix)`.
2377    if e.suffix.is_some() || a.prefix.is_none() {
2378        return 0;                                                            // c:2698
2379    }
2380
2381    // c:2654 — int min = 0, max = 0.
2382    let mut min: i32 = 0;
2383    let mut max: i32 = 0;
2384
2385    // c:2655-2667 — walk b..e, splicing prefix sub-chains and the b
2386    // nodes themselves into a flat chain `chain`. We use a Vec since
2387    // we re-index it during the walk loop below.
2388    let mut chain: Vec<Box<crate::ported::zle::comp_h::Cline>> = Vec::new();
2389    let mut cur = b;
2390    while let Some(mut b_node) = cur {
2391        cur = b_node.next.take();
2392        // c:2656 — `if ((*p = t = b->prefix))` — splice prefix sub-list.
2393        let mut walk_pref = b_node.prefix.take();
2394        while let Some(mut p_node) = walk_pref {
2395            walk_pref = p_node.next.take();
2396            chain.push(p_node);
2397        }
2398        // c:2661-2664 — clear suffix/prefix, drop CLF_SUF, accumulate.
2399        b_node.suffix = None;
2400        b_node.prefix = None;
2401        b_node.flags &= !CLF_SUF;
2402        min += b_node.min;
2403        max += b_node.max;
2404        // c:2665 — `*p = b; p = &(b->next)`.
2405        chain.push(b_node);
2406    }
2407
2408    // c:2668 — `*p = e->prefix`. Splice e's prefix chain onto the tail.
2409    // We move it out (e.prefix is overwritten inside the loop anyway).
2410    let mut walk_e = e.prefix.take();
2411    let op_index = chain.len();                                              // c:2652 op marker
2412    let mut had_op = false;
2413    while let Some(mut node) = walk_e {
2414        walk_e = node.next.take();
2415        chain.push(node);
2416        had_op = true;
2417    }
2418
2419    // c:2669 — `ca = a->prefix`.
2420    let ca: Option<Box<crate::ported::zle::comp_h::Cline>> = a.prefix.clone();
2421
2422    // c:2671 — `while (n)`. Walk the chain index by index, calling
2423    // join_psfx with a fresh deep-clone of chain[i..] in e.prefix and
2424    // a fresh deep-clone of ca in a.prefix.
2425    let mut i = 0usize;
2426    while i < chain.len() {
2427        // c:2672 — `e->prefix = cp_cline(n, 1)`. Inline a deep clone of
2428        // chain[i..] as a fresh Cline chain.
2429        let mut head: Option<Box<crate::ported::zle::comp_h::Cline>> = None;
2430        let mut tail: *mut Option<Box<crate::ported::zle::comp_h::Cline>> = &mut head;
2431        for src in &chain[i..] {
2432            let mut clone = Box::new((**src).clone());
2433            clone.next = None;
2434            // c:201-204 — deep clone of prefix/suffix.
2435            clone.prefix = cp_cline(src.prefix.as_deref(), 0);
2436            clone.suffix = cp_cline(src.suffix.as_deref(), 0);
2437            unsafe {
2438                *tail = Some(clone);
2439                let nn = (*tail).as_mut().unwrap();
2440                tail = &mut nn.next;
2441            }
2442        }
2443        e.prefix = head;
2444
2445        // c:2673 — `a->prefix = cp_cline(ca, 1)`.
2446        a.prefix = cp_cline(ca.as_deref(), 1);
2447
2448        let f = e.flags;                                                     // c:2676 / c:2683
2449        if anew != 0 {
2450            join_psfx(e, a, None, None, 0);                                  // c:2678
2451            e.flags = f;                                                     // c:2679
2452            if e.prefix.is_some() {                                          // c:2680
2453                return max - min;                                            // c:2681
2454            }
2455        } else {
2456            join_psfx(a, e, None, None, 0);                                  // c:2685
2457            e.flags = f;                                                     // c:2686
2458            if a.prefix.is_some() {                                          // c:2687
2459                return max - min;                                            // c:2688
2460            }
2461        }
2462        // c:2690 — `min -= n->min`.
2463        min -= chain[i].min;
2464
2465        // c:2692 — `if (n == op) break`.
2466        if had_op && i == op_index {
2467            break;
2468        }
2469        i += 1;                                                              // c:2694 n = n->next
2470    }
2471    max - min                                                                // c:2696
2472}
2473
2474/// Direct port of `static int sub_match(cmdata md, char *str, int len,
2475///                                       int sfx)` from
2476/// `Src/Zle/compmatch.c:2301`. Accumulates the longest common
2477/// prefix (or suffix when `sfx` set) between the substring
2478/// `str[..len]` and the data in `md`, advancing `md.str`/`md.len`
2479/// as it consumes characters.
2480///
2481/// Returns the count of matched bytes — the C source's "ret" value.
2482pub fn sub_match(md: &mut cmdata, str: &str, len: i32, sfx: i32) -> i32 {   // c:2301
2483    let mut ret = 0i32;
2484    let str_bytes = str.as_bytes();
2485    let mut remaining = len as usize;
2486    let start_idx: usize = if sfx != 0 { (len as usize).min(str_bytes.len()) } else { 0 };
2487
2488    // c:2319 — outer while-len loop: refill md, find common prefix
2489    // (or suffix), accumulate ret, then re-enter for next cline node.
2490    while remaining > 0 {                                                    // c:2319
2491        if check_cmdata(md, sfx) != 0 {                                      // c:2320
2492            return ret;
2493        }
2494
2495        let md_bytes = md.str.as_bytes();
2496        let mut l: usize = 0;
2497        let md_len_usize = md.len as usize;
2498        let cap = remaining.min(md_len_usize);
2499
2500        // c:2329-2331 — accumulate matching chars from the chosen end.
2501        while l < cap {
2502            let s_idx: isize = if sfx != 0 {
2503                start_idx as isize - (l as isize) - 1 - (ret as isize)
2504            } else {
2505                (ret as isize) + (l as isize)
2506            };
2507            let m_len = md_bytes.len();
2508            let m_idx: isize = if sfx != 0 {
2509                m_len as isize - (l as isize) - 1
2510            } else {
2511                l as isize
2512            };
2513            if s_idx < 0 || m_idx < 0 { break; }
2514            let s_pos = s_idx as usize;
2515            let m_pos = m_idx as usize;
2516            if s_pos >= str_bytes.len() || m_pos >= md_bytes.len() { break; }
2517            if str_bytes[s_pos] != md_bytes[m_pos] { break; }
2518            l += 1;
2519        }
2520
2521        if l == 0 { return ret; }                                            // c:2380 no progress
2522
2523        // c:2335-2349 — meta-character boundary correction. Avoid
2524        // ending in the middle of a `Meta x` 2-byte sequence.
2525        const META_BYTE: u8 = 0x83;
2526        let check_pos: isize = if sfx != 0 {
2527            start_idx as isize - (l as isize) - (ret as isize)
2528        } else {
2529            (ret as isize) + (l as isize) - 1
2530        };
2531        if check_pos >= 0 && (check_pos as usize) < str_bytes.len()
2532            && str_bytes[check_pos as usize] == META_BYTE && l > 0
2533        {
2534            l -= 1;
2535        }
2536
2537        // c:2400 — md.len -= l; md.str = md.str + l (or md.str - l for sfx).
2538        md.len -= l as i32;
2539        if sfx != 0 {
2540            // suffix-mode: strip from the END of md.str.
2541            md.str = md.str.chars().take(
2542                md.str.chars().count().saturating_sub(l),
2543            ).collect();
2544        } else {
2545            // prefix-mode: skip first l bytes.
2546            md.str = md.str.chars().skip(l).collect();
2547        }
2548
2549        ret += l as i32;                                                     // c:2418
2550        remaining = remaining.saturating_sub(l);
2551
2552        if remaining == 0 || md.len == 0 {                                   // c:2421
2553            break;
2554        }
2555    }
2556    ret                                                                      // c:2441
2557}
2558
2559/// Port of `undo_cmdata(Cmdata md, int sfx)` from Src/Zle/compmatch.c:2188.
2560/// Direct port of `static Cline undo_cmdata(cmdata md, int sfx)` from
2561/// `Src/Zle/compmatch.c:2188`. Puts the not-yet-matched portion
2562/// of `md` back into the previous cline node so it can be revisited
2563/// on a different match path.
2564pub fn undo_cmdata(md: &cmdata, sfx: i32) -> Option<Box<crate::ported::zle::comp_h::Cline>> { // c:2188
2565    use crate::ported::zle::comp_h::CLF_LINE;
2566    let mut r = md.pcl.as_deref().cloned()?;                                 // c:2189 r = md->pcl
2567
2568    if md.line != 0 {                                                        // c:2191
2569        r.word = None;                                                       // c:2192
2570        r.wlen = 0;                                                          // c:2193
2571        r.flags |= CLF_LINE;                                                 // c:2194
2572        r.llen = md.len;                                                     // c:2195
2573        // c:2197 — line = str - (sfx ? len : 0).
2574        let off = if sfx != 0 { md.len as usize } else { 0 };
2575        r.line = Some(md.str.chars().skip(md.str.len().saturating_sub(off + md.len as usize)).collect());
2576    } else if md.len != md.olen {                                            // c:2199
2577        r.wlen = md.len;                                                     // c:2201
2578        let off = if sfx != 0 { md.len as usize } else { 0 };
2579        r.word = Some(md.str.chars().skip(md.str.len().saturating_sub(off + md.len as usize)).collect());
2580    }
2581    Some(Box::new(r))                                                        // c:2206
2582}
2583