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