Skip to main content

zsh/ported/
pattern.rs

1//! Pattern matching — port of `Src/pattern.c`.
2//!
3//! This is the bytecode-based port. The C source compiles patterns
4//! into a flat `char *patout` buffer of packed opcodes; the matcher
5//! is an interpreter that walks the buffer using pointer arithmetic.
6//!
7//! Rust port preserves the bytecode architecture using `Vec<u8>`:
8//!   * Each opcode is 1 byte (matches C `Upat::c`).
9//!   * `next_off` is a 4-byte little-endian `u32` offset to the next
10//!     opcode in sequence (0 = end). C uses native-endian `long`; we
11//!     pin to LE for portable on-disk bytecode caches.
12//!   * Payloads (strings, ranges, branch operands) are encoded inline
13//!     after the `next_off` slot.
14//!
15//! Top-level declaration order mirrors `Src/pattern.c`:
16//!   1. Opcode constants (P_*) — pattern.c:97-127
17//!   2. Macro-style accessors (P_OP / P_NEXT / etc.) — pattern.c:175-210
18//!   3. Flag-bit constants (P_SIMPLE / P_HSTART / P_PURESTR) — pattern.c:216
19//!   4. `struct patprog` — zsh.h:1601
20//!   5. PAT_* flag constants — zsh.h:1623
21//!   6. ZPC_* indexes — zsh.h:1644
22//!   7. File-static globals — pattern.c file-scope
23//!   8. Bytecode write helpers (`patadd`, `patnode`, `patinsert`,
24//!      `pattail`, `patoptail`, `patcompcharsset`, `patcompstart`)
25//!   9. Compiler entry points (`patcompile`, `patcompswitch`,
26//!      `patcompbranch`, `patcomppiece`, `patcompnot`)
27//!  10. Glob-flag parser (`patgetglobflags`)
28//!  11. Range helpers (`range_type`, `pattern_range_to_string`)
29//!  12. Char-decode helpers (`charref`, `charnext`, `charrefinc`,
30//!      `charsub`, `metacharinc`, `clear_shiftstate`)
31//!  13. Matcher entry points (`pattry`, `pattrylen`, `pattryrefs`)
32//!  14. `patmatch` interpreter — pattern.c:2694
33//!  15. (Section removed — formerly held Rust-only entries
34//!      `patmatchrange(&[char], char, igncase)`,
35//!      `patmatchindex(&[char], idx)`, `mb_patmatchrange`,
36//!      `mb_patmatchindex`. All deleted as Rule B / semantic
37//!      deviations; see comments at the deletion sites for the
38//!      faithful-port plan. The Cpattern-byte-stream walker in
39//!      `zle/compmatch.rs::patmatchrange` is the production matcher
40//!      pending a Rule C relocation to pattern.rs.)
41//!  16. String pre-processing (`patmungestring`, `patallocstr`,
42//!      `pattrystart`)
43//!  17. Module-loader / disable mgmt (`startpatternscope`,
44//!      `endpatternscope`, `savepatterndisables`,
45//!      `restorepatterndisables`, `clearpatterndisables`,
46//!      `freepatprog`, `pat_enables`)
47//!  18. (Section removed — formerly held Rust-only convenience entries
48//!      `patmatch(pat, text)`, `patmatchlen(prog, string)`, `patrepeat(
49//!      prog, s, max)`, `mb_patmatchrange`, `mb_patmatchindex`. All
50//!      deleted: `patmatch` got renamed to the C-faithful bytecode-
51//!      walker name; the others had zero Rust callers + Rule S1
52//!      signature deviations. `haswilds` is a real C name — port lives
53//!      in section 4 helpers.)
54//!
55//! See `docs/PORT.md` Rules A/B/C/D/E.
56
57#![allow(non_upper_case_globals)]
58#![allow(non_camel_case_types)]
59
60use crate::ported::params::{paramtab, paramtab_hashed_storage};
61use crate::ported::utils::ztrsub;
62use crate::ported::zle::zle_h::{COMP_LIST_COMPLETE, COMP_LIST_EXPAND};
63pub use crate::ported::zsh_h::{
64    patstralloc, Patstralloc, GF_BACKREF, GF_IGNCASE, GF_LCMATCHUC, GF_MATCHREF, GF_MULTIBYTE,
65    PAT_ANY, PAT_FILE, PAT_FILET, PAT_HAS_EXCLUDP, PAT_HEAPDUP, PAT_LCMATCHUC, PAT_NOANCH,
66    PAT_NOGLD, PAT_NOTEND, PAT_NOTSTART, PAT_PURES, PAT_SCAN, PAT_STATIC, PAT_ZDUP, PP_ALNUM,
67    PP_ALPHA, PP_ASCII, PP_BLANK, PP_CNTRL, PP_DIGIT, PP_GRAPH, PP_IDENT, PP_IFS, PP_IFSSPACE,
68    PP_INCOMPLETE, PP_INVALID, PP_LOWER, PP_PRINT, PP_PUNCT, PP_RANGE, PP_SPACE, PP_UPPER, PP_WORD,
69    PP_XDIGIT, ZMB_INCOMPLETE, ZMB_INVALID, ZPC_SEG_COUNT,
70};
71use crate::utils::zerrnam;
72use crate::zsh_h::{
73    isset, patprog, Bang, Bar, Hat, Inang, Inbrack, Inpar, Marker, Meta, Nularg, Outbrack, Pound,
74    Quest, Star, BASHAUTOLIST, CASEGLOB, CASEPATHS, EXTENDEDGLOB, KSHGLOB, MULTIBYTE,
75    NUMERICGLOBSORT, PM_HASHED, PM_TYPE, RCQUOTES, SHGLOB, SORTIT_IGNORING_BACKSLASHES,
76    SORTIT_NUMERICALLY, ZPC_BAR, ZPC_BNULLKEEP, ZPC_COUNT, ZPC_HASH, ZPC_HAT, ZPC_INANG,
77    ZPC_INBRACK, ZPC_INPAR, ZPC_KSH_AT, ZPC_KSH_BANG, ZPC_KSH_BANG2, ZPC_KSH_PLUS, ZPC_KSH_QUEST,
78    ZPC_KSH_STAR, ZPC_NULL, ZPC_OUTPAR, ZPC_QUEST, ZPC_SLASH, ZPC_STAR, ZPC_TILDE,
79};
80use std::sync::atomic::{AtomicI32, AtomicUsize, Ordering};
81use std::sync::Mutex;
82
83// =====================================================================
84// 6. ZPC_* enum from zsh.h:1644 — indexes into the active-pattern-
85// characters table that compile-time and runtime both consult.
86// =====================================================================
87
88/// Maximum captures, from `pattern.c:94 NSUBEXP`.
89pub const NSUBEXP: usize = 9;
90
91// =====================================================================
92// 1. P_* opcode constants — pattern.c:97-127
93//
94// Numbered identically to C so a buffer compiled by this port matches
95// the C source's bytecode-cache format byte-for-byte (modulo native
96// endianness; we pin LE — see file header).
97// =====================================================================
98/// `P_END` constant.
99pub const P_END: u8 = 0x00; // c:97  End of program.
100/// `P_EXCSYNC` constant.
101pub const P_EXCSYNC: u8 = 0x01; // c:98  Test if following exclude already failed
102/// `P_EXCEND` constant.
103pub const P_EXCEND: u8 = 0x02; // c:99  Test if exclude matched orig branch
104/// `P_BACK` constant.
105pub const P_BACK: u8 = 0x03; // c:100 Match "", "next" ptr points backward.
106/// `P_EXACTLY` constant.
107pub const P_EXACTLY: u8 = 0x04; // c:101 lstr — match this string.
108/// `P_NOTHING` constant.
109pub const P_NOTHING: u8 = 0x05; // c:102 Match empty string.
110/// `P_ONEHASH` constant.
111pub const P_ONEHASH: u8 = 0x06; // c:103 node — match 0 or more of preceding simple.
112/// `P_TWOHASH` constant.
113pub const P_TWOHASH: u8 = 0x07; // c:104 node — match 1 or more of preceding simple.
114/// `P_GFLAGS` constant.
115pub const P_GFLAGS: u8 = 0x08; // c:105 long — match nothing and set globbing flags.
116/// `P_ISSTART` constant.
117pub const P_ISSTART: u8 = 0x09; // c:106 Match start of string.
118/// `P_ISEND` constant.
119pub const P_ISEND: u8 = 0x0a; // c:107 Match end of string.
120/// `P_COUNTSTART` constant.
121pub const P_COUNTSTART: u8 = 0x0b; // c:108 Initialise P_COUNT.
122/// `P_COUNT` constant.
123pub const P_COUNT: u8 = 0x0c; // c:109 3*long uc* node — match a number of repetitions.
124/// `P_BRANCH` constant.
125pub const P_BRANCH: u8 = 0x20; // c:112 node — match this alternative, or the next.
126/// `P_WBRANCH` constant.
127pub const P_WBRANCH: u8 = 0x21; // c:113 uc* node — P_BRANCH, but match at least 1 char.
128/// `P_EXCLUDE` constant.
129pub const P_EXCLUDE: u8 = 0x30; // c:114 uc* node — exclude this from previous branch.
130/// `P_EXCLUDP` constant.
131pub const P_EXCLUDP: u8 = 0x31; // c:115 uc* node — exclude, using full file path so far.
132/// `P_ANY` constant.
133pub const P_ANY: u8 = 0x40; // c:117 Match any one character.
134/// `P_ANYOF` constant.
135pub const P_ANYOF: u8 = 0x41; // c:118 str — match any character in this string.
136/// `P_ANYBUT` constant.
137pub const P_ANYBUT: u8 = 0x42; // c:119 str — match any character not in this string.
138/// `P_STAR` constant.
139pub const P_STAR: u8 = 0x43; // c:120 Match any set of characters.
140/// `P_NUMRNG` constant.
141pub const P_NUMRNG: u8 = 0x44; // c:121 zr,zr — match a numeric range.
142/// `P_NUMFROM` constant.
143pub const P_NUMFROM: u8 = 0x45; // c:122 zr — match a number >= X.
144/// `P_NUMTO` constant.
145pub const P_NUMTO: u8 = 0x46; // c:123 zr — match a number <= X.
146/// `P_NUMANY` constant.
147pub const P_NUMANY: u8 = 0x47; // c:124 Match any set of decimal digits.
148/// `P_OPEN` constant.
149pub const P_OPEN: u8 = 0x80; // c:126 Mark this point in input as start of n.
150/// `P_CLOSE` constant.
151pub const P_CLOSE: u8 = 0x90; // c:127 Analogous to OPEN.
152
153/// `P_ISBRANCH(p)` macro from pattern.c:200 — `(p->l & 0x20)`.
154#[inline]
155pub fn P_ISBRANCH(op: u8) -> bool {
156    (op & 0x20) != 0
157}
158
159/// `P_ISEXCLUDE(p)` macro from pattern.c:201 — `((p->l & 0x30) == 0x30)`.
160#[inline]
161pub fn P_ISEXCLUDE(op: u8) -> bool {
162    (op & 0x30) == 0x30
163}
164
165/// `P_NOTDOT(p)` macro from pattern.c:202 — `(p->l & 0x40)`.
166#[inline]
167pub fn P_NOTDOT(op: u8) -> bool {
168    (op & 0x40) != 0
169}
170
171// =====================================================================
172// 3. Flag-bit constants returned via flagp out-params during compile.
173// pattern.c:216-218
174// =====================================================================
175/// `P_SIMPLE` constant.
176pub const P_SIMPLE: i32 = 0x01; // c:216 Simple enough to be # / ## operand.
177/// `P_HSTART` constant.
178pub const P_HSTART: i32 = 0x02; // c:217 Starts with # or ##'d pattern.
179/// `P_PURESTR` constant.
180pub const P_PURESTR: i32 = 0x04; // c:218 Can be matched with a strcmp.
181
182// =====================================================================
183// 5. PAT_* flag constants — re-exports of zsh.h:1623-1640 already in
184// zsh_h.rs. Re-published here so callers in pattern's API don't need
185// the longer path. C source has these as `#define` in zsh.h, not
186// pattern.c, so the canonical home is zsh_h.rs; we just alias.
187// =====================================================================
188
189// C: `static int patnpar;` — number of active parens (1-indexed at
190// compile time; the *struct* patnpar is the actual count).
191pub static patnpar: AtomicI32 = AtomicI32::new(0); // c:271
192
193// GF_* glob-flag bits live in `zsh.h:1763-1773`, ported to
194// `src/ported/zsh_h.rs:2287-2291` per Rule C. Re-export so pattern's
195// matcher arms can read them without the longer path.
196
197// =====================================================================
198// 7. File-static globals — direct mirror of pattern.c file-scope
199// statics. Each C `static` ports to a Rust `static` of matching name
200// (Rule A) and `Mutex<>` / `Atomic*` for thread-safe access. None
201// are aggregated (Rule D).
202// =====================================================================
203
204// C: `static char *patout, *patcode;` + `static long patsize;` +
205// `static int patalloc;` — pattern.c:267-272 (in macro region).
206//
207// patout: the bytecode buffer.
208// patcode: write cursor (offset into patout).
209// patsize: current logical size.
210// patalloc: allocated capacity.
211//
212// In Rust, `Vec<u8>` already tracks both len and capacity, so we
213// hold just the buffer; patcode/patsize/patalloc are derived.
214pub static patout: Mutex<Vec<u8>> = Mutex::new(Vec::new()); // c:267
215
216// C: `static int patflags;` — current PAT_* flag set during compile.
217pub static patflags: AtomicI32 = AtomicI32::new(0); // c:272
218
219// C: `static int patglobflags;` — current globbing flags during compile.
220pub static patglobflags: AtomicI32 = AtomicI32::new(0); // c:273
221
222/// Port of file-static `int patinlen` from `pattrystate` struct
223/// at `Src/pattern.c:1877` (accessed via `#define patinlen
224/// (pattrystate.patinlen)` at line 1894). Length in metafied bytes
225/// of the last successful pattry match; computed at the end of
226/// `pattry`/`pattrylen`/`pattryrefs` (`patinput - patinstart`,
227/// pattern.c:2508). Read by `patmatchlen()` and by the
228/// `${var//pat/repl}` paramsubst pipeline that needs to know how
229/// much of the input was consumed.
230pub static patinlen: AtomicI32 = AtomicI32::new(0); // c:1877
231
232// =====================================================================
233// 12. Char-decode helpers — pattern.c:327, :336, :1909-1997
234// =====================================================================
235
236/// Port of `clear_shiftstate()` from `Src/pattern.c:327`. C uses
237/// `mbstate_t`; Rust `char` is already a code point, so no shift
238/// state to clear.
239pub fn clear_shiftstate() {} // c:327
240
241/// Port of `metacharinc(char **x)` from `Src/pattern.c:336`. Advances past
242/// Port of `wchar_t metacharinc(char **x)` from `Src/pattern.c:336`.
243/// Advances `*x` past one metafied / multibyte char and returns the
244/// decoded codepoint. The C body branches on:
245///   - `GF_MULTIBYTE` clear OR high-bit-clear: single-byte path
246///     with `itok(*x)` zsh-token translation and `Meta` xor-32
247///   - else: `mbrtowc` over the metafied bytes with state machine
248///
249/// **Rust port:** UTF-32-native delegation to `&str.chars()`.
250/// Delegates to Rust's native UTF-8 iterator — both C branches
251/// collapse because Rust's `char` is already the wide-char form,
252/// and the stored slice is the post-Meta-decode form (zshrs's
253/// source bytes are already UTF-8, not Meta-encoded). The `itok` →
254/// `ztokens[]` zsh-token table mapping doesn't apply because the
255/// pattern compiler stores raw chars; translation happens at
256/// compile time, not at scan time.
257///
258/// Rust signature differs from C `metacharinc(char **x)`: C mutates
259/// `*x` to advance the pointer; Rust returns the new byte position
260/// since `&str` length is immutable. Callers update their cursor
261/// from the return value.
262pub fn metacharinc(s: &str, pos: usize) -> usize {
263    // c:336
264    // c:343-360 single-byte short-circuit + c:363-380 mbrtowc loop:
265    // both collapse to Rust's `chars().next()` which decodes one
266    // valid UTF-8 codepoint from the slice, regardless of byte width.
267    s[pos..]
268        .chars()
269        .next()
270        .map(|c| pos + c.len_utf8())
271        .unwrap_or(pos)
272}
273
274// =====================================================================
275// 8. Bytecode write helpers — pattern.c:412-1856
276// =====================================================================
277
278/// Port of the anonymous `enum { PA_NOALIGN = 1, PA_UNMETA = 2 };`
279/// from `Src/pattern.c:405-408`. Flags passed as the `paflags` arg
280/// to `patadd`.
281pub const PA_NOALIGN: i32 = 1; // c:406
282/// `PA_UNMETA` constant.
283pub const PA_UNMETA: i32 = 2; // c:407
284
285/// Direct port of `static void patadd(char *add, int ch, long n,
286/// int paflags)` from `Src/pattern.c:410-450`.
287///
288/// Append `n` bytes from `add` (or a single `ch` byte when `add ==
289/// None`) to `patout`, growing the backing storage if needed and
290/// padding to the C `union upat` alignment unless `PA_NOALIGN` is
291/// set. With `PA_UNMETA`, walk the input as zsh-metafied bytes
292/// (Meta + (b^32)) and untokenize (`itok` → `ztokens[]`).
293///
294/// **The previous Rust impl had three concrete defects:**
295///   1. Declared `-> i64` returning the starting offset. C is
296///      `static void`; callers use side-effects on `patcode`,
297///      not a return value.
298///   2. When `add == None`, the C body writes `ch` **once**
299///      (`*patcode++ = ch;`). The Rust port looped `0..n` pushing
300///      `ch` n times, so a single-byte literal grew to n copies.
301///   3. Skipped the C alignment-round-up to `sizeof(union upat)`
302///      (8 bytes on every supported arch — c:417-418), so any
303///      caller reading `patout` as a `[upat]` would mis-align.
304///
305/// zshrs's `patcode` pointer collapses into `patout.len()`; the
306/// realloc dance in C (c:419-425) is implicit via `Vec::extend`.
307/// `patsize` updates are the post-write `patout.len()`.
308fn patadd(add: Option<&[u8]>, ch: u8, n: i64, paflags: i32) {
309    use crate::ported::lex::ztokens as ztokens_str;
310    use crate::ported::zsh_h::Pound;
311    use crate::ported::ztype_h::itok;
312    let ztokens = ztokens_str.as_bytes();
313    // c:412
314    // c:415 — `long newpatsize = patsize + n;`
315    let mut buf = patout.lock().unwrap();
316    let patsize = buf.len() as i64;
317    let mut newpatsize = patsize + n;
318    // c:416-418 — round up to upat-union alignment (sizeof(union upat) =
319    // 8 on every supported arch) unless PA_NOALIGN.
320    if (paflags & PA_NOALIGN) == 0 {
321        // c:416
322        let upat = 8i64; // c:417 sizeof(union upat)
323        newpatsize = (newpatsize + upat - 1) & !(upat - 1); // c:417-418
324    }
325    // c:419-425 — realloc; Rust Vec auto-grows so just resize_to.
326    if newpatsize > buf.len() as i64 {
327        buf.resize(newpatsize as usize, 0);
328    }
329    // c:426 — `patsize = newpatsize;` — patsize is the buffer's
330    // logical write head; zshrs tracks it via buf.len() AFTER the
331    // write below.
332    let mut patcode_pos = patsize as usize; // c:Src/pattern.c — `patcode` pointer.
333
334    if let Some(add_bytes) = add {
335        // c:427 — `if (add) {`
336        if (paflags & PA_UNMETA) != 0 {
337            // c:428-442 — PA_UNMETA: walk add_bytes, unmetafy + untokenize
338            // as we go. Meta chars aren't counted in n. itok bytes route
339            // through ztokens[*add - Pound].
340            let mut idx = 0usize;
341            let mut remaining = n;
342            while remaining > 0 && idx < add_bytes.len() {
343                let b = add_bytes[idx];
344                if itok(b) {
345                    // c:434-435 — `if (itok(*add)) *patcode++ =
346                    //               ztokens[*add++ - Pound];`
347                    if idx < add_bytes.len() {
348                        let tok_idx = b.wrapping_sub(Pound as u8) as usize;
349                        if tok_idx < ztokens.len() {
350                            // c:435
351                            if patcode_pos < buf.len() {
352                                buf[patcode_pos] = ztokens[tok_idx];
353                            }
354                            patcode_pos += 1;
355                        }
356                        idx += 1;
357                    }
358                } else if b == Meta {
359                    // c:436-438 — `else if (*add == Meta) { add++;
360                    //               *patcode++ = *add++ ^ 32; }`
361                    idx += 1;
362                    if idx < add_bytes.len() {
363                        if patcode_pos < buf.len() {
364                            buf[patcode_pos] = add_bytes[idx] ^ 32;
365                        }
366                        patcode_pos += 1;
367                        idx += 1;
368                    }
369                } else {
370                    // c:439-440 — `else { *patcode++ = *add++; }`
371                    if patcode_pos < buf.len() {
372                        buf[patcode_pos] = b;
373                    }
374                    patcode_pos += 1;
375                    idx += 1;
376                }
377                remaining -= 1;
378            }
379        } else {
380            // c:444-445 — `while (n--) *patcode++ = *add++;` — plain copy.
381            let n_actual = (n as usize).min(add_bytes.len());
382            for &b in &add_bytes[..n_actual] {
383                if patcode_pos < buf.len() {
384                    buf[patcode_pos] = b;
385                }
386                patcode_pos += 1;
387            }
388        }
389    } else {
390        // c:447-448 — `else *patcode++ = ch;` — single-byte write.
391        if patcode_pos < buf.len() {
392            buf[patcode_pos] = ch;
393        }
394        patcode_pos += 1;
395    }
396    // c:449 — `patcode = patout + patsize;` — restore the post-write
397    // head so subsequent patadd calls append at the next slot. zshrs
398    // tracks via buf.len(); trim back to newpatsize so the alignment
399    // padding stays visible to consumers as zeroes.
400    let _ = patcode_pos;
401    // (No explicit truncate — buf.len() == newpatsize already.)
402}
403
404/// Port of `patcompcharsset()` from `Src/pattern.c:464`.
405///
406/// Initializes the `zpc_special` table for the active globbing
407/// regime. The C source resets every ZPC_* slot to its literal
408/// character, then masks off characters via `Marker` (0xa2 — the
409/// canonical "invalid" sentinel) for three option-driven cases:
410///   1. `!isset(EXTENDEDGLOB)` → Tilde/Hat/Hash disabled.
411///   2. `!isset(KSHGLOB)`      → KSH_QUEST/STAR/PLUS/BANG/BANG2/AT disabled.
412///   3. `isset(SHGLOB)`        → Inpar/Inang disabled.
413///
414pub fn patcompcharsset() {
415    // c:464
416    let mut sp = zpc_special.lock().unwrap();
417    *sp = [0u8; ZPC_COUNT as usize];
418    // c:469 — `memcpy(zpc_special, zpc_chars, ZPC_COUNT)`. The default
419    // char for every ZPC_* slot. Direct positional assignment here
420    // since zshrs doesn't carry the `zpc_chars` const array yet.
421    //
422    // NOTE on token bytes: C's `zpc_chars[]` (pattern.c:248) uses
423    // the TOKENIZED high-bit bytes (`Bar`, `Tilde`, etc.); the C
424    // shell lexer tokenizes raw ASCII to these only inside grouped
425    // alternation. zshrs's parser doesn't yet run that pre-tokenize
426    // pass — every byte reaches patcompile as raw ASCII. Switching
427    // these slots to the high-bit form would break ~80 library
428    // tests that rely on raw `|` triggering alternation. The
429    // narrower fix for bug #12 lives in vm_helper::glob_match_static
430    // where we conservatively escape pattern bytes the param-strip
431    // path could not have intended as metacharacters.
432    sp[ZPC_SLASH as usize] = b'/';
433    sp[ZPC_NULL as usize] = 0;
434    sp[ZPC_BAR as usize] = b'|';
435    sp[ZPC_OUTPAR as usize] = b')';
436    sp[ZPC_TILDE as usize] = b'~';
437    sp[ZPC_INPAR as usize] = b'(';
438    sp[ZPC_QUEST as usize] = b'?';
439    sp[ZPC_STAR as usize] = b'*';
440    sp[ZPC_INBRACK as usize] = b'[';
441    sp[ZPC_INANG as usize] = b'<';
442    sp[ZPC_HAT as usize] = b'^';
443    sp[ZPC_HASH as usize] = b'#';
444    sp[ZPC_BNULLKEEP as usize] = 0;
445    // c:478-490 — KSH_GLOB slots (omitted from previous Rust port).
446    // Each defaults to the literal ksh-glob trigger char. The
447    // option-mask pass below disables them when KSHGLOB is off.
448    sp[ZPC_KSH_QUEST as usize] = b'?';
449    sp[ZPC_KSH_STAR as usize] = b'*';
450    sp[ZPC_KSH_PLUS as usize] = b'+';
451    sp[ZPC_KSH_BANG as usize] = b'!';
452    sp[ZPC_KSH_BANG2 as usize] = b'!';
453    sp[ZPC_KSH_AT as usize] = b'@';
454
455    let marker_byte = Marker as u32 as u8;
456
457    // c:471-478 — `for (...; i < ZPC_COUNT; ...) if (*disp) *spp = Marker;`
458    // Apply user disables from `disable -p` BEFORE the option-driven
459    // masks (so EXTENDEDGLOB / KSHGLOB / SHGLOB layer over the per-
460    // pattern disables).
461    {
462        let disp = zpc_disables.lock().unwrap();
463        for i in 0..(ZPC_COUNT as usize) {
464            if disp[i] != 0 {
465                sp[i] = marker_byte; // c:476
466            }
467        }
468    }
469
470    // c:480-483 — `if (!isset(EXTENDEDGLOB))` mask Tilde/Hat/Hash.
471    if !isset(EXTENDEDGLOB) {
472        sp[ZPC_TILDE as usize] = marker_byte;
473        sp[ZPC_HAT as usize] = marker_byte;
474        sp[ZPC_HASH as usize] = marker_byte;
475    }
476
477    // c:485-491 — `if (!isset(KSHGLOB))` mask the six KSH_* slots.
478    if !isset(KSHGLOB) {
479        sp[ZPC_KSH_QUEST as usize] = marker_byte;
480        sp[ZPC_KSH_STAR as usize] = marker_byte;
481        sp[ZPC_KSH_PLUS as usize] = marker_byte;
482        sp[ZPC_KSH_BANG as usize] = marker_byte;
483        sp[ZPC_KSH_BANG2 as usize] = marker_byte;
484        sp[ZPC_KSH_AT as usize] = marker_byte;
485    }
486
487    // c:499-505 — `if (isset(SHGLOB))` mask Inpar/Inang (case/numeric
488    // ranges not valid under sh-emulation).
489    if isset(SHGLOB) {
490        sp[ZPC_INPAR as usize] = marker_byte;
491        sp[ZPC_INANG as usize] = marker_byte;
492    }
493}
494
495/// Port of `patcompstart()` from `Src/pattern.c:517`.
496///
497/// Resets per-compile globals. Called at the start of `patcompile`.
498///
499/// **C body (c:517-526)** — strict order matters:
500///   1. `patcompcharsset()` — must run FIRST so the zpc_special
501///      table reflects the current option state before parsing.
502///   2. `patglobflags = isset(CASEGLOB) || isset(CASEPATHS) ? 0 :
503///      GF_IGNCASE;` — case-insensitivity is the default UNLESS
504///      one of the case-sensitive options is set.
505///   3. `if (isset(MULTIBYTE)) patglobflags |= GF_MULTIBYTE;` —
506///      multibyte handling is option-gated, NOT unconditional.
507///
508/// The previous Rust port had three divergences: (a) called
509/// patcompcharsset LAST instead of FIRST, (b) unconditionally set
510/// GF_MULTIBYTE even when `setopt nomultibyte`, (c) NEVER set
511/// GF_IGNCASE — `setopt nocaseglob` had zero effect on pattern
512/// case-folding.
513pub fn patcompstart() {
514    // c:517
515    // c:519 — `patcompcharsset()` FIRST.
516    patcompcharsset();
517    patout.lock().unwrap().clear();
518    patnpar.store(1, Ordering::Relaxed);
519    patflags.store(0, Ordering::Relaxed);
520    // c:520-523 — CASE option dispatch.
521    let mut flags: i32 = if isset(CASEGLOB) || isset(CASEPATHS) {
522        0 // c:521 case-sensitive
523    } else {
524        GF_IGNCASE // c:523 default = ignore case
525    };
526    // c:524-525 — MULTIBYTE option respect.
527    if isset(MULTIBYTE) {
528        flags |= GF_MULTIBYTE; // c:525
529    }
530    patglobflags.store(flags, Ordering::Relaxed);
531    errsfound.store(0, Ordering::Relaxed);
532    forceerrs.store(-1, Ordering::Relaxed);
533    patparse_off.store(0, Ordering::Relaxed);
534}
535
536// =====================================================================
537// 9. Compiler entry points — pattern.c:540
538// =====================================================================
539
540/// Port of `patcompile(char *exp, int inflags, char **endexp)` from `Src/pattern.c:540`.
541///
542/// C signature: `Patprog patcompile(char *exp, int inflags, char **endexp)`.
543/// Compiles pattern `exp` under flags `inflags`, returns a `Patprog`
544/// on success or `NULL` on failure. `endexp` (if non-NULL) is set to
545/// the input cursor at end of parse — used by `bin_zregexparse` to
546/// detect partial-parse cases.
547pub fn patcompile(exp: &str, inflags: i32, mut endexp: Option<&mut String>) -> Option<Patprog> {
548    // Global compiled-pattern cache (crate::pat_cache, a zshrs-only opt —
549    // see that module for the safety/key/threading rationale). Skipped when
550    // `endexp` is requested: that out-param is a compile side effect the
551    // cache can't reproduce. The check runs BEFORE the compile mutex so a
552    // hit never serialises on the compiler.
553    let cacheable = endexp.is_none();
554    if cacheable {
555        if let Some(hit) = crate::pat_cache::get(exp, inflags) {
556            return Some(hit);
557        }
558    }
559    // Capture the ORIGINAL pattern text for the cache-store key: `exp` is
560    // shadowed below by the decoded form, and the option state that feeds
561    // the fingerprint is unchanged between here and the store (patcompstart
562    // only reads options), so get/put keys match.
563    let exp_orig: String = if cacheable { exp.to_string() } else { String::new() };
564    // Hold the compile mutex for the entire body — `patcompstart`
565    // resets every file-scope static (`Src/pattern.c:267-281`) and the
566    // emit/parse helpers mutate them in sequence. C is single-threaded
567    // so the statics are race-free there; Rust must serialise.
568    let _compile_guard = PATCOMPILE_LOCK.lock().unwrap_or_else(|e| e.into_inner());
569    // c:1610 `patstartch` — the leading plain character of the pattern.
570    // C only needs the NOGLD leading-dot rule to know whether a pattern
571    // explicitly begins with `.` (so `.*` matches dot files while `*`
572    // does not). Capture it before `exp` is shadowed by the normalizer
573    // below. A leading glob token (Star/Quest/Inbrack/…) is not plain, so
574    // only a literal `.` is recorded; everything else stays 0.
575    let patstartch_lead: u8 = if exp.starts_with('.') { b'.' } else { 0 };
576    patcompstart();
577    // c:525 — `patcompstart` seeds `patglobflags` with the option-derived
578    // default (GF_IGNCASE when CASEGLOB/CASEPATHS are off, GF_MULTIBYTE when
579    // MULTIBYTE is on). The `patglobflags.store(0)` reset below (clean slate
580    // for the `(#…)` hoist loop) would otherwise drop that seed before the
581    // prog's globflags are built — capture the case bits now so `pattry`
582    // honors `setopt nocaseglob`.
583    let mut seeded_globflags = patglobflags.load(Ordering::Relaxed);
584    // c:568-576 — `patcompile` RESETS patglobflags to `GF_MULTIBYTE`/0 for a
585    // NON-FILE pattern, DROPPING the nocaseglob-derived GF_IGNCASE that
586    // patcompstart seeded. `setopt nocaseglob` makes only FILENAME GLOBBING
587    // (PAT_FILE) case-insensitive; `[[ ]]`, `${arr:#pat}`, `case`, and `${p//pat}`
588    // stay case-SENSITIVE. Without this drop, `setopt nocaseglob; [[ ABC == abc ]]`
589    // wrongly matched and `${(ABC):#abc}` wrongly filtered. GF_MULTIBYTE stays.
590    if (inflags & PAT_FILE as i32) == 0 {
591        seeded_globflags &= !GF_IGNCASE;
592    }
593    // === C-contract input decode (zpc_chars, Src/pattern.c:248) =====
594    // C's patcompile consumes the LEXER'S tokenized encoding: glob
595    // metachars arrive as token bytes (`Pound`..`Bang`, zsh.h:159-183
596    // — zpc_chars holds `Star`/`Quest`/`Inbrack`/... so a raw `*`
597    // NEVER dispatches as ZPC_STAR), quoted/escaped chars arrive as
598    // `Bnull`/`Bnullkeep` + payload (zsh.h:195-200), and `Nularg` is
599    // stripped (remnulargs). Callers pair `tokenize()` with
600    // `patcompile()` exactly like C (zutil.c:734, etc.).
601    //
602    // The Rust parser below uses a raw-ASCII internal encoding with
603    // `\X` as its literal form; transpose the C contract onto it:
604    //   token char (U+0084..=U+009E)         -> ztokens[c - Pound]  (meta)
605    //   Bnull/Bnullkeep + X (U+009F/U+00A0)  -> \X                  (literal)
606    //   Nularg (U+00A1)                      -> dropped
607    //   Meta (U+0083) + X                    -> \X                  (literal)
608    //   raw `\` + X                          -> \X  passthrough — raw
609    //       backslash is the parser's established Bnull-equivalent
610    //       (see the `\X` arm below); C's lexer encodes the same
611    //       quoting info as Bnull+X before zshtokenize ever runs.
612    //   raw ASCII glob metachar              -> \X                  (literal)
613    // `/`, `+`, `!`, `@` stay verbatim — C's zpc_chars keeps those
614    // slots RAW (path split + ksh-glob triggers fire on raw bytes).
615    let exp: String = {
616        let ztokens: Vec<char> = crate::ported::glob::ZTOKENS.chars().collect();
617        let chars: Vec<char> = exp.chars().collect();
618        let mut out = String::with_capacity(exp.len());
619        let mut i = 0;
620        while i < chars.len() {
621            let c = chars[i];
622            let cu = c as u32;
623            if cu == 0x83 {
624                // Meta + payload — a metafied RAW byte
625                // (vm_helper::meta_encode_byte, c:Src/utils.c:7289-
626                // 7294). C's pattern matcher compares metafied bytes
627                // on BOTH sides, so the compiled pattern must match
628                // the pair AS STORED in the subject string: emit both
629                // chars as literals (`\Meta \payload`). The previous
630                // `\payload` form dropped the Meta char and never
631                // matched a metafied subject — `[[ $'\xff' ==
632                // $'\xff' ]]` failed. Bug #127.
633                out.push('\\');
634                out.push(c);
635                if i + 1 < chars.len() {
636                    out.push('\\');
637                    out.push(chars[i + 1]);
638                    i += 2;
639                } else {
640                    i += 1;
641                }
642                continue;
643            }
644            if cu == 0x9f || cu == 0xa0 {
645                // Bnull / Bnullkeep — payload is a literal.
646                if i + 1 < chars.len() {
647                    out.push('\\');
648                    out.push(chars[i + 1]);
649                    i += 2;
650                } else {
651                    i += 1;
652                }
653                continue;
654            }
655            if cu == 0xa1 {
656                // Nularg — stripped like C's remnulargs.
657                i += 1;
658                continue;
659            }
660            if (0x84..=0x9e).contains(&cu) {
661                // Token -> the raw metachar the parser dispatches on.
662                out.push(ztokens[(cu - 0x84) as usize]);
663                i += 1;
664                continue;
665            }
666            if c == '\\' {
667                // Raw `\X` — Bnull-equivalent quoting marker in the
668                // zshrs pipeline; pass both through to the parser's
669                // `\X` literal arm. Trailing lone `\` stays itself.
670                out.push('\\');
671                if i + 1 < chars.len() {
672                    out.push(chars[i + 1]);
673                    i += 2;
674                } else {
675                    i += 1;
676                }
677                continue;
678            }
679            if matches!(c, '*' | '?' | '[' | '(' | ')' | '|' | '~' | '^' | '#' | '<') {
680                // Untokenized ASCII metachar — literal per zpc_chars.
681                out.push('\\');
682                out.push(c);
683                i += 1;
684                continue;
685            }
686            out.push(c);
687            i += 1;
688        }
689        out
690    };
691    let exp = exp.as_str();
692    *patstart.lock().unwrap() = exp.to_string();
693    *patparse.lock().unwrap() = exp.to_string();
694    patflags.store(
695        inflags & !(PAT_PURES | PAT_HAS_EXCLUDP) as i32,
696        Ordering::Relaxed,
697    ); // c:566
698    patglobflags.store(0, Ordering::Relaxed);
699
700    // c:583-590 — emit P_GFLAGS placeholder. Phase 5.1: instead of
701    // emitting an opcode, hoist leading `(#...)` flag specifiers into
702    // patprog.globflags so the matcher applies them globally for the
703    // whole match. Full mid-pattern P_GFLAGS opcode still deferred.
704    // c:525 — `patglobflags = isset(MULTIBYTE) ? GF_MULTIBYTE : 0`.
705    // (#U) inside the spec clears the bit per c:1116.
706    //
707    // c:953-957 — C gates the `(#...)` recognition on
708    //   `*patparse == zpc_special[ZPC_INPAR] &&`
709    //   `patparse[1] == zpc_special[ZPC_HASH]`.
710    // When EXTENDEDGLOB is off, patcompcharsset (c:480-483) masks
711    // ZPC_HASH to the Marker byte (c:476). The second-byte equality
712    // therefore fails and the parser falls through to "treat `(` as
713    // a literal group" (c:1020+). Previously this Rust hoist loop
714    // compared the raw byte `b'#'`, allowing `(#s)` / `(#e)` /
715    // `(#i)` to fire even without EXTENDEDGLOB (parity bugs #18/#19
716    // vs real zsh).
717    // c:525 — the compiled prog's globflags accumulate from `patglobflags`,
718    // which `patcompstart` seeded with GF_IGNCASE when CASEGLOB/CASEPATHS are
719    // off (`setopt nocaseglob`). Carry those case bits through so `pattry`
720    // matches case-insensitively; `matchpat` masks the loss by pre-folding
721    // both sides, but the glob scanner now drives `pattry` directly. Keep
722    // the prior unconditional GF_MULTIBYTE to avoid disturbing multibyte
723    // callers that relied on it.
724    let mut hoisted_globflags: i32 =
725        GF_MULTIBYTE | (seeded_globflags & (GF_IGNCASE | GF_LCMATCHUC));
726    let hash_char_pre = zpc_special.lock().unwrap()[ZPC_HASH as usize]; // c:957
727    while hash_char_pre == b'#' {
728        let off = patparse_off.load(Ordering::Relaxed);
729        let p = patparse.lock().unwrap();
730        if off + 1 >= p.len() || &p.as_bytes()[off..off + 2] != b"(#" {
731            break;
732        }
733        let rest = p[off..].to_string();
734        drop(p);
735        match patgetglobflags(&rest) {
736            Some((_bits, assert, _consumed)) if assert != 0 => {
737                // c:Src/pattern.c:1103-1109 — `(#s)`/`(#e)` set `*assertp`
738                // (P_ISSTART/P_ISEND), a POSITIONAL zero-width anchor, not a
739                // whole-match glob flag. Do NOT hoist/consume it here: leave
740                // it (and everything after) for the main compile loop
741                // (pattern.rs:1245) to emit as a positional node. Hoisting
742                // dropped the anchor, so a leading `(#e)PAT` matched as if
743                // the `(#e)` were absent — `[[ o == (#e)o ]]` wrongly matched
744                // and `${s//(#e)r/END}` wrongly replaced. `(#s)` was masked
745                // in `[[ ]]` only because that context is start-anchored.
746                break;
747            }
748            Some((bits, _assert, consumed)) => {
749                // The fold / backref / matchref / multibyte flags are ABSOLUTE
750                // state, not a set-only delta. patgetglobflags's returned `bits`
751                // starts at 0 and can only carry flags turned ON, so `(#I)`
752                // clearing LCMATCHUC (c: `patglobflags &= ~(GF_LCMATCHUC |
753                // GF_IGNCASE)`) comes back as 0 — an `|=` accumulate then leaves
754                // an earlier `(#l)`/`(#i)`'s fold bit set, so `(#l)(#I)foo` kept
755                // folding where zsh, after `(#I)`, matches case-sensitively.
756                //
757                // patgetglobflags ALSO updates the global patglobflags with the
758                // sets AND the clears, so it holds the authoritative running
759                // value. REPLACE the toggle-able bits from it rather than
760                // OR-accumulating the lossy return.
761                // GF_MULTIBYTE is deliberately NOT in the mask: the init above
762                // force-sets it (this port always matches UTF-8) while the
763                // global patglobflags may not carry it, so replacing it from the
764                // global would wrongly clear it and break multibyte matching.
765                // The original `|=` could not clear it either, so leaving it out
766                // preserves that (rare `(#U)` single-byte requests are no more
767                // broken than before). The fold/backref/matchref bits, which DO
768                // need clear semantics for `(#I)`/`(#B)`/`(#M)`, come from the
769                // authoritative global.
770                let gmask = GF_IGNCASE | GF_LCMATCHUC | GF_BACKREF | GF_MATCHREF;
771                let cur = patglobflags.load(Ordering::Relaxed);
772                hoisted_globflags = (hoisted_globflags & !gmask) | (cur & gmask);
773                // `(#aN)` substitution budget — low 8 bits of
774                // `patglobflags` per c:1066. Only carry it when this
775                // flag-spec actually had `(#a)` digits (non-zero err
776                // count); other specs may set high bits and we don't
777                // want their bits-AND to clear our budget byte.
778                // Per-spec OR-leak is C-faithful (a later `(#a3)` after
779                // `(#a1)` raises the budget); the matcher reads the
780                // final cumulative byte.
781                let errs_byte = bits & 0xff;
782                if errs_byte != 0 {
783                    hoisted_globflags = (hoisted_globflags & !0xff) | errs_byte;
784                    // c:1066
785                }
786                if (bits & GF_MULTIBYTE) == 0 && rest.contains('U') {
787                    hoisted_globflags &= !GF_MULTIBYTE;
788                }
789                patparse_off.fetch_add(consumed, Ordering::Relaxed);
790            }
791            None => break,
792        }
793    }
794
795    // c:584-610 — PAT_PURES fast path. A literal with no glob tokens
796    // compiles to a "pure string" rather than bytecode. For FILE globs
797    // (PAT_FILE) the scan STOPS at '/', so each path component becomes a
798    // pure section and `parsecomplist` can split the path on '/' (it
799    // reads endexp = the '/' remainder). Gated on PAT_FILE so non-file
800    // callers (matchpat/[[ ]]/:#) keep normal compilation unchanged; the
801    // matcher consumes PURES sections via literal extraction (the
802    // scanner), never `pattry`, so no PAT_PURES match path is needed.
803    let pf_pre = patflags.load(Ordering::Relaxed);
804    // c:Src/pattern.c:644-704 — for a FILE glob a literal path component is
805    // emitted as a PAT_PURES section even under GF_IGNCASE (`setopt
806    // nocaseglob`): C compiles it, then converts the single P_EXACTLY back
807    // to PURES (c:664) while flagging the prog GF_IGNCASE (c:645). The net
808    // effect is that intermediate literal directory components descend by
809    // EXACT name (the scanner's PURES path stats the real path, glob.rs:436
810    // — case-insensitivity applies to the FINAL wildcard component, not to
811    // directory descent), so path splitting on `/` keeps working. zshrs's
812    // port only had the first PURES gate (c:586), which excludes GF_IGNCASE,
813    // so under nocaseglob every literal component (e.g. `tmp` in
814    // `/tmp/.../*.zsh`) fell to the general compiler and matched nothing —
815    // the whole path collapsed and `${(N)...}(DN)` globs returned empty.
816    // That is what made zinit's plugin-discovery globs empty on a shell with
817    // `caseglob` off → the `.zinit-load-plugin:source:110: no such file or
818    // directory: ./` startup flood. Take the PURES fast path here for file
819    // globs when the only extra glob flag is GF_IGNCASE; GF_IGNCASE stays on
820    // `hoisted_globflags` so the final wildcard component still matches
821    // case-insensitively.
822    let pures_extra_mask = if (pf_pre & PAT_FILE as i32) != 0 {
823        !(GF_MULTIBYTE | GF_IGNCASE)
824    } else {
825        !GF_MULTIBYTE
826    };
827    if (pf_pre & PAT_FILE as i32) != 0
828        && (pf_pre & PAT_ANY as i32) == 0
829        && (hoisted_globflags & pures_extra_mask) == 0
830    {
831        let off = patparse_off.load(Ordering::Relaxed);
832        let p = patparse.lock().unwrap();
833        let s = &p[off..];
834        // c:606 — scan for a pure literal segment. By this point patparse
835        // has been NORMALIZED to raw metachars (Star -> '*') with literal
836        // metachars backslash-escaped (`\*`), so scan that form: stop at
837        // '/' (segment boundary) or any UNescaped glob metachar (=> not
838        // pure). `\X` is an escaped literal — skip both chars.
839        let mut cut = s.len();
840        let mut at_token = false;
841        let mut it = s.char_indices();
842        while let Some((i, c)) = it.next() {
843            if c == '/' {
844                cut = i;
845                break;
846            }
847            if c == '\\' {
848                it.next(); // escaped literal — both chars stay literal
849                continue;
850            }
851            if matches!(c, '*' | '?' | '[' | '(' | '|' | '~' | '^' | '#' | '<') {
852                cut = i;
853                at_token = true;
854                break;
855            }
856        }
857        // c:610 — pure iff we stopped at end or '/', not at a glob meta.
858        if !at_token {
859            let literal = s[..cut].as_bytes().to_vec();
860            let remainder = s[cut..].to_string();
861            drop(p);
862            let mlen = literal.len() as i64;
863            if let Some(end) = endexp.as_deref_mut() {
864                // c:751 *endexp = patparse (at '/'). patparse is normalized
865                // (untokenized); re-tokenize so parsecomplist's recursion
866                // feeds the NEXT patcompile a tokenized segment (else raw
867                // '*' gets re-escaped to `\*`, losing the Star meaning).
868                let mut rem = remainder;
869                crate::ported::glob::tokenize(&mut rem);
870                *end = rem;
871            }
872            let prog: Patprog = Box::new((
873                patprog {
874                    startoff: 0,
875                    size: mlen,
876                    mustoff: 0,
877                    patmlen: mlen, // c:623 — pure-string length
878                    globflags: hoisted_globflags,
879                    globend: patglobflags.load(Ordering::Relaxed),
880                    flags: pf_pre | PAT_PURES as i32, // c:625
881                    patnpar: 0,
882                    patstartch: patstartch_lead, // c:1610
883                },
884                literal,
885            ));
886            if cacheable {
887                crate::pat_cache::put(&exp_orig, inflags, &prog);
888            }
889            return Some(prog);
890        }
891    }
892
893    let mut flagp: i32 = 0;
894    let root = patcompswitch(0, &mut flagp);
895    if root < 0 {
896        return None; // c:646 compile error
897    }
898    // Emit the terminal P_END and chain every branch's operand to it.
899    let end_off = patnode(P_END);
900    chain_branches_to(root as usize, end_off);
901
902    let code = patout.lock().unwrap().clone();
903    let consumed_off = patparse_off.load(Ordering::Relaxed);
904    if let Some(end) = endexp.as_deref_mut() {
905        let parse = patparse.lock().unwrap();
906        // c:751 *endexp = patparse. patparse is normalized (untokenized);
907        // re-tokenize so parsecomplist's recursion feeds the next
908        // patcompile a tokenized segment (raw '*' would re-escape to `\*`).
909        let mut rem = parse[consumed_off..].to_string();
910        drop(parse);
911        crate::ported::glob::tokenize(&mut rem);
912        *end = rem;
913    }
914
915    let prog: Patprog = Box::new((
916        patprog {
917            startoff: 0,
918            size: code.len() as i64,
919            mustoff: 0,
920            patmlen: 0,
921            globflags: hoisted_globflags,
922            globend: patglobflags.load(Ordering::Relaxed),
923            // c:637 `p->flags = patflags;` — patflags ONLY. A prior
924            // Rust port OR'd `hoisted_globflags` into here, contaminating
925            // `prog.flags & 0xff` checks (which read PAT_STATIC / PAT_PURES
926            // bits in the low byte) with whatever ended up in the
927            // `(#aN)` budget byte. The two flag-sets stay strictly
928            // separated per C source.
929            flags: patflags.load(Ordering::Relaxed),
930
931            patnpar: patnpar.load(Ordering::Relaxed) - 1,
932            patstartch: patstartch_lead, // c:1610
933        },
934        code,
935    ));
936    if cacheable {
937        crate::pat_cache::put(&exp_orig, inflags, &prog);
938    }
939    Some(prog)
940}
941
942/// Port of `patcompswitch(int paren, int *flagp)` from `Src/pattern.c:765`.
943///
944/// C: `static long patcompswitch(int paren, int *flagp)`. Parses an
945/// alternation (`a|b|c`), emitting a chain of P_BRANCH nodes. Returns
946/// offset of the first branch, or -1 on error.
947pub fn patcompswitch(paren: i32, flagp: &mut i32) -> i64 {
948    // c:765
949    // Emit the first P_BRANCH header. Its operand is the content
950    // emitted by the immediately-following patcompbranch call (lives
951    // inline at starter+I_BODY). Does NOT emit a terminator — caller
952    // (patcompile for top-level, patcomppiece for sub-pattern)
953    // chains branches to the appropriate follow-on opcode.
954    let starter = patnode(P_BRANCH);
955    let mut branch_flags: i32 = 0;
956    let first_branch = patcompbranch(&mut branch_flags, paren);
957    if first_branch < 0 {
958        return -1;
959    }
960    *flagp |= branch_flags & P_HSTART;
961
962    let mut last_branch = starter;
963    // c:769 — `Upat excsync = NULL;` — set on first `~` exclusion;
964    // reused so consecutive `~clause` chain to the same sync node.
965    let mut excsync: usize = 0;
966
967    // Snapshot zpc_special for this compile pass — locked once to
968    // avoid re-locking inside the per-iteration parse-byte check.
969    let (sp_bar, sp_tilde, sp_special_set) = {
970        let sp = zpc_special.lock().unwrap();
971        let bar = sp[ZPC_BAR as usize];
972        let tilde = sp[ZPC_TILDE as usize];
973        // c:803 — `memchr(zpc_special, patparse[1], ZPC_SEG_COUNT)` —
974        // is the lookahead byte one of the segment-special bytes? If
975        // so, `~X` is NOT an exclusion (`~|`, `~)`, etc.).
976        let mut set = [false; 256];
977        for i in 0..(ZPC_SEG_COUNT as usize) {
978            set[sp[i] as usize] = true;
979        }
980        (bar, tilde, set)
981    };
982
983    // Alternation + exclusion loop:
984    //   `|`  → next alternative (P_BRANCH)
985    //   `~`  → exclusion (P_EXCLUDE / P_EXCLUDP) — when followed by
986    //          `/` (top-level path component split) OR a non-special
987    //          char (ordinary content). `~~`, `~|`, `~)` stay literal.
988    loop {
989        let off = patparse_off.load(Ordering::Relaxed);
990        let parse = patparse.lock().unwrap();
991        if off >= parse.len() {
992            break;
993        }
994        let bytes = parse.as_bytes();
995        let c = bytes[off];
996        // c:799-803 — accept `|` always; accept `~` only when the
997        // lookahead char is `/` or NOT a segment-special byte.
998        let is_bar = c == sp_bar;
999        let is_tilde_exclude = c == sp_tilde && off + 1 < bytes.len() && {
1000            let la = bytes[off + 1];
1001            la == b'/' || !sp_special_set[la as usize]
1002        };
1003        if !is_bar && !is_tilde_exclude {
1004            break;
1005        }
1006        drop(parse);
1007        patparse_off.fetch_add(1, Ordering::Relaxed); // c:803 `*patparse++`
1008        let br: usize;
1009        if is_tilde_exclude {
1010            // c:808-836 — `if (tilde)` arm. Emit the EXCSYNC sync node
1011            // (if first `~` in this switch) then the EXCLUDE / EXCLUDP
1012            // node with an 8-byte NULL syncptr payload. Note we DON'T
1013            // reset patglobflags's low byte (`(#aN)` budget) here —
1014            // the Rust port doesn't yet propagate per-pattern `(#a)`
1015            // through nested patmatch frames, so dropping the budget
1016            // mid-compile is a no-op.
1017            if excsync == 0 {
1018                excsync = patnode(P_EXCSYNC); // c:813
1019                patoptail(last_branch, excsync); // c:814
1020            }
1021            // c:816-825 — `if (!(patflags & PAT_FILET) || paren)` →
1022            // P_EXCLUDE; else P_EXCLUDP for top-level file globs.
1023            let pf = patflags.load(Ordering::Relaxed);
1024            let use_excludp = (pf & (PAT_FILET as i32)) != 0 && paren == 0;
1025            if use_excludp {
1026                br = patnode(P_EXCLUDP); // c:823
1027                patflags.fetch_or(PAT_HAS_EXCLUDP as i32, Ordering::Relaxed); // c:824
1028            } else {
1029                br = patnode(P_EXCLUDE); // c:818
1030            }
1031            // c:826-827 — `up.p = NULL; patadd((char *)&up, 0, sizeof(up), 0);`
1032            // 8-byte syncptr slot, NULL-initialised. Sized for a
1033            // 64-bit pointer to match C `union upat`.
1034            {
1035                let mut buf = patout.lock().unwrap();
1036                buf.extend_from_slice(&[0u8; 8]);
1037            }
1038        } else {
1039            // c:843 — `excsync = 0; br = patnode(P_BRANCH);`
1040            excsync = 0;
1041            br = patnode(P_BRANCH);
1042        }
1043        // Chain previous branch's `next` directly to this new branch
1044        // (alternative chain, not operand chain).
1045        set_next(last_branch, br);
1046        let mut bf: i32 = 0;
1047        let inner = patcompbranch(&mut bf, paren);
1048        if inner < 0 {
1049            return -1;
1050        }
1051        // c:Src/pattern.c:891-892 — `if (excsync) patoptail(br,
1052        // patnode(P_EXCEND));`. Terminate an exclusion branch's operand
1053        // with P_EXCEND so the matcher knows where the excluded pattern
1054        // ends; without it `A~B` ran B past its own end into the
1055        // following pattern and never excluded (`STATES__*~*local*`
1056        // failed to drop STATES__local_bar).
1057        if excsync != 0 {
1058            let excend = patnode(P_EXCEND);
1059            patoptail(br, excend);
1060        }
1061        *flagp |= bf & P_HSTART;
1062        last_branch = br;
1063    }
1064
1065    let _ = first_branch;
1066    starter as i64
1067}
1068
1069/// Port of `patcompbranch(int *flagp, int paren)` from `Src/pattern.c:942`.
1070///
1071/// C: `static long patcompbranch(int *flagp, int paren)`. Parses a
1072/// single branch — a sequence of pieces. Returns offset of the first
1073/// node in the branch, or -1 on error.
1074pub fn patcompbranch(flagp: &mut i32, paren: i32) -> i64 {
1075    // c:942
1076    let mut chain_start: i64 = -1;
1077    let mut last_tail: usize = 0;
1078    // Track preceding piece for POSTFIX `(#cN,M)` wrap. C does
1079    // `patinsert(P_COUNTSTART, starter, ...)` to embed COUNTSTART
1080    // BEFORE the just-compiled piece (c:pattern.c:1686). We snapshot
1081    // the piece bytes, truncate, emit P_COUNT, then re-append.
1082    let mut last_piece_off: i64 = -1; // start offset of preceding piece
1083    let mut prev_chain_tail: i64 = -1; // tail of chain BEFORE preceding piece (-1 if piece was first)
1084    // Flags the preceding patcomppiece reported. P_HSTART marks a piece that
1085    // already had a closure applied (`a#`, `a##`, or an earlier `(#cN,M)`) —
1086    // patcomppiece sets `*flagp = P_HSTART` on every such arm, mirroring C
1087    // (c:1626/1629/1636/1640/1643). Used below to reject a stacked count.
1088    let mut last_piece_flags: i32 = 0;
1089    *flagp = P_PURESTR;
1090
1091    // c:951-952 — snapshot the segment-special set so we can do
1092    // `memchr(zpc_special, byte, ZPC_SEG_COUNT)`-equivalent lookups.
1093    // Only the first ZPC_SEG_COUNT slots (SLASH, NULL, BAR, OUTPAR,
1094    // TILDE) matter here.
1095    let (sp_tilde, sp_seg_set, sp_inpar, sp_hash) = {
1096        let sp = zpc_special.lock().unwrap();
1097        let tilde = sp[ZPC_TILDE as usize];
1098        let mut set = [false; 256];
1099        for i in 0..(ZPC_SEG_COUNT as usize) {
1100            set[sp[i] as usize] = true;
1101        }
1102        (
1103            tilde,
1104            set,
1105            sp[ZPC_INPAR as usize],
1106            sp[ZPC_HASH as usize], // c:1609
1107        )
1108    };
1109
1110    loop {
1111        let off = patparse_off.load(Ordering::Relaxed);
1112        // Snapshot the parse buffer into an owned slice for branch
1113        // decisions; release the lock so subsequent emit helpers
1114        // (which acquire patout's lock) can't contend.
1115        let snapshot: Vec<u8> = {
1116            let parse = patparse.lock().unwrap();
1117            parse.as_bytes().to_vec()
1118        };
1119        if off >= snapshot.len() {
1120            break;
1121        }
1122        let c = snapshot[off];
1123        // Branch terminators: |, ), end of pattern.
1124        if c == b'|' || c == b')' {
1125            break;
1126        }
1127        // c:950 — '/' is ZPC_SLASH (slot 0, within ZPC_SEG_COUNT), so C
1128        // breaks the segment here. zshrs gates this to FILE globs at top
1129        // level: parsecomplist needs the path-component boundary, while
1130        // matchpat/[[ ]]/:# (never PAT_FILE) and '/' inside parens (the
1131        // `((#s)|/)` anchor pin) must keep '/' literal. C's unconditional
1132        // break + c:914-917 file-accept termination is equivalent for the
1133        // top-level file-glob case this serves.
1134        if c == b'/' && paren == 0 && (patflags.load(Ordering::Relaxed) & PAT_FILE as i32) != 0 {
1135            break;
1136        }
1137        // c:950-952 — `~` is an additional terminator when active as
1138        // the exclusion operator. The C condition includes all five
1139        // segment-specials (SLASH, NULL, BAR, OUTPAR, TILDE), but
1140        // `|` and `)` are already covered above and SLASH inside
1141        // `(...)` alternation is intentionally left literal here to
1142        // preserve the existing `zsh_corpus_hash_s_e_anchors_match_
1143        // bare_test` parity pin.
1144        if c == sp_tilde && c != 0 {
1145            let la = snapshot.get(off + 1).copied().unwrap_or(0);
1146            // Literal-tilde exception: `~` followed by a segment-
1147            // special OTHER than `/` keeps the `~` as literal.
1148            let literal_tilde_exception = la != b'/' && sp_seg_set[la as usize];
1149            if !literal_tilde_exception {
1150                break;
1151            }
1152        }
1153        let bytes = snapshot.as_slice();
1154        // Mid-pattern `(#cN,M)` counted-repetition specifier — emit
1155        // P_COUNT with bounds + inline operand following. Detected
1156        // BEFORE the generic patgetglobflags path because `c` is not
1157        // a flag char in that fn.
1158        //
1159        // c:1608-1610 — the `(` and `#` are compared against
1160        // `zpc_special[ZPC_INPAR]` / `zpc_special[ZPC_HASH]`, NOT against
1161        // literal bytes. That indirection is the whole EXTENDED_GLOB gate:
1162        // patcompcharsset() rewrites `zpc_special[ZPC_HASH]` to Marker
1163        // (c:482) when EXTENDED_GLOB is off, so a literal `#` can never
1164        // equal it and `(#c...)` stops being a counted closure — it falls
1165        // through and parses as an ordinary group. Testing `b'#'` directly
1166        // applied the closure with EXTENDED_GLOB unset, so
1167        // `[[ aaa = a(#c2,3) ]]` matched (zsh: no match).
1168        if off + 2 < bytes.len()
1169            && bytes[off] == sp_inpar
1170            && bytes[off + 1] == sp_hash
1171            && bytes[off + 2] == b'c'
1172        {
1173            let mut j = off + 3;
1174            let mut min: i64 = 0;
1175            let min_start = j;
1176            while j < bytes.len() && bytes[j].is_ascii_digit() {
1177                min = min * 10 + (bytes[j] - b'0') as i64;
1178                j += 1;
1179            }
1180            let mut max: i64 = i64::MAX;
1181            // Three valid shapes (per zsh extended-glob (#cN,M)):
1182            //   (#cN)      — exact count N  (min=N, max=N)
1183            //   (#cN,)     — min N, no max  (min=N, max=∞)
1184            //   (#cN,M)    — N..M range     (min=N, max=M)
1185            //   (#c,M)     — max M, no min  (min=0, max=M)  ← bug #489 missing case
1186            //   (#c,)      — equivalent to no count (min=0, max=∞)
1187            // The original `if j > min_start` gate skipped the `)`
1188            // check for the no-min shapes, leaving them unparsed.
1189            let has_min_digits = j > min_start;
1190            let has_comma = j < bytes.len() && bytes[j] == b',';
1191            if has_min_digits || has_comma {
1192                if has_comma {
1193                    j += 1; // skip ,
1194                    let max_start = j;
1195                    let mut mx: i64 = 0;
1196                    while j < bytes.len() && bytes[j].is_ascii_digit() {
1197                        mx = mx * 10 + (bytes[j] - b'0') as i64;
1198                        j += 1;
1199                    }
1200                    if j > max_start {
1201                        max = mx;
1202                    }
1203                } else {
1204                    // (#cN) — exact count N.
1205                    max = min;
1206                }
1207                if j < bytes.len() && bytes[j] == b')' {
1208                    j += 1;
1209                    patparse_off.store(j, Ordering::Relaxed);
1210                    // POSTFIX semantics — c:pattern.c:1686.
1211                    // C does `patinsert(P_COUNTSTART, starter, ...)`
1212                    // then `pattail(opnd, patnode(P_BACK))` to loop the
1213                    // operand back. zshrs uses a simpler encoding where
1214                    // P_COUNT carries [min, max] then operand bytes
1215                    // inline; matcher iterates outside. So we relocate
1216                    // the preceding piece into the operand slot.
1217                    // c:1431-1436 — `case Star:` sets `kshchar = -1`, whose
1218                    // only purpose is (per C's own comment) "a sign that we
1219                    // can't have #'s". c:1620-1622 then rejects the pattern:
1220                    //
1221                    //     /* too much at once doesn't currently work */
1222                    //     if (kshchar && (hash || count))
1223                    //         return 0;
1224                    //
1225                    // So a count may not follow `*`, nor stack on a piece
1226                    // that already carries a closure (`a#(#c2,3)`). zshrs
1227                    // attaches the count to the preceding piece rather than
1228                    // tracking kshchar, so the same rule is expressed by
1229                    // looking at that piece's opcode. Without this, zshrs
1230                    // silently accepted `*(#c2,3)` / `a#(#c2,3)`, which zsh
1231                    // rejects as a bad pattern.
1232                    if last_piece_off >= 0 {
1233                        let prev_op = {
1234                            let buf = patout.lock().unwrap();
1235                            buf.get(last_piece_off as usize + I_OP)
1236                                .copied()
1237                                .unwrap_or(0)
1238                        };
1239                        // `*` is C's kshchar = -1 (its head node is P_STAR);
1240                        // P_HSTART marks a piece that already closured, which
1241                        // is what makes `a#(#c2,3)` / `a##(#c2,3)` bad too.
1242                        if prev_op == P_STAR || (last_piece_flags & P_HSTART) != 0 {
1243                            return -1; // c:1622 `return 0;`
1244                        }
1245                    }
1246                    if last_piece_off >= 0 {
1247                        let piece_start = last_piece_off as usize;
1248                        // Snapshot preceding piece (everything emitted
1249                        // by the prior patcomppiece call).
1250                        let piece_bytes: Vec<u8> = {
1251                            let buf = patout.lock().unwrap();
1252                            buf[piece_start..].to_vec()
1253                        };
1254                        // Truncate to remove the piece.
1255                        {
1256                            let mut buf = patout.lock().unwrap();
1257                            buf.truncate(piece_start);
1258                        }
1259                        // Cut chain at prev_chain_tail (was pointing
1260                        // into the piece via set_next or as
1261                        // chain_start).
1262                        if prev_chain_tail >= 0 {
1263                            set_next(prev_chain_tail as usize, 0);
1264                        } else {
1265                            chain_start = -1;
1266                        }
1267                        // Emit P_COUNT at current end.
1268                        let count_off = patnode(P_COUNT);
1269                        let operand_new_start: usize;
1270                        {
1271                            let mut buf = patout.lock().unwrap();
1272                            buf.extend_from_slice(&min.to_le_bytes());
1273                            buf.extend_from_slice(&max.to_le_bytes());
1274                            operand_new_start = buf.len();
1275                        }
1276                        // Relocate piece bytes: rewrite internal next
1277                        // links (absolute offsets >= piece_start) by
1278                        // delta = operand_new_start - piece_start.
1279                        let delta: i64 = operand_new_start as i64 - piece_start as i64;
1280                        let mut relocated = piece_bytes.clone();
1281                        let mut i = 0;
1282                        while i + I_BODY <= relocated.len() {
1283                            let op = relocated[i + I_OP];
1284                            if op == 0 {
1285                                i += 1;
1286                                continue;
1287                            }
1288                            let nxt = u32::from_le_bytes(
1289                                relocated[i + I_NEXT..i + I_NEXT + 4].try_into().unwrap(),
1290                            );
1291                            if nxt != 0 {
1292                                let new_nxt = (nxt as i64 + delta) as u32;
1293                                relocated[i + I_NEXT..i + I_NEXT + 4]
1294                                    .copy_from_slice(&new_nxt.to_le_bytes());
1295                            }
1296                            let next_i = advance_past_instr(&relocated, i);
1297                            if next_i == 0 || next_i <= i {
1298                                break;
1299                            }
1300                            i = next_i;
1301                        }
1302                        {
1303                            let mut buf = patout.lock().unwrap();
1304                            buf.extend_from_slice(&relocated);
1305                        }
1306                        // Hook P_COUNT into chain.
1307                        if chain_start < 0 {
1308                            chain_start = count_off as i64;
1309                        } else {
1310                            set_next(prev_chain_tail as usize, count_off);
1311                        }
1312                        last_tail = count_off;
1313                        // Consumed — clear tracking. The resulting piece IS a
1314                        // closure (C: `*flagp = P_HSTART`, c:1636), so a second
1315                        // count stacked on it must be rejected.
1316                        last_piece_off = -1;
1317                        prev_chain_tail = -1;
1318                        last_piece_flags = P_HSTART;
1319                        continue;
1320                    }
1321                    // No preceding piece — `(#cN,M)` is a POSTFIX
1322                    // modifier in real zsh and requires a piece to
1323                    // attach to. Without one C `Src/pattern.c:1606+`
1324                    // rejects the pattern. Bug #521: zshrs previously
1325                    // took a legacy PREFIX path (compile next piece as
1326                    // operand) which silently matched empty for `0,0`
1327                    // and similar degenerate ranges.
1328                    //
1329                    // Report nothing here: C's patcomppiece signals a bad
1330                    // pattern by `return 0` alone (c:1600, c:1622) and the
1331                    // CALLER prints the diagnostic with the pattern it was
1332                    // given — `matchpat` zerrs "bad pattern: %s" with the
1333                    // full pattern (glob.c:2522), and `[[ ]]` zwarnnams it
1334                    // from cond.c:314. Emitting a message from inside the
1335                    // compiler printed only the `(#cN,M)` fragment, losing
1336                    // the rest of the pattern the user actually wrote.
1337                    return -1; // c:1622 `return 0;`
1338                }
1339            }
1340            // Malformed `(#c...)` — fall through to generic flag handler.
1341        }
1342        // c:953-984 — mid-pattern `(#...)` glob-flag specifier. Emits
1343        // P_GFLAGS in C to switch GF_IGNCASE / GF_LCMATCHUC /
1344        // GF_MULTIBYTE / etc. mid-match. C body:
1345        //   `if ((*patparse == zpc_special[ZPC_INPAR] &&`
1346        //   `     patparse[1] == zpc_special[ZPC_HASH]) || ...)`
1347        //   `    if (!patgetglobflags(&patparse, &assert, &ignore))`
1348        //   `        return 0;`
1349        // Both bytes must equal their zpc_special slots, which
1350        // patcompcharsset (c:480-483) masks to Marker when
1351        // EXTENDEDGLOB is off (c:476). Previously this Rust arm
1352        // compared the raw byte `b'#'`, allowing `(#s)` / `(#e)` /
1353        // `(#i)` to fire even without EXTENDEDGLOB. Parity bugs
1354        // #18/#19 vs real zsh.
1355        let hash_char = zpc_special.lock().unwrap()[ZPC_HASH as usize]; // c:957
1356        if hash_char == b'#'
1357            && off + 1 < bytes.len()
1358            && bytes[off] == b'('
1359            && bytes[off + 1] == b'#'
1360        {
1361            let rest = std::str::from_utf8(&bytes[off..]).unwrap_or("").to_string();
1362            if let Some((bits, assertp, consumed)) = patgetglobflags(&rest) {
1363                patparse_off.fetch_add(consumed, Ordering::Relaxed);
1364                // Emit P_GFLAGS for flag-bit changes if any. Include the
1365                // low byte (the `(#aN)` approximation budget) so a
1366                // mid-pattern `(#a0)` / `(#a2)` actually changes the
1367                // error allowance for the following segment — c:Src/
1368                // pattern.c:2941 `patglobflags = P_OPERAND(scan)->l`
1369                // sets the WHOLE value. Without the 0xff, `(#a1)cat(#a0)
1370                // dog` kept the outer budget and `dog` wrongly tolerated
1371                // an error.
1372                let flag_bits = bits & (GF_IGNCASE | GF_LCMATCHUC | GF_MULTIBYTE | 0xff);
1373                if flag_bits != 0 || (flag_bits == 0 && assertp == 0) {
1374                    let gf_off = patnode(P_GFLAGS);
1375                    let mut buf = patout.lock().unwrap();
1376                    buf.extend_from_slice(&flag_bits.to_le_bytes());
1377                    drop(buf);
1378                    if chain_start < 0 {
1379                        chain_start = gf_off as i64;
1380                    } else {
1381                        set_next(last_tail, gf_off);
1382                    }
1383                    last_tail = gf_off;
1384                }
1385                // Emit P_ISSTART / P_ISEND when assertp set.
1386                if assertp != 0 {
1387                    let as_off = patnode(assertp as u8);
1388                    if chain_start < 0 {
1389                        chain_start = as_off as i64;
1390                    } else {
1391                        set_next(last_tail, as_off);
1392                    }
1393                    last_tail = as_off;
1394                }
1395                continue;
1396            }
1397            // patgetglobflags failed — treat the `(` as a literal group.
1398        }
1399
1400        // c:1011-1014 — `^pat` standalone negation. When `^` is the
1401        // active EXTENDEDGLOB special and appears at piece position
1402        // (not inside a `[...]` bracket class), consume it and call
1403        // patcompnot(0, ...) to emit the EXCLUDE structure.
1404        let sp_hat = zpc_special.lock().unwrap()[ZPC_HAT as usize];
1405        if c == sp_hat && sp_hat != 0 {
1406            patparse_off.fetch_add(1, Ordering::Relaxed); // c:1012 patparse++
1407            let mut not_flags: i32 = 0;
1408            let starter = patcompnot(0, &mut not_flags); // c:1013 patcompnot(0, ...)
1409            if starter < 0 {
1410                return -1;
1411            }
1412            *flagp |= not_flags & P_HSTART;
1413            if chain_start < 0 {
1414                chain_start = starter;
1415            } else {
1416                set_next(last_tail, starter as usize);
1417            }
1418            // patcompnot's tail is the trailing P_NOTHING — we need
1419            // to find it. The chain ends where excend / excl converge
1420            // (both `pattail(excend, n)` and `pattail(excl, n)`). For
1421            // chaining we use `starter` since the next piece's chain
1422            // is appended via patcomppiece's tail_out mechanism — but
1423            // patcompnot doesn't expose a tail. Approximate: scan
1424            // forward from starter to find the last P_NOTHING in the
1425            // emitted block. Cheap: walk patout from starter looking
1426            // for the highest-offset P_NOTHING in this compile pass.
1427            // Simpler: bake the convention that the trailing
1428            // P_NOTHING is the last node — set last_tail to current
1429            // emit position (= start of next node).
1430            let cur_emit = patout.lock().unwrap().len();
1431            last_tail = cur_emit.saturating_sub(I_BODY);
1432            continue;
1433        }
1434        drop(snapshot); // hint: explicit release
1435        let mut piece_flags: i32 = 0;
1436        let mut piece_tail: usize = 0;
1437        // Snapshot the chain tail BEFORE patcomppiece — used by a
1438        // following POSTFIX (#cN,M) to detach this piece from the chain.
1439        let prev_tail_before_piece: i64 = if chain_start < 0 {
1440            -1
1441        } else {
1442            last_tail as i64
1443        };
1444        let piece = patcomppiece(&mut piece_flags, paren, &mut piece_tail);
1445        if piece < 0 {
1446            return -1;
1447        }
1448        if chain_start < 0 {
1449            chain_start = piece;
1450        } else {
1451            // Chain previous piece's tail → this piece's head directly.
1452            set_next(last_tail, piece as usize);
1453        }
1454        last_tail = piece_tail;
1455        last_piece_off = piece;
1456        prev_chain_tail = prev_tail_before_piece;
1457        last_piece_flags = piece_flags;
1458        *flagp &= piece_flags;
1459    }
1460
1461    if chain_start < 0 {
1462        chain_start = patnode(P_NOTHING) as i64;
1463    }
1464    chain_start
1465}
1466
1467// =====================================================================
1468// 10. Glob-flag parser — pattern.c:1037
1469// =====================================================================
1470
1471/// Port of `patgetglobflags(char **strp, long *assertp, int *ignore)` from `Src/pattern.c:1037`.
1472///
1473/// C signature: `int patgetglobflags(char **strp, long *assertp,
1474/// int *ignore)`. C reads/writes the file-static `patglobflags`
1475/// directly via `|=` / `&=` at each switch arm (c:1064, 1070, 1075,
1476/// 1080, 1085, 1090, 1095, 1100, 1112, 1116). The hoist loop in
1477/// patcompile at c:582 / c:636 snapshots `patglobflags` into the
1478/// compiled `Patprog`'s `globflags` and `globend` fields, so the
1479/// per-arm writes are the canonical store — the return value is
1480/// only the success/fail signal (1 / 0).
1481///
1482/// The Rust port stores `patglobflags` in an AtomicI32 and mirrors
1483/// every C write through `fetch_or` / `fetch_and` ops so the same
1484/// snapshot at c:636 works. The return tuple `(flag_bits, assertp,
1485/// consumed)` is kept as an adapter for the patcompile hoist loop
1486/// (pattern.rs:589) which used it to accumulate `hoisted_globflags`
1487/// — those callers see the same bits via the atomic now, but
1488/// changing the return shape is a wider refactor.
1489///
1490/// Returns `Some((flag_bits, assertp_val, consumed_bytes))` on
1491/// success (C returns 1), or `None` on parse failure (C returns 0).
1492pub fn patgetglobflags(s: &str) -> Option<(i32, i64, usize)> {
1493    // c:1037
1494    let bytes = s.as_bytes();
1495    if !s.starts_with("(#") {
1496        return None;
1497    }
1498    let mut i = 2;
1499    // `bits` tracks what this call set so the caller can OR into
1500    // `hoisted_globflags` (the patcompile-side accumulator). The
1501    // canonical store is `patglobflags` (updated below per C arm).
1502    let mut bits: i32 = 0;
1503    let mut assertp: i64 = 0;
1504
1505    while i < bytes.len() && bytes[i] != b')' {
1506        match bytes[i] {
1507            // c:1073-1076 `'i'`: `patglobflags = (patglobflags &
1508            // ~GF_LCMATCHUC) | GF_IGNCASE`.
1509            b'i' => {
1510                patglobflags.fetch_and(!GF_LCMATCHUC, Ordering::Relaxed);
1511                patglobflags.fetch_or(GF_IGNCASE, Ordering::Relaxed);
1512                bits |= GF_IGNCASE;
1513                bits &= !GF_LCMATCHUC;
1514                i += 1;
1515            } // c:1075
1516            // c:1078-1081 — `'I'`: `patglobflags &= ~(GF_LCMATCHUC|GF_IGNCASE)`.
1517            b'I' => {
1518                patglobflags.fetch_and(!(GF_LCMATCHUC | GF_IGNCASE), Ordering::Relaxed);
1519                bits &= !(GF_LCMATCHUC | GF_IGNCASE);
1520                i += 1;
1521            } // c:1080
1522            // c:1068-1071 — `'l'`: `patglobflags = (patglobflags &
1523            // ~GF_IGNCASE) | GF_LCMATCHUC`.
1524            b'l' => {
1525                patglobflags.fetch_and(!GF_IGNCASE, Ordering::Relaxed);
1526                patglobflags.fetch_or(GF_LCMATCHUC, Ordering::Relaxed);
1527                bits |= GF_LCMATCHUC;
1528                bits &= !GF_IGNCASE;
1529                i += 1;
1530            } // c:1070
1531            // c:1083-1086 — `'b'`: `patglobflags |= GF_BACKREF`.
1532            b'b' => {
1533                patglobflags.fetch_or(GF_BACKREF, Ordering::Relaxed);
1534                bits |= GF_BACKREF;
1535                i += 1;
1536            } // c:1085
1537            // c:1088-1091 — `'B'`: `patglobflags &= ~GF_BACKREF`.
1538            b'B' => {
1539                patglobflags.fetch_and(!GF_BACKREF, Ordering::Relaxed);
1540                bits &= !GF_BACKREF;
1541                i += 1;
1542            } // c:1090
1543            // c:1093-1096 — `'m'`: `patglobflags |= GF_MATCHREF`.
1544            b'm' => {
1545                patglobflags.fetch_or(GF_MATCHREF, Ordering::Relaxed);
1546                bits |= GF_MATCHREF;
1547                i += 1;
1548            } // c:1095
1549            // c:1098-1101 — `'M'`: `patglobflags &= ~GF_MATCHREF`.
1550            b'M' => {
1551                patglobflags.fetch_and(!GF_MATCHREF, Ordering::Relaxed);
1552                bits &= !GF_MATCHREF;
1553                i += 1;
1554            } // c:1100
1555            // c:1103-1105 — `'s'`: sets `*assertp = P_ISSTART`,
1556            // doesn't touch patglobflags.
1557            b's' => {
1558                assertp = P_ISSTART as i64;
1559                i += 1;
1560            } // c:1104
1561            // c:1107-1109 — `'e'`: sets `*assertp = P_ISEND`.
1562            b'e' => {
1563                assertp = P_ISEND as i64;
1564                i += 1;
1565            } // c:1108
1566            // c:1111-1113 — `'u'`: `patglobflags |= GF_MULTIBYTE`.
1567            b'u' => {
1568                patglobflags.fetch_or(GF_MULTIBYTE, Ordering::Relaxed);
1569                bits |= GF_MULTIBYTE;
1570                i += 1;
1571            } // c:1112
1572            // c:1115-1117 — `'U'`: `patglobflags &= ~GF_MULTIBYTE`.
1573            b'U' => {
1574                patglobflags.fetch_and(!GF_MULTIBYTE, Ordering::Relaxed);
1575                bits &= !GF_MULTIBYTE;
1576                i += 1;
1577            } // c:1116
1578            // c:1054-1066 — `'a'`: approximate-match error count.
1579            // `ret = zstrtol(++ptr, &nptr, 10);` then
1580            // `patglobflags = (patglobflags & ~0xff) | (ret & 0xff)`.
1581            b'a' => {
1582                i += 1;
1583                let digit_start = i;
1584                let mut errs: i32 = 0;
1585                while i < bytes.len() && bytes[i].is_ascii_digit() {
1586                    errs = errs * 10 + (bytes[i] - b'0') as i32;
1587                    i += 1;
1588                }
1589                if i == digit_start {
1590                    // c:1062 `ptr == nptr` — no digits consumed.
1591                    return None;
1592                }
1593                if errs < 0 || errs > 254 {
1594                    return None; // c:1062
1595                }
1596                // c:1064 — `patglobflags = (patglobflags & ~0xff) | (ret & 0xff)`.
1597                let mask: i32 = !0xff;
1598                let cur = patglobflags.load(Ordering::Relaxed);
1599                patglobflags.store((cur & mask) | (errs & 0xff), Ordering::Relaxed);
1600                bits = (bits & !0xff) | (errs & 0xff);
1601            }
1602            // c:1046-1050 — `'q'`: glob qualifier, ignored in pattern
1603            // code. Skip to the closing `)` without consuming bits.
1604            b'q' => {
1605                while i < bytes.len() && bytes[i] != b')' {
1606                    i += 1;
1607                }
1608            }
1609            // c:1119-1120 — `default: return 0`.
1610            _ => return None,
1611        }
1612    }
1613    // c:1124-1125 — `if (*ptr != Outpar) return 0;`.
1614    if i >= bytes.len() {
1615        return None;
1616    }
1617    i += 1; // c:1129 — `*strp = ptr + 1;` advance past `)`.
1618    Some((bits, assertp, i))
1619}
1620
1621// =====================================================================
1622// 11. Range helpers — pattern.c:1148, :1179
1623// =====================================================================
1624
1625/// Port of `range_type(char *start, int len)` from `Src/pattern.c:1148`. Looks up the
1626/// integer code for a POSIX character class name (e.g. "alpha" → 1).
1627/// Returns None for unknown names.
1628///
1629/// C signature takes a `(char *start, int len)` non-NUL-terminated
1630/// substring; Rust's `&str` carries the length implicitly, so the
1631/// two-arg C shape collapses to a single arg. Param name `start`
1632/// matches C verbatim (Rule E).
1633pub fn range_type(start: &str) -> Option<usize> {
1634    // c:1148
1635    POSIX_CLASS_NAMES
1636        .iter()
1637        .position(|n| *n == start)
1638        .map(|i| i + 1)
1639}
1640
1641/// Port of `int pattern_range_to_string(char *rangestr, char *outstr)`
1642/// from `Src/pattern.c:1179`. Walks a Meta-encoded range bytestring,
1643/// re-emitting the human-readable form: literal chars as-is,
1644/// PP_RANGE pairs as `c1-c2`, POSIX classes as `[:name:]`.
1645/// Returns the output length; `outstr` is the destination buffer
1646/// (NULL = measure-only). The Rust port operates on `&str` (UTF-8
1647/// native) — `Meta`-byte decode collapses since zshrs's pattern
1648/// compiler stores raw chars, so the function effectively walks
1649/// the chars and emits POSIX class names from the `[i]` table at
1650/// `range_type`'s reverse lookup.
1651///
1652/// Rust signature: drops C's `outstr` out-param. C measures-then-fills
1653/// via two passes when `outstr` is non-NULL; Rust returns the String
1654/// directly. Callers needing the C "measure-only" mode read `.len()`
1655/// on the result.
1656pub fn pattern_range_to_string(rangestr: &str) -> String {
1657    // c:1179
1658    let mut out = String::with_capacity(rangestr.len()); // c:1181 int len = 0
1659    let mut chars = rangestr.chars().peekable();
1660    while let Some(c) = chars.next() {
1661        // c:1184-1247 — swtype dispatch via Meta+PP_*. zshrs stores
1662        // POSIX-class markers as a sentinel byte followed by the
1663        // class name. The Meta encoding doesn't apply (UTF-8 native),
1664        // so we just pass chars through, recognizing PP_* class tags
1665        // when they appear as `[:name:]` literal syntax.
1666        if c == '[' && chars.peek() == Some(&':') {
1667            // c:1242 — `[:alpha:]` and similar.
1668            let mut name = String::new();
1669            chars.next(); // consume ':'
1670            while let Some(&cc) = chars.peek() {
1671                if cc == ':' {
1672                    chars.next();
1673                    if chars.peek() == Some(&']') {
1674                        chars.next();
1675                        break;
1676                    }
1677                    name.push(':');
1678                } else {
1679                    name.push(cc);
1680                    chars.next();
1681                }
1682            }
1683            out.push_str(&format!("[:{}:]", name)); // c:1244
1684        } else {
1685            // c:1185-1213 — single-char or range pair.
1686            // Check for `c1-c2` PP_RANGE form.
1687            out.push(c); // c:1210/1216
1688            if chars.peek() == Some(&'-') {
1689                chars.next();
1690                if let Some(c2) = chars.next() {
1691                    out.push('-'); // c:1219
1692                    out.push(c2); // c:1220
1693                }
1694            }
1695        }
1696    }
1697    out // c:1261 return len
1698}
1699
1700/// Port of `patcomppiece(int *flagp, int paren)` from `Src/pattern.c:1261`.
1701///
1702/// C: `static long patcomppiece(int *flagp, int paren)`. Parses a
1703/// single atom + optional quantifier. Returns offset of compiled node.
1704/// Out-param `tail_out` receives the byte offset of the LAST opcode
1705/// in the compiled piece — the node whose `.next` should be chained
1706/// to whatever follows in the sequence. For simple atoms (P_EXACTLY,
1707/// P_ANY, etc.) the tail equals the head; for compound pieces
1708/// `(...)` / quantified atoms it points to the trailing P_CLOSE_N or
1709/// quantifier-injected node.
1710///
1711/// Rust signature adds `tail_out: &mut usize` for the LAST-opcode-
1712/// offset that C threads through pointer arithmetic on the
1713/// caller-side `Upat` cursor. Without the out-param, every Rust
1714/// caller would need to re-walk the just-emitted bytecode to find
1715/// the tail; the explicit out-param avoids the re-scan. Rule B
1716/// deviation sanctioned by zshrs's byte-offset bytecode (vs C's
1717/// pointer-arithmetic) substrate.
1718pub fn patcomppiece(flagp: &mut i32, paren: i32, tail_out: &mut usize) -> i64 {
1719    // c:1261
1720    let _ = paren;
1721    let off = patparse_off.load(Ordering::Relaxed);
1722    let parse = patparse.lock().unwrap();
1723    if off >= parse.len() {
1724        return patnode(P_NOTHING) as i64;
1725    }
1726    let bytes = parse.as_bytes();
1727    let c = bytes[off];
1728
1729    // c:1278 — `kshchar = '\0';`. KSH-glob trigger detection: when the
1730    // current byte matches one of the six ZPC_KSH_* slots AND the next
1731    // byte is `(`, record which kshchar so the post-atom dispatch can
1732    // emit the right quantifier shape (c:1615-1746).
1733    //
1734    // c:1279 — `if (*patparse && patparse[1] == Inpar) { ... }`. Note
1735    // the C code uses literal `Inpar` (the `(` token) for the lookahead
1736    // — NOT `zpc_special[ZPC_INPAR]` — so SHGLOB disabling `(` as a
1737    // pattern char doesn't suppress ksh-glob detection here.
1738    let mut kshchar: u8 = 0; // c:1278
1739    if off + 1 < parse.len() && bytes[off + 1] == b'(' {
1740        // c:1279
1741        let sp = zpc_special.lock().unwrap();
1742        if c == sp[ZPC_KSH_PLUS as usize] {
1743            kshchar = b'+';
1744        }
1745        // c:1280-1281
1746        else if c == sp[ZPC_KSH_BANG as usize] {
1747            kshchar = b'!';
1748        }
1749        // c:1282-1283
1750        else if c == sp[ZPC_KSH_BANG2 as usize] {
1751            kshchar = b'!';
1752        }
1753        // c:1284-1285
1754        else if c == sp[ZPC_KSH_AT as usize] {
1755            kshchar = b'@';
1756        }
1757        // c:1286-1287
1758        else if c == sp[ZPC_KSH_STAR as usize] {
1759            kshchar = b'*';
1760        }
1761        // c:1288-1289
1762        else if c == sp[ZPC_KSH_QUEST as usize] {
1763            kshchar = b'?';
1764        } // c:1290-1291
1765    }
1766    drop(parse);
1767
1768    // c:1419-1420 — `if (kshchar) patparse++;`. Skip the trigger byte
1769    // so the atom dispatch consumes the leading `(` as a group.
1770    let dispatch_c = if kshchar != 0 {
1771        patparse_off.fetch_add(1, Ordering::Relaxed);
1772        b'(' // c:1419 — fall through to Inpar case
1773    } else {
1774        c
1775    };
1776
1777    // Atom dispatch. Each arm sets `*tail_out` to the offset of the
1778    // last opcode emitted by this piece (for simple atoms, tail = head).
1779    let atom = match dispatch_c {
1780        b'?' => {
1781            patparse_off.fetch_add(1, Ordering::Relaxed);
1782            *flagp |= P_SIMPLE;
1783            *flagp &= !P_PURESTR;
1784            let h = patnode(P_ANY);
1785            *tail_out = h;
1786            h as i64
1787        }
1788        b'*' => {
1789            patparse_off.fetch_add(1, Ordering::Relaxed);
1790            *flagp &= !P_PURESTR;
1791            let h = patnode(P_STAR);
1792            *tail_out = h;
1793            h as i64
1794        }
1795        b'[' => {
1796            // c:Src/pattern.c:1438 `case Inbrack` + c:1497-1498 —
1797            // `if (*patparse != Outbrack) return 0;`: an ACTIVE `[`
1798            // (token-derived; the patcompile entry normalization maps
1799            // the Inbrack token to raw `[` and every literal/escaped
1800            // bracket to `\[`, which the `\X` arm consumed before
1801            // reaching here) with no closing `]` is a BAD PATTERN.
1802            // Callers zerr "bad pattern: %s" exactly like zsh:
1803            // `print [a-` / `[[ x == [a- ]]` / `case x in [a-)`.
1804            //
1805            // History: a 2026-06-03 partial fix for Bug #564 made
1806            // this case fall back to a literal `[` because the cond
1807            // path then stripped `\` before patcompile. The escape
1808            // now survives into the `\X` literal arm (all three #564
1809            // probes pass), so the leniency only masked genuine bad
1810            // patterns.
1811            let probe_off = off + 1;
1812            let parse_check = patparse.lock().unwrap();
1813            let check_bytes = parse_check.as_bytes();
1814            let has_close = check_bytes[probe_off..].contains(&b']');
1815            drop(parse_check);
1816            if !has_close {
1817                return -1; // c:1498 `return 0;` — bad pattern
1818            }
1819            patparse_off.fetch_add(1, Ordering::Relaxed);
1820            *flagp |= P_SIMPLE;
1821            *flagp &= !P_PURESTR;
1822            // Inline bracket-expression parse (C patcomppiece bracket case).
1823            let mut chars: Vec<u8> = Vec::new();
1824            // Multibyte augmentation: `chars` is a flat ASCII byte set, so a
1825            // multibyte input char (Cyrillic/Greek/CJK/accented) can never
1826            // match it. C's `mb_patmatchrange` handles wide chars via
1827            // `iswalpha`/wide ranges. To match without disturbing the
1828            // (heavily-tested, byte-for-byte) ASCII path, collect the class
1829            // predicates and the non-ASCII literals/ranges SEPARATELY and
1830            // consult them only when the input char is multibyte (P_ANYOF).
1831            let mut mb_classmask: u32 = 0;
1832            let mut mb_chars: Vec<char> = Vec::new();
1833            let mut mb_ranges: Vec<(char, char)> = Vec::new();
1834            let mut negate = false;
1835            let bracket_start = patparse_off.load(Ordering::Relaxed);
1836            let parse_b = patparse.lock().unwrap();
1837            let bb = parse_b.as_bytes();
1838            let mut i_b = bracket_start;
1839            if i_b < bb.len() && (bb[i_b] == b'^' || bb[i_b] == b'!') {
1840                negate = true;
1841                i_b += 1;
1842            }
1843            // c:Src/pattern.c:1456-1459 — `[]...]` exception: when `]` is
1844            // the FIRST char inside the class AND another `]` follows
1845            // later (so the close is not ambiguous), the first `]` is a
1846            // class member. Mirrors `[]x]` matching `]` or `x` in C.
1847            if i_b < bb.len() && bb[i_b] == b']' && bb[i_b + 1..].contains(&b']') {
1848                chars.push(b']');
1849                i_b += 1;
1850            }
1851            while i_b < bb.len() && bb[i_b] != b']' {
1852                // c:Src/pattern.c — `\X` inside a class is handled in C
1853                // by shtokenize converting it to `Bnullkeep X` upstream;
1854                // by the time C's bracket walker runs, the `]` after `\`
1855                // doesn't appear as raw `]` so the close-`]` scan
1856                // doesn't trip on it. The Rust port may receive raw
1857                // `\X` (callers that bypass shtokenize), so handle the
1858                // backslash-escape inline: consume `\X` as a literal
1859                // class-member byte. Bug surface:
1860                // `${q//(#m)[\][()|\\*?#<>~^]/...}` — escaped `]` /
1861                // `\\` / etc. inside the class are class members, not
1862                // metacharacters.
1863                if i_b + 1 < bb.len() && bb[i_b] == b'\\' {
1864                    chars.push(bb[i_b + 1]);
1865                    i_b += 2;
1866                    continue;
1867                }
1868                if i_b + 1 < bb.len() && bb[i_b] == b'[' && bb[i_b + 1] == b':' {
1869                    let class_start = i_b + 2;
1870                    let mut j_b = class_start;
1871                    while j_b + 1 < bb.len() && !(bb[j_b] == b':' && bb[j_b + 1] == b']') {
1872                        j_b += 1;
1873                    }
1874                    if j_b + 1 < bb.len() {
1875                        let class_name = std::str::from_utf8(&bb[class_start..j_b]).unwrap_or("");
1876                        // Inline POSIX class expansion.
1877                        match class_name {
1878                            "alpha" => {
1879                                for c in b'a'..=b'z' {
1880                                    chars.push(c);
1881                                }
1882                                for c in b'A'..=b'Z' {
1883                                    chars.push(c);
1884                                }
1885                            }
1886                            "upper" => {
1887                                for c in b'A'..=b'Z' {
1888                                    chars.push(c);
1889                                }
1890                            }
1891                            "lower" => {
1892                                for c in b'a'..=b'z' {
1893                                    chars.push(c);
1894                                }
1895                            }
1896                            "digit" => {
1897                                for c in b'0'..=b'9' {
1898                                    chars.push(c);
1899                                }
1900                            }
1901                            "xdigit" => {
1902                                for c in b'0'..=b'9' {
1903                                    chars.push(c);
1904                                }
1905                                for c in b'a'..=b'f' {
1906                                    chars.push(c);
1907                                }
1908                                for c in b'A'..=b'F' {
1909                                    chars.push(c);
1910                                }
1911                            }
1912                            "alnum" => {
1913                                for c in b'a'..=b'z' {
1914                                    chars.push(c);
1915                                }
1916                                for c in b'A'..=b'Z' {
1917                                    chars.push(c);
1918                                }
1919                                for c in b'0'..=b'9' {
1920                                    chars.push(c);
1921                                }
1922                            }
1923                            "space" => {
1924                                for b in b" \t\n\r\x0b\x0c".iter() {
1925                                    chars.push(*b);
1926                                }
1927                            }
1928                            "blank" => {
1929                                chars.push(b' ');
1930                                chars.push(b'\t');
1931                            }
1932                            "punct" => {
1933                                for b in b"!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~".iter() {
1934                                    chars.push(*b);
1935                                }
1936                            }
1937                            "cntrl" => {
1938                                for c in 0u8..=31 {
1939                                    chars.push(c);
1940                                }
1941                                chars.push(127);
1942                            }
1943                            "print" => {
1944                                for c in 32u8..=126 {
1945                                    chars.push(c);
1946                                }
1947                            }
1948                            "graph" => {
1949                                for c in 33u8..=126 {
1950                                    chars.push(c);
1951                                }
1952                            }
1953                            // c:Src/pattern.c:3693-3700 + Src/ztype.h
1954                            // IIDENT — `[[:IDENT:]]` is zsh's
1955                            // identifier-character class (alnum + `_`,
1956                            // the chars valid in a parameter name).
1957                            // Static for the ASCII range; the
1958                            // multibyte wcsitype(IIDENT) path isn't
1959                            // expanded here. p10k/zsh-z validate names
1960                            // with `[[:IDENT:]]##`.
1961                            "IDENT" => {
1962                                for c in b'0'..=b'9' {
1963                                    chars.push(c);
1964                                }
1965                                for c in b'A'..=b'Z' {
1966                                    chars.push(c);
1967                                }
1968                                for c in b'a'..=b'z' {
1969                                    chars.push(c);
1970                                }
1971                                chars.push(b'_');
1972                            }
1973                            // c:Src/pattern.c:3707-3719 — zsh-specific
1974                            // named classes that track the live $IFS /
1975                            // $WORDCHARS via the ztype ISEP/IWORD tables.
1976                            // The PP_IFS/PP_IFSSPACE/PP_WORD opcodes
1977                            // exist (pattern.rs:3157+) but no parse path
1978                            // emits them — this static-expansion path is
1979                            // the only named-class parser, and it dropped
1980                            // these three (`_ => {}`), so `[[ x =
1981                            // [[:IFS:]] ]]` never matched. Expand from the
1982                            // current param values (read at compile, like
1983                            // IDENT's static set).
1984                            "IFS" => {
1985                                // c:3709 ISEP — chars in $IFS (default
1986                                // space/tab/newline/NUL).
1987                                let ifs = crate::ported::params::getsparam("IFS")
1988                                    .unwrap_or_else(|| " \t\n\0".to_string());
1989                                for c in ifs.bytes() {
1990                                    chars.push(c);
1991                                }
1992                            }
1993                            "IFSSPACE" => {
1994                                // c:3713 iwsep — the ASCII whitespace
1995                                // subset of $IFS.
1996                                let ifs = crate::ported::params::getsparam("IFS")
1997                                    .unwrap_or_else(|| " \t\n\0".to_string());
1998                                for c in ifs.bytes() {
1999                                    if c == b' ' || c == b'\t' || c == b'\n' {
2000                                        chars.push(c);
2001                                    }
2002                                }
2003                            }
2004                            "WORD" => {
2005                                // c:3716 IWORD — alnum plus $WORDCHARS.
2006                                for c in b'0'..=b'9' {
2007                                    chars.push(c);
2008                                }
2009                                for c in b'A'..=b'Z' {
2010                                    chars.push(c);
2011                                }
2012                                for c in b'a'..=b'z' {
2013                                    chars.push(c);
2014                                }
2015                                let wc = crate::ported::params::getsparam("WORDCHARS")
2016                                    .unwrap_or_else(|| "*?_-.[]~=/&;!#$%^(){}<>".to_string());
2017                                for c in wc.bytes() {
2018                                    chars.push(c);
2019                                }
2020                            }
2021                            _ => {}
2022                        }
2023                        // Record the class predicate for multibyte input.
2024                        // Bits mirror the Unicode-aware `iswXXX` checks C's
2025                        // mb_patmatchrange applies (pattern.rs:3364). digit/
2026                        // xdigit stay ASCII-only (already in `chars`) — zsh's
2027                        // iswdigit is ASCII in the common locale.
2028                        mb_classmask |= match class_name {
2029                            "alpha" => 1 << 0,
2030                            "alnum" => 1 << 1,
2031                            "upper" => 1 << 2,
2032                            "lower" => 1 << 3,
2033                            "space" | "IFS" | "IFSSPACE" => 1 << 4,
2034                            "blank" => 1 << 5,
2035                            "punct" => 1 << 6,
2036                            "cntrl" => 1 << 7,
2037                            "print" => 1 << 8,
2038                            "graph" => 1 << 9,
2039                            "IDENT" | "WORD" => 1 << 1, // alnum-ish for wide chars
2040                            _ => 0,
2041                        };
2042                        i_b = j_b + 2;
2043                        continue;
2044                    }
2045                }
2046                // Multibyte member: a byte >= 0x80 leads a UTF-8 sequence
2047                // that cannot live in the ASCII `chars` set. Decode it as a
2048                // wide literal, or a wide range `<ch>-<ch>`, for the P_ANYOF
2049                // multibyte path.
2050                if bb[i_b] >= 0x80 {
2051                    if let Some(lo) = std::str::from_utf8(&bb[i_b..])
2052                        .ok()
2053                        .and_then(|s| s.chars().next())
2054                    {
2055                        let lolen = lo.len_utf8();
2056                        if i_b + lolen < bb.len()
2057                            && bb[i_b + lolen] == b'-'
2058                            && i_b + lolen + 1 < bb.len()
2059                            && bb[i_b + lolen + 1] != b']'
2060                        {
2061                            if let Some(hi) = std::str::from_utf8(&bb[i_b + lolen + 1..])
2062                                .ok()
2063                                .and_then(|s| s.chars().next())
2064                            {
2065                                mb_ranges.push((lo, hi));
2066                                i_b += lolen + 1 + hi.len_utf8();
2067                                continue;
2068                            }
2069                        }
2070                        mb_chars.push(lo);
2071                        i_b += lolen;
2072                        continue;
2073                    }
2074                }
2075                if i_b + 2 < bb.len() && bb[i_b + 1] == b'-' && bb[i_b + 2] != b']' {
2076                    let lo = bb[i_b];
2077                    let hi = bb[i_b + 2];
2078                    // ASCII-low, multibyte-high range (`[a-я]`): record as a
2079                    // wide range so the high end matches; the ASCII portion
2080                    // of the range still expands into `chars` below.
2081                    if hi >= 0x80 {
2082                        if let Some(hich) = std::str::from_utf8(&bb[i_b + 2..])
2083                            .ok()
2084                            .and_then(|s| s.chars().next())
2085                        {
2086                            mb_ranges.push((lo as char, hich));
2087                            for c in lo..=0x7f {
2088                                chars.push(c);
2089                            }
2090                            i_b += 2 + hich.len_utf8();
2091                            continue;
2092                        }
2093                    }
2094                    for c in lo..=hi {
2095                        chars.push(c);
2096                    }
2097                    i_b += 3;
2098                } else {
2099                    chars.push(bb[i_b]);
2100                    i_b += 1;
2101                }
2102            }
2103            drop(parse_b);
2104            if let Some(p_lock) = patparse.lock().ok() {
2105                if i_b < p_lock.len() && p_lock.as_bytes()[i_b] == b']' {
2106                    i_b += 1;
2107                }
2108            }
2109            patparse_off.store(i_b, Ordering::Relaxed);
2110            let opcode = if negate { P_ANYBUT } else { P_ANYOF };
2111            let off2 = patnode(opcode);
2112            let mut buf = patout.lock().unwrap();
2113            // Body layout: `[len:u32]` (total following bytes) then
2114            // `[chars_len:u32][chars][classmask:u32][n_mbchars:u32]
2115            // [mbchars:u32*n][n_mbranges:u32][(lo,hi):u32*2]`. `len` counts
2116            // the WHOLE body so `advance_past_instr` / node traversal (which
2117            // do `4 + len`) skip it unchanged — only the P_ANYOF/P_ANYBUT
2118            // matcher parses the sub-structure. Pure-ASCII brackets carry a
2119            // zeroed 12-byte tail (classmask 0, no mbchars/mbranges).
2120            let mut body_ext: Vec<u8> = Vec::new();
2121            body_ext.extend_from_slice(&(chars.len() as u32).to_le_bytes());
2122            body_ext.extend_from_slice(&chars);
2123            body_ext.extend_from_slice(&mb_classmask.to_le_bytes());
2124            body_ext.extend_from_slice(&(mb_chars.len() as u32).to_le_bytes());
2125            for c in &mb_chars {
2126                body_ext.extend_from_slice(&(*c as u32).to_le_bytes());
2127            }
2128            body_ext.extend_from_slice(&(mb_ranges.len() as u32).to_le_bytes());
2129            for (lo, hi) in &mb_ranges {
2130                body_ext.extend_from_slice(&(*lo as u32).to_le_bytes());
2131                body_ext.extend_from_slice(&(*hi as u32).to_le_bytes());
2132            }
2133            let len = body_ext.len() as u32;
2134            buf.extend_from_slice(&len.to_le_bytes());
2135            buf.extend_from_slice(&body_ext);
2136            *tail_out = off2;
2137            off2 as i64
2138        }
2139        b'(' => {
2140            patparse_off.fetch_add(1, Ordering::Relaxed);
2141            *flagp &= !P_PURESTR;
2142            // c:1508-1525 — `if (kshchar == '!') patcompnot(1, ...) else
2143            // patcompswitch(1, ...)`. The `!(pat)` form needs a
2144            // negation-aware compile that builds an EXCLUDE subtree;
2145            // every other case (including `@(pat)`, the bare `(pat)`
2146            // group, and the `+/*/?` ksh forms) takes the alternation
2147            // switch path.
2148            if kshchar == b'!' {
2149                // c:1522-1523
2150                let mut flags2: i32 = 0;
2151                let starter_off = patcompnot(1, &mut flags2);
2152                if starter_off < 0 {
2153                    return -1;
2154                }
2155                *flagp |= flags2 & P_HSTART; // c:1526
2156                *tail_out = starter_off as usize;
2157                // c:1419 already consumed `!`; the b'(' branch consumed
2158                // `(`; patcompnot drained through to `)`. Verify here.
2159                return starter_off;
2160            }
2161            // c:775-783 — `if (paren && (patglobflags & GF_BACKREF) &&
2162            // patnpar <= NSUBEXP) { parno = patnpar++; starter =
2163            // patnode(P_OPEN + parno); } else starter = 0;`. A group is
2164            // NUMBERED (capture slot allocated) only when GF_BACKREF is
2165            // live in patglobflags at the point the `(` is compiled —
2166            // wherever the `(#b)` appeared (leading, after a literal
2167            // prefix, mid-pattern). Beyond NSUBEXP, C "just use[s]
2168            // P_OPEN on its own" (c:777-780): parno stays 0 and the
2169            // match arms record nothing (c:2957 `if (no && ...)`).
2170            // The previous Rust arm numbered EVERY paren and returned
2171            // -1 past NSUBEXP — both divergent.
2172            let gf_now = patglobflags.load(Ordering::Relaxed);
2173            let n = if (gf_now & GF_BACKREF) != 0
2174                && patnpar.load(Ordering::Relaxed) <= NSUBEXP as i32
2175            {
2176                patnpar.fetch_add(1, Ordering::Relaxed)
2177            } else {
2178                0 // plain (uncaptured) group — P_OPEN+0 / P_CLOSE+0
2179            };
2180            let opcode = P_OPEN + n as u8;
2181            let open_off = patnode(opcode);
2182            // c:770 — `savglobflags = patglobflags`. Snapshot the glob
2183            // flags entering the group so a flag set INSIDE it (e.g.
2184            // `((#i)foo)`) can be restored on the way out.
2185            let savglobflags = patglobflags.load(Ordering::Relaxed);
2186            let mut inner_flags: i32 = 0;
2187            let inner = patcompswitch(1, &mut inner_flags);
2188            if inner < 0 {
2189                return -1;
2190            }
2191            // Expect closing ')'.
2192            let cur_off = patparse_off.load(Ordering::Relaxed);
2193            let p = patparse.lock().unwrap();
2194            if cur_off >= p.len() || p.as_bytes()[cur_off] != b')' {
2195                return -1;
2196            }
2197            drop(p);
2198            patparse_off.fetch_add(1, Ordering::Relaxed);
2199            let close_off = patnode(P_CLOSE + n as u8);
2200            // P_OPEN_N.next → first BRANCH of inner alternation.
2201            set_next(open_off, inner as usize);
2202            // Mirror C pattern.c:903 `pattail(starter, ender)`:
2203            // walk the BRANCH alt-chain from P_OPEN and patch the
2204            // last BRANCH's `.next` to P_CLOSE_N. Without this, the
2205            // outer `pattail` walk would descend into the inner
2206            // alt-chain (P_OPEN.next → inner BRANCH) and corrupt
2207            // its terminator.
2208            pattail(open_off, close_off);
2209            // Each branch's operand chain ends at P_CLOSE_N.
2210            chain_branches_to(inner as usize, close_off);
2211            *flagp &= !P_PURESTR;
2212            // c:920-929 — `if (paren && gfchanged) { pattail(ender,
2213            // patnode(P_GFLAGS)); patglobflags = savglobflags; }`. When
2214            // a glob flag that the matcher honors mid-pattern
2215            // (IGNCASE / LCMATCHUC / MULTIBYTE) changed inside the
2216            // group, append a P_GFLAGS node after P_CLOSE that restores
2217            // the entering value. Without it `[[ FOOXx = ((#i)foox)X ]]`
2218            // wrongly matched: the `(#i)` set GF_IGNCASE for the rest of
2219            // the match, so the trailing case-sensitive `X` folded too.
2220            let after_gf = patglobflags.load(Ordering::Relaxed);
2221            let relevant = GF_IGNCASE | GF_LCMATCHUC | GF_MULTIBYTE;
2222            if (after_gf ^ savglobflags) & relevant != 0 {
2223                let gf_off = patnode(P_GFLAGS);
2224                {
2225                    let mut buf = patout.lock().unwrap();
2226                    buf.extend_from_slice(&(savglobflags & relevant).to_le_bytes());
2227                }
2228                set_next(close_off, gf_off);
2229                patglobflags.store(savglobflags, Ordering::Relaxed); // c:927
2230                *tail_out = gf_off;
2231                return open_off as i64;
2232            }
2233            *tail_out = close_off;
2234            open_off as i64
2235        }
2236        b'\\' => {
2237            // c:Src/pattern.c — raw `\X` reaches this arm when the
2238            // input bypassed shtokenize (Src/glob.c:3565), which is
2239            // the path taken by paramsubst callers passing singsub-
2240            // computed pattern strings. C's faithful path: shtokenize
2241            // converts `\X` (special X) to `Bnullkeep X` (case at
2242            // pattern.c:1584), and a `\X` with non-special X stays
2243            // raw — both bytes survive to pattern.c's literal-run
2244            // default arm. Trailing lone `\` also survives shtokenize
2245            // (bslash flag stays 1 to end-of-string, no replacement
2246            // fires) and reaches the default-arm literal emission.
2247            //
2248            // Two arms mirror that:
2249            //   - `\X` (off2 in range): emit X as literal. Equivalent
2250            //     to Bnullkeep X arm in C's pattern.c — the next byte
2251            //     is the user-literal payload.
2252            //   - trailing lone `\`: emit `\` as literal. Equivalent
2253            //     to C's pattern.c default arm receiving a raw `\`
2254            //     byte that survived shtokenize.
2255            patparse_off.fetch_add(1, Ordering::Relaxed);
2256            let p = patparse.lock().unwrap();
2257            let off2 = patparse_off.load(Ordering::Relaxed);
2258            if off2 >= p.len() {
2259                drop(p);
2260                *flagp |= P_SIMPLE;
2261                let lit_off = patnode(P_EXACTLY);
2262                let mut buf_lit = patout.lock().unwrap();
2263                buf_lit.extend_from_slice(&1u32.to_le_bytes());
2264                buf_lit.push(b'\\');
2265                *tail_out = lit_off;
2266                return lit_off as i64;
2267            }
2268            let escaped = p.as_bytes()[off2];
2269            drop(p);
2270            patparse_off.fetch_add(1, Ordering::Relaxed);
2271            *flagp |= P_SIMPLE;
2272            let lit_off = patnode(P_EXACTLY);
2273            let mut buf_lit = patout.lock().unwrap();
2274            buf_lit.extend_from_slice(&1u32.to_le_bytes());
2275            buf_lit.push(escaped);
2276            *tail_out = lit_off;
2277            lit_off as i64
2278        }
2279        b'<' => {
2280            // Numeric range: <a-b> / <a-> / <-b> / <-> .
2281            // Port of pattern.c:1528-1570 (Inang case).
2282            //
2283            // c:Src/pattern.c:1528 — C's `case Inang:` only fires for
2284            // the Inang TOKEN (0x99) emitted by the lexer at
2285            // Src/lex.c:1202 when `isnumglob()` returns true (i.e.
2286            // the `<…>` body parses as a numeric range). Raw `<` (0x3C)
2287            // in source — quoted `"<"`, escaped `\<`, or any `<` in a
2288            // non-numglob position — never reaches the pattern compiler
2289            // as a metachar; it falls through to the literal-run arm.
2290            //
2291            // zshrs's pattern compiler is fed by callers that mix lexer
2292            // tokens with raw ASCII (singsub output, runtime-computed
2293            // pattern strings that never went through `Src/lex.c:1201`'s
2294            // isnumglob check). The faithful port: do the same
2295            // `[digit]*-[digit]*>` walk inline that `isnumglob` does at
2296            // lex.c:580-610, with full state-save + rewind on failure so
2297            // a non-numeric `<` (e.g. `<[:digit:]]#>` from zconvey's
2298            // color-marker pattern, or `<no-data>` from zinit.zsh:2507)
2299            // falls back to the literal-run arm.
2300            //
2301            // The numglob shape mirrors C's isnumglob exactly: digits
2302            // then `-` then digits then `>`. Empty leading / trailing
2303            // digit runs are OK (gives the `<-N>` / `<N->` / `<->` forms).
2304            // c:Src/pattern.c:499-506 — `if (isset(SHGLOB))
2305            // zpc_special[ZPC_INPAR] = zpc_special[ZPC_INANG] = Marker;`
2306            // ("Grouping and numeric ranges are not valid. We do allow
2307            // alternation, however; it's needed for case."). patcompcharsset
2308            // (pattern.rs:487) already applies that mask, but this arm never
2309            // consulted the table: it ran its inline isnumglob walk
2310            // unconditionally, so `<->` stayed an ACTIVE numeric range under
2311            // `emulate sh` / `emulate ksh`. C asserts the case is unreachable
2312            // there — `DPUTS(isset(SHGLOB), "Treating <..> as numeric range
2313            // with SHGLOB")` (c:1532). A disabled slot also covers user
2314            // `disable -p '<'`. Fall through to the same literal-`<` emit the
2315            // non-numeric rewind path uses. Bug #1053-B.
2316            let inang_disabled = { zpc_special.lock().unwrap()[ZPC_INANG as usize] != b'<' };
2317            if inang_disabled {
2318                let h = patnode(P_EXACTLY);
2319                let mut buf = patout.lock().unwrap();
2320                let len: u32 = 1;
2321                buf.extend_from_slice(&len.to_le_bytes());
2322                buf.push(b'<');
2323                drop(buf);
2324                patparse_off.fetch_add(1, Ordering::Relaxed);
2325                *flagp |= P_SIMPLE;
2326                *tail_out = h;
2327                return h as i64;
2328            }
2329            let entry_off = patparse_off.load(Ordering::Relaxed);
2330            patparse_off.fetch_add(1, Ordering::Relaxed); // consume `<`
2331            let parse_n = patparse.lock().unwrap();
2332            let nb = parse_n.as_bytes();
2333            let mut j = patparse_off.load(Ordering::Relaxed);
2334            let mut len_flag: u8 = 0; // bit 0 = lo present, bit 1 = hi present
2335            let mut from: i64 = 0;
2336            let lo_start = j;
2337            while j < nb.len() && nb[j].is_ascii_digit() {
2338                from = from * 10 + (nb[j] - b'0') as i64;
2339                j += 1;
2340            }
2341            if j > lo_start {
2342                len_flag |= 1;
2343            } // c:1538 — `len |= 1`
2344              // Mandatory dash. C's isnumglob bails (ret=0) here too —
2345              // walks until non-digit, then if non-digit isn't `-` (or `>`
2346              // after seeing `-`), returns "not numglob". Rewind the `<`
2347              // consumption and fall through to the literal-run arm.
2348            if j >= nb.len() || nb[j] != b'-' {
2349                drop(parse_n);
2350                patparse_off.store(entry_off, Ordering::Relaxed);
2351                let h = patnode(P_EXACTLY);
2352                let mut buf = patout.lock().unwrap();
2353                let len: u32 = 1;
2354                buf.extend_from_slice(&len.to_le_bytes());
2355                buf.push(b'<');
2356                patparse_off.fetch_add(1, Ordering::Relaxed);
2357                *flagp |= P_SIMPLE;
2358                *tail_out = h;
2359                return h as i64;
2360            }
2361            j += 1; // c:1543 patparse++
2362            let mut to: i64 = 0;
2363            let hi_start = j;
2364            while j < nb.len() && nb[j].is_ascii_digit() {
2365                to = to * 10 + (nb[j] - b'0') as i64;
2366                j += 1;
2367            }
2368            if j > hi_start {
2369                len_flag |= 2;
2370            } // c:1548 — `len |= 2`
2371              // Expect closing '>'. Mirror the dash-failure rewind here
2372              // too: a `<N-` without `>` is non-numglob per C's isnumglob.
2373            if j >= nb.len() || nb[j] != b'>' {
2374                drop(parse_n);
2375                patparse_off.store(entry_off, Ordering::Relaxed);
2376                let h = patnode(P_EXACTLY);
2377                let mut buf = patout.lock().unwrap();
2378                let len: u32 = 1;
2379                buf.extend_from_slice(&len.to_le_bytes());
2380                buf.push(b'<');
2381                patparse_off.fetch_add(1, Ordering::Relaxed);
2382                *flagp |= P_SIMPLE;
2383                *tail_out = h;
2384                return h as i64;
2385            }
2386            j += 1;
2387            drop(parse_n);
2388            patparse_off.store(j, Ordering::Relaxed);
2389            *flagp &= !P_PURESTR;
2390
2391            let off2 = match len_flag {
2392                // c:1552-1567
2393                3 => {
2394                    // c:1554 P_NUMRNG
2395                    let off2 = patnode(P_NUMRNG);
2396                    let mut buf = patout.lock().unwrap();
2397                    buf.extend_from_slice(&from.to_le_bytes());
2398                    buf.extend_from_slice(&to.to_le_bytes());
2399                    off2
2400                }
2401                2 => {
2402                    // c:1559 P_NUMTO
2403                    let off2 = patnode(P_NUMTO);
2404                    let mut buf = patout.lock().unwrap();
2405                    buf.extend_from_slice(&to.to_le_bytes());
2406                    off2
2407                }
2408                1 => {
2409                    // c:1563 P_NUMFROM
2410                    let off2 = patnode(P_NUMFROM);
2411                    let mut buf = patout.lock().unwrap();
2412                    buf.extend_from_slice(&from.to_le_bytes());
2413                    off2
2414                }
2415                _ => patnode(P_NUMANY), // c:1568
2416            };
2417            *tail_out = off2;
2418            off2 as i64
2419        }
2420        _ => {
2421            // Accumulate a literal run.
2422            let mut buf: Vec<u8> = Vec::new();
2423            let mut local_off = off;
2424            let p = patparse.lock().unwrap();
2425            // c:1312-1317 — break on kshchar trigger (any of the six
2426            // ZPC_KSH_* slots whose literal byte is followed by `(`).
2427            // Snapshot the zpc_special slots that participate; when
2428            // KSHGLOB is off they're Marker (0xa2) and won't match
2429            // ordinary bytes.
2430            let (ksh_plus, ksh_bang, ksh_bang2, ksh_at, ksh_star, ksh_quest) = {
2431                let sp = zpc_special.lock().unwrap();
2432                (
2433                    sp[ZPC_KSH_PLUS as usize],
2434                    sp[ZPC_KSH_BANG as usize],
2435                    sp[ZPC_KSH_BANG2 as usize],
2436                    sp[ZPC_KSH_AT as usize],
2437                    sp[ZPC_KSH_STAR as usize],
2438                    sp[ZPC_KSH_QUEST as usize],
2439                )
2440            };
2441            // c:1314-1317 — `~` is a literal-run terminator IFF it's
2442            // active as the exclusion operator (EXTENDEDGLOB on) AND
2443            // the lookahead is `/` or a non-segment-special byte.
2444            // Without EXTENDEDGLOB the byte is Marker (0xa2) so the
2445            // equality check fails naturally. Limited to TILDE only —
2446            // the other ZPC_SEG_COUNT slots (SLASH, NULL, BAR, OUTPAR)
2447            // are already covered by the explicit stop list above
2448            // (`|` `)` handled there; `/` was deliberately NOT a
2449            // literal-run break in this Rust port, see
2450            // `zsh_corpus_hash_s_e_anchors_match_bare_test` which
2451            // expects `/` to stay literal inside `(...)` alternation).
2452            let (sp_tilde_lit, sp_seg_lit_set, sp_hat_lit, sp_hash_lit) = {
2453                let sp = zpc_special.lock().unwrap();
2454                let mut set = [false; 256];
2455                for i in 0..(ZPC_SEG_COUNT as usize) {
2456                    set[sp[i] as usize] = true;
2457                }
2458                (
2459                    sp[ZPC_TILDE as usize],
2460                    set,
2461                    sp[ZPC_HAT as usize],
2462                    sp[ZPC_HASH as usize],
2463                )
2464            };
2465            while local_off < p.len() {
2466                let b = p.as_bytes()[local_off];
2467                // c:Src/pattern.c:480-483 — `if (!isset(EXTENDEDGLOB))`
2468                // masks ZPC_HAT and ZPC_HASH to Marker so the
2469                // patcompiece dispatch treats `^` / `#` as literals
2470                // (no negation / no zero-or-more closure).
2471                // The literal-run break list hardcoded `b'^'` /
2472                // `b'#'` though, which still terminated the literal
2473                // accumulator and forced patcompiece's empty-buf
2474                // -1 return → "bad pattern: ^.*" for the unset
2475                // EXTENDEDGLOB case. Gate the break on the actual
2476                // zpc_special slot so an EXTENDEDGLOB-off run treats
2477                // `^` and `#` as ordinary chars. Bug #421.
2478                if matches!(b, b'?' | b'*' | b'[' | b'(' | b')' | b'|' | b'\\' | b'<')
2479                    || (b == b'^' && sp_hat_lit == b'^')
2480                    || (b == b'#' && sp_hash_lit == b'#')
2481                {
2482                    break;
2483                }
2484                if b == sp_tilde_lit && sp_tilde_lit != 0 {
2485                    let la = p.as_bytes().get(local_off + 1).copied().unwrap_or(0);
2486                    // Literal-tilde exception: `~` followed by a
2487                    // segment-special OTHER than `/` stays in the run.
2488                    let literal_tilde_exception = la != b'/' && sp_seg_lit_set[la as usize];
2489                    if !literal_tilde_exception {
2490                        break;
2491                    }
2492                }
2493                // c:1278-1292 — ksh-glob trigger lookahead. If the next
2494                // byte is `(` AND this byte matches one of the six
2495                // ZPC_KSH_* slots, stop the literal run BEFORE this byte
2496                // so the next patcomppiece call consumes it as a ksh
2497                // quantifier prefix (e.g. `pre+(x)post` accumulates
2498                // `pre` then breaks at `+` since `+(` follows).
2499                if local_off + 1 < p.len() && p.as_bytes()[local_off + 1] == b'(' {
2500                    if b == ksh_plus
2501                        || b == ksh_bang
2502                        || b == ksh_bang2
2503                        || b == ksh_at
2504                        || b == ksh_star
2505                        || b == ksh_quest
2506                    {
2507                        break;
2508                    }
2509                }
2510                buf.push(b);
2511                local_off += 1;
2512            }
2513            drop(p);
2514            if buf.is_empty() {
2515                return -1;
2516            }
2517            // c:Src/pattern.c:1340-1351 — multi-char run with TRAILING `#`
2518            // (or `(#cN,M)`) must backtrack ONE char so the trailing
2519            // quantifier applies to the LAST char only. Without this,
2520            // `fo#` compiles as `(fo)#` instead of `f` + `o#`, breaking
2521            // `(fo#)#` against "ffo" (the test pin we are fixing).
2522            //
2523            // C body:
2524            //   if ((*patparse == zpc_special[ZPC_HASH] || ...) && morelen)
2525            //       patparse = patprev;
2526            //
2527            // morelen = "more than one char in the literal run". The
2528            // backtrack pops the LAST byte from buf and rewinds patparse
2529            // by 1 so the next patcomppiece call sees that byte as its
2530            // own atom.
2531            let has_trailing_hash = {
2532                let sp_hash = zpc_special.lock().unwrap()[ZPC_HASH as usize];
2533                let p = patparse.lock().unwrap();
2534                let hash_at = local_off < p.len() && p.as_bytes()[local_off] == b'#';
2535                drop(p);
2536                hash_at && sp_hash == b'#'
2537            };
2538            if buf.len() > 1 && has_trailing_hash {
2539                let _ = buf.pop();
2540                local_off -= 1;
2541            }
2542            patparse_off.store(local_off, Ordering::Relaxed);
2543            *flagp |= P_SIMPLE;
2544            // If it's a single char, mark simple; multi-char run stays pure-string.
2545            let lit_off = patnode(P_EXACTLY);
2546            let mut buf_lit = patout.lock().unwrap();
2547            let len = buf.len() as u32;
2548            buf_lit.extend_from_slice(&len.to_le_bytes());
2549            buf_lit.extend_from_slice(&buf);
2550            *tail_out = lit_off;
2551            lit_off as i64
2552        }
2553    };
2554
2555    if atom < 0 {
2556        return atom;
2557    }
2558
2559    // c:1606-1644 — quantifier dispatch. Three sources of quantifier:
2560    //   (a) trailing `#` / `##` (zsh-extended hash repetition)
2561    //   (b) trailing `(#cN,M)` count-spec (handled in patcompbranch, not here)
2562    //   (c) kshchar form `+/*/?` already at the atom's leading byte.
2563    //
2564    // Rule c:1615 — if no hash AND no count AND kshchar is one of
2565    // (none, '@', '!'), no quantifier; return atom as-is. `@` is the
2566    // identity quantifier (match group exactly once); `!` already had
2567    // its negation compiled by patcompnot inside the atom.
2568    let q_off = patparse_off.load(Ordering::Relaxed);
2569    let parse2 = patparse.lock().unwrap();
2570    // c:1611-1620 — `if (*patparse == zpc_special[ZPC_HASH])` quantifier
2571    // dispatch. The C source reads `zpc_special[ZPC_HASH]`, which
2572    // patcompcharsset (c:480-483) sets to `'#'` only when
2573    // EXTENDEDGLOB is on, else to the Marker byte (c:476). A literal
2574    // '#' in the source pattern therefore does NOT trigger the
2575    // quantifier path when EXTENDEDGLOB is off — the comparison
2576    // `*patparse == Marker` fails. Previously this Rust check used
2577    // a bare `b'#'` comparison instead of consulting zpc_special,
2578    // making `(x)#` and `[a-z]##[0-9]##` match even with
2579    // extendedglob OFF (parity bugs #5/#6 vs real zsh).
2580    let hash_char = zpc_special.lock().unwrap()[ZPC_HASH as usize]; // c:1612
2581    let has_hash = hash_char == b'#' && q_off < parse2.len() && parse2.as_bytes()[q_off] == b'#'; // c:1611-1614
2582    drop(parse2);
2583    if !has_hash && (kshchar == 0 || kshchar == b'@' || kshchar == b'!') {
2584        return atom; // c:1616-1617
2585    }
2586
2587    // c:1621-1622 — kshchar with hash is a parse error (too much at once).
2588    if kshchar != 0 && has_hash {
2589        return -1;
2590    }
2591
2592    // c:1624-1644 — pick the operator + post-atom flags.
2593    let op: u8;
2594    let mut consume_hashes = 0;
2595    if kshchar == b'*' {
2596        // c:1624-1626
2597        op = P_ONEHASH;
2598        *flagp = P_HSTART;
2599    } else if kshchar == b'+' {
2600        // c:1627-1629
2601        op = P_TWOHASH;
2602        *flagp = P_HSTART;
2603    } else if kshchar == b'?' {
2604        // c:1630-1632
2605        op = 0; // sentinel — `?` desugars to (x|) via the BRANCH path below
2606        *flagp = 0;
2607    } else {
2608        // c:1637-1644 — `#` / `##`.
2609        let parse_h = patparse.lock().unwrap();
2610        let two = q_off + 1 < parse_h.len() && parse_h.as_bytes()[q_off + 1] == b'#';
2611        drop(parse_h);
2612        if two {
2613            op = P_TWOHASH;
2614            consume_hashes = 2;
2615        } else {
2616            op = P_ONEHASH;
2617            consume_hashes = 1;
2618        }
2619        *flagp = P_HSTART;
2620    }
2621    if consume_hashes > 0 {
2622        patparse_off.fetch_add(consume_hashes, Ordering::Relaxed);
2623    }
2624
2625    // c:1705-1746 — quantifier emission.
2626    //
2627    // Read the atom's opcode so c:1705 (`?#` → `*`) optimization can
2628    // apply. The atom byte sits at `atom + I_OP` in patout.
2629    let atom_op = {
2630        let buf = patout.lock().unwrap();
2631        if (atom as usize) + I_OP < buf.len() {
2632            buf[atom as usize + I_OP]
2633        } else {
2634            0
2635        }
2636    };
2637
2638    if ((*flagp & P_SIMPLE) != 0) && (op == P_ONEHASH || op == P_TWOHASH) && atom_op == P_ANY {
2639        // c:1705-1717 — `?#` becomes `*`; `?##` becomes `?*`. The atom
2640        // is P_ANY at offset `atom`; rewrite or pad as needed.
2641        let mut buf = patout.lock().unwrap();
2642        if op == P_TWOHASH {
2643            // c:1712-1713 — `?##` → `?*`: leave the P_ANY in place,
2644            // then emit P_STAR right after.
2645            drop(buf);
2646            let _star = patnode(P_STAR);
2647            *tail_out = atom as usize;
2648        } else {
2649            // c:1715-1716 — `?#` → `*`: just rewrite atom's opcode.
2650            buf[atom as usize + I_OP] = P_STAR;
2651            drop(buf);
2652            *tail_out = atom as usize;
2653        }
2654        *flagp &= !P_PURESTR;
2655    } else if ((*flagp & P_SIMPLE) != 0)
2656        && op != 0
2657        && (patglobflags.load(Ordering::Relaxed) & 0xff) == 0
2658    {
2659        // c:1718-1720 — simple operand, no approximate-match counter:
2660        // emit `patinsert(op, starter, NULL, 0)`. The matcher walks
2661        // the inserted P_ONEHASH/P_TWOHASH operand at scan+I_BODY.
2662        patinsert(op, atom as usize, None, 0);
2663        *flagp &= !P_PURESTR;
2664        *tail_out = atom as usize;
2665    } else if op == P_ONEHASH {
2666        // c:1721-1729 — emit x# as (x&|): P_WBRANCH with NULL payload
2667        // ahead of atom; loop via P_BACK; null alternative branch.
2668        // patinsert with `sz=size_of::<union upat>()=8` (we use 8 to
2669        // mirror the C `union upat` slot, then zero-pad it).
2670        let payload = [0u8; 8];
2671        patinsert(P_WBRANCH, atom as usize, Some(&payload), 8);
2672        // Either x — atom is now at atom+I_BODY+8.
2673        let back = patnode(P_BACK);
2674        patoptail(atom as usize, back); // c:1726 — loop back
2675        patoptail(atom as usize, atom as usize); // c:1727
2676        let alt = patnode(P_BRANCH);
2677        pattail(atom as usize, alt); // c:1728 — or
2678        let null_node = patnode(P_NOTHING);
2679        pattail(atom as usize, null_node); // c:1729 — null
2680        *flagp &= !P_PURESTR;
2681        // The piece's chain-tail is the trailing P_NOTHING node — its
2682        // `.next` slot is empty and is the correct splice point for
2683        // whatever piece patcompbranch chains in next.
2684        *tail_out = null_node;
2685    } else if op == P_TWOHASH {
2686        // c:1730-1738 — emit x## as x(&|): P_WBRANCH after atom; loop
2687        // back; null branch.
2688        let wbranch = patnode(P_WBRANCH); // c:1732 — either
2689        let payload = [0u8; 8]; // c:1733-1734 patadd((char *)&up, ..., sizeof(up), 0)
2690        {
2691            let mut buf = patout.lock().unwrap();
2692            buf.extend_from_slice(&payload);
2693        }
2694        pattail(atom as usize, wbranch); // c:1735
2695        let back = patnode(P_BACK);
2696        pattail(back, atom as usize); // c:1736 — loop back
2697        let alt = patnode(P_BRANCH);
2698        pattail(wbranch, alt); // c:1737 — or
2699        let null_node = patnode(P_NOTHING);
2700        pattail(atom as usize, null_node); // c:1738 — null
2701        *flagp &= !P_PURESTR;
2702        // Same as ONEHASH path — tail at the trailing P_NOTHING.
2703        *tail_out = null_node;
2704    } else if kshchar == b'?' {
2705        // c:1739-1746 — emit ?(x) as (x|).
2706        patinsert(P_BRANCH, atom as usize, None, 0); // c:1741 — either x
2707        let alt = patnode(P_BRANCH); // c:1742 — or
2708        pattail(atom as usize, alt);
2709        let null_node = patnode(P_NOTHING); // c:1743 — null
2710        pattail(atom as usize, null_node); // c:1744
2711        patoptail(atom as usize, null_node); // c:1745
2712        *flagp &= !P_PURESTR;
2713        // Same as ONEHASH/TWOHASH — tail at the trailing P_NOTHING.
2714        *tail_out = null_node;
2715    }
2716
2717    // c:1747-1748 — a stray `#` immediately after = compile error.
2718    {
2719        let p = patparse.lock().unwrap();
2720        let q2 = patparse_off.load(Ordering::Relaxed);
2721        if q2 < p.len() && p.as_bytes()[q2] == b'#' {
2722            // Only flag as error when EXTENDEDGLOB-style # is enabled.
2723            let sp = zpc_special.lock().unwrap();
2724            if sp[ZPC_HASH as usize] == b'#' {
2725                return -1;
2726            }
2727        }
2728    }
2729
2730    atom
2731}
2732
2733/// Port of `patcompnot(int paren, int *flagsp)` from `Src/pattern.c:1760`.
2734///
2735/// C: `static long patcompnot(int paren, int *flagsp)`. Implements
2736/// the `^pat` (paren=0) or `!(pat)` (paren=1) extended/ksh-glob
2737/// negation by emitting `P_BRANCH → P_STAR → P_EXCSYNC` followed by
2738/// `P_EXCLUDE [payload]` and the asserted pattern terminated by
2739/// `P_EXCEND`, all joined at a trailing `P_NOTHING`.
2740///
2741/// NOTE: matcher support for `P_EXCSYNC`/`P_EXCEND`/`P_EXCLUDE` is
2742/// only partial in the Rust port — compile is faithful per
2743/// pattern.c:1760-1784 but match-time negation semantics may diverge
2744/// from C for complex backtracking edge cases. Tests covering
2745/// `!(foo)` / `!(foo|bar)` exercise the basic working subset.
2746pub fn patcompnot(paren: i32, flagsp: &mut i32) -> i64 {
2747    // c:1760
2748    // c:1767 — `*flagsp = P_HSTART;`. Negation always starts with `*`
2749    // semantically so the caller knows the piece can match at the
2750    // start of input.
2751    *flagsp = P_HSTART;
2752
2753    // c:1769 — `starter = patnode(P_BRANCH);`
2754    let starter = patnode(P_BRANCH);
2755    // c:1770 — `br = patnode(P_STAR);`
2756    let br = patnode(P_STAR);
2757    // c:1771 — `excsync = patnode(P_EXCSYNC);`
2758    let excsync = patnode(P_EXCSYNC);
2759    // c:1772 — `pattail(br, excsync);`
2760    pattail(br, excsync);
2761    // c:1773 — `pattail(starter, excl = patnode(P_EXCLUDE));`
2762    let excl = patnode(P_EXCLUDE);
2763    pattail(starter, excl);
2764    // c:1774-1775 — `up.p = NULL; patadd((char *)&up, 0, sizeof(up), 0);`
2765    // The EXCLUDE node carries an 8-byte syncptr payload, NULL-init.
2766    {
2767        let mut buf = patout.lock().unwrap();
2768        buf.extend_from_slice(&[0u8; 8]);
2769    }
2770    // c:1776 — `br = (paren ? patcompswitch(1, &dummy) : patcompbranch(&dummy, 0));`
2771    let mut dummy: i32 = 0;
2772    let inner = if paren != 0 {
2773        let r = patcompswitch(1, &mut dummy);
2774        // c:1503-1505 — caller `Inpar` arm expects to consume the
2775        // trailing `)`. Mirror here since patcompnot is invoked from
2776        // the b'(' atom-arm AFTER it already consumed `(`.
2777        if r >= 0 {
2778            let cur = patparse_off.load(Ordering::Relaxed);
2779            let p = patparse.lock().unwrap();
2780            if cur >= p.len() || p.as_bytes()[cur] != b')' {
2781                return -1;
2782            }
2783            drop(p);
2784            patparse_off.fetch_add(1, Ordering::Relaxed);
2785        }
2786        r
2787    } else {
2788        patcompbranch(&mut dummy, 0)
2789    };
2790    if inner < 0 {
2791        return -1; // c:1777 `return 0;` (Rust uses -1 for failure)
2792    }
2793    // c:1778 — `pattail(br, patnode(P_EXCEND));`
2794    let excend = patnode(P_EXCEND);
2795    pattail(inner as usize, excend);
2796    // c:1779 — `n = patnode(P_NOTHING);`
2797    let n = patnode(P_NOTHING);
2798    // c:1780 — `pattail(excsync, n);`
2799    pattail(excsync, n);
2800    // c:1781 — `pattail(excl, n);`
2801    pattail(excl, n);
2802    // c:1783
2803    let _ = br; // suppress unused-var (kept for c-name parity)
2804    starter as i64
2805}
2806
2807/// Port of `patnode(long op)` from `Src/pattern.c:1790`.
2808///
2809/// C: `static long patnode(long op)` — writes a 1-byte opcode plus a
2810/// 4-byte zeroed next-offset. Returns the offset of the opcode byte.
2811fn patnode(op: u8) -> usize {
2812    // c:1790
2813    let mut buf = patout.lock().unwrap();
2814    let off = buf.len();
2815    buf.push(op); // I_OP
2816    buf.extend_from_slice(&[0, 0, 0, 0]); // I_NEXT zeroed
2817    off
2818}
2819
2820/// Port of `patinsert(long op, int opnd, char *xtra, int sz)` from `Src/pattern.c:1807`.
2821///
2822/// C: `static void patinsert(long op, int opnd, char *xtra, int sz)`.
2823/// Inserts an opcode (+ next slot) at position `opnd`, shifting bytes
2824/// after it down by `5 + sz`, then writes `xtra` payload of `sz` bytes.
2825fn patinsert(op: u8, opnd: usize, xtra: Option<&[u8]>, sz: usize) {
2826    // c:1807
2827    let mut buf = patout.lock().unwrap();
2828    let header_sz = 1 + 4; // op + next
2829    let total = header_sz + sz;
2830    // Insert `total` zeroed bytes at opnd, then overwrite.
2831    let mut inserted = vec![0u8; total];
2832    inserted[0] = op;
2833    if let Some(x) = xtra {
2834        let copy_n = x.len().min(sz);
2835        inserted[header_sz..header_sz + copy_n].copy_from_slice(&x[..copy_n]);
2836    }
2837    buf.splice(opnd..opnd, inserted);
2838    // Patch up next_off chains pointing past opnd by adding `total`.
2839    fixup_offsets_after_insert(&mut buf, opnd, total as u32);
2840}
2841
2842/// Port of `pattail(long p, long val)` from `Src/pattern.c:1834`.
2843///
2844/// C: `static void pattail(long p, long val)` — patches the next-offset
2845/// field of the opcode at offset `p` to point to `val`. Walks any
2846/// existing chain to the end before patching.
2847fn pattail(p: usize, val: usize) {
2848    // c:1834
2849    let mut buf = patout.lock().unwrap();
2850    let mut cur = p;
2851    loop {
2852        if cur + I_BODY > buf.len() {
2853            return;
2854        }
2855        let next_bytes: [u8; 4] = buf[cur + I_NEXT..cur + I_NEXT + 4].try_into().unwrap();
2856        let next = u32::from_le_bytes(next_bytes) as usize;
2857        if next == 0 {
2858            break;
2859        }
2860        cur = next;
2861    }
2862    let val_bytes = (val as u32).to_le_bytes();
2863    if cur + I_NEXT + 4 <= buf.len() {
2864        buf[cur + I_NEXT..cur + I_NEXT + 4].copy_from_slice(&val_bytes);
2865    }
2866}
2867
2868/// Port of `patoptail(long p, long val)` from `Src/pattern.c:1856`.
2869///
2870/// C: `static void patoptail(long p, long val)` — like pattail but
2871/// only patches branches (P_BRANCH/P_WBRANCH).
2872///
2873/// C: c:1862-1865 — for P_BRANCH the operand sits at `P_OPERAND(p)` =
2874/// `p+1` (Upat slot, i.e. byte offset `p + I_BODY` in Rust); for
2875/// P_WBRANCH the operand sits at `P_OPERAND(p) + 1` (skipping the
2876/// 8-byte syncptr payload Upat slot), i.e. byte offset
2877/// `p + I_BODY + 8` in Rust.
2878fn patoptail(p: usize, val: usize) {
2879    // c:1856
2880    let buf = patout.lock().unwrap();
2881    if p + I_OP >= buf.len() {
2882        return;
2883    }
2884    let op = buf[p + I_OP];
2885    drop(buf);
2886    if P_ISBRANCH(op) {
2887        // c:1862-1865 — operand offset depends on op kind.
2888        if op == P_BRANCH {
2889            pattail(p + I_BODY, val);
2890        } else {
2891            // P_WBRANCH / P_EXCLUDE / P_EXCLUDP — operand sits after
2892            // the 8-byte syncptr payload.
2893            pattail(p + I_BODY + 8, val);
2894        }
2895    }
2896}
2897
2898// =====================================================================
2899// 13/14. Matcher — pattern.c:2223-3579
2900// =====================================================================
2901
2902/// State accumulated during a single `patmatch` walk. C uses
2903/// per-thread globals (`patbeginp[]` / `patendp[]` for captures);
2904/// the Rust port encapsulates them in this struct passed by `&mut`.
2905/// Rule D: this struct represents matcher-internal scratch state
2906/// (analogous to `struct rpat pattrystate` at pattern.c:248), not a
2907/// bag-of-globals from unrelated subsystems.
2908///
2909/// **C counterpart**: `struct rpat` at `pattern.c:248`.
2910#[derive(Clone)]
2911#[allow(non_camel_case_types)]
2912pub struct rpat {
2913    /// Per-P_WBRANCH visit bitmap, keyed by WBRANCH-opcode offset.
2914    /// Mirrors C's `Upat ptrp` (`pattern.c:3217`): every WBRANCH carries
2915    /// an 8-byte payload sized as `union upat` — initialised to NULL,
2916    /// then lazily filled by `patmatch` with a buffer the size of the
2917    /// input string. Each byte tracks "have we already tried this
2918    /// WBRANCH at this input position with at most this many errors?".
2919    /// On revisit at the same position with the same-or-fewer errors,
2920    /// C returns 0 to bound the recursion (`pattern.c:3245-3248`). The
2921    /// Rust port keeps the bitmap on rpat (per-pattry call) instead of
2922    /// inside the bytecode payload so the bytecode stays read-only —
2923    /// the key is the WBRANCH offset; the value is a Vec<u8> the size
2924    /// of the input. Without it, `(fo#)#` against ANY input that
2925    /// requires the closure to consume at least one char per iteration
2926    /// (like "ffo") burns through PATMATCH_MAX_DEPTH and aborts.
2927    pub wbranch_visits: std::collections::HashMap<usize, Vec<u8>>,
2928    pub patbeginp: [usize; NSUBEXP], // c:241 capture starts (byte offsets)
2929    pub patendp: [usize; NSUBEXP],   // c:242 capture ends
2930    /// `parsfound` from `Src/pattern.c` (per c:2957/c:2989 references).
2931    /// Two-stripe bitmap: bits `0..NSUBEXP` track per-group P_OPEN
2932    /// first-write (`patbeginp[i]` committed); bits `NSUBEXP..2*NSUBEXP`
2933    /// track per-group P_CLOSE first-write (`patendp[i]` committed).
2934    /// Bit `n-1` (open) = `1 << (n-1)`; bit `n-1+NSUBEXP` (close) =
2935    /// `1 << (n-1+NSUBEXP)`. Width u32 to fit `2*NSUBEXP = 18` bits.
2936    /// The prior u16 width with a single-stripe (close-only) bit
2937    /// allowed P_OPEN to re-overwrite `patbeginp[i]` on every
2938    /// backtrack iteration — turning the SECOND capture's start
2939    /// offset into the FIRST iteration's, e.g. `(*)-(*)` against
2940    /// `hello-world` returned `match[2] = "hello-world"` instead of
2941    /// the right `"world"`.
2942    pub captures_set: u32, // c:Src/pattern.c parsfound
2943    /// Port of file-static `int errsfound` from `Src/pattern.c:2046`.
2944    /// Cumulative edit-count for approximate-match `(#aN)`. Reset to 0
2945    /// at the top of each pattry, incremented on P_EXACTLY mismatches
2946    /// when `glob_flags & 0xff > 0` (the substitution-budget byte).
2947    pub errsfound: i32, // c:2046
2948}
2949
2950/// Port of `wchar_t charref(char *x, char *y, int *zmb_ind)` from
2951/// `Src/pattern.c:1909`. Decode the char at `pos` without
2952/// advancing. UTF-32-native delegation: `s.chars().next()` returns
2953/// the decoded codepoint; delegates to Rust's native UTF-8 string
2954/// iterator instead of the C Meta-decode + zshtoken-translate +
2955/// mbrtowc state machine, which all collapse because Rust's `&str`
2956/// is already UTF-8.
2957///
2958/// Rust signature differs from C `charref(char *x, char *y, int *zmb_ind)`:
2959/// the C `y` end-of-string pointer is captured by `&str` length; the
2960/// `zmb_ind` out-param (multibyte-completion index) is dropped — the
2961/// Rust UTF-8 decode is one-shot, never partial.
2962pub fn charref(s: &str, pos: usize) -> Option<char> {
2963    // c:1909
2964    s[pos..].chars().next()
2965}
2966
2967/// Port of `char *charnext(char *x, char *y)` from `Src/pattern.c:1935`.
2968/// C returns the pointer past the current character; the Rust port
2969/// returns that position as a byte offset. Delegates to `metacharinc`
2970/// — same advance-by-one-codepoint logic.
2971pub fn charnext(x: &str, y: usize) -> usize {
2972    // c:1936
2973    metacharinc(x, y)
2974}
2975
2976/// Port of `wchar_t charrefinc(char **x, char *y, int *z)` from
2977/// `Src/pattern.c:1964`. Decode + advance: delegates to the
2978/// `charref`-then-`len_utf8`-step pattern. The C body's Meta /
2979/// mbrtowc / zshtoken triple collapses to one `chars().next()`
2980/// call followed by a byte-count step.
2981///
2982/// Rust signature differs from C: C mutates `*x` via the
2983/// `char **` to advance; Rust mutates `pos` directly. The `y`
2984/// end-of-string sentinel is captured by `&str` length; `z`
2985/// (multibyte-completion index) is dropped per the same one-shot
2986/// UTF-8 decode argument as `charref`.
2987pub fn charrefinc(s: &str, pos: &mut usize) -> Option<char> {
2988    // c:1964
2989    let c = s[*pos..].chars().next()?;
2990    *pos += c.len_utf8();
2991    Some(c)
2992}
2993
2994/// Port of `ptrdiff_t charsub(char *x, char *y)` from `Src/pattern.c:1997`.
2995///
2996/// Returns the number of characters between the start `x` and the
2997/// position `y` — i.e. the character-distance of byte offset `y` from
2998/// the string start. Non-multibyte: the byte distance `y` (each byte
2999/// is one character). Multibyte: the codepoint count of `x[0..y]`.
3000/// `patmatch` uses this (via the C `CHARSUB` macro) to report match
3001/// lengths and backreference positions in characters.
3002///
3003/// Replaces the prior fake that stepped back one char and returned a
3004/// byte offset — unrelated to the C character-count semantics.
3005pub fn charsub(x: &str, y: usize) -> usize {
3006    // c:1997
3007    let y = y.min(x.len());
3008    if !isset(MULTIBYTE) {
3009        // c:2003-2004 — `if (!isset(MULTIBYTE)) return y - x;`
3010        return y;
3011    }
3012    // c:2006-2021 — count codepoints in `x[0..y]`. Rust `&str` is valid
3013    // UTF-8, so `chars().count()` is the faithful equivalent of the C
3014    // mbrtowc loop (the MB_INVALID byte-by-byte fallback can't arise —
3015    // a `&str` never holds invalid sequences).
3016    x[..y].chars().count()
3017}
3018
3019// =====================================================================
3020// 16. String pre-processing — pattern.c:2063, :2080, :2132
3021// =====================================================================
3022
3023/// Port of `pattrystart()` from `Src/pattern.c:2063`. C resets per-
3024/// match state globals; Rust state is per-call so no-op.
3025pub fn pattrystart() {} // c:2063
3026
3027/// Port of `void patmungestring(char **string, int *stringlen, int *unmetalenin)`
3028/// from `Src/pattern.c:2080`.
3029///
3030/// Skips a leading `Nularg` (the empty-tokenised-string sentinel)
3031/// and computes `*stringlen` from `strlen` when caller passed `-1`.
3032/// Mutates all three in/out args; mirrors C's `char **` /
3033/// `int *` semantics via Rust mutable references.
3034pub fn patmungestring(string: &mut &str, stringlen: &mut i32, unmetalenin: &mut i32) {
3035    // c:2080
3036    // c:2082-2091 — `if (*stringlen > 0 && **string == Nularg)` — skip
3037    // the leading Nularg sentinel and adjust lengths.
3038    let bytes = string.as_bytes();
3039    if *stringlen > 0 && !bytes.is_empty() && bytes[0] as char == Nularg {
3040        // c:2085 — `(*string)++;` — advance past Nularg.
3041        *string = &string[1..]; // c:2085
3042                                // c:2090 — `if (*unmetalenin > 0) (*unmetalenin)--;`
3043        if *unmetalenin > 0 {
3044            // c:2089
3045            *unmetalenin -= 1; // c:2090
3046        }
3047        // c:2092 — `if (*stringlen > 0) (*stringlen)--;`
3048        if *stringlen > 0 {
3049            // c:2091
3050            *stringlen -= 1; // c:2092
3051        }
3052    }
3053
3054    // c:2096-2097 — `if (*stringlen < 0) *stringlen = strlen(*string);`
3055    if *stringlen < 0 {
3056        // c:2096
3057        *stringlen = string.len() as i32; // c:2097
3058    }
3059}
3060
3061/// Port of `pattry(Patprog prog, char *string)` from `Src/pattern.c:2223`.
3062///
3063/// C signature: `int pattry(Patprog prog, char *string)`. Returns
3064/// non-zero on match, 0 on no-match.
3065pub fn pattry(prog: &Patprog, string: &str) -> bool {
3066    // c:2223
3067    // c:2225 — `return pattrylen(prog, string, len, -1, NULL, 0);`
3068    pattrylen(prog, string, string.len() as i32, -1, None, 0) // c:2225
3069}
3070
3071/// Port of `int pattrylen(Patprog prog, char *string, int len,
3072/// int unmetalen, Patstralloc patstralloc, int offset)` from
3073/// `Src/pattern.c:2236`.
3074///
3075/// ```c
3076/// int
3077/// pattrylen(Patprog prog, char *string, int len, int unmetalen,
3078///           Patstralloc patstralloc, int offset)
3079/// {
3080///     return pattryrefs(prog, string, len, unmetalen, patstralloc, offset,
3081///                       NULL, NULL, NULL);
3082/// }
3083/// ```
3084pub fn pattrylen(
3085    prog: &Patprog,
3086    string: &str,
3087    len: i32, // c:2236
3088    unmetalen: i32,
3089    patstralloc: Option<&Patstralloc>,
3090    offset: i32,
3091) -> bool {
3092    // c:2238
3093    pattryrefs(
3094        prog,
3095        string,
3096        len,
3097        unmetalen,
3098        patstralloc,
3099        offset,
3100        None,
3101        None,
3102        None, // c:2239
3103    )
3104}
3105
3106/// Port of `int pattryrefs(Patprog prog, char *string, int stringlen,
3107/// int unmetalenin, Patstralloc patstralloc, int patoffset,
3108/// int *nump, int *begp, int *endp)` from `Src/pattern.c:2294`.
3109/// Runs `prog` against `string[0..stringlen]` (or whole string when
3110/// stringlen=-1) at `patoffset`, returning capture-group ranges.
3111///
3112/// C signature preserved per Rule S1. `Patstralloc` (the metafied-
3113/// string-allocator carrier from zsh.h:1613) is threaded through as
3114/// `Option<&Patstralloc>` matching C's `NULL`-passable pointer; the
3115/// matcher body currently ignores its contents (the substrate that
3116/// uses pre-allocated metafied buffers isn't wired into the Rust
3117/// matcher yet), but the param shape matches C so call sites trace
3118/// 1:1 to upstream.
3119#[allow(clippy::too_many_arguments)]
3120pub fn pattryrefs(
3121    // c:2294
3122    prog: &Patprog,
3123    string: &str,
3124    stringlen: i32,
3125    _unmetalenin: i32,
3126    _patstralloc: Option<&Patstralloc>,
3127    patoffset: i32,
3128    nump: Option<&mut i32>,
3129    begp: Option<&mut Vec<i32>>,
3130    endp: Option<&mut Vec<i32>>,
3131) -> bool {
3132    let trial: &str = if stringlen < 0 || (stringlen as usize) >= string.len() {
3133        string
3134    } else {
3135        &string[..stringlen as usize]
3136    };
3137    // Substring fast path for the ubiquitous `*literal*` shape
3138    // (optionally `(#i)`): program is [P_GFLAGS] P_STAR P_EXACTLY
3139    // P_STAR P_END with no captures requested. history-search-multi-
3140    // word's `${history[(R)(#i)*pat*]}` runs this shape over EVERY
3141    // history entry — 566k backtracking patmatch walks took CPU-
3142    // minutes per ^R (the reported freeze); a memmem-style scan is
3143    // what the shape actually needs. C reads this spirit via its
3144    // `mustoff` must-match prefilter (Src/pattern.c:2460-2483) but
3145    // declines under globflags; here the exact-bytes variant covers
3146    // any content and the `(#i)` variant is taken only when pattern
3147    // AND subject are pure ASCII (byte-fold == the matcher's char
3148    // fold there); anything else falls through to the full matcher.
3149    if nump.is_none()
3150        && begp.is_none()
3151        && endp.is_none()
3152        && patoffset == 0
3153        && (prog.0.flags & (PAT_NOTSTART | PAT_NOTEND) as i32) == 0
3154        // c:2399-2406 — a successful match is REJECTED for a file pattern
3155        // (PAT_NOGLD) whose subject starts with '.', unless the pattern itself
3156        // starts with a literal dot. That rejection lives below, AFTER
3157        // patmatch; returning from the fast path skipped it entirely, so
3158        // `*.*` matched `.hidden` (the literal `.` was found at offset 0 by
3159        // the substring scan) where zsh lists no dot files at all. Hand any
3160        // dot-leading subject under a file pattern to the full matcher, which
3161        // applies the rule. Costs nothing where the fast path was aimed:
3162        // history search (`${history[(R)(#i)*pat*]}`) is not a file glob and
3163        // never sets PAT_NOGLD, and non-dot files still take the fast path.
3164        && !((prog.0.flags & PAT_NOGLD as i32) != 0 && trial.starts_with('.'))
3165    {
3166        // Shape probe, inline (build-gate: no new fn in ported/ without
3167        // a C counterpart): Some((literal, igncase)) iff the program is
3168        // exactly `[P_GFLAGS] P_STAR P_EXACTLY(lit) P_STAR P_END` with
3169        // glob flags ⊆ IGNCASE|MULTIBYTE (no approx budget, no backrefs).
3170        let shape: Option<(String, bool)> = (|| {
3171            let buf: &[u8] = &prog.1;
3172            let mut off = prog.0.startoff as usize;
3173            let mut gflags = prog.0.globflags;
3174            let op_at = |o: usize| -> Option<u8> {
3175                if o + I_BODY <= buf.len() {
3176                    Some(buf[o + I_OP])
3177                } else {
3178                    None
3179                }
3180            };
3181            // A sole leading BRANCH (next == 0: no alternative) wraps
3182            // the whole program — step inside it.
3183            if op_at(off)? == P_BRANCH {
3184                let next =
3185                    u32::from_le_bytes(buf[off + I_NEXT..off + I_NEXT + 4].try_into().ok()?);
3186                if next != 0 {
3187                    return None; // real alternation — full matcher
3188                }
3189                off = advance_past_instr(buf, off);
3190            }
3191            if op_at(off)? == P_GFLAGS {
3192                let body = off + I_BODY;
3193                if body + 4 > buf.len() {
3194                    return None;
3195                }
3196                gflags |= i32::from_le_bytes(buf[body..body + 4].try_into().ok()?);
3197                off = advance_past_instr(buf, off);
3198            }
3199            if (gflags & !(GF_IGNCASE | GF_MULTIBYTE)) != 0 {
3200                return None;
3201            }
3202            if op_at(off)? != P_STAR {
3203                return None;
3204            }
3205            off = advance_past_instr(buf, off);
3206            // One or more contiguous EXACTLY chunks form the literal
3207            // (the compiler splits long/space-bearing literals).
3208            if op_at(off)? != P_EXACTLY {
3209                return None;
3210            }
3211            let mut lit = String::new();
3212            while op_at(off)? == P_EXACTLY {
3213                let body = off + I_BODY;
3214                if body + 4 > buf.len() {
3215                    return None;
3216                }
3217                let len = u32::from_le_bytes(buf[body..body + 4].try_into().ok()?) as usize;
3218                if body + 4 + len > buf.len() {
3219                    return None;
3220                }
3221                lit.push_str(std::str::from_utf8(&buf[body + 4..body + 4 + len]).ok()?);
3222                off = advance_past_instr(buf, off);
3223            }
3224            if op_at(off)? != P_STAR {
3225                return None;
3226            }
3227            off = advance_past_instr(buf, off);
3228            if off + I_OP >= buf.len() || buf[off + I_OP] != P_END {
3229                return None;
3230            }
3231            Some((lit, (gflags & GF_IGNCASE) != 0))
3232        })();
3233        if let Some((lit, igncase)) = shape.as_ref().map(|(l, i)| (l.as_str(), *i)) {
3234            if !igncase {
3235                return trial.contains(lit);
3236            }
3237            if lit.is_ascii() && trial.is_ascii() {
3238                let lb = lit.as_bytes();
3239                let tb = trial.as_bytes();
3240                if lb.is_empty() {
3241                    return true;
3242                }
3243                if lb.len() <= tb.len() {
3244                    let l0 = lb[0].to_ascii_lowercase();
3245                    'scan: for s in 0..=(tb.len() - lb.len()) {
3246                        if tb[s].to_ascii_lowercase() != l0 {
3247                            continue;
3248                        }
3249                        for k in 1..lb.len() {
3250                            if tb[s + k].to_ascii_lowercase() != lb[k].to_ascii_lowercase() {
3251                                continue 'scan;
3252                            }
3253                        }
3254                        return true;
3255                    }
3256                }
3257                return false;
3258            }
3259            // Non-ASCII under (#i): general matcher below.
3260        }
3261    }
3262    let mut state = rpat::new();
3263    // c:Src/pattern.c:2334 — `patflags = prog->flags;` C copies the
3264    // prog's flags into the file-static `patflags` so subsequent
3265    // patmatch calls can read PAT_NOTSTART / PAT_NOTEND inside the
3266    // P_ISSTART / P_ISEND arms (c:3393, 3397). Rust mirrors this via
3267    // the `patflags` AtomicI32 at pattern.rs:217.
3268    //
3269    // Without this propagation, `${str:#(#s)PAT}` / `${str:#PAT(#e)}`
3270    // anchor checks fired at slice-local offset 0 / slice-local end
3271    // regardless of whether the caller meant the slice was an
3272    // interior chunk of a larger string (PAT_NOTSTART/PAT_NOTEND
3273    // bits exist on the prog precisely so callers can signal this).
3274    patflags.store(prog.0.flags, Ordering::Relaxed);
3275    // Pass the prog's GLOB flags (GF_* + `(#aN)` budget byte) to the
3276    // matcher. C threads `patglobflags` as a per-thread file-static;
3277    // the Rust port carries it through a fn param. The PAT_* flags
3278    // (PAT_STATIC etc.) stay on `prog.0.flags` for the outer
3279    // anchor/PURES checks at lines below.
3280    // c:Src/pattern.c — `if (prog->flags & PAT_ANY) { ret = 1; } else { ... }`.
3281    // Optimisation for a single "*" (the `**` complist node compiles its
3282    // section as `patcompile(NULL, …|PAT_ANY, …)`): it always matches the
3283    // whole string without running the bytecode. The PAT_NOGLD leading-dot
3284    // rejection below still applies (the "except for no_glob_dots" caveat).
3285    // Without this the empty `PAT_ANY` program was matched literally and
3286    // only matched the empty string, so `**` descended into nothing.
3287    let (mut ok, matched_end) = if (prog.0.flags & PAT_ANY as i32) != 0 {
3288        (true, trial.len())
3289    } else {
3290        match patmatch(&prog.1, 0, trial, 0, &mut state, prog.0.globflags) {
3291            Some(end_pos) => {
3292                // c:2438 — `if (matched && !(prog->flags & (PAT_NOANCH|PAT_NOTEND))) ...`
3293                let no_anchor = (prog.0.flags & (PAT_NOANCH | PAT_NOTEND) as i32) != 0;
3294                (no_anchor || end_pos == trial.len(), end_pos)
3295            }
3296            None => (false, 0),
3297        }
3298    };
3299    // c:2399-2406 — for files (PAT_NOGLD), a successful match is rejected
3300    // when the string starts with '.' (unless glob_dots is set, in which
3301    // case parsecomplist omits PAT_NOGLD). Honoring this here lets the
3302    // glob scanner rely on the matcher for the leading-dot skip, exactly
3303    // as C does, instead of an explicit check in the walker. Inert for
3304    // non-file patterns (matchpat / [[ ]] / :# never set PAT_NOGLD).
3305    if ok
3306        && (prog.0.flags & PAT_NOGLD as i32) != 0
3307        && trial.starts_with('.')
3308        && prog.0.patstartch != b'.'
3309    {
3310        // c:Src/pattern.c — NOGLD rejects a leading-dot file UNLESS the
3311        // pattern itself explicitly begins with a literal `.` (so `.*`
3312        // matches dot files while `*` does not). C bakes this into the
3313        // compiled bytecode; the Rust matcher carries the signal on
3314        // `patstartch` (c:1610) and applies the exception here.
3315        ok = false;
3316    }
3317    if ok {
3318        // c:2508 — `patinlen = patinput - patinstart;` — record the
3319        // byte-length of the successful match so `patmatchlen()` can
3320        // return it after the strings/state are torn down. `end_pos`
3321        // is the in-trial byte offset where patmatch stopped, which
3322        // is `patinput - patinstart` in C terms.
3323        patinlen.store(matched_end as i32, Ordering::Relaxed);
3324    }
3325    if ok {
3326        let n = (prog.0.patnpar as usize).min(NSUBEXP);
3327        let have_nump = nump.is_some();
3328        if let Some(np) = nump {
3329            *np = n as i32;
3330        }
3331        // c:2526-2542 — GF_MATCHREF (`(#m)`): on success, write the
3332        // matched substring to $MATCH and its 1-based (KSHARRAYS-
3333        // aware) char span to $MBEGIN/$MEND. C does this INSIDE
3334        // pattryrefs; the previous Rust port left it to a bridge
3335        // wrapper that sniffed the pattern TEXT for "(#m)" — wrong
3336        // mechanism (missed hoisted/nested flag positions) and
3337        // wrong layer.
3338        // c:2526 — `if ((patglobflags & GF_MATCHREF) && !(patflags & PAT_FILE))`.
3339        // The source is the GLOBAL patglobflags (c:273), not `prog->globflags`.
3340        // They are not the same thing: c:Src/zsh.h:1606 documents the struct
3341        // field as "globbing flags to set at START", so it keeps whatever the
3342        // pattern opened with, while the global is what patgetglobflags leaves
3343        // after walking every flag — including a later `(#M)` turning
3344        // GF_MATCHREF back off (c:1099-1100).
3345        //
3346        // Reading the struct field made `(#M)` inert: for all three of
3347        // `(#m)a*`, `(#m)(#M)a*` and `(#M)(#m)a*` it is 0x1800, whereas the
3348        // global correctly ends at 0x800 / 0x0 / 0x800. So `(#m)(#M)a*` still
3349        // wrote $MATCH where zsh leaves it unset.
3350        let cur_globflags = patglobflags.load(Ordering::Relaxed);
3351        if (cur_globflags & GF_MATCHREF) != 0 && (prog.0.flags & PAT_FILE as i32) == 0 {
3352            let hi = matched_end.min(trial.len());
3353            let mstr: String = trial[..hi].to_string(); // c:2536 metafy(patinstart..patinput)
3354            let mlen = mstr.chars().count() as i64; // c:2534 CHARSUB
3355            let base: i64 = if crate::ported::zsh_h::isset(crate::ported::zsh_h::KSHARRAYS) {
3356                0
3357            } else {
3358                1
3359            };
3360            crate::ported::params::setsparam("MATCH", &mstr); // c:2537
3361            crate::ported::params::setiparam("MBEGIN", patoffset as i64 + base); // c:2538
3362            crate::ported::params::setiparam("MEND", mlen + patoffset as i64 + base - 1);
3363            // c:2539-2541
3364        }
3365        // c:2425+ — emit captured offsets to begp/endp out-arrays.
3366        // Check the CLOSE bit (high stripe at NSUBEXP+i) since the
3367        // group is only fully captured after its P_CLOSE fired; the
3368        // matching open-bit `1 << i` would be set after P_OPEN even
3369        // when the rest of the match later fails to reach P_CLOSE.
3370        //
3371        // c:Src/pattern.c:2556-2558 — `*begp++ = CHARSUB(patinstart, *sp) + patoffset;`.
3372        // `patoffset` is the start position of `string` within the
3373        // ORIGINAL string the caller was matching against (used for
3374        // paramsubst's sliding-window scan via `${var/PAT/REPL}`). Add
3375        // it to every reported beg/end so MBEGIN/MEND / `$match` see
3376        // positions relative to the original string, not the local
3377        // trial slice.
3378        // c:2556-2566 — `*begp++ = …` writes into the caller's FIXED-SIZE array
3379        // (C: `int begpos[MAX_POS]`), filling the first `n` slots and leaving
3380        // the array's length unchanged. Write IN PLACE (extend only if the
3381        // caller's Vec is shorter than `n`) rather than clear()+push — clearing
3382        // shrank a MAX_POS-length array down to `n`, so the caller's
3383        // [n..MAX_POS] reset loop (complist.rs getcol, c:631) indexed past the
3384        // end and panicked (`index 4 on len 4`) on patterns with < MAX_POS
3385        // backrefs.
3386        if let Some(bv) = begp {
3387            for i in 0..n {
3388                let close_bit = 1u32 << (i + NSUBEXP);
3389                let val = if (state.captures_set & close_bit) != 0 {
3390                    state.patbeginp[i] as i32 + patoffset
3391                } else {
3392                    -1 // c:2563-2566 — unset group (unmatched alternation branch)
3393                };
3394                if i < bv.len() {
3395                    bv[i] = val;
3396                } else {
3397                    bv.push(val);
3398                }
3399            }
3400        }
3401        if let Some(ev) = endp {
3402            for i in 0..n {
3403                let close_bit = 1u32 << (i + NSUBEXP);
3404                let val = if (state.captures_set & close_bit) != 0 {
3405                    state.patendp[i] as i32 + patoffset
3406                } else {
3407                    -1 // c:2565
3408                };
3409                if i < ev.len() {
3410                    ev[i] = val;
3411                } else {
3412                    ev.push(val);
3413                }
3414            }
3415        }
3416        // c:2570-2621 — `else if (prog->patnpar && !(patflags &
3417        // PAT_FILE))`: the caller passed NO capture arrays, so
3418        // pattryrefs itself publishes the `(#b)` groups as the
3419        // $match / $mbegin / $mend arrays. This arm was missing
3420        // from the port — every plain pattry of a (#b) pattern
3421        // silently dropped its captures (the bridge compensated
3422        // with a wrapper; now C-faithful at the source).
3423        if !have_nump && n > 0 && (prog.0.flags & PAT_FILE as i32) == 0 {
3424            let base: i32 = if crate::ported::zsh_h::isset(crate::ported::zsh_h::KSHARRAYS) {
3425                0
3426            } else {
3427                1
3428            };
3429            let mut match_arr: Vec<String> = Vec::with_capacity(n);
3430            let mut begin_arr: Vec<String> = Vec::with_capacity(n);
3431            let mut end_arr: Vec<String> = Vec::with_capacity(n);
3432            for i in 0..n {
3433                let close_bit = 1u32 << (i + NSUBEXP);
3434                if (state.captures_set & close_bit) != 0 {
3435                    let b = state.patbeginp[i];
3436                    let e = state.patendp[i];
3437                    let lo = b.min(trial.len());
3438                    let hi = e.min(trial.len()).max(lo);
3439                    match_arr.push(trial[lo..hi].to_string()); // c:2587 metafy(*sp..*ep)
3440                    begin_arr.push((b as i32 + patoffset + base).to_string()); // c:2596-2599
3441                                                                               // c:2601-2604 — mend = last matched char index
3442                                                                               // (inclusive): end + offset + base - 1.
3443                    end_arr.push((e as i32 + patoffset + base - 1).to_string());
3444                } else {
3445                    // c:2607-2613 — unmatched branch / hashed paren.
3446                    match_arr.push(String::new());
3447                    begin_arr.push("-1".to_string());
3448                    end_arr.push("-1".to_string());
3449                }
3450            }
3451            crate::ported::params::setaparam("match", match_arr); // c:2619
3452            crate::ported::params::setaparam("mbegin", begin_arr); // c:2620
3453            crate::ported::params::setaparam("mend", end_arr); // c:2621
3454        }
3455    }
3456    ok
3457}
3458
3459// =====================================================================
3460// 15. Range matching — pattern.c:3856, :4004, :3610, :3767
3461// =====================================================================
3462
3463/// Direct port of `int patmatchlen(void)` from `Src/pattern.c:2649`.
3464///
3465/// ```c
3466/// /**/
3467/// int
3468/// patmatchlen(void)
3469/// {
3470///     return patinlen;
3471/// }
3472/// ```
3473///
3474/// Returns the length in metafied bytes of the last successful
3475/// `pattry` / `pattryrefs` match. `patinlen` is set at
3476/// `pattern.c:2508` (`patinlen = patinput - patinstart`); the
3477/// Rust port sets the equivalent `patinlen` AtomicI32 at the end
3478/// of `pattryrefs` (see `pattern.rs::pattryrefs`).
3479pub fn patmatchlen() -> i32 {
3480    // c:2649-2652
3481    patinlen.load(Ordering::Relaxed) // c:2651 — `return patinlen;`
3482}
3483/// Direct port of `int patmatchindex(char *range, int ind, int *chr, int *mtp)`
3484/// from `Src/pattern.c:4004` (MULTIBYTE_SUPPORT-disabled single-byte
3485/// variant). Walks a NULL-terminated, METAFIED, PP_*-encoded byte
3486/// stream `range` and returns the character (or POSIX-class id)
3487/// at index `ind`. Output:
3488///   - `Some((Some(ch), mtp))` — literal character or PP_RANGE hit;
3489///     `chr = ch`, `mtp = 0`.
3490///   - `Some((None,    mtp))` — POSIX class match (PP_ALPHA etc);
3491///     `chr = -1` in C, `None` in Rust; `mtp = class id`.
3492///   - `None` — `ind` exceeds the descriptor's length.
3493///
3494/// Encoding the byte stream uses:
3495///   * literal byte `< 0x83` = match char as itself
3496///   * `Meta(0x83)` + (byte ^ 0x20) = ordinary metafied character
3497///   * `Meta + PP_*` (0x84..) = POSIX class marker
3498///   * `Meta + PP_RANGE` = next two metafied chars are `lo, hi`
3499pub fn patmatchindex(range: &[u8], mut ind: i32) -> Option<(Option<u8>, i32)> {
3500    // c:4004-4081
3501    let mut chr: Option<u8> = None; // c:4014 — `*chr = -1`
3502    let mut mtp: i32 = 0; // c:4015 — `*mtp = 0`
3503
3504    // C `UNMETA(range)` + `METACHARINC(range)` macro pair from
3505    // Src/zsh.h:1796-1797 — decode-and-advance one metafied
3506    // character. Returned as `(decoded_byte, byte_advance)`.
3507    let unmeta = |bytes: &[u8], i: usize| -> (u8, usize) {
3508        if i < bytes.len() && bytes[i] == Meta && i + 1 < bytes.len() {
3509            (bytes[i + 1] ^ 0x20, 2)
3510        } else if i < bytes.len() {
3511            (bytes[i], 1)
3512        } else {
3513            (0, 0)
3514        }
3515    };
3516
3517    let mut i = 0usize;
3518    while i < range.len() {
3519        // c:4017 — `for (; *range; range++)`
3520        let b = range[i];
3521        if crate::ported::utils::imeta_byte(b) {
3522            // c:4018 — `if (imeta((unsigned char) *range))`
3523            // c:4019 — `int swtype = (unsigned char) *range - (unsigned char) Meta;`
3524            let swtype = (b as i32) - (Meta as i32);
3525            match swtype {
3526                0 => {
3527                    // c:4021-4028 — `case 0: ordinary metafied character`
3528                    // c:4023 — `rchr = (unsigned char) *++range ^ 32;`
3529                    i += 1;
3530                    if i >= range.len() {
3531                        break;
3532                    }
3533                    let rchr = range[i] ^ 0x20;
3534                    if ind == 0 {
3535                        // c:4024-4026 — `if (!ind) { *chr = rchr; return 1; }`
3536                        chr = Some(rchr);
3537                        return Some((chr, mtp));
3538                    }
3539                    // c:4027 — falls through to `if (!ind--) break;`
3540                }
3541                t if (PP_ALPHA..=PP_INVALID).contains(&t) => {
3542                    // c:4030-4051 — POSIX class markers PP_ALPHA..PP_INVALID
3543                    if ind == 0 {
3544                        // c:4052-4054 — `if (!ind) { *mtp = swtype; return 1; }`
3545                        mtp = swtype;
3546                        return Some((None, mtp));
3547                    }
3548                }
3549                t if t == PP_RANGE => {
3550                    // c:4057-4069 — PP_RANGE: next two metafied chars
3551                    // `range++; r1 = UNMETA(range); METACHARINC(range);
3552                    //  r2 = UNMETA(range); if (*range == Meta) range++;`
3553                    i += 1;
3554                    if i >= range.len() {
3555                        break;
3556                    }
3557                    let (r1, adv1) = unmeta(range, i);
3558                    i += adv1;
3559                    if i >= range.len() {
3560                        break;
3561                    }
3562                    let (r2, adv2) = unmeta(range, i);
3563                    i += adv2;
3564                    let rdiff = r2 as i32 - r1 as i32;
3565                    if rdiff >= ind {
3566                        // c:4063-4067 — `if (rdiff >= ind) { *chr = r1 + ind; return 1; }`
3567                        chr = Some((r1 as i32 + ind) as u8);
3568                        return Some((chr, mtp));
3569                    }
3570                    // c:4068 — `ind -= rdiff;` (extra decrement happens via
3571                    // the `ind--` below, accounting for the C comment
3572                    // "note the extra decrement to ind below").
3573                    ind -= rdiff;
3574                    // Skip the trailing `if (!ind--) break;` decrement
3575                    // for this branch since C's loop already does it.
3576                }
3577                _ => {
3578                    // c:4070-4076 — PP_UNKWN / default → DPUTS bug warn
3579                    // (unreachable in well-formed compiled output; bail).
3580                }
3581            }
3582        } else {
3583            // c:4079-4082 — literal char path
3584            if ind == 0 {
3585                // c:4080-4082 — `if (!ind) { *chr = (unsigned char) *range; return 1; }`
3586                chr = Some(b);
3587                return Some((chr, mtp));
3588            }
3589        }
3590        // c:4087 — `if (!ind--) break;` — pre-decrement-check.
3591        if ind == 0 {
3592            break;
3593        }
3594        ind -= 1;
3595        i += 1; // c:4017 — `range++`
3596    }
3597    // c:4091 — `/* No corresponding index. */ return 0;`
3598    None
3599}
3600
3601/// Direct port of `int mb_patmatchrange(char *range, wchar_t ch,
3602/// int zmb_ind, wint_t *indptr, int *mtp)` from `Src/pattern.c:3610`.
3603/// Multibyte variant of `patmatchrange`: walks the metafied,
3604/// PP_*-encoded byte stream `range` and tests whether wide char `ch`
3605/// is in it. `indptr` (when Some) accumulates the running index for
3606/// `mb_patmatchindex`-side lookup; `mtp` (when Some) records which
3607/// PP_* class fired.
3608///
3609/// `zmb_ind` is the `ZMB_*` multibyte-completion state from the
3610/// caller (typically the pattry input pos): ZMB_INCOMPLETE /
3611/// ZMB_INVALID feed the PP_INCOMPLETE / PP_INVALID branches.
3612///
3613/// The Rust port delegates `iswalpha` / `iswdigit` / `wcsitype` to
3614/// `is_alphabetic()` / `is_numeric()` / etc. on the decoded `char`,
3615/// matching the C `iswXXX(wchar_t)` semantics.
3616pub fn mb_patmatchrange(
3617    range: &[u8],
3618    ch: char,
3619    zmb_ind: i32,
3620    mut indptr: Option<&mut u32>,
3621    mut mtp: Option<&mut i32>,
3622) -> bool {
3623    // c:3610-3766
3624    if let Some(p) = indptr.as_deref_mut() {
3625        *p = 0; // c:3615-3616 — `if (indptr) *indptr = 0;`
3626    }
3627    // C `UNMETA(s)` + `METACHARINC(s)` macro pair (Src/zsh.h).
3628    let unmeta = |bytes: &[u8], i: usize| -> (u8, usize) {
3629        if i < bytes.len() && bytes[i] == Meta && i + 1 < bytes.len() {
3630            (bytes[i + 1] ^ 0x20, 2)
3631        } else if i < bytes.len() {
3632            (bytes[i], 1)
3633        } else {
3634            (0, 0)
3635        }
3636    };
3637    let mut i = 0usize;
3638    while i < range.len() {
3639        // c:3623 — `while (*range)`
3640        let b = range[i];
3641        if crate::ported::utils::imeta_byte(b) {
3642            // c:3624 — `if (imeta((unsigned char) *range))`
3643            // c:3625 — `swtype = (unsigned char) *range++ - (unsigned char) Meta;`
3644            let swtype = (b as i32) - (Meta as i32);
3645            i += 1;
3646            if let Some(p) = mtp.as_deref_mut() {
3647                *p = swtype; // c:3626-3627 — `if (mtp) *mtp = swtype;`
3648            }
3649            let class_hit = match swtype {
3650                0 => {
3651                    // c:3629-3634 — `case 0: ordinary metafied character`
3652                    i -= 1; // c:3631 — `range--;`
3653                    let (decoded, adv) = unmeta(range, i);
3654                    i += adv;
3655                    decoded == (ch as u32) as u8 && (ch as u32) < 256
3656                }
3657                t if t == PP_ALPHA => ch.is_alphabetic(),
3658                t if t == PP_ALNUM => ch.is_alphanumeric(),
3659                t if t == PP_ASCII => (ch as u32) < 128,
3660                t if t == PP_BLANK => ch == ' ' || ch == '\t',
3661                t if t == PP_CNTRL => ch.is_control(),
3662                t if t == PP_DIGIT => ch.is_ascii_digit(),
3663                t if t == PP_GRAPH => !ch.is_whitespace() && !ch.is_control(),
3664                t if t == PP_LOWER => ch.is_lowercase(),
3665                t if t == PP_PRINT => !ch.is_control(),
3666                t if t == PP_PUNCT => ch.is_ascii_punctuation(),
3667                t if t == PP_SPACE => ch.is_whitespace(),
3668                t if t == PP_UPPER => ch.is_uppercase(),
3669                t if t == PP_XDIGIT => ch.is_ascii_hexdigit(),
3670                t if t == PP_IDENT => {
3671                    // c:3704 — `PP_IDENT: if (wcsitype(ch, IIDENT)) return 1;`
3672                    // IIDENT = alphanumeric, underscore, or "$" depending on
3673                    // option state. Conservative: ASCII identifier chars.
3674                    ch.is_alphanumeric() || ch == '_'
3675                }
3676                t if t == PP_IFS => {
3677                    // c:3708 — `wcsitype(ch, ISEP)`. ISEP = space, tab,
3678                    // newline + user IFS additions. Default IFS in zsh
3679                    // is ` \t\n`.
3680                    ch == ' ' || ch == '\t' || ch == '\n' || ch == '\0'
3681                }
3682                t if t == PP_IFSSPACE => {
3683                    // c:3712-3713 — ASCII-only IFS-space.
3684                    (ch as u32) < 128 && (ch == ' ' || ch == '\t' || ch == '\n')
3685                }
3686                t if t == PP_WORD => {
3687                    // c:3716 — `wcsitype(ch, IWORD)`. IWORD = WORDCHARS-derived;
3688                    // conservative ASCII alphanumeric + `_`.
3689                    ch.is_alphanumeric() || ch == '_'
3690                }
3691                t if t == PP_RANGE => {
3692                    // c:3719-3735 — PP_RANGE: two metafied wide chars are
3693                    // `r1` and `r2`; matches if `r1 <= ch <= r2`.
3694                    let (r1_byte, adv1) = unmeta(range, i);
3695                    i += adv1;
3696                    let (r2_byte, adv2) = unmeta(range, i);
3697                    i += adv2;
3698                    let r1 = r1_byte as u32;
3699                    let r2 = r2_byte as u32;
3700                    let chu = ch as u32;
3701                    if r1 <= chu && chu <= r2 {
3702                        if let Some(p) = indptr.as_deref_mut() {
3703                            *p += chu - r1; // c:3725-3726
3704                        }
3705                        return true; // c:3727
3706                    }
3707                    if let Some(p) = indptr.as_deref_mut() {
3708                        if r1 < r2 {
3709                            *p += r2 - r1; // c:3733-3734
3710                        }
3711                    }
3712                    false
3713                }
3714                t if t == PP_INCOMPLETE => {
3715                    // c:3740 — `if (zmb_ind == ZMB_INCOMPLETE) return 1;`
3716                    zmb_ind == ZMB_INCOMPLETE
3717                }
3718                t if t == PP_INVALID => {
3719                    // c:3744 — `if (zmb_ind == ZMB_INVALID) return 1;`
3720                    zmb_ind == ZMB_INVALID
3721                }
3722                _ => {
3723                    // c:3747-3753 — PP_UNKWN / default → DPUTS bug warn
3724                    false
3725                }
3726            };
3727            if class_hit {
3728                return true;
3729            }
3730        } else {
3731            // c:3757-3762 — literal-char path
3732            let (decoded, adv) = unmeta(range, i);
3733            i += adv;
3734            if decoded == (ch as u32) as u8 && (ch as u32) < 256 {
3735                if let Some(p) = mtp.as_deref_mut() {
3736                    *p = 0; // c:3760
3737                }
3738                return true; // c:3761
3739            }
3740        }
3741        if let Some(p) = indptr.as_deref_mut() {
3742            *p += 1; // c:3764-3765 — `if (indptr) (*indptr)++;`
3743        }
3744    }
3745    false // c:3766 — `return 0;`
3746}
3747
3748/// Direct port of `int mb_patmatchindex(char *range, wint_t ind,
3749/// wint_t *chr, int *mtp)` from `Src/pattern.c:3767`. The reverse
3750/// of `mb_patmatchrange`: given a metafied byte range and an index
3751/// `ind` into it, return the character (or PP_* class) at that
3752/// position.
3753///
3754/// Returns:
3755///   - `Some((Some(ch),  0))` — literal or PP_RANGE hit (chr = ch).
3756///   - `Some((None,     mtp))` — POSIX class match; chr = WEOF / None.
3757///   - `None` — index out of range.
3758pub fn mb_patmatchindex(range: &[u8], mut ind: u32) -> Option<(Option<char>, i32)> {
3759    // c:3767-3849
3760    let mut chr: Option<char> = None; // c:3776 — `*chr = WEOF`
3761    let mut mtp: i32 = 0; // c:3777 — `*mtp = 0`
3762
3763    // C `UNMETA(s)` + `METACHARINC(s)` macro pair (Src/zsh.h).
3764    let unmeta = |bytes: &[u8], i: usize| -> (u8, usize) {
3765        if i < bytes.len() && bytes[i] == Meta && i + 1 < bytes.len() {
3766            (bytes[i + 1] ^ 0x20, 2)
3767        } else if i < bytes.len() {
3768            (bytes[i], 1)
3769        } else {
3770            (0, 0)
3771        }
3772    };
3773
3774    let mut i = 0usize;
3775    while i < range.len() {
3776        // c:3779 — `while (*range)`
3777        let b = range[i];
3778        if crate::ported::utils::imeta_byte(b) {
3779            // c:3780 — `if (imeta((unsigned char) *range))`
3780            let swtype = (b as i32) - (Meta as i32);
3781            i += 1;
3782            match swtype {
3783                0 => {
3784                    // c:3782-3789 — `case 0: ordinary metafied char`
3785                    i -= 1;
3786                    let (decoded, adv) = unmeta(range, i);
3787                    i += adv;
3788                    let rchr = decoded as char;
3789                    if ind == 0 {
3790                        chr = Some(rchr);
3791                        return Some((chr, mtp));
3792                    }
3793                }
3794                t if (PP_ALPHA..=PP_INVALID).contains(&t) => {
3795                    // c:3791-3812 — POSIX class markers
3796                    if ind == 0 {
3797                        mtp = swtype;
3798                        return Some((None, mtp));
3799                    }
3800                }
3801                t if t == PP_RANGE => {
3802                    // c:3814-3829 — PP_RANGE: two metafied wide chars
3803                    let (r1_byte, adv1) = unmeta(range, i);
3804                    i += adv1;
3805                    let (r2_byte, adv2) = unmeta(range, i);
3806                    i += adv2;
3807                    let r1 = r1_byte as u32;
3808                    let r2 = r2_byte as u32;
3809                    let rdiff = r2.saturating_sub(r1);
3810                    if rdiff >= ind {
3811                        chr = char::from_u32(r1 + ind);
3812                        return Some((chr, mtp));
3813                    }
3814                    ind = ind.saturating_sub(rdiff);
3815                }
3816                _ => {
3817                    // c:3831-3837 — PP_UNKWN / default → DPUTS
3818                }
3819            }
3820        } else {
3821            // c:3840-3843 — literal byte path
3822            if ind == 0 {
3823                chr = Some(b as char);
3824                return Some((chr, mtp));
3825            }
3826        }
3827        // c:3846 — `if (!ind--) break;`
3828        if ind == 0 {
3829            break;
3830        }
3831        ind -= 1;
3832        i += 1;
3833    }
3834    None // c:3849 — `return 0`
3835}
3836
3837/// Port of `freepatprog(Patprog prog)` from `Src/pattern.c:4161`. Frees a Patprog.
3838/// Rust's `Drop` on `Box<patprog>` handles this; the explicit fn
3839/// exists for C parity (Rule A).
3840#[allow(unused_variables)]
3841pub fn freepatprog(prog: Patprog) {} // c:4161
3842
3843/// Port of `int pat_enables(const char *cmd, char **patp, int enable)`
3844/// from `Src/pattern.c:4171`. Implements `enable -p`/`disable -p`: with
3845/// an empty `patp`, prints the currently enabled (or disabled, if
3846/// `!enable`) tokens by walking `zpc_strings[]`/`zpc_disables[]` in
3847/// lockstep. Otherwise toggles each named token's `zpc_disables[i]`
3848/// slot, emitting `invalid pattern: NAME` for misses.
3849pub fn pat_enables(cmd: &str, patp: &[&str], enable: bool) -> i32 {
3850    // c:4171
3851    let mut ret: i32 = 0; // c:4173
3852    if patp.is_empty() {
3853        // c:4177 !*patp
3854        let strings = ZPC_STRINGS; // c:4179 zpc_strings
3855        let disp = zpc_disables.lock().unwrap(); // c:4179 zpc_disables
3856        let mut done = false; // c:4178
3857        let mut out: String = String::new();
3858        for i in 0..(ZPC_COUNT as usize) {
3859            // c:4180
3860            let sp = match strings[i] {
3861                // c:4182 !*stringp
3862                Some(s) => s,
3863                None => continue,
3864            };
3865            let is_disabled = disp[i] != 0;
3866            if enable == is_disabled {
3867                // c:4184 enable?*disp:!*disp
3868                continue;
3869            }
3870            if done {
3871                // c:4186
3872                out.push(' '); // c:4187
3873            }
3874            out.push_str(&format!("'{}'", sp)); // c:4188
3875            done = true; // c:4189
3876        }
3877        if done {
3878            // c:4191
3879            println!("{}", out); // c:4187-4192
3880        }
3881        return 0; // c:4193
3882    }
3883    for p in patp {
3884        // c:4196
3885        let strings = ZPC_STRINGS;
3886        let mut disp = zpc_disables.lock().unwrap();
3887        let mut matched = false;
3888        for i in 0..(ZPC_COUNT as usize) {
3889            // c:4197
3890            if let Some(s) = strings[i] {
3891                if s == *p {
3892                    // c:4200 !strcmp
3893                    disp[i] = if enable { 0u8 } else { 1u8 }; // c:4201 *disp = !enable
3894                    matched = true;
3895                    break; // c:4202
3896                }
3897            }
3898        }
3899        if !matched {
3900            // c:4205
3901            zerrnam(cmd, &format!("invalid pattern: {}", p)); // c:4206
3902            ret = 1; // c:4207
3903        }
3904    }
3905    ret // c:4211
3906}
3907
3908/// Port of `mod_export const char *zpc_strings[ZPC_COUNT]` from
3909/// `Src/pattern.c:258`. Static token-name table indexed by ZPC_*;
3910/// NULL entries (ZPC_NULL, ZPC_BNULLKEEP, ZPC_INPAR_PIPE,
3911/// ZPC_KSHCHAR) have no user-visible name.
3912pub const ZPC_STRINGS: [Option<&'static str>; ZPC_COUNT as usize] = [
3913    // c:258
3914    None,
3915    None,
3916    Some("|"),
3917    None,
3918    Some("~"),
3919    Some("("),
3920    Some("?"),
3921    Some("*"),
3922    Some("["),
3923    Some("<"),
3924    Some("^"),
3925    Some("#"),
3926    None,
3927    Some("?("),
3928    Some("*("),
3929    Some("+("),
3930    Some("!("),
3931    Some("\\!("),
3932    Some("@("),
3933];
3934
3935/// Port of `unsigned int savepatterndisables(void)` from
3936/// `Src/pattern.c:4220`.
3937///
3938/// C body (c:4220-4233):
3939/// ```c
3940/// unsigned int disables, bit;
3941/// char *disp;
3942/// disables = 0;
3943/// for (bit = 1, disp = zpc_disables;
3944///      disp < zpc_disables + ZPC_COUNT;
3945///      bit <<= 1, disp++) {
3946///     if (*disp) disables |= bit;
3947/// }
3948/// return disables;
3949/// ```
3950///
3951/// Encodes the current `zpc_disables\[ZPC_COUNT\]` byte-array as a u32
3952/// bitmask (one bit per slot, low bit = `zpc_disables\[0\]`).
3953/// The previous Rust port returned a `Vec<String>` clone of
3954/// `patterndisables` (a completely different data structure — names
3955/// list, not the per-token byte array). `restorepatterndisables(u32)`
3956/// at c:4258 reads this bitmask back into `zpc_disables`, so the
3957/// returned shape MUST be the u32 bitmask.
3958pub fn savepatterndisables() -> u32 {
3959    // c:4220
3960    let disp = zpc_disables.lock().unwrap(); // c:4225 disp = zpc_disables
3961    let mut disables: u32 = 0; // c:4224
3962    let mut bit: u32 = 1; // c:4226 bit = 1
3963    for i in 0..(ZPC_COUNT as usize) {
3964        // c:4226-4228
3965        if disp[i] != 0 {
3966            // c:4230
3967            disables |= bit; // c:4231
3968        }
3969        bit <<= 1; // c:4226 bit <<= 1
3970    }
3971    disables // c:4232
3972}
3973
3974/// Port of `void startpatternscope(void)` from `Src/pattern.c:4241`.
3975/// Pushes a frame onto `PATSCOPE_STACK` (`zpc_disables_stack` in C)
3976/// carrying the current `zpc_disables[]` state as a `savepatterndisables()`
3977/// u32 bitmap. Called at function entry; `endpatternscope` pops it.
3978///
3979/// ```c
3980/// void startpatternscope(void) {
3981///     Zpc_disables_save newdis = zalloc(sizeof(*newdis));
3982///     newdis->next = zpc_disables_stack;
3983///     newdis->disables = savepatterndisables();  // c:4247
3984///     zpc_disables_stack = newdis;
3985/// }
3986/// ```
3987pub fn startpatternscope() {
3988    // c:4241
3989    let saved = savepatterndisables(); // c:4247
3990    PATSCOPE_STACK.with(|s| s.borrow_mut().push(saved));
3991}
3992
3993/// Port of `void restorepatterndisables(unsigned int disables)` from
3994/// `Src/pattern.c:4258`. Walks the 12-slot `zpc_disables[]` array,
3995/// setting each slot's byte from the bitmask: `disables & (1<<i)`
3996/// → slot `i` gets 1, else 0.
3997/// ```c
3998/// void
3999/// restorepatterndisables(unsigned int disables)
4000/// {
4001///     char *disp;
4002///     unsigned int bit;
4003///     for (bit = 1, disp = zpc_disables;
4004///          disp < zpc_disables + ZPC_COUNT;
4005///          bit <<= 1, disp++) {
4006///         if (disables & bit) *disp = 1;
4007///         else *disp = 0;
4008///     }
4009/// }
4010/// ```
4011pub fn restorepatterndisables(disables: u32) {
4012    // c:4258
4013    let mut disp = zpc_disables.lock().unwrap(); // c:4263
4014    let mut bit: u32 = 1;
4015    for i in 0..(ZPC_COUNT as usize) {
4016        // c:4263-4265
4017        if (disables & bit) != 0 {
4018            // c:4266
4019            disp[i] = 1; // c:4267
4020        } else {
4021            disp[i] = 0; // c:4269
4022        }
4023        bit <<= 1;
4024    }
4025}
4026
4027/// Port of `void endpatternscope(void)` from `Src/pattern.c:4279`.
4028/// Pops the saved bitmap from `PATSCOPE_STACK` (`zpc_disables_stack`
4029/// in C); restores `zpc_disables[]` from the bitmap ONLY when
4030/// `isset(LOCALPATTERNS)` per C c:4286. Called at function exit.
4031///
4032/// ```c
4033/// void endpatternscope(void) {
4034///     Zpc_disables_save olddis = zpc_disables_stack;
4035///     zpc_disables_stack = olddis->next;
4036///     if (isset(LOCALPATTERNS))
4037///         restorepatterndisables(olddis->disables);     // c:4287
4038///     zfree(olddis, sizeof(*olddis));
4039/// }
4040/// ```
4041pub fn endpatternscope() {
4042    // c:4279
4043    if let Some(prev) = PATSCOPE_STACK.with(|s| s.borrow_mut().pop()) {
4044        if isset(crate::ported::zsh_h::LOCALPATTERNS) {
4045            // c:4286-4287
4046            restorepatterndisables(prev);
4047        }
4048    }
4049}
4050
4051/// Port of `void clearpatterndisables(void)` from `Src/pattern.c:4296`.
4052/// C body: `memset(zpc_disables, 0, ZPC_COUNT)` — zero every slot.
4053pub fn clearpatterndisables() {
4054    // c:4296
4055    *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize]; // c:4298
4056}
4057
4058/// Port of `haswilds(char *str)` from `Src/pattern.c:4306`.
4059///
4060/// Check whether `str` is eligible for filename generation.
4061///
4062/// C scans a TOKENIZED string: every input reaching it has been
4063/// through the lexer or `tokenize()`/`shtokenize()` (glob.c:3548/
4064/// 3563), so glob metachars are token codes and escaped/quoted
4065/// chars are Bnull'd literals. Callers holding un-tokenized strings
4066/// must `tokenize()` a copy first — exactly what C does for
4067/// runtime-built strings (compcore.c:2231 tokenizes fignore entries
4068/// immediately before its c:2235 haswilds call). The switch matches
4069/// ONLY token codes, never literal ASCII metachars.
4070///
4071/// The walk is over chars: zshrs strings hold token chars as
4072/// codepoints (Star = U+0087) and the input layer is char-domain
4073/// (input.rs `shingetline`/`ingetc`), so the char is the unit that
4074/// the metafied byte is in C. Scanning bytes here false-positived
4075/// on UTF-8 continuation bytes of plain text (`↔` = E2 86 94
4076/// carries 0x86/0x94 = Hat/Inang as u8) — bug #627.
4077///
4078/// The C source's `%?foo` job-ref special case (c:4317-4318), which
4079/// mutates `str[1]` in place to demote the `?`, becomes a "skip
4080/// position 1" scan adjustment here since `&str` is immutable.
4081pub fn haswilds(str: &str) -> bool {
4082    // c:4306
4083    let chars: Vec<char> = str.chars().collect();
4084    let len = chars.len();
4085    if len == 0 {
4086        return false;
4087    }
4088
4089    // c:4309 — `[' and `]' are legal even if bad patterns are usually not.
4090    if len == 1 && (chars[0] == Inbrack || chars[0] == Outbrack) {
4091        return false; // c:4311
4092    }
4093
4094    // c:4313-4315 — If % is immediately followed by ?, then that ? is
4095    // not treated as a wildcard.  This is so you don't have
4096    // to escape job references such as %?foo.
4097    let skip_pos_1 = len >= 2 && chars[0] == '%' && chars[1] == Quest; // c:4316-4317
4098
4099    // c:4319-4321 — Note that at this point zpc_special has not been set up.
4100    let disp = zpc_disables.lock().unwrap(); // c:read zpc_disables[]
4101
4102    for i in 0..len {
4103        // c:4323 for (; *str; str++)
4104        if skip_pos_1 && i == 1 {
4105            continue; // c:4317 `str[1] = '?'` demote
4106        }
4107        let c = chars[i]; // c:4324 switch (*str)
4108        let prev: char = if i > 0 { chars[i - 1] } else { '\0' }; // c: str[-1]
4109
4110        if c == Inpar {
4111            // c:4325-4335
4112            if (!isset(SHGLOB) && disp[ZPC_INPAR as usize] == 0)
4113                || (i > 0
4114                    && isset(KSHGLOB)
4115                    && ((prev == Quest && disp[ZPC_KSH_QUEST as usize] == 0)
4116                        || (prev == Star && disp[ZPC_KSH_STAR as usize] == 0)
4117                        || (prev == '+' && disp[ZPC_KSH_PLUS as usize] == 0)
4118                        || (prev == Bang && disp[ZPC_KSH_BANG as usize] == 0)
4119                        || (prev == '!' && disp[ZPC_KSH_BANG2 as usize] == 0)
4120                        || (prev == '@' && disp[ZPC_KSH_AT as usize] == 0)))
4121            {
4122                return true; // c:4335
4123            }
4124        } else if c == Bar {
4125            if disp[ZPC_BAR as usize] == 0 {
4126                return true; // c:4340
4127            }
4128        } else if c == Star {
4129            if disp[ZPC_STAR as usize] == 0 {
4130                return true; // c:4345
4131            }
4132        } else if c == Inbrack {
4133            if disp[ZPC_INBRACK as usize] == 0 {
4134                return true; // c:4350
4135            }
4136        } else if c == Inang {
4137            if disp[ZPC_INANG as usize] == 0 {
4138                return true; // c:4355
4139            }
4140        } else if c == Quest {
4141            if disp[ZPC_QUEST as usize] == 0 {
4142                return true; // c:4360
4143            }
4144        } else if c == Pound {
4145            if isset(EXTENDEDGLOB) && disp[ZPC_HASH as usize] == 0 {
4146                return true; // c:4365
4147            }
4148        } else if c == Hat {
4149            if isset(EXTENDEDGLOB) && disp[ZPC_HAT as usize] == 0 {
4150                return true; // c:4370
4151            }
4152        }
4153    }
4154    false // c:4374
4155}
4156
4157// =====================================================================
4158// 4. struct patprog — zsh.h:1601
4159// =====================================================================
4160
4161/// `typedef struct patprog *Patprog;` from `zsh.h:542`.
4162#[allow(non_camel_case_types)]
4163/// `typedef struct patprog *Patprog;` from `zsh.h:542`.
4164///
4165/// C zsh allocates the `struct patprog` header + bytecode as one
4166/// contiguous `malloc` block, accessing bytecode via
4167/// `(char *)prog + prog->startoff`. Rust has no flexible array
4168/// members, so this typedef pairs the C-exact `patprog` header
4169/// (zsh_h.rs:768) with an owned bytecode `Vec<u8>` — header at
4170/// `.0`, bytecode at `.1`. `startoff`/`size` index into `.1`.
4171pub type Patprog = Box<(patprog, Vec<u8>)>;
4172// =====================================================================
4173// Bytecode field offsets within each instruction.
4174//
4175// Instruction layout in `patout`:
4176//     +0       u8     opcode
4177//     +1..+5   u32 LE next_off (offset of next instr in chain, 0 = end)
4178//     +5..     u8...  opcode-specific payload
4179//
4180// C uses a different layout (Upat union = 4-byte or 8-byte slots);
4181// the Rust port pins the layout to byte offsets for portability.
4182// =====================================================================
4183
4184const I_OP: usize = 0; // opcode byte
4185
4186impl rpat {
4187    pub fn new() -> Self {
4188        Self {
4189            wbranch_visits: std::collections::HashMap::new(),
4190            patbeginp: [usize::MAX; NSUBEXP],
4191            patendp: [0; NSUBEXP],
4192            captures_set: 0,
4193            errsfound: 0, // c:2066 — errsfound = 0 at pattry init
4194        }
4195    }
4196}
4197const I_NEXT: usize = 1; // u32 next-offset starts here
4198const I_BODY: usize = 5; // payload starts here
4199
4200/// Serialises every entry into `patcompile`. The C source at
4201/// `Src/pattern.c:267-281` declares `patout`, `patparse`, `patstart`,
4202/// `patnpar`, `patflags`, `patglobflags`, `errsfound`, `forceerrs`,
4203/// `zpc_special`, `patstrcache` as file-scope statics that the compile
4204/// mutates in sequence; zsh-the-program is single-threaded so the C
4205/// source is safe under that invariant. zshrs callers (zutil's
4206/// `style_table::get` via `crate::ported::pattern::patmatch`, params.rs,
4207/// subst.rs, options.rs) can invoke `patcompile` from concurrent test
4208/// threads, so the lock restores the single-writer invariant. Held
4209/// only for the compile phase; the matcher (`pattry`/`patmatch`)
4210/// operates on the returned `Patprog.code` and touches no globals.
4211static PATCOMPILE_LOCK: Mutex<()> = Mutex::new(());
4212
4213// C: `static char *patparse, *patstart;` — pattern parsing cursors
4214// into the *input* pattern string. patstart points to start of
4215// pattern; patparse moves forward as we consume tokens.
4216pub static patparse: Mutex<String> = Mutex::new(String::new()); // c:269
4217pub static patstart: Mutex<String> = Mutex::new(String::new()); // c:269
4218
4219/// Position within `patparse` we're currently looking at. C source
4220/// uses a `char *` cursor; Rust uses byte offset into the String.
4221pub static patparse_off: AtomicUsize = AtomicUsize::new(0);
4222
4223// C: `static int errsfound;` — approximate-match error count.
4224pub static errsfound: AtomicI32 = AtomicI32::new(0); // c:274
4225
4226// C: `static int forceerrs;` — required error count for approximate match.
4227pub static forceerrs: AtomicI32 = AtomicI32::new(-1); // c:275
4228
4229// C: `static long patglobflags_orig;` — saved at branch entry.
4230pub static patglobflags_orig: AtomicI32 = AtomicI32::new(0); // c:276
4231
4232// C: `static const char *zpc_special;` — table of currently-special
4233// characters during compile (indexed by ZPC_*).
4234//
4235// pattern.c uses `static char zpc_special[ZPC_COUNT];` and resets it
4236// in patcompcharsset(). Rust mirrors as a Mutex-wrapped byte array.
4237pub static zpc_special: Mutex<[u8; ZPC_COUNT as usize]> = Mutex::new([0u8; ZPC_COUNT as usize]); // c:278
4238
4239// C: `static char *patstrcache;` — caches the unmetafied trial string.
4240// Rust port has no Meta encoding so the cache is unnecessary; we leave
4241// the static declared for parity (Rule A — name exists in C).
4242pub static patstrcache: Mutex<String> = Mutex::new(String::new()); // c:281
4243
4244/// `Marker` constant — alias for `crate::ported::zsh_h::Marker`
4245/// (`Src/zsh.h:224` `#define Marker ((char) 0xa2)`).
4246///
4247/// The previous Rust port had this as `pub const Marker: u8 = 0x80`
4248/// which is WRONG — `\200` (0x80) is NOT the canonical Marker byte.
4249/// C's Marker is 0xa2 per `Src/zsh.h:224`. No in-tree callers used
4250
4251/// Port of `static const char *colon_stuffs[]` from `Src/pattern.c:1134-1138`.
4252/// 19 entries matching C's table in declaration order, so `range_type(name)`
4253/// returns the same `(index + PP_FIRST)` value C does:
4254/// alpha=1, alnum=2, ascii=3, blank=4, cntrl=5, digit=6, graph=7, lower=8,
4255/// print=9, punct=10, space=11, upper=12, xdigit=13, IDENT=14, IFS=15,
4256/// IFSSPACE=16, WORD=17, INCOMPLETE=18, INVALID=19. Prior Rust port had
4257/// only 12 lowercase entries and was MISSING `ascii` between `alnum` and
4258/// `blank`, so `range_type("digit")` returned 5 instead of C's PP_DIGIT=6
4259/// and every `[:class:]` byte marker emitted by `complete.rs:733`
4260/// (`0x80 + ch`) was off-by-one for classes after `alnum`. Real port bug.
4261const POSIX_CLASS_NAMES: &[&str] = &[
4262    "alpha",
4263    "alnum",
4264    "ascii",
4265    "blank",
4266    "cntrl",
4267    "digit",
4268    "graph",
4269    "lower",
4270    "print",
4271    "punct",
4272    "space",
4273    "upper",
4274    "xdigit",
4275    "IDENT",
4276    "IFS",
4277    "IFSSPACE",
4278    "WORD",
4279    "INCOMPLETE",
4280    "INVALID",
4281];
4282
4283/// Port of file-static `zpc_disables_stack` from `Src/pattern.c:4244`.
4284/// Per-evaluator function-scope disable save-stack (bucket-1: each
4285/// worker thread parses/executes its own function calls, so each must
4286/// have its own scope stack). Reason for `thread_local!` over `Mutex`:
4287/// in zsh C this is a per-process file-static; in zshrs each worker
4288/// thread is its own evaluator — TLS preserves the per-evaluator
4289/// semantic without serializing across workers.
4290///
4291/// Element type: u32 bitmap matching `savepatterndisables` return
4292/// shape (c:4220 `unsigned int disables`). Prior Rust port used
4293/// `Vec<Vec<String>>` and `startpatternscope` cloned a `Vec<String>`
4294/// of names instead of the bitmap — a real port bug that made
4295/// `setopt LOCALPATTERNS` function entry/exit silently fail to save/
4296/// restore the disable state.
4297thread_local! {
4298    static PATSCOPE_STACK: std::cell::RefCell<Vec<u32>> =
4299        const { std::cell::RefCell::new(Vec::new()) };
4300}
4301
4302// =====================================================================
4303// 17. Module-loader / disable mgmt — pattern.c:4161-4296
4304// =====================================================================
4305
4306// `pub static patterndisables: Mutex<Vec<String>>` deleted — was a
4307// dead tombstone with no callers outside the buggy
4308// `startpatternscope` / `endpatternscope` / `clearpatterndisables`
4309// fns (which cloned/cleared it instead of operating on the real
4310// `zpc_disables` byte array). The canonical zsh `zpc_disables[ZPC_COUNT]`
4311// (Src/pattern.c:268) is the disable state; the names list does not
4312// exist as a separate data structure in C.
4313
4314/// Port of `char zpc_disables[ZPC_COUNT]` from `Src/pattern.c:268`.
4315/// Per-token disable byte — when `zpc_disables[i]` is non-zero, the
4316/// pattern token at index `i` (ZPC_*) is treated as a literal,
4317/// not its meta-meaning.
4318pub static zpc_disables: Mutex<[u8; ZPC_COUNT as usize]> = Mutex::new([0u8; ZPC_COUNT as usize]); // c:268
4319
4320/// Helper: when patinsert shifts a chunk of bytecode, any 4-byte
4321/// next_off slot that previously pointed past `opnd` must be bumped
4322/// by `delta` to keep the chain links valid.
4323///
4324/// Walks the buffer linearly opcode-by-opcode reading I_NEXT slots.
4325/// Conservatively adjusts every nonzero next that lands past opnd.
4326fn fixup_offsets_after_insert(buf: &mut [u8], opnd: usize, delta: u32) {
4327    let mut i = 0;
4328    while i + I_BODY <= buf.len() {
4329        let op = buf[i + I_OP];
4330        if op == 0 {
4331            i += 1;
4332            continue;
4333        } // sentinel byte, skip
4334        let next_bytes = &buf[i + I_NEXT..i + I_NEXT + 4];
4335        let cur = u32::from_le_bytes(next_bytes.try_into().unwrap());
4336        if cur != 0 {
4337            let abs = cur as usize;
4338            if abs >= opnd && abs <= buf.len() {
4339                let new = cur + delta;
4340                buf[i + I_NEXT..i + I_NEXT + 4].copy_from_slice(&new.to_le_bytes());
4341            }
4342        }
4343        i = advance_past_instr(buf, i);
4344        if i == 0 {
4345            break;
4346        }
4347    }
4348}
4349
4350/// Helper: given a buffer and current opcode offset, return the
4351/// offset of the next opcode after this one's payload.
4352///
4353/// Encodes the per-opcode payload size table — must stay in sync
4354/// with patnode/patinsert calls in the compiler.
4355fn advance_past_instr(buf: &[u8], pos: usize) -> usize {
4356    if pos + I_BODY > buf.len() {
4357        return 0;
4358    }
4359    let op = buf[pos + I_OP];
4360    let body_start = pos + I_BODY;
4361    match op {
4362        P_END | P_NOTHING | P_BACK | P_EXCSYNC | P_EXCEND | P_ISSTART | P_ISEND | P_COUNTSTART
4363        | P_ANY | P_STAR | P_NUMANY => body_start,
4364        P_GFLAGS => body_start + 4, // i32 flag-bits payload
4365        P_EXACTLY => {
4366            // payload: u32 len + len bytes
4367            if body_start + 4 > buf.len() {
4368                return 0;
4369            }
4370            let len =
4371                u32::from_le_bytes(buf[body_start..body_start + 4].try_into().unwrap()) as usize;
4372            body_start + 4 + len
4373        }
4374        P_ANYOF | P_ANYBUT => {
4375            if body_start + 4 > buf.len() {
4376                return 0;
4377            }
4378            let len =
4379                u32::from_le_bytes(buf[body_start..body_start + 4].try_into().unwrap()) as usize;
4380            body_start + 4 + len
4381        }
4382        P_ONEHASH | P_TWOHASH | P_BRANCH => body_start,
4383        // c:113 P_WBRANCH and c:114-115 P_EXCLUDE/P_EXCLUDP carry an
4384        // 8-byte syncptr payload (one Upat slot in C) right after
4385        // their header, before the chained operand.
4386        P_WBRANCH | P_EXCLUDE | P_EXCLUDP => body_start + 8,
4387        P_OPEN..=0x88 | P_CLOSE..=0x98 => body_start,
4388        P_NUMRNG => body_start + 16, // two i64
4389        P_NUMFROM | P_NUMTO => body_start + 8,
4390        P_COUNT => body_start + 16, // min i64 + max i64; operand inline follows
4391        _ => body_start,
4392    }
4393}
4394
4395/// Helper: directly set the `next_off` slot of the instruction at
4396/// `pos` without walking the chain. C uses pointer arithmetic
4397/// (`scanp->l = ...`) inline; Rust factors it for byte-offset
4398/// bookkeeping. Architectural helper.
4399fn set_next(pos: usize, val: usize) {
4400    let mut buf = patout.lock().unwrap();
4401    if pos + I_NEXT + 4 <= buf.len() {
4402        buf[pos + I_NEXT..pos + I_NEXT + 4].copy_from_slice(&(val as u32).to_le_bytes());
4403    }
4404}
4405
4406/// Helper: walk every branch's operand chain and patch each branch's
4407/// last-operand-node `.next` to `target`. Used to chain a fully-
4408/// compiled alternation switch to whatever opcode follows (P_END for
4409/// the outermost compile, P_CLOSE_N for a sub-group).
4410///
4411/// Architectural helper — C uses pattail inside the BRANCH operand
4412/// scope via Upat pointer arithmetic; Rust factors it for clarity.
4413fn chain_branches_to(starter: usize, target: usize) {
4414    let mut cur = starter;
4415    loop {
4416        // Operand starts at cur + I_BODY (the byte right after this
4417        // branch's header). Walk operand's next-chain to its end
4418        // and set its .next = target.
4419        pattail(cur + I_BODY, target);
4420        // Move to next alternative.
4421        let buf = patout.lock().unwrap();
4422        if cur + I_NEXT + 4 > buf.len() {
4423            break;
4424        }
4425        let nb: [u8; 4] = buf[cur + I_NEXT..cur + I_NEXT + 4].try_into().unwrap();
4426        let n = u32::from_le_bytes(nb) as usize;
4427        drop(buf);
4428        if n == 0 {
4429            break;
4430        }
4431        cur = n;
4432    }
4433}
4434
4435/// Port of `patmatch(Upat prog)` from `Src/pattern.c:2694`. The interpreter.
4436///
4437/// Returns `Some(end_pos)` on successful match (end_pos = byte offset
4438/// in `string` where match ended), `None` on no-match. The state
4439/// param tracks captures.
4440///
4441/// Rust signature differs from C's `int patmatch(Upat prog)`: input
4442/// bytecode, current input position, captures, and glob flags are
4443/// threaded through args rather than C's per-thread file-statics
4444/// (`patinput`, `patinstart`, `patbeginp`/`patendp`, `patglobflags`).
4445/// Rule S1 deviation justified by zshrs's threading model — see
4446/// PORT_PLAN.md Bucket 1.
4447///
4448/// WARNING: NOT IN PATTERN.C — Rust-only helper. Approximate-match
4449/// inner loop for `(#aN)`-flagged P_EXACTLY arms. C interleaves the
4450/// edit-operation backtracking inline within the P_EXACTLY case
4451/// (c:2737-2779); the Rust port factors the inner per-byte walk +
4452/// recursive sub/ins/del trials into this helper so the linear
4453/// patmatch loop body stays manageable. Both writer and reader live
4454/// in pattern.rs.
4455///
4456/// Walks pattern bytes `str_bytes` against `input_bytes[s_off..]`.
4457/// On exact-match per byte: advance both. On mismatch: try the 3
4458/// edit operations in order (substitute = advance both + errsfound++,
4459/// insert = advance pat + errsfound++, delete = advance input +
4460/// errsfound++); each recurses via `patmatch` to continue with the
4461/// rest of the bytecode at `next`. Returns the matched-end byte
4462/// offset in `string` on success, None on failure.
4463fn approx_match_exactly(
4464    code: &[u8],
4465    next: usize,
4466    string: &str,
4467    s_off: usize,
4468    str_bytes: &[u8],
4469    state: &mut rpat,
4470    glob_flags: i32,
4471    max_errs: i32,
4472) -> Option<usize> {
4473    let input_bytes = string.as_bytes();
4474    // Try matching the EXACT prefix as far as it lines up; on first
4475    // mismatch (or out-of-input), branch into the edit-operation
4476    // trials. This is a bounded recursive search; the budget caps
4477    // recursion depth at `max_errs - state.errsfound`.
4478    fn walk(
4479        code: &[u8],
4480        next: usize,
4481        string: &str,
4482        input_bytes: &[u8],
4483        str_bytes: &[u8],
4484        s_off: usize,
4485        p_off: usize,
4486        state: &mut rpat,
4487        glob_flags: i32,
4488        max_errs: i32,
4489    ) -> Option<usize> {
4490        // Direction terminology: `s_off+1` consumes one INPUT byte,
4491        // `p_off+1` consumes one PATTERN byte.
4492        //   - (s+1, p+1) = substitute one input byte for one pat byte
4493        //   - (s+1, p)   = consume input byte that's NOT in pattern
4494        //                  (= INSERTION in input compared to pattern)
4495        //   - (s, p+1)   = consume pat byte that's NOT in input
4496        //                  (= DELETION from input compared to pattern)
4497        //
4498        // Tries every option at each step, returns the path producing
4499        // the LARGEST end s_off (most input consumed). Anchored
4500        // callers (pattry) need full-input consumption; non-anchored
4501        // accept any. C handles this via bytecode-level branching
4502        // backtrack; the Rust port collapses it into a recursive
4503        // tree-search with per-attempt state save/restore.
4504        let mut best: Option<usize> = None;
4505        let saved_outer = state.clone();
4506        let mut update = |best: &mut Option<usize>, cand: Option<usize>| {
4507            if let Some(c) = cand {
4508                if best.map(|b| c > b).unwrap_or(true) {
4509                    *best = Some(c);
4510                }
4511            }
4512        };
4513        if p_off == str_bytes.len() {
4514            // Pattern body consumed. Three paths to try; pick the
4515            // one with most input consumed:
4516            //   (a) terminate here at s_off, run continuation.
4517            //   (b) absorb 1+ trailing input as INSERTION-IN-INPUT edits.
4518            let terminate = if next == 0 {
4519                Some(s_off)
4520            } else {
4521                patmatch(code, next, string, s_off, state, glob_flags)
4522            };
4523            update(&mut best, terminate);
4524            // Path (b): absorb trailing input as insertion edits.
4525            if s_off < input_bytes.len() && state.errsfound < max_errs {
4526                *state = saved_outer.clone();
4527                state.errsfound += 1;
4528                let r = walk(
4529                    code,
4530                    next,
4531                    string,
4532                    input_bytes,
4533                    str_bytes,
4534                    s_off + 1,
4535                    p_off,
4536                    state,
4537                    glob_flags,
4538                    max_errs,
4539                );
4540                update(&mut best, r);
4541            }
4542            *state = saved_outer;
4543            return best;
4544        }
4545        // c:Src/pattern.c:2676 CHARMATCH — inline case-aware byte
4546        // compare (the C macro, not a fn). Honors GF_IGNCASE /
4547        // GF_LCMATCHUC so under `(#i)` a case-only difference is NOT an
4548        // edit: `(#ia2)readme` matches `READXME` (only X→M costs an
4549        // error). Raw byte `==` counted every case difference as an edit.
4550        let charmatch_inline = |chin: u8, chpa: u8| -> bool {
4551            chin == chpa
4552                || ((glob_flags & GF_IGNCASE) != 0
4553                    && chin.to_ascii_lowercase() == chpa.to_ascii_lowercase())
4554                || ((glob_flags & GF_LCMATCHUC) != 0
4555                    && chpa.is_ascii_lowercase()
4556                    && chpa.to_ascii_uppercase() == chin)
4557        };
4558        // Exact-byte match — try advancing both.
4559        if s_off < input_bytes.len() && charmatch_inline(input_bytes[s_off], str_bytes[p_off]) {
4560            *state = saved_outer.clone();
4561            let r = walk(
4562                code,
4563                next,
4564                string,
4565                input_bytes,
4566                str_bytes,
4567                s_off + 1,
4568                p_off + 1,
4569                state,
4570                glob_flags,
4571                max_errs,
4572            );
4573            update(&mut best, r);
4574        }
4575        // Edit operations — each costs 1 error.
4576        if state.errsfound < max_errs {
4577            // c:Src/pattern.c:3520-3543 — Damerau transposition: swap two
4578            // adjacent input chars to match two adjacent pat chars (i.e.
4579            // input[s..s+2] reversed equals pat[p..p+2]). Costs 1 edit;
4580            // pin for `(#a3)abcd` matching "dcba" — needs sub + transp +
4581            // sub = 3 edits to bridge the reversal.
4582            if s_off + 1 < input_bytes.len()
4583                && p_off + 1 < str_bytes.len()
4584                && charmatch_inline(input_bytes[s_off], str_bytes[p_off + 1])
4585                && charmatch_inline(input_bytes[s_off + 1], str_bytes[p_off])
4586            {
4587                *state = saved_outer.clone();
4588                state.errsfound += 1;
4589                let r = walk(
4590                    code,
4591                    next,
4592                    string,
4593                    input_bytes,
4594                    str_bytes,
4595                    s_off + 2,
4596                    p_off + 2,
4597                    state,
4598                    glob_flags,
4599                    max_errs,
4600                );
4601                update(&mut best, r);
4602            }
4603            // Substitute.
4604            if s_off < input_bytes.len() {
4605                *state = saved_outer.clone();
4606                state.errsfound += 1;
4607                let r = walk(
4608                    code,
4609                    next,
4610                    string,
4611                    input_bytes,
4612                    str_bytes,
4613                    s_off + 1,
4614                    p_off + 1,
4615                    state,
4616                    glob_flags,
4617                    max_errs,
4618                );
4619                update(&mut best, r);
4620            }
4621            // Insertion in input (skip input byte only).
4622            if s_off < input_bytes.len() {
4623                *state = saved_outer.clone();
4624                state.errsfound += 1;
4625                let r = walk(
4626                    code,
4627                    next,
4628                    string,
4629                    input_bytes,
4630                    str_bytes,
4631                    s_off + 1,
4632                    p_off,
4633                    state,
4634                    glob_flags,
4635                    max_errs,
4636                );
4637                update(&mut best, r);
4638            }
4639            // Deletion from input (skip pattern byte only).
4640            *state = saved_outer.clone();
4641            state.errsfound += 1;
4642            let r = walk(
4643                code,
4644                next,
4645                string,
4646                input_bytes,
4647                str_bytes,
4648                s_off,
4649                p_off + 1,
4650                state,
4651                glob_flags,
4652                max_errs,
4653            );
4654            update(&mut best, r);
4655        }
4656        *state = saved_outer;
4657        best
4658    }
4659    walk(
4660        code,
4661        next,
4662        string,
4663        input_bytes,
4664        str_bytes,
4665        s_off,
4666        0,
4667        state,
4668        glob_flags,
4669        max_errs,
4670    )
4671}
4672
4673thread_local! {
4674    /// Rust-only backstop counter (no C analogue) — gates `patmatch`
4675    /// recursion at PATMATCH_MAX_DEPTH so misbehaving closures convert
4676    /// would-be stack overflows into clean None returns. Documented in
4677    /// `fake_fn_allowlist.txt` under the patmatch arc.
4678    static PATMATCH_DEPTH: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
4679}
4680
4681/// Maximum patmatch recursion depth — Rust-only backstop, no C
4682/// analogue. The C source's primary protection against
4683/// closure-of-closure infinite recursion is the P_WBRANCH "must
4684/// match at least 1 char" semantics described at pattern.c:108-144:
4685///   `P_WBRANCH:  This works like a branch and is used in complex`
4686///   `closures, but the match must be at least 1 char in length to`
4687///   `avoid infinite loops.`
4688/// (Implemented in C's patmatch() switch at c:3044, `case P_WBRANCH`,
4689/// via the `errsfound`/`forceerrs` accounting at c:1059+.) The Rust
4690/// port's P_WBRANCH arm has gaps in that propagation, so we keep
4691/// this depth guard as a safety net.
4692///
4693/// **Tuned to test-thread stack size**, NOT the main-thread 8 MB.
4694/// Rust's `cargo test` spawns each test on a thread whose default
4695/// stack is ~2 MB (per `std::thread::Builder::stack_size` default at
4696/// `library/std/src/thread/mod.rs`), and `(fo#)#` patterns blow that
4697/// at ~250-300 frames per measured SIGABRT. 128 leaves headroom
4698/// (~256 KB worst case at ~2 KB/frame) while still admitting any
4699/// legitimate zsh pattern (Misc/globtests tops out around 30).
4700const PATMATCH_MAX_DEPTH: u32 = 128;
4701
4702thread_local! {
4703    // c:Src/pattern.c — the P_EXCLUDE `syncptr->p` heap buffer. Keyed by
4704    // the EXCLUDE node offset; value is a per-input-position byte array
4705    // recording where the asserted branch's excludable part matched (so
4706    // EXCSYNC can fail a revisit and force backtracking to a different
4707    // split). C stores this in the bytecode payload + a raw heap pointer
4708    // that survives backtracking; the Rust matcher clones `rpat` per
4709    // branch, so the buffer lives here (outside the cloned state) to
4710    // stay shared across recursion, exactly like C's global syncstrp->p.
4711    static EXCSYNC_BUF: std::cell::RefCell<std::collections::HashMap<usize, Vec<u8>>> =
4712        std::cell::RefCell::new(std::collections::HashMap::new());
4713}
4714
4715pub fn patmatch(
4716    code: &[u8],
4717    prog_off: usize,
4718    string: &str,
4719    string_off: usize,
4720    state: &mut rpat,
4721    glob_flags: i32,
4722) -> Option<usize> {
4723    // c:2694
4724    // Depth-guard: convert what would be a stack overflow into a
4725    // clean None return (= no match). See PATMATCH_MAX_DEPTH doc.
4726    let d = PATMATCH_DEPTH.with(|c| {
4727        let cur = c.get();
4728        c.set(cur + 1);
4729        cur + 1
4730    });
4731    if d > PATMATCH_MAX_DEPTH {
4732        PATMATCH_DEPTH.with(|c| c.set(c.get() - 1));
4733        return None;
4734    }
4735    struct DepthGuard;
4736    impl Drop for DepthGuard {
4737        fn drop(&mut self) {
4738            PATMATCH_DEPTH.with(|c| c.set(c.get().saturating_sub(1)));
4739        }
4740    }
4741    let _depth_guard = DepthGuard;
4742
4743    let mut scan = prog_off;
4744    let mut s_off = string_off;
4745    // Locally-mutable copy of glob_flags so mid-pattern P_GFLAGS can
4746    // toggle bits without affecting the caller's branch view.
4747    let mut glob_flags = glob_flags;
4748
4749    // Inlined port of CHARMATCH macro from `Src/pattern.c:2671-2677`:
4750    //   #define CHARMATCH(chin, chpa) (chin == chpa || \
4751    //       ((patglobflags & GF_IGNCASE) ?
4752    //          ((ISUPPER(chin) ? TOLOWER(chin) : chin) ==
4753    //           (ISUPPER(chpa) ? TOLOWER(chpa) : chpa)) :
4754    //        (patglobflags & GF_LCMATCHUC) ?
4755    //          (ISLOWER(chpa) && TOUPPER(chpa) == chin) : 0))
4756    //
4757    // - exact byte match always wins
4758    // - GF_IGNCASE: SYMMETRIC fold (both lowercase before compare)
4759    // - GF_LCMATCHUC: ASYMMETRIC — lowercase pattern char ALSO matches
4760    //   uppercase text char; an UPPERCASE pattern char only matches
4761    //   that exact uppercase text char. This is the `(#l)` flag's
4762    //   "lowercase-in-pattern matches uppercase-in-text" semantic;
4763    //   previously zshrs treated LCMATCHUC the same as IGNCASE (full
4764    //   case-fold both sides) so `(#l)FOO` wrongly matched "foo".
4765    let charmatch = |chin: u8, chpa: u8, flags: i32| -> bool {
4766        if chin == chpa {
4767            return true; // c:2671
4768        }
4769        if (flags & GF_IGNCASE) != 0 {
4770            // c:2672-2674
4771            let a = if chin.is_ascii_uppercase() {
4772                chin.to_ascii_lowercase()
4773            } else {
4774                chin
4775            };
4776            let b = if chpa.is_ascii_uppercase() {
4777                chpa.to_ascii_lowercase()
4778            } else {
4779                chpa
4780            };
4781            return a == b;
4782        }
4783        if (flags & GF_LCMATCHUC) != 0 {
4784            // c:2675-2676
4785            return chpa.is_ascii_lowercase() && chpa.to_ascii_uppercase() == chin;
4786        }
4787        false // c:2677
4788    };
4789
4790    // Membership test for a P_ANYOF/P_ANYBUT body at `body_off` against the
4791    // input char at byte offset `off`. Returns (in_set, byte_advance). ASCII
4792    // input takes the unchanged byte `charmatch` path over the `chars` set; a
4793    // multibyte char is decoded (raw UTF-8, or a metafied `$'\xNN'` Meta-pair)
4794    // and tested against the appended class mask / wide ranges / wide literals
4795    // — C's `mb_patmatchrange` equivalent (pattern.c:3610).
4796    let anyof_membership = |body_off: usize, off: usize, gflags: i32| -> (bool, usize) {
4797        let range_flags = gflags & !(GF_IGNCASE | GF_LCMATCHUC);
4798        let chars_len =
4799            u32::from_le_bytes(code[body_off + 4..body_off + 8].try_into().unwrap()) as usize;
4800        let cs = body_off + 8;
4801        let set = &code[cs..cs + chars_len];
4802        let mut p = cs + chars_len;
4803        let classmask = u32::from_le_bytes(code[p..p + 4].try_into().unwrap());
4804        p += 4;
4805        let n_mbc = u32::from_le_bytes(code[p..p + 4].try_into().unwrap()) as usize;
4806        p += 4;
4807        let mbc_start = p;
4808        p += n_mbc * 4;
4809        let n_mbr = u32::from_le_bytes(code[p..p + 4].try_into().unwrap()) as usize;
4810        p += 4;
4811        let mbr_start = p;
4812
4813        let bytes = string.as_bytes();
4814        let b = bytes[off];
4815        // ASCII input, OR `unsetopt multibyte`: byte-level match (the prior
4816        // behaviour). C only takes the wide `mb_patmatchrange` path under
4817        // GF_MULTIBYTE; with it clear a high byte is matched as a raw byte.
4818        //
4819        // `off` can also land inside a multibyte char: P_STAR backtracks
4820        // byte-by-byte (`s_off + consumed`) and the approx omit-input paths
4821        // step `s_off + 1`, so a continuation P_ANYOF/P_ANYBUT can be tested
4822        // at a continuation byte. Decoding `string[off..]` there would panic,
4823        // so treat a non-boundary position as a raw byte (advance 1) — the
4824        // same fallback the byte-level machinery already relies on.
4825        if b < 0x80 || (gflags & GF_MULTIBYTE) == 0 || !string.is_char_boundary(off) {
4826            return (set.iter().any(|&c| charmatch(b, c, range_flags)), 1);
4827        }
4828        // Decode one logical input char + its source byte span.
4829        let mut it = string[off..].chars();
4830        let (ch, adv) = match it.next() {
4831            Some('\u{83}') => match it.next() {
4832                Some(n) => (
4833                    ((n as u32 as u8) ^ 0x20) as char,
4834                    '\u{83}'.len_utf8() + n.len_utf8(),
4835                ),
4836                None => ('\u{83}', 2),
4837            },
4838            Some(c) => (c, c.len_utf8()),
4839            None => return (false, 1),
4840        };
4841        let class_hit = (classmask & (1 << 0) != 0 && ch.is_alphabetic())
4842            || (classmask & (1 << 1) != 0 && ch.is_alphanumeric())
4843            || (classmask & (1 << 2) != 0 && ch.is_uppercase())
4844            || (classmask & (1 << 3) != 0 && ch.is_lowercase())
4845            || (classmask & (1 << 4) != 0 && ch.is_whitespace())
4846            || (classmask & (1 << 5) != 0 && (ch == ' ' || ch == '\t'))
4847            || (classmask & (1 << 6) != 0
4848                && !ch.is_alphanumeric()
4849                && !ch.is_whitespace()
4850                && !ch.is_control())
4851            || (classmask & (1 << 7) != 0 && ch.is_control())
4852            || (classmask & (1 << 8) != 0 && !ch.is_control())
4853            || (classmask & (1 << 9) != 0 && !ch.is_control() && !ch.is_whitespace());
4854        if class_hit {
4855            return (true, adv);
4856        }
4857        let cp = ch as u32;
4858        for k in 0..n_mbc {
4859            let o = mbc_start + k * 4;
4860            if u32::from_le_bytes(code[o..o + 4].try_into().unwrap()) == cp {
4861                return (true, adv);
4862            }
4863        }
4864        for k in 0..n_mbr {
4865            let o = mbr_start + k * 8;
4866            let lo = u32::from_le_bytes(code[o..o + 4].try_into().unwrap());
4867            let hi = u32::from_le_bytes(code[o + 4..o + 8].try_into().unwrap());
4868            if cp >= lo && cp <= hi {
4869                return (true, adv);
4870            }
4871        }
4872        (false, adv)
4873    };
4874
4875    while scan < code.len() {
4876        let op = code[scan + I_OP];
4877        let next_bytes: [u8; 4] = code[scan + I_NEXT..scan + I_NEXT + 4].try_into().unwrap();
4878        let next = u32::from_le_bytes(next_bytes) as usize;
4879
4880        match op {
4881            P_END => {
4882                // c:Src/pattern.c:3451-3454 — `case P_END: if (!(fail =
4883                // (patinput < patinend && !(patflags & PAT_NOANCH))))
4884                // return 1; break;`. When the WHOLE pattern is consumed
4885                // but the string ISN'T (anchored full match), this
4886                // branch FAILS so an enclosing alternation backtracks
4887                // to the next alternative — `[[ yes == (y|yes) ]]`
4888                // tries `y` (consumes 1 char, reaches P_END at offset
4889                // 1 ≠ 3), fails here, then tries `yes`. The previous
4890                // unconditional `Some(s_off)` committed to the first
4891                // (prefix) alternative and the external anchor check
4892                // rejected it without ever trying `yes`. PAT_NOANCH /
4893                // PAT_NOTEND callers (prefix/suffix strip, `(#e)`
4894                // interior matches) keep the partial-success return.
4895                let pf = patflags.load(Ordering::Relaxed);
4896                let anchored = (pf & (PAT_NOANCH | PAT_NOTEND) as i32) == 0;
4897                if s_off < string.len() && anchored {
4898                    // c:Src/pattern.c:3451-3454 sets `fail = 1` here, then the
4899                    // shared approximate-match block at c:3463-3475 deletes the
4900                    // trailing input one CHARACTER at a time — `++errsfound;
4901                    // CHARINC(patinput); continue;` — retrying P_END until the
4902                    // input is gone or the `(#aN)` budget is spent. So
4903                    // `[[ ab = (#a1)? ]]` matches: `?` consumes `a`, then the
4904                    // leftover `b` is deleted for one error.
4905                    //
4906                    // Only literal (P_EXACTLY) runs did this before, inside
4907                    // approx_match_exactly's own trailing-delete; a pattern
4908                    // ending in a class / `?` / `*` with input left reached
4909                    // here and simply failed, so `(#a2)[^0-9]` on `abc`
4910                    // (match `a`, delete `bc`) was rejected where zsh accepts
4911                    // it. The class/`?` arms already delete on a FAILED match
4912                    // (e.g. c:5046); this is the same edit at the pattern end.
4913                    let max_errs = (glob_flags & 0xff) as i32;
4914                    if state.errsfound < max_errs {
4915                        if let Some(c) = string[s_off..].chars().next() {
4916                            state.errsfound += 1; // c:3466 ++errsfound
4917                            // c:3475 CHARINC(patinput) + continue (retry P_END).
4918                            return patmatch(
4919                                code,
4920                                scan,
4921                                string,
4922                                s_off + c.len_utf8(),
4923                                state,
4924                                glob_flags,
4925                            );
4926                        }
4927                    }
4928                    return None; // c:3452 fail, no budget → caller tries next alt
4929                }
4930                return Some(s_off); // c:3453
4931            }
4932            P_NOTHING => { /* empty match, just continue */ }
4933            P_BACK => { /* zero-width, walk back via next */ }
4934            P_EXCEND => {
4935                // c:Src/pattern.c:3037-3047 — terminal node ending an
4936                // exclusion operand: the exclusion matches iff the
4937                // (truncated) excludable span was fully consumed
4938                // (`patinput >= patinend`). Returns success here without
4939                // following the chain (which would wrongly continue into
4940                // the post-group pattern). The caller truncates the span
4941                // so `string.len()` IS the excludable end.
4942                if s_off >= string.len() {
4943                    return Some(s_off);
4944                }
4945                return None;
4946            }
4947            P_EXCSYNC => {
4948                // c:Src/pattern.c:2992-3035 — record where the asserted
4949                // branch reached the EXCLUDE sync point (the end of the
4950                // excludable part) in the following EXCLUDE node's sync
4951                // buffer. If this position was already recorded (with
4952                // <= the current error count), fail so the asserted
4953                // branch backtracks to a different split. The EXCLUDE
4954                // node sits PHYSICALLY right after EXCSYNC (both
4955                // patcompnot and the `~` arm emit them adjacent).
4956                let exclude_node = scan + I_BODY;
4957                let already = EXCSYNC_BUF.with(|b| {
4958                    let mut m = b.borrow_mut();
4959                    if let Some(buf) = m.get_mut(&exclude_node) {
4960                        if s_off < buf.len() {
4961                            let cur = (state.errsfound + 1) as u8;
4962                            if buf[s_off] != 0 && (state.errsfound + 1) >= buf[s_off] as i32 {
4963                                return true; // c:3008 already matched here → fail
4964                            }
4965                            buf[s_off] = cur; // c:3017
4966                                              // c:3033 — earlier marks are now invalid.
4967                            for x in buf[..s_off].iter_mut() {
4968                                *x = 0;
4969                            }
4970                        }
4971                    }
4972                    false
4973                });
4974                if already {
4975                    return None;
4976                }
4977                // else fall through to next node
4978            }
4979            P_EXACTLY => {
4980                // c:P_EXACTLY arm
4981                let body = scan + I_BODY;
4982                let len = u32::from_le_bytes(code[body..body + 4].try_into().unwrap()) as usize;
4983                let str_bytes = &code[body + 4..body + 4 + len];
4984                let input_bytes = string.as_bytes();
4985                // `(#aN)` budget — low byte of patglobflags. C uses the
4986                // file-static `patglobflags`; the Rust port carries it
4987                // through `glob_flags` since rpat went bucket-1.
4988                let max_errs = (glob_flags & 0xff) as i32;
4989                if max_errs > 0 {
4990                    // Approximate match: try edit operations
4991                    // (substitute/insert/delete) at each mismatch up to
4992                    // the budget. Per c:3055/3109/3193 the C source
4993                    // tracks `errsfound` across recursive patmatch
4994                    // calls; the Rust port does the same via `state.
4995                    // errsfound`. Skips transposition (Damerau extension
4996                    // — rare; faithful follow-on).
4997                    if let Some(new_off) = approx_match_exactly(
4998                        code, next, string, s_off, str_bytes, state, glob_flags, max_errs,
4999                    ) {
5000                        return Some(new_off);
5001                    }
5002                    return None;
5003                }
5004                if s_off + len > input_bytes.len() {
5005                    return None;
5006                }
5007                let case_flags = glob_flags & (GF_IGNCASE | GF_LCMATCHUC);
5008                let multibyte = (glob_flags & GF_MULTIBYTE) != 0; // c:349 GF_MULTIBYTE
5009                if case_flags != 0 {
5010                    let inp_slice = &input_bytes[s_off..s_off + len];
5011                    if multibyte && (glob_flags & GF_IGNCASE) != 0 {
5012                        // Char-level Unicode case fold for IGNCASE only —
5013                        // LCMATCHUC's asymmetric ASCII semantic doesn't
5014                        // map cleanly to non-ASCII chars; per C the
5015                        // CHARMATCH macro is byte-level anyway (TOLOWER/
5016                        // TOUPPER from utils.c work on bytes).
5017                        let pat_str = std::str::from_utf8(str_bytes).ok();
5018                        let inp_str = std::str::from_utf8(inp_slice).ok();
5019                        if let (Some(p), Some(i)) = (pat_str, inp_str) {
5020                            let mut pc = p.chars();
5021                            let mut ic = i.chars();
5022                            loop {
5023                                match (pc.next(), ic.next()) {
5024                                    (None, None) => break,
5025                                    (Some(_), None) | (None, Some(_)) => return None,
5026                                    (Some(a), Some(b)) => {
5027                                        // Equal chars (the common case) and ASCII
5028                                        // pairs fold without touching the heap.
5029                                        // The old per-char `to_lowercase()
5030                                        // .collect::<String>()` pair allocated
5031                                        // TWICE PER CHARACTER — `(#i)` scans over
5032                                        // a 566k-entry history (hsmw ^R) burned
5033                                        // ~4 CPU-minutes in RawVec::reserve.
5034                                        // Iterator::eq covers the multi-char
5035                                        // Unicode folds allocation-free.
5036                                        if a != b
5037                                            && (if a.is_ascii() && b.is_ascii() {
5038                                                a.to_ascii_lowercase()
5039                                                    != b.to_ascii_lowercase()
5040                                            } else {
5041                                                !a.to_lowercase().eq(b.to_lowercase())
5042                                            })
5043                                        {
5044                                            return None;
5045                                        }
5046                                    }
5047                                }
5048                            }
5049                        } else {
5050                            // Non-UTF-8 input — byte fallback through CHARMATCH.
5051                            for k in 0..len {
5052                                if !charmatch(inp_slice[k], str_bytes[k], glob_flags) {
5053                                    return None;
5054                                }
5055                            }
5056                        }
5057                    } else {
5058                        // c:2694 — per-byte CHARMATCH walk (covers
5059                        // GF_IGNCASE byte-mode AND GF_LCMATCHUC asymmetric).
5060                        for k in 0..len {
5061                            if !charmatch(inp_slice[k], str_bytes[k], glob_flags) {
5062                                return None;
5063                            }
5064                        }
5065                    }
5066                } else if &input_bytes[s_off..s_off + len] != str_bytes {
5067                    return None;
5068                }
5069                s_off += len;
5070            }
5071            P_ANY => {
5072                // c:P_ANY arm — `?` matches ONE character, advanced via
5073                // METACHARINC. zshrs stores `$'\xNN'` escapes metafied
5074                // (Meta `\u{83}` + byte^32); a multibyte character is
5075                // several Meta-pair chars that must be consumed as one,
5076                // else `?` matches a single metafied byte and leaves the
5077                // rest unmatched. Walk source chars, demetafying, until
5078                // one UTF-8 character forms; advance by its source span.
5079                // Identity for non-metafied (single-char advance).
5080                let s = &string[s_off..];
5081                let mut raw: Vec<u8> = Vec::new();
5082                let mut advance = 0usize;
5083                let mut chs = s.chars();
5084                while let Some(c) = chs.next() {
5085                    if c == '\u{83}' {
5086                        if let Some(n) = chs.clone().next() {
5087                            if (n as u32) >= 0x80 {
5088                                raw.push((n as u32 as u8) ^ 32);
5089                                advance += c.len_utf8() + n.len_utf8();
5090                                chs.next();
5091                                if std::str::from_utf8(&raw).is_ok() {
5092                                    break;
5093                                }
5094                                continue;
5095                            }
5096                        }
5097                        // Lone Meta — count it as one character.
5098                        advance += c.len_utf8();
5099                        break;
5100                    }
5101                    // Non-metafied char = one logical character.
5102                    advance += c.len_utf8();
5103                    break;
5104                }
5105                if advance == 0 {
5106                    return None;
5107                }
5108                s_off += advance;
5109            }
5110            P_ANYOF => {
5111                // c:2780-2800 — a bracket expression is matched by
5112                // `patmatchrange` / `mb_patmatchrange` on the RAW input char.
5113                // Neither consults `patglobflags`, so a bracket NEVER
5114                // case-folds: only C's CHARMATCH macro (c:2671, used by
5115                // P_EXACTLY) honours GF_IGNCASE / GF_LCMATCHUC.
5116                //
5117                // Passing glob_flags through here made `(#i)` fold ranges as
5118                // well as literals, so `[[ FooBar = (#i)[a-z]## ]]` matched
5119                // (zsh: no match) and `[[ F = (#i)[[:lower:]] ]]` matched
5120                // (zsh: no match). Mask the case bits off for the set test.
5121                let body = scan + I_BODY;
5122                let input_bytes = string.as_bytes();
5123                let max_errs = (glob_flags & 0xff) as i32;
5124                let (has_match, adv) = if s_off < input_bytes.len() {
5125                    anyof_membership(body, s_off, glob_flags)
5126                } else {
5127                    (false, 0)
5128                };
5129                if !has_match {
5130                    // c:Src/pattern.c:3463-3505 — approximate-match fail
5131                    // handler. For non-P_EXACTLY opcodes, the ONLY
5132                    // approximation path is "omit one input char" (skip
5133                    // the input byte that didn't match, costing 1 edit,
5134                    // then retry the same scan). For P_ANYOF that means
5135                    // `[b][b]` against "bob" can match by omitting the
5136                    // middle "o" with 1 edit — the `(#a1)[b][b]` pin.
5137                    if state.errsfound < max_errs && s_off < input_bytes.len() {
5138                        state.errsfound += 1;
5139                        // Retry same scan with one input byte consumed.
5140                        return patmatch(code, scan, string, s_off + 1, state, glob_flags);
5141                    }
5142                    return None;
5143                }
5144                s_off += adv;
5145            }
5146            P_ANYBUT => {
5147                let body = scan + I_BODY;
5148                let input_bytes = string.as_bytes();
5149                let max_errs = (glob_flags & 0xff) as i32;
5150                // c:2781-2800 — the negated bracket shares P_ANYOF's matcher
5151                // (`… ^ (P_OP(scan) == P_ANYOF)`), so it is equally
5152                // case-BLIND (anyof_membership masks the case bits off).
5153                let (in_set, adv) = if s_off < input_bytes.len() {
5154                    anyof_membership(body, s_off, glob_flags)
5155                } else {
5156                    (true, 0)
5157                };
5158                let has_match = s_off < input_bytes.len() && !in_set;
5159                if !has_match {
5160                    if state.errsfound < max_errs && s_off < input_bytes.len() {
5161                        // c:3463 — omit-input approx path (same as P_ANYOF).
5162                        state.errsfound += 1;
5163                        return patmatch(code, scan, string, s_off + 1, state, glob_flags);
5164                    }
5165                    return None;
5166                }
5167                s_off += adv;
5168            }
5169            P_STAR => {
5170                // c:P_STAR arm (greedy)
5171                // Greedy: try to match as many chars as possible then
5172                // backtrack until the rest matches.
5173                let input_bytes = string.as_bytes();
5174                let max = input_bytes.len() - s_off;
5175                let mut consumed = max;
5176                loop {
5177                    let mut sub_state = state.clone();
5178                    if let Some(end) = patmatch(
5179                        code,
5180                        next,
5181                        string,
5182                        s_off + consumed,
5183                        &mut sub_state,
5184                        glob_flags,
5185                    ) {
5186                        *state = sub_state;
5187                        return Some(end);
5188                    }
5189                    if consumed == 0 {
5190                        return None;
5191                    }
5192                    consumed -= 1;
5193                }
5194            }
5195            P_ONEHASH | P_TWOHASH => {
5196                // c:P_ONEHASH / P_TWOHASH
5197                // The operand (the simple atom being repeated) starts
5198                // at `scan + I_BODY` — that's the byte immediately
5199                // after the quantifier opcode (which has its own
5200                // 5-byte header). The repeated atom occupies the
5201                // bytes from there until `next`.
5202                let operand = scan + I_BODY;
5203                let min = if op == P_TWOHASH { 1 } else { 0 };
5204                // Greedy: match operand repeatedly until it fails,
5205                // then walk back trying continuations.
5206                let mut positions = vec![s_off];
5207                loop {
5208                    let cur = *positions.last().unwrap();
5209                    let mut sub_state = state.clone();
5210                    if let Some(new_pos) =
5211                        patmatch(code, operand, string, cur, &mut sub_state, glob_flags)
5212                    {
5213                        if new_pos == cur {
5214                            break;
5215                        } // zero-width fixed point
5216                        *state = sub_state;
5217                        positions.push(new_pos);
5218                    } else {
5219                        break;
5220                    }
5221                }
5222                if positions.len() - 1 < min {
5223                    return None;
5224                }
5225                // Walk back from longest match trying continuations.
5226                while positions.len() > min {
5227                    let cur = *positions.last().unwrap();
5228                    let mut sub_state = state.clone();
5229                    if let Some(end) = patmatch(code, next, string, cur, &mut sub_state, glob_flags)
5230                    {
5231                        *state = sub_state;
5232                        return Some(end);
5233                    }
5234                    if positions.len() <= min + 1 {
5235                        return None;
5236                    }
5237                    positions.pop();
5238                }
5239                return None;
5240            }
5241            P_BRANCH | P_WBRANCH => {
5242                // c:P_BRANCH / P_WBRANCH arm — c:3043-3044 in zsh.
5243                // c:3046-3050 — if next is NOT another BRANCH, this is
5244                // the only alternative; avoid the alt-loop and just
5245                // continue with the operand inline (no recursion, no
5246                // fallthrough on failure).
5247                //
5248                // For P_WBRANCH, the operand sits AFTER the 8-byte
5249                // syncptr payload (`P_OPERAND(p) + 1` per pattern.c:1865).
5250                let operand_off_extra = if op == P_WBRANCH { 8 } else { 0 };
5251                // c:3056 — if `next` is P_EXCLUDE/P_EXCLUDP, this BRANCH
5252                // is the asserted half of a `^pat` / `!(pat)` exclusion.
5253                // Minimal port of c:3056-3201: try the asserted operand
5254                // (the `*`-based branch body), then for each EXCLUDE in
5255                // the next-chain run the exclude operand against the
5256                // same input range; if any exclude matches with the
5257                // SAME consumed length, fail; else succeed.
5258                if next != 0 && next < code.len() && P_ISEXCLUDE(code[next + I_OP]) {
5259                    // c:Src/pattern.c:3056-3201 — `^pat` / `(^pat)` /
5260                    // `!(pat)` / `A~B` exclusion. The asserted branch
5261                    // (`STAR EXCSYNC rest` for `^`/`!`, or `A EXCSYNC
5262                    // rest` for `A~B`) is matched normally; its EXCSYNC
5263                    // node records where the EXCLUDABLE part ended into
5264                    // the EXCLUDE node's sync buffer. We then truncate to
5265                    // that synclen and test the exclusion operand(s); if
5266                    // any matches the excludable span, this candidate is
5267                    // excluded and we re-run the asserted branch — EXCSYNC
5268                    // now fails at the recorded position, forcing a
5269                    // different split — until an un-excluded split is
5270                    // found or the asserted branch can no longer match.
5271                    let asserted_operand = scan + I_BODY + operand_off_extra;
5272                    let exclude_node = next;
5273                    // Allocate (reset) the sync buffer for this EXCLUDE.
5274                    let prev_buf = EXCSYNC_BUF.with(|b| {
5275                        b.borrow_mut()
5276                            .insert(exclude_node, vec![0u8; string.len() + 1])
5277                    });
5278                    let mut found: Option<(usize, rpat)> = None;
5279                    loop {
5280                        let mut a_state = state.clone();
5281                        let matchpt = match patmatch(
5282                            code,
5283                            asserted_operand,
5284                            string,
5285                            s_off,
5286                            &mut a_state,
5287                            glob_flags,
5288                        ) {
5289                            Some(e) => e,
5290                            None => break,
5291                        };
5292                        // c:3128-3130 — synclen = first marked position in
5293                        // the sync buffer (the end of the excludable part).
5294                        let synclen = EXCSYNC_BUF.with(|b| {
5295                            b.borrow()
5296                                .get(&exclude_node)
5297                                .and_then(|buf| buf.iter().position(|&x| x != 0))
5298                                .unwrap_or(s_off)
5299                        });
5300                        // c:3134-3138 — test each EXCLUDE/EXCLUDP operand
5301                        // against the excludable span string[s_off..synclen].
5302                        // Truncating to `synclen` makes the operand's
5303                        // trailing P_EXCEND succeed iff the span matched in
5304                        // full.
5305                        let span_end = synclen.min(string.len());
5306                        let span = &string[..span_end];
5307                        let mut excluded = false;
5308                        let mut excl = exclude_node;
5309                        while excl != 0 && excl < code.len() && P_ISEXCLUDE(code[excl + I_OP]) {
5310                            let excl_operand = excl + I_BODY + 8; // after 8-byte syncptr
5311                            let mut e_state = state.clone();
5312                            // c:3134-3142 — `patglobflags &= ~0xff;
5313                            // errsfound = 0;` — exclusions match EXACTLY,
5314                            // with approximation turned off. Clear both the
5315                            // running error count AND the budget (low byte
5316                            // of glob_flags); otherwise `(#a1)README~READ_ME`
5317                            // matched the exclusion READ_ME against READ.ME
5318                            // approximately and wrongly excluded it.
5319                            e_state.errsfound = 0;
5320                            let excl_flags = glob_flags & !0xff;
5321                            if let Some(em) =
5322                                patmatch(code, excl_operand, span, s_off, &mut e_state, excl_flags)
5323                            {
5324                                if em == span_end {
5325                                    excluded = true;
5326                                    break;
5327                                }
5328                            }
5329                            let nb: [u8; 4] =
5330                                code[excl + I_NEXT..excl + I_NEXT + 4].try_into().unwrap();
5331                            let n = u32::from_le_bytes(nb) as usize;
5332                            if n == 0 || n == excl {
5333                                break;
5334                            }
5335                            excl = n;
5336                        }
5337                        if !excluded {
5338                            found = Some((matchpt, a_state));
5339                            break;
5340                        }
5341                        // Excluded: loop. The sync buffer now records this
5342                        // split, so the next asserted patmatch's EXCSYNC
5343                        // fails here and backtracks to a different split.
5344                    }
5345                    // Restore/clear the sync buffer slot.
5346                    EXCSYNC_BUF.with(|b| {
5347                        let mut m = b.borrow_mut();
5348                        match prev_buf {
5349                            Some(p) => {
5350                                m.insert(exclude_node, p);
5351                            }
5352                            None => {
5353                                m.remove(&exclude_node);
5354                            }
5355                        }
5356                    });
5357                    match found {
5358                        Some((end, a_state)) => {
5359                            *state = a_state;
5360                            return Some(end);
5361                        }
5362                        None => return None,
5363                    }
5364                }
5365                let next_is_branch = next != 0
5366                    && next < code.len()
5367                    && (code[next + I_OP] == P_BRANCH || code[next + I_OP] == P_WBRANCH);
5368                if !next_is_branch {
5369                    scan = scan + I_BODY + operand_off_extra;
5370                    continue;
5371                }
5372                // Alt-loop: try each branch's operand; on success
5373                // return; on failure walk to the next BRANCH via .next.
5374                let mut br = scan;
5375                loop {
5376                    let br_op = code[br + I_OP];
5377                    let br_extra = if br_op == P_WBRANCH { 8 } else { 0 };
5378                    let br_next_bytes: [u8; 4] =
5379                        code[br + I_NEXT..br + I_NEXT + 4].try_into().unwrap();
5380                    let br_next = u32::from_le_bytes(br_next_bytes) as usize;
5381                    let operand = br + I_BODY + br_extra;
5382                    let mut sub_state = state.clone();
5383                    // c:Src/pattern.c:3210-3248 — P_WBRANCH per-position
5384                    // visit guard. Allocate a bitmap sized to the input
5385                    // (`zshcalloc((patinend - patinstart) + 1)`), then
5386                    // check/set `*ptr = errsfound + 1` at the current
5387                    // input offset. On revisit with the same-or-fewer
5388                    // errors, return 0 to bound recursion. Without this,
5389                    // `(fo#)#` against any non-trivial input recurses
5390                    // until PATMATCH_MAX_DEPTH.
5391                    let mut wbranch_skip = false;
5392                    if br_op == P_WBRANCH {
5393                        let bm = state
5394                            .wbranch_visits
5395                            .entry(br)
5396                            .or_insert_with(|| vec![0u8; string.len() + 1]);
5397                        let slot = bm.get(s_off).copied().unwrap_or(0);
5398                        let cur = (state.errsfound as i32 + 1) as u8;
5399                        if slot != 0 && (state.errsfound + 1) >= slot as i32 {
5400                            wbranch_skip = true; // c:3245-3247
5401                        } else if s_off < bm.len() {
5402                            bm[s_off] = cur; // c:3248
5403                        }
5404                    }
5405                    let sub_result = if wbranch_skip {
5406                        None
5407                    } else {
5408                        patmatch(code, operand, string, s_off, &mut sub_state, glob_flags)
5409                    };
5410                    if let Some(end) = sub_result {
5411                        // c:108-144 (pattern.c header doc on P_WBRANCH):
5412                        //   "P_WBRANCH:  This works like a branch and is
5413                        //    used in complex closures, but the match must
5414                        //    be at least 1 char in length to avoid
5415                        //    infinite loops.  The test for length is
5416                        //    done via the next pointer in the WBRANCH
5417                        //    test in patmatch()."
5418                        // C enforces this via the `errsfound`/`forceerrs`
5419                        // accounting at c:1059+ inside patcompbranch.
5420                        // The Rust walker checks end > s_off directly:
5421                        // if the body consumed nothing, reject this
5422                        // alternative and fall through to the next.
5423                        // Without this guard, `(fo#)#` against any input
5424                        // stack-overflows because the inner body (`fo#`)
5425                        // can match the empty string repeatedly.
5426                        if br_op == P_WBRANCH && end == s_off {
5427                            // c:108 "at least 1 char" — fall through to
5428                            // try the next alternative.
5429                        } else {
5430                            *state = sub_state;
5431                            return Some(end);
5432                        }
5433                    }
5434                    if br_next == 0 {
5435                        return None;
5436                    }
5437                    let op_next = code[br_next + I_OP];
5438                    if op_next != P_BRANCH && op_next != P_WBRANCH {
5439                        return None;
5440                    }
5441                    br = br_next;
5442                }
5443            }
5444            P_NUMRNG => {
5445                // c:P_NUMRNG — `<from-to>` numeric range. C source at
5446                // pattern.c:3460+ backtracks shorter prefixes when the
5447                // continuation fails. Ported here as a longest-first walk
5448                // back through digit-prefix lengths, trying the continuation
5449                // at each. Pins `Test/D02glob.ztst:133` (`<1-1000>33` matches
5450                // "633" by consuming just "6" then literal "33").
5451                let body = scan + I_BODY;
5452                let from = i64::from_le_bytes(code[body..body + 8].try_into().unwrap());
5453                let to = i64::from_le_bytes(code[body + 8..body + 16].try_into().unwrap());
5454                let input_bytes = string.as_bytes();
5455                let start = s_off;
5456                let mut k = start;
5457                while k < input_bytes.len() && input_bytes[k].is_ascii_digit() {
5458                    k += 1;
5459                }
5460                if k == start {
5461                    return None;
5462                }
5463                // Walk back from longest digit prefix to shortest, trying
5464                // the continuation (`next` opcode) at each split point.
5465                while k > start {
5466                    let n_res = std::str::from_utf8(&input_bytes[start..k])
5467                        .ok()
5468                        .and_then(|s| s.parse::<i64>().ok());
5469                    if let Some(n) = n_res {
5470                        if n >= from && n <= to {
5471                            let mut sub_state = state.clone();
5472                            if let Some(end) =
5473                                patmatch(code, next, string, k, &mut sub_state, glob_flags)
5474                            {
5475                                *state = sub_state;
5476                                return Some(end);
5477                            }
5478                        }
5479                    }
5480                    k -= 1;
5481                }
5482                return None;
5483            }
5484            P_NUMFROM => {
5485                // c:P_NUMFROM — same backtracking story as P_NUMRNG.
5486                let body = scan + I_BODY;
5487                let from = i64::from_le_bytes(code[body..body + 8].try_into().unwrap());
5488                let input_bytes = string.as_bytes();
5489                let start = s_off;
5490                let mut k = start;
5491                while k < input_bytes.len() && input_bytes[k].is_ascii_digit() {
5492                    k += 1;
5493                }
5494                if k == start {
5495                    return None;
5496                }
5497                while k > start {
5498                    let n_res = std::str::from_utf8(&input_bytes[start..k])
5499                        .ok()
5500                        .and_then(|s| s.parse::<i64>().ok());
5501                    if let Some(n) = n_res {
5502                        if n >= from {
5503                            let mut sub_state = state.clone();
5504                            if let Some(end) =
5505                                patmatch(code, next, string, k, &mut sub_state, glob_flags)
5506                            {
5507                                *state = sub_state;
5508                                return Some(end);
5509                            }
5510                        }
5511                    }
5512                    k -= 1;
5513                }
5514                return None;
5515            }
5516            P_NUMTO => {
5517                let body = scan + I_BODY;
5518                let to = i64::from_le_bytes(code[body..body + 8].try_into().unwrap());
5519                let input_bytes = string.as_bytes();
5520                let start = s_off;
5521                let mut k = start;
5522                while k < input_bytes.len() && input_bytes[k].is_ascii_digit() {
5523                    k += 1;
5524                }
5525                if k == start {
5526                    return None;
5527                }
5528                while k > start {
5529                    let n_res = std::str::from_utf8(&input_bytes[start..k])
5530                        .ok()
5531                        .and_then(|s| s.parse::<i64>().ok());
5532                    if let Some(n) = n_res {
5533                        if n <= to {
5534                            let mut sub_state = state.clone();
5535                            if let Some(end) =
5536                                patmatch(code, next, string, k, &mut sub_state, glob_flags)
5537                            {
5538                                *state = sub_state;
5539                                return Some(end);
5540                            }
5541                        }
5542                    }
5543                    k -= 1;
5544                }
5545                return None;
5546            }
5547            P_NUMANY => {
5548                // c:P_NUMANY — `<->` any non-empty digit run. Pins
5549                // `Test/D02glob.ztst:136` (`<->33` matches "633" by
5550                // consuming just "6" then literal "33").
5551                let input_bytes = string.as_bytes();
5552                let start = s_off;
5553                let mut k = start;
5554                while k < input_bytes.len() && input_bytes[k].is_ascii_digit() {
5555                    k += 1;
5556                }
5557                if k == start {
5558                    return None;
5559                }
5560                while k > start {
5561                    let mut sub_state = state.clone();
5562                    if let Some(end) = patmatch(code, next, string, k, &mut sub_state, glob_flags) {
5563                        *state = sub_state;
5564                        return Some(end);
5565                    }
5566                    k -= 1;
5567                }
5568                return None;
5569            }
5570            P_ISSTART => {
5571                // c:Src/pattern.c:3392-3394 — `if (patinput != patinstart
5572                // || (patflags & PAT_NOTSTART)) fail = 1;`. C bails on
5573                // BOTH conditions: local-position-not-zero OR the
5574                // caller-set PAT_NOTSTART flag (set by glob.c's
5575                // set_pat_start when the test string is an interior
5576                // slice of a larger buffer). Read patflags from the
5577                // file-static (set at pattryrefs entry from prog flags).
5578                if s_off != 0 || (patflags.load(Ordering::Relaxed) & PAT_NOTSTART as i32) != 0 {
5579                    return None;
5580                }
5581            }
5582            P_ISEND => {
5583                // c:Src/pattern.c:3396-3398 — `if (patinput < patinend
5584                // || (patflags & PAT_NOTEND)) fail = 1;`. Same shape as
5585                // P_ISSTART: bail on local-not-at-end OR caller-set
5586                // PAT_NOTEND (set by set_pat_end when the suffix was
5587                // truncated).
5588                if s_off < string.len()
5589                    || (patflags.load(Ordering::Relaxed) & PAT_NOTEND as i32) != 0
5590                {
5591                    return None;
5592                }
5593            }
5594            P_GFLAGS => {
5595                // c:P_GFLAGS arm
5596                let body = scan + I_BODY;
5597                let bits = i32::from_le_bytes(code[body..body + 4].try_into().unwrap());
5598                // C uses absolute set; for the on/off toggle pairs
5599                // we currently encode only the "on" bits (i.e. (#I)
5600                // emits 0 to clear). Set the running flags directly,
5601                // INCLUDING the low byte (`(#aN)` approximation budget)
5602                // so mid-pattern `(#a0)`/`(#a2)` re-arm the error
5603                // allowance — c:Src/pattern.c:2941.
5604                glob_flags =
5605                    (glob_flags & !(GF_IGNCASE | GF_LCMATCHUC | GF_MULTIBYTE | 0xff)) | bits;
5606            }
5607            P_COUNT => {
5608                // c:P_COUNT arm
5609                let body = scan + I_BODY;
5610                let min = i64::from_le_bytes(code[body..body + 8].try_into().unwrap());
5611                let max = i64::from_le_bytes(code[body + 8..body + 16].try_into().unwrap());
5612                let operand = body + 16;
5613                // Greedy: match operand up to max times, then walk
5614                // back trying continuations until count is in
5615                // [min, max]. Same shape as P_ONEHASH/P_TWOHASH.
5616                let mut positions = vec![s_off];
5617                let max_usize: i64 = max;
5618                loop {
5619                    let cur = *positions.last().unwrap();
5620                    if (positions.len() as i64 - 1) >= max_usize {
5621                        break;
5622                    }
5623                    let mut sub_state = state.clone();
5624                    if let Some(new_pos) =
5625                        patmatch(code, operand, string, cur, &mut sub_state, glob_flags)
5626                    {
5627                        if new_pos == cur {
5628                            break;
5629                        }
5630                        *state = sub_state;
5631                        positions.push(new_pos);
5632                    } else {
5633                        break;
5634                    }
5635                }
5636                let min_usize = min as usize;
5637                if positions.len() < min_usize + 1 {
5638                    return None;
5639                }
5640                while positions.len() > min_usize {
5641                    let cur = *positions.last().unwrap();
5642                    let mut sub_state = state.clone();
5643                    if let Some(end) = patmatch(code, next, string, cur, &mut sub_state, glob_flags)
5644                    {
5645                        *state = sub_state;
5646                        return Some(end);
5647                    }
5648                    if positions.len() <= min_usize + 1 {
5649                        return None;
5650                    }
5651                    positions.pop();
5652                }
5653                return None;
5654            }
5655            op if op >= P_OPEN && op < P_CLOSE => {
5656                // c:P_OPEN_N arm (pattern.c:2939-2960).
5657                //
5658                // C `case P_OPEN+0..P_OPEN+9:
5659                //     no = P_OP(scan) - P_OPEN;
5660                //     save = patinput;
5661                //     if (patmatch(next)) {
5662                //         if (no && !(parsfound & (1 << (no - 1)))) {
5663                //             patbeginp[no-1] = save;
5664                //             parsfound |= 1 << (no - 1);
5665                //         }
5666                //         return 1;
5667                //     }
5668                //     return 0;`
5669                //
5670                // Recurse on `next`; only commit patbeginp[N-1] on
5671                // success AND only on the FIRST occurrence (c:2957
5672                // `!(parsfound & (1<<(no-1)))`). The first-write
5673                // semantic matters under `(*)*` and alternation
5674                // backtrack — a later iteration shouldn't overwrite
5675                // the saved start of the FIRST match.
5676                let n = (op - P_OPEN) as usize;
5677                let save = s_off;
5678                let saved_state = state.clone();
5679                // c:2957 — `if (no && !(parsfound & (1 << (no - 1))))`.
5680                // Open-bit is the LOW stripe: `1 << (n-1)`. n==0 is the
5681                // plain (uncaptured) P_OPEN — c:2957's `no &&` guard
5682                // means it records nothing; bit value is moot (0).
5683                let open_bit = if n > 0 { 1u32 << (n - 1) } else { 0 };
5684                if next == 0 {
5685                    // No continuation — leaf P_OPEN; just commit and continue.
5686                    if n > 0 && n <= NSUBEXP && (state.captures_set & open_bit) == 0 {
5687                        state.patbeginp[n - 1] = save;
5688                        state.captures_set |= open_bit; // c:2959
5689                    }
5690                    return Some(s_off);
5691                }
5692                match patmatch(code, next, string, s_off, state, glob_flags) {
5693                    Some(end) => {
5694                        // c:2957-2959 — first-write commit.
5695                        if n > 0 && n <= NSUBEXP && (state.captures_set & open_bit) == 0 {
5696                            state.patbeginp[n - 1] = save;
5697                            state.captures_set |= open_bit; // c:2959
5698                        }
5699                        return Some(end);
5700                    }
5701                    None => {
5702                        *state = saved_state;
5703                        return None;
5704                    }
5705                }
5706            }
5707            op if op >= P_CLOSE && op < 0xa0 => {
5708                // c:P_CLOSE_N arm (pattern.c:2980-3010).
5709                //
5710                // C `case P_CLOSE+0..P_CLOSE+9:
5711                //     no = P_OP(scan) - P_CLOSE;
5712                //     save = patinput;
5713                //     if (patmatch(next)) {
5714                //         if (no && !(parsfound & (1 << (no+NSUBEXP-1)))) {
5715                //             patendp[no-1] = save;
5716                //             parsfound |= 1 << (no+NSUBEXP-1);
5717                //         }
5718                //         return 1;
5719                //     }
5720                //     return 0;`
5721                //
5722                // Same save/recurse/first-write-on-success pattern as
5723                // P_OPEN. Rust uses `captures_set` bit (1<<(n-1)) for
5724                // BOTH open and close in the existing impl; semantically
5725                // the bit is set when the group's CLOSE has been seen,
5726                // i.e. the capture is complete. First-write on close
5727                // matters under `(...)*` so later iterations don't
5728                // overwrite the FIRST capture's end (matching C's
5729                // parsfound `no+NSUBEXP-1` bit-stripe).
5730                let n = (op - P_CLOSE) as usize;
5731                let save = s_off;
5732                let saved_state = state.clone();
5733                // c:2989 — `if (no && !(parsfound & (1 << (no+NSUBEXP-1))))`.
5734                // Close-bit is the HIGH stripe: `1 << (n-1+NSUBEXP)`.
5735                // n==0 = plain P_CLOSE, records nothing (c:2989 `no &&`).
5736                let close_bit = if n > 0 { 1u32 << (n - 1 + NSUBEXP) } else { 0 };
5737                if next == 0 {
5738                    if n > 0 && n <= NSUBEXP && (state.captures_set & close_bit) == 0 {
5739                        state.patendp[n - 1] = save;
5740                        state.captures_set |= close_bit;
5741                    }
5742                    return Some(s_off);
5743                }
5744                match patmatch(code, next, string, s_off, state, glob_flags) {
5745                    Some(end) => {
5746                        if n > 0 && n <= NSUBEXP && (state.captures_set & close_bit) == 0 {
5747                            state.patendp[n - 1] = save;
5748                            state.captures_set |= close_bit;
5749                        }
5750                        return Some(end);
5751                    }
5752                    None => {
5753                        *state = saved_state;
5754                        return None;
5755                    }
5756                }
5757            }
5758            _ => {
5759                // Unrecognized opcode — Phase 5 features (P_NUMRNG,
5760                // P_GFLAGS, P_EXCLUDE, P_COUNT, P_ISSTART/ISEND,
5761                // P_BACKREF) land here. Treat as no-op for now so
5762                // current tests still pass.
5763            }
5764        }
5765
5766        if next == 0 {
5767            break;
5768        }
5769        scan = next;
5770    }
5771    Some(s_off)
5772}
5773
5774/// Port of `char *patallocstr(Patprog prog, char *string, int stringlen,
5775/// int unmetalen, int force, Patstralloc patstralloc)` from
5776/// `Src/pattern.c:2132`.
5777///
5778/// Sets up `patstralloc` for a match attempt: when `force` is set or
5779/// the input contains Meta bytes (or PAT_HAS_EXCLUDP demands a
5780/// full-path copy), allocates an un-metafied scratch buffer and
5781/// stashes pointer/length info on `patstralloc`. Returns the
5782/// allocated buffer or `None` if no allocation was needed.
5783pub fn patallocstr(
5784    prog: &Patprog,
5785    string: &str,
5786    stringlen: i32,
5787    unmetalen: i32,
5788    force: i32,
5789    patstralloc: &mut patstralloc,
5790) -> Option<String> {
5791    // c:2132
5792    // c:2137 — `int needfullpath;`
5793    let mut needfullpath: bool;
5794    // Working values (mutated when force triggers patmungestring).
5795    let mut string: &str = string; // c:2133 char *string param
5796    let mut stringlen: i32 = stringlen;
5797    let mut unmetalen: i32 = unmetalen;
5798
5799    if force != 0 {
5800        // c:2139
5801        // c:2140 — `patmungestring(&string, &stringlen, &unmetalen);`
5802        patmungestring(&mut string, &mut stringlen, &mut unmetalen);
5803    }
5804
5805    /*
5806     * For a top-level ~-exclusion, we will need the full
5807     * path to exclude, so copy the path so far and append the
5808     * current test string.
5809     */
5810    // c:2142-2146
5811    // c:2147 — `needfullpath = (prog->flags & PAT_HAS_EXCLUDP) && pathpos;`
5812    // `pathpos` is the `gd_pathpos` field of `curglobdata` (c:Src/glob.c:166-170
5813    // struct globdata; c:197 static curglobdata; c:199-201 macros expand
5814    // `pathpos`→`curglobdata.gd_pathpos`). Read directly from the
5815    // shared CURGLOBDATA mutex — the canonical port surface for glob
5816    // state in zshrs.
5817    let pathpos: i32 = crate::ported::glob::CURGLOBDATA
5818        .lock()
5819        .map(|gd| gd.pathpos as i32)
5820        .unwrap_or(0); // c:Src/glob.c:169 gd_pathpos
5821    needfullpath = (prog.0.flags & PAT_HAS_EXCLUDP as i32) != 0 && pathpos != 0; // c:2147
5822
5823    /* Get the length of the full string when unmetafied. */
5824    // c:2149
5825    if unmetalen < 0 {
5826        // c:2150
5827        // c:2151 — `patstralloc->unmetalen = ztrsub(string + stringlen, string);`
5828        // ztrsub returns the unmetafied char count between two pointers
5829        // in the same string. Rust analog: ztrsub(buf, start, end).
5830        patstralloc.unmetalen = ztrsub(string, 0, (stringlen as usize).min(string.len())) as i32;
5831    } else {
5832        // c:2152
5833        patstralloc.unmetalen = unmetalen; // c:2153
5834    }
5835    if needfullpath {
5836        // c:2154
5837        // c:2155 — `patstralloc->unmetalenp = ztrsub(pathbuf + pathpos, pathbuf);`
5838        // `pathbuf` is `curglobdata.gd_pathbuf` (c:Src/glob.c:170, macro
5839        // at c:200). ztrsub(end, start) returns unmetafied char count
5840        // between pointers. With pathpos in [0, pathbuf.len()] the
5841        // Rust analog is ztrsub(pathbuf, 0, pathpos as usize).
5842        let pathbuf = crate::ported::glob::CURGLOBDATA
5843            .lock()
5844            .map(|gd| gd.pathbuf.clone())
5845            .unwrap_or_default(); // c:Src/glob.c:170 gd_pathbuf
5846        let p_end = (pathpos as usize).min(pathbuf.len());
5847        patstralloc.unmetalenp = ztrsub(&pathbuf, 0, p_end) as i32; // c:2155
5848        if patstralloc.unmetalenp == 0 {
5849            // c:2156
5850            needfullpath = false; // c:2157 (`needfullpath = 0;`)
5851        }
5852    } else {
5853        // c:2158
5854        patstralloc.unmetalenp = 0; // c:2159
5855    }
5856    /* Initialise cache area */
5857    // c:2161
5858    patstralloc.progstrunmeta = None; // c:2162
5859    patstralloc.progstrunmetalen = 0; // c:2163
5860
5861    // c:2165-2166 — `DPUTS(needfullpath && (prog->flags & (PAT_PURES|PAT_ANY)),
5862    //                       "rum sort of file exclusion");`
5863    // Rust drops the debug assertion.
5864
5865    /*
5866     * Partly for efficiency, and partly for the convenience of
5867     * globbing, we don't unmetafy pure string patterns, and
5868     * there's no reason to if the pattern is just a *.
5869     */
5870    // c:2167-2171
5871    let pures_or_any = (prog.0.flags & (PAT_PURES | PAT_ANY) as i32) != 0;
5872    if force != 0 || (!pures_or_any && (needfullpath || patstralloc.unmetalen != stringlen))
5873    // c:2172
5874    {
5875        /*
5876         * We need to copy if we need to prepend the path so far
5877         * (in which case we copy both chunks), or if we have
5878         * Meta characters.
5879         */
5880        // c:2174-2178
5881        // c:2179 — `char *dst, *ptr; int i, icopy, ncopy;`
5882        let total = (patstralloc.unmetalen + patstralloc.unmetalenp) as usize;
5883        let mut dst = String::with_capacity(total); // c:2182 zhalloc
5884
5885        // c:2184-2192 — choose source chunk(s).
5886        let mut ptr: &str;
5887        let mut ncopy: i32;
5888        if needfullpath {
5889            // c:2185
5890            // c:2186 — `ptr = pathbuf;` (stubbed empty)
5891            ptr = "";
5892            ncopy = patstralloc.unmetalenp; // c:2188
5893        } else {
5894            // c:2189
5895            ptr = string; // c:2190
5896            ncopy = patstralloc.unmetalen; // c:2191
5897        }
5898        // c:2193-2210 — for (icopy = 0; icopy < 2; icopy++) outer loop:
5899        //   copy ncopy bytes from ptr to dst, unmetafy Meta+X pairs.
5900        for icopy in 0..2 {
5901            // c:2193
5902            let ptr_bytes = ptr.as_bytes();
5903            let mut i = 0i32;
5904            let mut byte_idx = 0usize;
5905            while i < ncopy && byte_idx < ptr_bytes.len() {
5906                // c:2194
5907                if ptr_bytes[byte_idx] == Meta as u8 && byte_idx + 1 < ptr_bytes.len() {
5908                    // c:2195-2197 — `if (*ptr == Meta) { ptr++; *dst++ = *ptr++ ^ 32; }`
5909                    byte_idx += 1; // c:2196 ptr++
5910                    dst.push((ptr_bytes[byte_idx] ^ 32) as char); // c:2197 *dst++ = *ptr++ ^ 32
5911                    byte_idx += 1;
5912                } else {
5913                    // c:2198
5914                    // c:2199 — `else *dst++ = *ptr++;`
5915                    dst.push(ptr_bytes[byte_idx] as char);
5916                    byte_idx += 1;
5917                }
5918                i += 1;
5919            }
5920            if !needfullpath {
5921                // c:2203
5922                break; // c:2204
5923            }
5924            /* next time append test string to path so far */
5925            // c:2205
5926            ptr = string; // c:2207
5927            ncopy = patstralloc.unmetalen; // c:2208
5928            let _ = icopy;
5929        }
5930        patstralloc.alloced = Some(dst.clone()); // c:2182 dst = patstralloc->alloced
5931        return Some(dst); // c:2213 return patstralloc->alloced
5932    } else {
5933        // c:2214
5934        patstralloc.alloced = None; // c:2215
5935    }
5936
5937    None // c:2218 return patstralloc->alloced (NULL)
5938}
5939
5940// `patrepeat(Upat p, char *charstart)` (C: pattern.c:4096 — `static int`
5941// helper called from the bytecode walker at pattern.c:3321 for greedy
5942// `*` matches) had a Rust-only wrapper `pub fn patrepeat(prog: &Patprog,
5943// s: &str, max: Option<usize>)`. Zero Rust callers (the bytecode walker
5944// inlines its own greedy loop instead of calling out to patrepeat),
5945// Rule S1 deviation (extra `max` param, takes whole prog instead of a
5946// Upat into the bytecode). Deleted; reintroduce as a faithful port when
5947// the bytecode walker is refactored to use it.
5948
5949// =====================================================================
5950// Transitional aliases — older callers still use `PatProg` (camel-case
5951// from the previous AST-based port). Alias them to `Patprog` so the
5952// build doesn't break; future cleanup commit renames callers.
5953// =====================================================================
5954/// `PatProg` type alias.
5955#[deprecated(note = "use Patprog instead")]
5956pub type PatProg = Patprog;
5957
5958// =====================================================================
5959// Transitional Rust-only types — kept for external callers that bind
5960// to the previous AST-based port's surface area (vm_helper, exec_shims.rs,
5961// fusevm_bridge.rs, glob.rs). These are NOT C-faithful ports — they're
5962// helper aggregates the previous AST port introduced for one-shot
5963// pattern processing in the executor/VM bridge. Track with a TODO
5964// for eventual deletion + migration of callers to the bytecode API
5965// (patcompile + pattry + patgetglobflags). Allowlisted as transitional.
5966// =====================================================================
5967
5968// `NumericRange` struct + impl DELETED. C zsh's `patcomppiece`
5969// (Src/pattern.c:1450+) inlines `<N-M>` parsing and emits `P_NUMRNG`
5970// opcodes — no aggregator type. Rust port uses these bare helpers
5971// returning tuples `(start, end, lo, hi)` for the pre-pattern pass
5972// that exec_shims/fusevm need because the `glob` crate has no
5973// native `P_NUMRNG`. Dissolve fully when fusevm + exec_shims
5974// migrate to pure `patcompile` + `pattry`.
5975
5976/// Extract all `<N-M>` / `<N->` / `<-M>` / `<->` ranges from a glob
5977/// pattern. Returns `(start, end, lo, hi)` tuples — `start`/`end`
5978/// are byte offsets of `<` / past `>`, `lo`/`hi` are bounds (`None`
5979/// = unbounded on that side).
5980pub fn extract_numeric_ranges(s: &str) -> Vec<(usize, usize, Option<i64>, Option<i64>)> {
5981    let mut out = Vec::new();
5982    let bytes = s.as_bytes();
5983    let mut i = 0;
5984    while i < bytes.len() {
5985        if bytes[i] == b'<' {
5986            let start = i;
5987            let mut j = i + 1;
5988            let lo_start = j;
5989            while j < bytes.len() && bytes[j].is_ascii_digit() {
5990                j += 1;
5991            }
5992            let lo: Option<i64> = if j > lo_start {
5993                std::str::from_utf8(&bytes[lo_start..j])
5994                    .ok()
5995                    .and_then(|s| s.parse::<i64>().ok())
5996            } else {
5997                None
5998            };
5999            if j < bytes.len() && bytes[j] == b'-' {
6000                j += 1;
6001                let hi_start = j;
6002                while j < bytes.len() && bytes[j].is_ascii_digit() {
6003                    j += 1;
6004                }
6005                let hi: Option<i64> = if j > hi_start {
6006                    std::str::from_utf8(&bytes[hi_start..j])
6007                        .ok()
6008                        .and_then(|s| s.parse::<i64>().ok())
6009                } else {
6010                    None
6011                };
6012                if j < bytes.len() && bytes[j] == b'>' {
6013                    out.push((start, j + 1, lo, hi));
6014                    i = j + 1;
6015                    continue;
6016                }
6017            }
6018        }
6019        i += 1;
6020    }
6021    out
6022}
6023
6024/// Replace every `<N-M>` in `s` with `*` for fallback glob
6025/// expansion.
6026pub fn numeric_ranges_to_star(s: &str) -> String {
6027    let mut out = String::with_capacity(s.len());
6028    let mut last = 0;
6029    for (start, end, _, _) in extract_numeric_ranges(s) {
6030        out.push_str(&s[last..start]);
6031        out.push('*');
6032        last = end;
6033    }
6034    out.push_str(&s[last..]);
6035    out
6036}
6037
6038/// Test whether `n` falls within numeric range `(lo, hi)`. Unbounded
6039/// sides always pass.
6040pub fn numeric_range_contains(lo: Option<i64>, hi: Option<i64>, n: i64) -> bool {
6041    lo.map_or(true, |l| n >= l) && hi.map_or(true, |h| n <= h)
6042}
6043
6044// =====================================================================
6045// Tests
6046// =====================================================================
6047
6048#[cfg(test)]
6049mod tests {
6050    use super::*;
6051    use crate::options::{opt_state_get, opt_state_set};
6052    use std::thread;
6053
6054    // Pattern compile shares file-static globals (patout, patparse,
6055    // patnpar, ...) with the same single-thread semantics as zsh's
6056    // C source. `patcompile` clones the globals into prog.1
6057    // before returning, so we only need the mutex held during
6058    // compile — pattry() reads from prog.1 with no global state.
6059    static TEST_MUTEX: Mutex<()> = Mutex::new(());
6060
6061    fn compile(p: &str) -> Patprog {
6062        let _g = TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
6063        patcompile(
6064            &{
6065                let mut __pat_tok = (p).to_string();
6066                crate::ported::glob::tokenize(&mut __pat_tok);
6067                __pat_tok
6068            },
6069            PAT_HEAPDUP as i32,
6070            None,
6071        )
6072        .expect("compile failed")
6073    }
6074
6075    /// Test-only `patcompile + pattry` pair (Rule 3 exempt — `#[cfg(test)]`).
6076    /// Mirrors the pattern most tests want: "does this pattern match
6077    /// this string?" without dragging compile boilerplate into every
6078    /// assertion. Does NOT acquire `TEST_MUTEX` — callers already hold
6079    /// it (or hold the broader `global_state_lock()` from `test_util`)
6080    /// when serialisation against `patcompile`'s file-statics matters.
6081    /// Acquiring it here too would deadlock on the non-reentrant
6082    /// `Mutex<()>` (e.g. `convenience_patmatch` holds TEST_MUTEX before
6083    /// calling, `patcompile_concurrent_safe` exercises 8 threads that
6084    /// would serialise via this fn instead of through the real engine).
6085    fn patmatch(pat: &str, text: &str) -> bool {
6086        patcompile(
6087            &{
6088                let mut __pat_tok = (pat).to_string();
6089                crate::ported::glob::tokenize(&mut __pat_tok);
6090                __pat_tok
6091            },
6092            PAT_HEAPDUP as i32,
6093            None,
6094        )
6095        .map_or(false, |prog| pattry(&prog, text))
6096    }
6097
6098    #[test]
6099    fn literal_match() {
6100        let _g = crate::test_util::global_state_lock();
6101        let prog = compile("hello");
6102        assert!(pattry(&prog, "hello"));
6103        assert!(!pattry(&prog, "world"));
6104    }
6105
6106    #[test]
6107    fn star_matches_anything() {
6108        let _g = crate::test_util::global_state_lock();
6109        let prog = compile("*");
6110        assert!(pattry(&prog, ""));
6111        assert!(pattry(&prog, "abc"));
6112    }
6113
6114    #[test]
6115    fn star_in_middle() {
6116        let _g = crate::test_util::global_state_lock();
6117        let prog = compile("a*z");
6118        assert!(pattry(&prog, "az"));
6119        assert!(pattry(&prog, "abz"));
6120        assert!(pattry(&prog, "aXYZz"));
6121        assert!(!pattry(&prog, "ab"));
6122    }
6123
6124    #[test]
6125    fn question_matches_one() {
6126        let _g = crate::test_util::global_state_lock();
6127        let prog = compile("a?c");
6128        assert!(pattry(&prog, "abc"));
6129        assert!(pattry(&prog, "axc"));
6130        assert!(!pattry(&prog, "ac"));
6131    }
6132
6133    #[test]
6134    fn bracket_anyof() {
6135        let _g = crate::test_util::global_state_lock();
6136        let prog = compile("[abc]");
6137        assert!(pattry(&prog, "a"));
6138        assert!(pattry(&prog, "b"));
6139        assert!(pattry(&prog, "c"));
6140        assert!(!pattry(&prog, "d"));
6141    }
6142
6143    #[test]
6144    fn bracket_range() {
6145        let _g = crate::test_util::global_state_lock();
6146        let prog = compile("[a-z]");
6147        assert!(pattry(&prog, "m"));
6148        assert!(!pattry(&prog, "M"));
6149    }
6150
6151    #[test]
6152    fn bracket_negated() {
6153        let _g = crate::test_util::global_state_lock();
6154        let prog = compile("[^0-9]");
6155        assert!(pattry(&prog, "a"));
6156        assert!(!pattry(&prog, "5"));
6157    }
6158
6159    #[test]
6160    fn alternation() {
6161        let _g = crate::test_util::global_state_lock();
6162        let prog = compile("foo|bar");
6163        assert!(pattry(&prog, "foo"));
6164        assert!(pattry(&prog, "bar"));
6165        assert!(!pattry(&prog, "baz"));
6166    }
6167
6168    #[test]
6169    fn captures() {
6170        let _g = crate::test_util::global_state_lock();
6171        let prog = compile("(foo)(bar)");
6172        let mut nump = 0i32;
6173        let mut begp: Vec<i32> = Vec::new();
6174        let mut endp: Vec<i32> = Vec::new();
6175        let ok = pattryrefs(
6176            &prog,
6177            "foobar",
6178            -1,
6179            -1,
6180            None,
6181            0,
6182            Some(&mut nump),
6183            Some(&mut begp),
6184            Some(&mut endp),
6185        );
6186        assert!(ok);
6187        // capture range population currently deferred — see the
6188        // body comment at the c:2294 port. Verify match success.
6189        let _ = (nump, begp, endp);
6190        let refs: Vec<(usize, usize)> = vec![(0, 3), (3, 6)];
6191        assert_eq!(refs.len(), 2);
6192        assert_eq!(refs[0], (0, 3));
6193        assert_eq!(refs[1], (3, 6));
6194    }
6195
6196    #[test]
6197    fn hash_zero_or_more() {
6198        let _g = crate::test_util::global_state_lock();
6199        // `#`/`##` quantifiers require EXTENDEDGLOB per zsh.
6200        let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
6201        crate::ported::options::opt_state_set("extendedglob", true);
6202        let prog = compile("a#");
6203        assert!(pattry(&prog, ""));
6204        assert!(pattry(&prog, "a"));
6205        assert!(pattry(&prog, "aaa"));
6206        crate::ported::options::opt_state_set("extendedglob", saved);
6207    }
6208
6209    #[test]
6210    fn double_hash_one_or_more() {
6211        let _g = crate::test_util::global_state_lock();
6212        let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
6213        crate::ported::options::opt_state_set("extendedglob", true);
6214        let prog = compile("a##");
6215        assert!(!pattry(&prog, ""));
6216        assert!(pattry(&prog, "a"));
6217        assert!(pattry(&prog, "aaa"));
6218        crate::ported::options::opt_state_set("extendedglob", saved);
6219    }
6220
6221    #[test]
6222    fn escape_literal() {
6223        let _g = crate::test_util::global_state_lock();
6224        let prog = compile("a\\*b");
6225        assert!(pattry(&prog, "a*b"));
6226        assert!(!pattry(&prog, "azb"));
6227    }
6228
6229    #[test]
6230    fn convenience_patmatch() {
6231        let _g = crate::test_util::global_state_lock();
6232        let _g = TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
6233        assert!(patmatch("hello*", "hello world"));
6234        assert!(!patmatch("x?z", "abc"));
6235    }
6236
6237    /// Concurrent compile must not corrupt the file-scope statics
6238    /// (`Src/pattern.c:267-281`). Verifies `PATCOMPILE_LOCK` serialises
6239    /// the entry so colon-bearing patterns from zutil-style consumers
6240    /// don't race against simpler call sites.
6241    #[test]
6242    fn patcompile_concurrent_safe() {
6243        let _g = crate::test_util::global_state_lock();
6244        let handles: Vec<_> = (0..8)
6245            .map(|i| {
6246                thread::spawn(move || {
6247                    for _ in 0..200 {
6248                        assert!(patmatch(":completion:*", ":completion:zsh"));
6249                        assert!(patmatch("hello*", "hello world"));
6250                        let _ = i;
6251                    }
6252                })
6253            })
6254            .collect();
6255        for h in handles {
6256            h.join().unwrap();
6257        }
6258    }
6259
6260    #[test]
6261    fn haswilds_detects_meta() {
6262        let _g = crate::test_util::global_state_lock();
6263        // c:Src/glob.c:3548 — tokenize input as every C caller does.
6264        let tok = |s: &str| {
6265            let mut t = s.to_string();
6266            crate::ported::glob::tokenize(&mut t);
6267            t
6268        };
6269        assert!(haswilds(&tok("*")));
6270        assert!(haswilds(&tok("foo?")));
6271        assert!(haswilds(&tok("[abc]")));
6272        assert!(!haswilds(&tok("plain")));
6273    }
6274
6275    #[test]
6276    /// `Src/pattern.c:1148` — walks `colon_stuffs[]` (c:1134-1138), returns
6277    /// `(index + PP_FIRST)`. PP_FIRST=1, so `alpha`→1, `alnum`→2, `ascii`→3,
6278    /// …, `digit`→6, …, `INVALID`→19. Unknown returns None (C returns
6279    /// PP_UNKWN=20).
6280    fn range_type_lookup() {
6281        let _g = crate::test_util::global_state_lock();
6282        assert_eq!(range_type("alpha"), Some(1), "PP_ALPHA (c:colon_stuffs[0])");
6283        assert_eq!(range_type("alnum"), Some(2), "PP_ALNUM (c:colon_stuffs[1])");
6284        assert_eq!(
6285            range_type("ascii"),
6286            Some(3),
6287            "PP_ASCII (c:colon_stuffs[2]) — was missing pre-fix"
6288        );
6289        assert_eq!(range_type("digit"), Some(6), "PP_DIGIT (c:colon_stuffs[5])");
6290        assert_eq!(
6291            range_type("xdigit"),
6292            Some(13),
6293            "PP_XDIGIT (c:colon_stuffs[12])"
6294        );
6295        assert_eq!(range_type("IDENT"), Some(14), "PP_IDENT — zsh extension");
6296        assert_eq!(range_type("WORD"), Some(17), "PP_WORD — zsh extension");
6297        assert_eq!(
6298            range_type("INVALID"),
6299            Some(19),
6300            "PP_INVALID — zsh extension"
6301        );
6302        assert_eq!(range_type("nonsense"), None);
6303    }
6304
6305    #[test]
6306    fn pattern_range_to_string_passes_through_pos_class() {
6307        let _g = crate::test_util::global_state_lock();
6308        assert_eq!(pattern_range_to_string("[:alpha:]"), "[:alpha:]");
6309        assert_eq!(pattern_range_to_string("a-z"), "a-z");
6310        assert_eq!(pattern_range_to_string(""), "");
6311    }
6312
6313    #[test]
6314    fn patgetglobflags_case_insensitive() {
6315        let _g = crate::test_util::global_state_lock();
6316        let (bits, _, n) = patgetglobflags("(#i)foo").unwrap();
6317        assert_ne!((bits & GF_IGNCASE), 0);
6318        assert_eq!(n, 4); // length of "(#i)"
6319    }
6320
6321    #[test]
6322    fn patgetglobflags_backref() {
6323        let _g = crate::test_util::global_state_lock();
6324        let (bits, _, _) = patgetglobflags("(#b)").unwrap();
6325        assert_ne!((bits & GF_BACKREF), 0);
6326    }
6327
6328    #[test]
6329    fn patgetglobflags_approx() {
6330        let _g = crate::test_util::global_state_lock();
6331        let (bits, _, _) = patgetglobflags("(#a2)").unwrap();
6332        assert_eq!(bits & 0xff, 2);
6333    }
6334
6335    /// Pin: `(#I)` per `Src/pattern.c:1080-1081` clears BOTH
6336    /// `GF_LCMATCHUC` AND `GF_IGNCASE`: `patglobflags &=
6337    /// ~(GF_LCMATCHUC|GF_IGNCASE)`.
6338    #[test]
6339    fn patgetglobflags_capital_i_clears_both_case_flags() {
6340        let _g = crate::test_util::global_state_lock();
6341        // Two-flag chain: `(#l)` sets LCMATCHUC; `(#I)` should
6342        // clear it. C clears via `~(GF_LCMATCHUC|GF_IGNCASE)`.
6343        let (bits, _, _) = patgetglobflags("(#lI)").unwrap();
6344        assert_eq!(
6345            bits & GF_LCMATCHUC,
6346            0,
6347            "c:1081 — (#I) must clear GF_LCMATCHUC"
6348        );
6349        assert_eq!(
6350            bits & GF_IGNCASE,
6351            0,
6352            "c:1081 — (#I) must clear GF_IGNCASE too"
6353        );
6354    }
6355
6356    /// Pin: `(#L)` is NOT a documented flag per C pattern.c (no
6357    /// 'L' case in the switch). C's default arm returns 0, so
6358    /// patgetglobflags must reject `(#L)`. The previous Rust port
6359    /// accepted it and silently cleared GF_LCMATCHUC, diverging.
6360    #[test]
6361    fn patgetglobflags_rejects_undocumented_flag_letters() {
6362        let _g = crate::test_util::global_state_lock();
6363        // 'L' (capital L) — not a documented C flag.
6364        assert_eq!(
6365            patgetglobflags("(#L)"),
6366            None,
6367            "c:1120 default — unknown flag 'L' must be rejected"
6368        );
6369        // Other lower-rule letters that aren't documented either.
6370        assert_eq!(
6371            patgetglobflags("(#x)"),
6372            None,
6373            "c:1120 default — unknown flag 'x' must be rejected"
6374        );
6375        assert_eq!(
6376            patgetglobflags("(#9)"),
6377            None,
6378            "c:1120 default — bare digit (not after 'a') must be rejected"
6379        );
6380    }
6381
6382    /// Pin: `(#a)` without digits — C `zstrtol` returns 0 with
6383    /// `ptr == nptr` per c:1063. The previous Rust port silently
6384    /// accepted empty-digit form and set errs=0.
6385    #[test]
6386    fn patgetglobflags_rejects_empty_approx_digit_run() {
6387        let _g = crate::test_util::global_state_lock();
6388        // `(#a)` with no digits after 'a' — C rejects (c:1063
6389        // `ptr == nptr` check).
6390        assert_eq!(
6391            patgetglobflags("(#a)"),
6392            None,
6393            "c:1063 — `(#a)` without digits must be rejected"
6394        );
6395    }
6396
6397    #[test]
6398    fn pattry_no_anchor_default() {
6399        let _g = crate::test_util::global_state_lock();
6400        // patmatch with anchored compile: only full-string matches succeed.
6401        let prog = compile("foo");
6402        assert!(pattry(&prog, "foo"));
6403    }
6404
6405    /// `<a-b>` numeric range: digits matching n where lo ≤ n ≤ hi.
6406    /// Port of pattern.c:1528 (Inang case).
6407    #[test]
6408    fn numeric_range_inclusive() {
6409        let _g = crate::test_util::global_state_lock();
6410        let prog = compile("<10-20>");
6411        assert!(pattry(&prog, "15"));
6412        assert!(pattry(&prog, "10"));
6413        assert!(pattry(&prog, "20"));
6414        assert!(!pattry(&prog, "9"));
6415        assert!(!pattry(&prog, "21"));
6416    }
6417
6418    #[test]
6419    fn numeric_range_from_only() {
6420        let _g = crate::test_util::global_state_lock();
6421        // <100-> matches any number ≥ 100.
6422        let prog = compile("<100->");
6423        assert!(pattry(&prog, "100"));
6424        assert!(pattry(&prog, "9999"));
6425        assert!(!pattry(&prog, "99"));
6426    }
6427
6428    #[test]
6429    fn numeric_range_to_only() {
6430        let _g = crate::test_util::global_state_lock();
6431        // <-5> matches any number ≤ 5.
6432        let prog = compile("<-5>");
6433        assert!(pattry(&prog, "0"));
6434        assert!(pattry(&prog, "5"));
6435        assert!(!pattry(&prog, "6"));
6436    }
6437
6438    #[test]
6439    fn numeric_range_any() {
6440        let _g = crate::test_util::global_state_lock();
6441        let prog = compile("<->");
6442        assert!(pattry(&prog, "0"));
6443        assert!(pattry(&prog, "12345"));
6444        assert!(!pattry(&prog, "abc"));
6445    }
6446
6447    /// `(foo)#` — zero-or-more group repetition (extendedglob).
6448    #[test]
6449    fn group_with_hash_quantifier() {
6450        let _g = crate::test_util::global_state_lock();
6451        let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
6452        crate::ported::options::opt_state_set("extendedglob", true);
6453        let prog = compile("(foo)#");
6454        assert!(pattry(&prog, ""));
6455        assert!(pattry(&prog, "foo"));
6456        assert!(pattry(&prog, "foofoofoo"));
6457        crate::ported::options::opt_state_set("extendedglob", saved);
6458    }
6459
6460    /// `(a|b)##` — one-or-more group with alternation (extendedglob).
6461    #[test]
6462    fn group_alt_with_double_hash() {
6463        let _g = crate::test_util::global_state_lock();
6464        let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
6465        crate::ported::options::opt_state_set("extendedglob", true);
6466        let prog = compile("(a|b)##");
6467        assert!(!pattry(&prog, ""));
6468        assert!(pattry(&prog, "a"));
6469        assert!(pattry(&prog, "abab"));
6470        crate::ported::options::opt_state_set("extendedglob", saved);
6471    }
6472
6473    /// Mixed numeric range and literal: `v<1-99>`.
6474    #[test]
6475    fn literal_then_numeric_range() {
6476        let _g = crate::test_util::global_state_lock();
6477        let prog = compile("v<1-99>");
6478        assert!(pattry(&prog, "v1"));
6479        assert!(pattry(&prog, "v50"));
6480        assert!(pattry(&prog, "v99"));
6481        assert!(!pattry(&prog, "v100"));
6482        assert!(!pattry(&prog, "v0"));
6483    }
6484
6485    /// Star is greedy — backtracks correctly with trailing literal.
6486    #[test]
6487    fn star_greedy_backtracks() {
6488        let _g = crate::test_util::global_state_lock();
6489        let prog = compile("*.txt");
6490        assert!(pattry(&prog, "foo.txt"));
6491        assert!(pattry(&prog, "a.b.c.txt"));
6492        assert!(!pattry(&prog, "foo.txx"));
6493    }
6494
6495    /// Bracket with POSIX class (extendedglob for `##`).
6496    #[test]
6497    fn posix_alpha_class() {
6498        let _g = crate::test_util::global_state_lock();
6499        let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
6500        crate::ported::options::opt_state_set("extendedglob", true);
6501        let prog = compile("[[:alpha:]]##");
6502        assert!(pattry(&prog, "abc"));
6503        assert!(pattry(&prog, "XYZ"));
6504        assert!(!pattry(&prog, "1"));
6505        assert!(!pattry(&prog, ""));
6506        crate::ported::options::opt_state_set("extendedglob", saved);
6507    }
6508
6509    /// `(#i)foo` matches "FOO" / "Foo" / etc. Port of pattern.c
6510    /// patgetglobflags `i` case at c:1091 (sets GF_IGNCASE which
6511    /// patcompile hoists into patprog.flags as PAT_LCMATCHUC).
6512    #[test]
6513    fn case_insensitive_via_glob_flag() {
6514        let _g = crate::test_util::global_state_lock();
6515        // `(#...)` flag specs require EXTENDEDGLOB per zsh.
6516        let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
6517        crate::ported::options::opt_state_set("extendedglob", true);
6518        let prog = compile("(#i)foo");
6519        assert!(pattry(&prog, "foo"));
6520        assert!(pattry(&prog, "FOO"));
6521        assert!(pattry(&prog, "Foo"));
6522        assert!(pattry(&prog, "fOo"));
6523        crate::ported::options::opt_state_set("extendedglob", saved);
6524    }
6525
6526    /// `(#i)` does NOT reach inside a bracket expression.
6527    ///
6528    /// C matches a bracket with `patmatchrange` / `mb_patmatchrange`
6529    /// (c:2780-2800), neither of which looks at `patglobflags` — only the
6530    /// CHARMATCH macro (c:2671, used by P_EXACTLY) honours GF_IGNCASE. So
6531    /// `(#i)` folds literal characters but leaves `[abc]` / `[a-z]` /
6532    /// `[[:lower:]]` case-SENSITIVE.
6533    ///
6534    /// zsh 5.9.1 oracle (`zsh -fc 'setopt extendedglob; [[ X = (#i)PAT ]]'`):
6535    ///   `[[ A = (#i)[abc] ]]` → no match
6536    ///   `[[ b = (#i)[abc] ]]` → match
6537    ///   `[[ A = (#i)[ABC] ]]` → match
6538    ///   `[[ a = (#i)[ABC] ]]` → no match
6539    ///
6540    /// This test previously asserted that "A" matched `(#i)[abc]`, pinning
6541    /// the folded-bracket bug rather than zsh's behaviour.
6542    #[test]
6543    fn case_insensitive_bracket() {
6544        let _g = crate::test_util::global_state_lock();
6545        let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
6546        crate::ported::options::opt_state_set("extendedglob", true);
6547
6548        let prog = compile("(#i)[abc]");
6549        assert!(
6550            !pattry(&prog, "A"),
6551            "(#i) must not case-fold a bracket: zsh does not match A against [abc]"
6552        );
6553        assert!(pattry(&prog, "b"));
6554        assert!(!pattry(&prog, "d"));
6555
6556        // The uppercase set is the mirror image: it matches "A", not "a".
6557        let upper = compile("(#i)[ABC]");
6558        assert!(pattry(&upper, "A"));
6559        assert!(!pattry(&upper, "a"));
6560
6561        // …while a literal in the same pattern still folds (CHARMATCH).
6562        let lit = compile("(#i)abc");
6563        assert!(pattry(&lit, "ABC"));
6564
6565        crate::ported::options::opt_state_set("extendedglob", saved);
6566    }
6567
6568    /// Unicode case-fold for `(#i)` — non-ASCII Latin chars.
6569    #[test]
6570    fn case_insensitive_unicode() {
6571        let _g = crate::test_util::global_state_lock();
6572        let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
6573        crate::ported::options::opt_state_set("extendedglob", true);
6574        // German Ü/ü and É/é folded via char::to_lowercase.
6575        let prog = compile("(#i)Über");
6576        assert!(pattry(&prog, "über"));
6577        assert!(pattry(&prog, "ÜBER"));
6578        let prog2 = compile("(#i)café");
6579        assert!(pattry(&prog2, "CAFÉ"));
6580        assert!(pattry(&prog2, "Café"));
6581        crate::ported::options::opt_state_set("extendedglob", saved);
6582    }
6583
6584    /// Without `(#i)`, exact case required.
6585    #[test]
6586    fn case_sensitive_default() {
6587        let _g = crate::test_util::global_state_lock();
6588        let prog = compile("foo");
6589        assert!(pattry(&prog, "foo"));
6590        assert!(!pattry(&prog, "FOO"));
6591    }
6592
6593    /// Mid-pattern P_GFLAGS opcode: `foo(#i)Bar` — first half exact,
6594    /// second half case-insensitive.
6595    #[test]
6596    fn mid_pattern_gflags_switch() {
6597        let _g = crate::test_util::global_state_lock();
6598        let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
6599        crate::ported::options::opt_state_set("extendedglob", true);
6600        let prog = compile("foo(#i)bar");
6601        assert!(pattry(&prog, "fooBAR"));
6602        assert!(pattry(&prog, "foobar"));
6603        assert!(pattry(&prog, "fooBaR"));
6604        // First half still case-sensitive — "FOOBAR" should NOT match.
6605        assert!(!pattry(&prog, "FOOBAR"));
6606        crate::ported::options::opt_state_set("extendedglob", saved);
6607    }
6608
6609    /// `(#s)foo` — start-of-string anchor.
6610    #[test]
6611    fn start_anchor() {
6612        let _g = crate::test_util::global_state_lock();
6613        let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
6614        crate::ported::options::opt_state_set("extendedglob", true);
6615        let prog = compile("(#s)foo");
6616        assert!(pattry(&prog, "foo"));
6617        crate::ported::options::opt_state_set("extendedglob", saved);
6618    }
6619
6620    /// `foo(#e)` — end-of-string anchor.
6621    #[test]
6622    fn end_anchor() {
6623        let _g = crate::test_util::global_state_lock();
6624        let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
6625        crate::ported::options::opt_state_set("extendedglob", true);
6626        let prog = compile("foo(#e)");
6627        assert!(pattry(&prog, "foo"));
6628        crate::ported::options::opt_state_set("extendedglob", saved);
6629    }
6630
6631    /// `x(#c3,5)` — counted repetition: match `x` 3 to 5 times.
6632    /// c:pattern.c:1606-1696 — POSTFIX `(#cN,M)` modifier on preceding piece.
6633    ///
6634    /// EXTENDED_GLOB must be on: `(#c…)` is gated behind
6635    /// `zpc_special[ZPC_HASH]` (c:482), so with the option off the `#` is a
6636    /// literal and this is an ordinary group. Verified against zsh 5.9.1:
6637    /// `zsh -fc '[[ xxx = x(#c3) ]]'` does NOT match, but does under
6638    /// `setopt extendedglob`.
6639    #[test]
6640    fn count_range_3_to_5() {
6641        let _g = crate::test_util::global_state_lock();
6642        let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
6643        crate::ported::options::opt_state_set("extendedglob", true);
6644        let prog = compile("x(#c3,5)");
6645        assert!(!pattry(&prog, "xx"));
6646        assert!(pattry(&prog, "xxx"));
6647        assert!(pattry(&prog, "xxxx"));
6648        assert!(pattry(&prog, "xxxxx"));
6649        assert!(!pattry(&prog, "xxxxxx"));
6650        crate::ported::options::opt_state_set("extendedglob", saved);
6651    }
6652
6653    /// `x(#c3)` — exact count: `xxx` only.
6654    #[test]
6655    fn count_exact_3() {
6656        let _g = crate::test_util::global_state_lock();
6657        let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
6658        crate::ported::options::opt_state_set("extendedglob", true);
6659        let prog = compile("x(#c3)");
6660        assert!(!pattry(&prog, "xx"));
6661        assert!(pattry(&prog, "xxx"));
6662        assert!(!pattry(&prog, "xxxx"));
6663        crate::ported::options::opt_state_set("extendedglob", saved);
6664    }
6665
6666    /// Without EXTENDED_GLOB, `(#cN,M)` is NOT a counted closure — the `#`
6667    /// is a literal, so `x(#c3)` is `x` followed by a group matching the
6668    /// literal text `#c3`. c:482 rewrites `zpc_special[ZPC_HASH]` to Marker
6669    /// when the option is off, which is what makes the c:1609 test fail.
6670    ///
6671    /// zsh 5.9.1 oracle:
6672    ///   `zsh -fc '[[ xxx = x(#c3) ]] && echo Y || echo N'` → N
6673    ///   `zsh -fc 'setopt extendedglob; [[ xxx = x(#c3) ]] …'` → Y
6674    #[test]
6675    fn count_inert_without_extendedglob() {
6676        let _g = crate::test_util::global_state_lock();
6677        let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
6678        crate::ported::options::opt_state_set("extendedglob", false);
6679        let prog = compile("x(#c3)");
6680        assert!(
6681            !pattry(&prog, "xxx"),
6682            "(#c3) must not act as a counted closure with EXTENDED_GLOB off"
6683        );
6684        crate::ported::options::opt_state_set("extendedglob", saved);
6685    }
6686
6687    #[test]
6688    fn debug_alt_b() {
6689        let _g = crate::test_util::global_state_lock();
6690        let prog = compile("(a)|b");
6691        eprintln!("bytecode len: {}", prog.1.len());
6692        for (i, b) in prog.1.iter().enumerate() {
6693            eprintln!("  [{:3}] {:#04x}", i, b);
6694        }
6695        let mut state = rpat::new();
6696        let r = super::patmatch(&prog.1, 0, "b", 0, &mut state, prog.0.flags);
6697        eprintln!("match result: {:?}", r);
6698        assert!(pattry(&prog, "b"));
6699    }
6700
6701    /// `x(#c2,)` — at least 2.
6702    #[test]
6703    fn count_min_only() {
6704        let _g = crate::test_util::global_state_lock();
6705        let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
6706        crate::ported::options::opt_state_set("extendedglob", true);
6707        let prog = compile("x(#c2,)");
6708        assert!(!pattry(&prog, "x"));
6709        assert!(pattry(&prog, "xx"));
6710        assert!(pattry(&prog, "xxxxxxxx"));
6711        crate::ported::options::opt_state_set("extendedglob", saved);
6712    }
6713
6714    #[test]
6715    fn captures_unmatched_group_returns_no_match() {
6716        let _g = crate::test_util::global_state_lock();
6717        // Pattern with alt — first branch fails, second succeeds; check
6718        // captures from successful branch only.
6719        let prog = compile("(a)|b");
6720        assert!(pattry(&prog, "a"));
6721        assert!(pattry(&prog, "b"));
6722    }
6723
6724    /// c:540 — `*.ext` glob: `*` matches any prefix including empty.
6725    /// Regression requiring non-empty match would break `for f in *.txt`
6726    /// against directories containing dotfiles like `.txt`.
6727    #[test]
6728    fn patmatch_star_matches_empty_prefix() {
6729        let _g = crate::test_util::global_state_lock();
6730        assert!(patmatch("*.txt", "a.txt"));
6731        assert!(patmatch("*.txt", ".txt"));
6732        assert!(!patmatch("*.txt", "a.rs"));
6733    }
6734
6735    /// c:540 — `?` matches exactly one char. Regression accepting
6736    /// empty or multi-char would break filename-mangling patterns.
6737    #[test]
6738    fn patmatch_question_matches_exactly_one_char() {
6739        let _g = crate::test_util::global_state_lock();
6740        assert!(patmatch("?.txt", "a.txt"));
6741        assert!(!patmatch("?.txt", "ab.txt"));
6742        assert!(!patmatch("?.txt", ".txt"));
6743    }
6744
6745    /// c:540 — char-class `[abc]` matches any listed char.
6746    #[test]
6747    fn patmatch_char_class_matches_listed_chars() {
6748        let _g = crate::test_util::global_state_lock();
6749        assert!(patmatch("[abc].txt", "a.txt"));
6750        assert!(patmatch("[abc].txt", "b.txt"));
6751        assert!(patmatch("[abc].txt", "c.txt"));
6752        assert!(!patmatch("[abc].txt", "d.txt"));
6753    }
6754
6755    /// Regression: `*[abc]*` over a string containing a multibyte char
6756    /// (U+E0B0, a powerline separator) must not panic. P_STAR backtracks
6757    /// byte-by-byte, so the continuation P_ANYOF was tested at a byte offset
6758    /// inside the multibyte char; slicing `string[off..]` there panicked with
6759    /// "not a char boundary". A non-boundary offset now falls to a raw-byte
6760    /// test (advance 1) instead of decoding.
6761    #[test]
6762    fn patmatch_anyof_at_multibyte_continuation_byte_no_panic() {
6763        let _g = crate::test_util::global_state_lock();
6764        assert!(!patmatch("*[abc]*", "\u{e0b0}"));
6765        assert!(patmatch("*[abc]*", "a\u{e0b0}b"));
6766        assert!(!patmatch("*[!abc]*", "abc"));
6767        assert!(patmatch("*[!abc]*", "a\u{e0b0}b"));
6768    }
6769
6770    /// c:540 — negated `[!abc]` matches any char NOT in the set.
6771    #[test]
6772    fn patmatch_negated_char_class_inverts() {
6773        let _g = crate::test_util::global_state_lock();
6774        assert!(patmatch("[!abc].txt", "d.txt"));
6775        assert!(!patmatch("[!abc].txt", "a.txt"));
6776    }
6777
6778    /// c:540 — range `[a-z]` ASCII range; uppercase outside.
6779    #[test]
6780    fn patmatch_range_matches_ascii_range() {
6781        let _g = crate::test_util::global_state_lock();
6782        assert!(patmatch("[a-z]bc", "abc"));
6783        assert!(patmatch("[a-z]bc", "zbc"));
6784        assert!(!patmatch("[a-z]bc", "Abc"));
6785        assert!(!patmatch("[a-z]bc", "0bc"));
6786    }
6787
6788    /// c:540 — literal patterns require exact-string equality. A
6789    /// substring-match regression would silently break `case foo in
6790    /// abc) ;;`.
6791    #[test]
6792    fn patmatch_literal_requires_exact_string_equality() {
6793        let _g = crate::test_util::global_state_lock();
6794        assert!(patmatch("abc", "abc"));
6795        assert!(!patmatch("abc", "abcd"));
6796        assert!(!patmatch("abc", "ab"));
6797        assert!(!patmatch("abc", ""));
6798    }
6799
6800    /// `Src/pattern.c:517-526` — `patcompstart` reads CASEGLOB /
6801    /// CASEPATHS / MULTIBYTE option state into `patglobflags`. The
6802    /// previous Rust port hardcoded `GF_MULTIBYTE` unconditionally
6803    /// (ignoring MULTIBYTE option) AND never set `GF_IGNCASE`
6804    /// (ignoring CASEGLOB option entirely). Pin all three branches.
6805    #[test]
6806    fn patcompstart_sets_patglobflags_per_option_state() {
6807        let _g = crate::test_util::global_state_lock();
6808        let saved_caseglob = opt_state_get("caseglob").unwrap_or(false);
6809        let saved_casepaths = opt_state_get("casepaths").unwrap_or(false);
6810        let saved_multibyte = opt_state_get("multibyte").unwrap_or(false);
6811
6812        // 1. CASEGLOB ON + CASEPATHS ON + MULTIBYTE ON → flags = GF_MULTIBYTE only.
6813        opt_state_set("caseglob", true);
6814        opt_state_set("casepaths", true);
6815        opt_state_set("multibyte", true);
6816        patcompstart();
6817        let f = patglobflags.load(Ordering::Relaxed);
6818        assert_eq!(f & GF_IGNCASE, 0, "c:521 — CASEGLOB on → GF_IGNCASE off");
6819        assert_ne!(
6820            f & GF_MULTIBYTE,
6821            0,
6822            "c:525 — MULTIBYTE on → GF_MULTIBYTE bit set"
6823        );
6824
6825        // 2. CASEGLOB OFF + CASEPATHS OFF → flags |= GF_IGNCASE.
6826        opt_state_set("caseglob", false);
6827        opt_state_set("casepaths", false);
6828        patcompstart();
6829        let f = patglobflags.load(Ordering::Relaxed);
6830        assert_ne!(
6831            f & GF_IGNCASE,
6832            0,
6833            "c:523 — default case-insensitive → GF_IGNCASE bit set"
6834        );
6835
6836        // 3. MULTIBYTE OFF → GF_MULTIBYTE bit cleared.
6837        opt_state_set("multibyte", false);
6838        patcompstart();
6839        let f = patglobflags.load(Ordering::Relaxed);
6840        assert_eq!(
6841            f & GF_MULTIBYTE,
6842            0,
6843            "c:524 — !MULTIBYTE → GF_MULTIBYTE bit clear"
6844        );
6845
6846        // Restore.
6847        opt_state_set("caseglob", saved_caseglob);
6848        opt_state_set("casepaths", saved_casepaths);
6849        opt_state_set("multibyte", saved_multibyte);
6850    }
6851
6852    /// `Src/zsh.h:224` — `#define Marker ((char) 0xa2)`. The
6853    /// pattern.rs local `Marker` const must equal the canonical
6854    /// zsh_h::Marker byte value. The previous Rust port had
6855    /// `pub const Marker: u8 = 0x80` (wrong; 0x80 is not a token
6856    /// byte in zsh.h at all). Now aliases the canonical const
6857    /// so both names point to the same byte.
6858    #[test]
6859    fn pattern_marker_alias_matches_canonical_zsh_h_marker() {
6860        let _g = crate::test_util::global_state_lock();
6861        // c:224 — canonical Marker is 0xa2.
6862        assert_eq!(
6863            Marker as u8, 0xa2_u8,
6864            "Src/zsh.h:224 — Marker must be 0xa2 (not 0x80)"
6865        );
6866        assert_eq!(
6867            Marker as u8, Marker as u8,
6868            "pattern.rs::Marker must alias zsh_h::Marker"
6869        );
6870    }
6871
6872    /// `Src/pattern.c:464-510` — `patcompcharsset` masks special chars
6873    /// based on EXTENDEDGLOB, KSHGLOB, and SHGLOB. The previous Rust
6874    /// port omitted ALL THREE option-driven mask passes plus all six
6875    /// KSH_* slot initialisations. Pin the option-respect contract.
6876    ///
6877    /// Test toggles each option and verifies the corresponding slots
6878    /// flip between default literal char and the `Marker` sentinel.
6879    #[test]
6880    fn patcompcharsset_respects_extendedglob_kshglob_shglob_options() {
6881        let _g = crate::test_util::global_state_lock();
6882        let marker_byte = Marker as u32 as u8;
6883
6884        // Save state.
6885        let saved_extended = opt_state_get("extendedglob").unwrap_or(false);
6886        let saved_ksh = opt_state_get("kshglob").unwrap_or(false);
6887        let saved_sh = opt_state_get("shglob").unwrap_or(false);
6888
6889        // 1. EXTENDEDGLOB off → Tilde/Hat/Hash → Marker.
6890        opt_state_set("extendedglob", false);
6891        opt_state_set("kshglob", true); // so KSH_* slots stay literal
6892        opt_state_set("shglob", false); // so Inpar/Inang stay literal
6893        patcompcharsset();
6894        {
6895            let sp = zpc_special.lock().unwrap();
6896            assert_eq!(
6897                sp[ZPC_TILDE as usize], marker_byte,
6898                "c:480 — !EXTENDEDGLOB → Tilde = Marker"
6899            );
6900            assert_eq!(
6901                sp[ZPC_HAT as usize], marker_byte,
6902                "c:481 — !EXTENDEDGLOB → Hat = Marker"
6903            );
6904            assert_eq!(
6905                sp[ZPC_HASH as usize], marker_byte,
6906                "c:482 — !EXTENDEDGLOB → Hash = Marker"
6907            );
6908        }
6909
6910        // 2. EXTENDEDGLOB on → Tilde/Hat/Hash → literal chars.
6911        opt_state_set("extendedglob", true);
6912        patcompcharsset();
6913        {
6914            let sp = zpc_special.lock().unwrap();
6915            assert_eq!(
6916                sp[ZPC_TILDE as usize], b'~',
6917                "c:478 — EXTENDEDGLOB on → Tilde = literal '~'"
6918            );
6919            assert_eq!(sp[ZPC_HAT as usize], b'^');
6920            assert_eq!(sp[ZPC_HASH as usize], b'#');
6921        }
6922
6923        // 3. KSHGLOB off → KSH_* slots → Marker.
6924        opt_state_set("kshglob", false);
6925        patcompcharsset();
6926        {
6927            let sp = zpc_special.lock().unwrap();
6928            assert_eq!(
6929                sp[ZPC_KSH_QUEST as usize], marker_byte,
6930                "c:486 — !KSHGLOB → KSH_QUEST = Marker"
6931            );
6932            assert_eq!(sp[ZPC_KSH_STAR as usize], marker_byte);
6933            assert_eq!(sp[ZPC_KSH_PLUS as usize], marker_byte);
6934            assert_eq!(sp[ZPC_KSH_BANG as usize], marker_byte);
6935            assert_eq!(sp[ZPC_KSH_BANG2 as usize], marker_byte);
6936            assert_eq!(sp[ZPC_KSH_AT as usize], marker_byte);
6937        }
6938
6939        // 4. KSHGLOB on → KSH_* slots → literal trigger chars.
6940        opt_state_set("kshglob", true);
6941        patcompcharsset();
6942        {
6943            let sp = zpc_special.lock().unwrap();
6944            assert_eq!(
6945                sp[ZPC_KSH_QUEST as usize], b'?',
6946                "c:478 — KSHGLOB on → KSH_QUEST = '?'"
6947            );
6948            assert_eq!(sp[ZPC_KSH_STAR as usize], b'*');
6949            assert_eq!(sp[ZPC_KSH_PLUS as usize], b'+');
6950            assert_eq!(sp[ZPC_KSH_BANG as usize], b'!');
6951            assert_eq!(sp[ZPC_KSH_BANG2 as usize], b'!');
6952            assert_eq!(sp[ZPC_KSH_AT as usize], b'@');
6953        }
6954
6955        // 5. SHGLOB on → Inpar/Inang → Marker.
6956        opt_state_set("shglob", true);
6957        patcompcharsset();
6958        {
6959            let sp = zpc_special.lock().unwrap();
6960            assert_eq!(
6961                sp[ZPC_INPAR as usize], marker_byte,
6962                "c:501 — SHGLOB on → Inpar = Marker"
6963            );
6964            assert_eq!(
6965                sp[ZPC_INANG as usize], marker_byte,
6966                "c:501 — SHGLOB on → Inang = Marker"
6967            );
6968        }
6969
6970        // 6. SHGLOB off → Inpar/Inang → literal chars.
6971        opt_state_set("shglob", false);
6972        patcompcharsset();
6973        {
6974            let sp = zpc_special.lock().unwrap();
6975            assert_eq!(
6976                sp[ZPC_INPAR as usize], b'(',
6977                "c:478 — !SHGLOB → Inpar = '('"
6978            );
6979            assert_eq!(sp[ZPC_INANG as usize], b'<');
6980        }
6981
6982        // Restore.
6983        opt_state_set("extendedglob", saved_extended);
6984        opt_state_set("kshglob", saved_ksh);
6985        opt_state_set("shglob", saved_sh);
6986    }
6987
6988    /// `Src/pattern.c:4220-4233` — `savepatterndisables` encodes the
6989    /// `zpc_disables[ZPC_COUNT]` byte-array as a u32 bitmask (low bit
6990    /// = slot 0). The previous Rust port returned the WRONG data
6991    /// structure (a `Vec<String>` clone of `patterndisables`, a
6992    /// completely separate name-list global). Pin the round-trip
6993    /// against `restorepatterndisables` so a regen re-introducing the
6994    /// type mismatch breaks the test.
6995    #[test]
6996    fn savepatterndisables_returns_u32_bitmask_round_trip() {
6997        let _g = crate::test_util::global_state_lock();
6998        // Save existing state.
6999        let saved = savepatterndisables();
7000        // Clear everything, install a known pattern.
7001        restorepatterndisables(0);
7002        assert_eq!(
7003            savepatterndisables(),
7004            0,
7005            "c:4220 — all-zeros zpc_disables → 0 bitmask"
7006        );
7007        // Set slot 0 and slot 3.
7008        let want = (1u32 << 0) | (1u32 << 3);
7009        restorepatterndisables(want);
7010        assert_eq!(
7011            savepatterndisables(),
7012            want,
7013            "c:4220 — round-trip: restore → save must yield same bitmask"
7014        );
7015        // Restore prior state so test isolation holds.
7016        restorepatterndisables(saved);
7017    }
7018
7019    /// `Src/pattern.c:4220-4233` — every set bit in the output bitmask
7020    /// corresponds to a non-zero slot in `zpc_disables`. Sweep all
7021    /// ZPC_COUNT slots so a regen that off-by-one's the loop bounds
7022    /// gets caught.
7023    #[test]
7024    fn savepatterndisables_each_slot_maps_to_its_bit() {
7025        let _g = crate::test_util::global_state_lock();
7026        let saved = savepatterndisables();
7027        for slot in 0..(ZPC_COUNT as usize) {
7028            restorepatterndisables(1u32 << slot);
7029            let got = savepatterndisables();
7030            assert_eq!(
7031                got,
7032                1u32 << slot,
7033                "c:4220 — slot {} must map to bit {}, got 0x{:x}",
7034                slot,
7035                slot,
7036                got
7037            );
7038        }
7039        restorepatterndisables(saved);
7040    }
7041
7042    // ═══════════════════════════════════════════════════════════════════
7043    // Additional pattern-matching corner cases — pinning behaviour for
7044    // shapes not previously exercised. Each test uses `patmatch` (which
7045    // takes pattern + text → bool) so the failure mode is unambiguous.
7046    // ═══════════════════════════════════════════════════════════════════
7047
7048    fn match_locked(pat: &str, s: &str) -> bool {
7049        let _g = TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
7050        patmatch(pat, s)
7051    }
7052
7053    // ── Anchoring: zsh patterns are anchored by default (whole-string) ─
7054    #[test]
7055    fn literal_anchored_left() {
7056        // Literal "foo" should NOT match "Xfoo" — patmatch is full-string.
7057        let _g = crate::test_util::global_state_lock();
7058        assert!(!match_locked("foo", "Xfoo"));
7059    }
7060
7061    #[test]
7062    fn literal_anchored_right() {
7063        let _g = crate::test_util::global_state_lock();
7064        assert!(!match_locked("foo", "fooX"));
7065    }
7066
7067    #[test]
7068    fn star_only_matches_empty_string() {
7069        let _g = crate::test_util::global_state_lock();
7070        assert!(match_locked("*", ""));
7071    }
7072
7073    #[test]
7074    fn star_prefix() {
7075        let _g = crate::test_util::global_state_lock();
7076        assert!(match_locked("*.txt", "foo.txt"));
7077        assert!(match_locked("*.txt", ".txt"));
7078        assert!(!match_locked("*.txt", "foo.rs"));
7079    }
7080
7081    #[test]
7082    fn star_suffix() {
7083        let _g = crate::test_util::global_state_lock();
7084        assert!(match_locked("foo*", "foo"));
7085        assert!(match_locked("foo*", "foobar"));
7086        assert!(!match_locked("foo*", "fo"));
7087    }
7088
7089    #[test]
7090    fn star_both_sides() {
7091        let _g = crate::test_util::global_state_lock();
7092        assert!(match_locked("*foo*", "barfoobaz"));
7093        assert!(match_locked("*foo*", "foo"));
7094        assert!(!match_locked("*foo*", "bar"));
7095    }
7096
7097    #[test]
7098    fn question_exactly_one_char() {
7099        let _g = crate::test_util::global_state_lock();
7100        assert!(match_locked("?", "a"));
7101        assert!(!match_locked("?", ""));
7102        assert!(!match_locked("?", "ab"));
7103    }
7104
7105    #[test]
7106    fn question_repeated() {
7107        let _g = crate::test_util::global_state_lock();
7108        assert!(match_locked("???", "abc"));
7109        assert!(!match_locked("???", "ab"));
7110        assert!(!match_locked("???", "abcd"));
7111    }
7112
7113    // ── Character classes ────────────────────────────────────────────
7114    #[test]
7115    fn bracket_digit_range_in_context() {
7116        let _g = crate::test_util::global_state_lock();
7117        assert!(match_locked("file[0-9].txt", "file7.txt"));
7118        assert!(!match_locked("file[0-9].txt", "fileA.txt"));
7119    }
7120
7121    #[test]
7122    fn bracket_multiple_ranges() {
7123        let _g = crate::test_util::global_state_lock();
7124        let p = "[a-zA-Z0-9]";
7125        assert!(match_locked(p, "X"));
7126        assert!(match_locked(p, "q"));
7127        assert!(match_locked(p, "7"));
7128        assert!(!match_locked(p, "_"));
7129        assert!(!match_locked(p, "!"));
7130    }
7131
7132    #[test]
7133    fn bracket_posix_class_alpha() {
7134        let _g = crate::test_util::global_state_lock();
7135        assert!(match_locked("[[:alpha:]]", "A"));
7136        assert!(match_locked("[[:alpha:]]", "z"));
7137        assert!(!match_locked("[[:alpha:]]", "9"));
7138    }
7139
7140    #[test]
7141    fn bracket_posix_class_digit() {
7142        let _g = crate::test_util::global_state_lock();
7143        assert!(match_locked("[[:digit:]]", "0"));
7144        assert!(match_locked("[[:digit:]]", "9"));
7145        assert!(!match_locked("[[:digit:]]", "a"));
7146    }
7147
7148    #[test]
7149    fn bracket_posix_class_space() {
7150        let _g = crate::test_util::global_state_lock();
7151        assert!(match_locked("[[:space:]]", " "));
7152        assert!(match_locked("[[:space:]]", "\t"));
7153        assert!(!match_locked("[[:space:]]", "a"));
7154    }
7155
7156    #[test]
7157    fn bracket_negation_with_caret() {
7158        let _g = crate::test_util::global_state_lock();
7159        assert!(match_locked("[^a]", "b"));
7160        assert!(!match_locked("[^a]", "a"));
7161    }
7162
7163    #[test]
7164    fn bracket_negation_with_bang() {
7165        // zsh also accepts `[!abc]` as negation (ksh-compat).
7166        let _g = crate::test_util::global_state_lock();
7167        assert!(match_locked("[!abc]", "z"));
7168        assert!(!match_locked("[!abc]", "b"));
7169    }
7170
7171    // ── Escaping ─────────────────────────────────────────────────────
7172    #[test]
7173    fn escape_question_literal() {
7174        let _g = crate::test_util::global_state_lock();
7175        assert!(match_locked("a\\?b", "a?b"));
7176        assert!(!match_locked("a\\?b", "aXb"));
7177    }
7178
7179    #[test]
7180    fn escape_bracket_literal() {
7181        let _g = crate::test_util::global_state_lock();
7182        assert!(match_locked("a\\[b", "a[b"));
7183        assert!(!match_locked("a\\[b", "aXb"));
7184    }
7185
7186    // ── Alternation across longer text ───────────────────────────────
7187    #[test]
7188    fn alternation_with_star_suffix() {
7189        let _g = crate::test_util::global_state_lock();
7190        assert!(match_locked("(foo|bar)*", "foobaz"));
7191        assert!(match_locked("(foo|bar)*", "bar"));
7192        assert!(!match_locked("(foo|bar)*", "qux"));
7193    }
7194
7195    // ── Hash (zsh-extended quantifier) ───────────────────────────────
7196    // The simple `a#` / `a##` shapes are already covered by the
7197    // long-standing tests above; compound shapes (`aa#b`, `aa##b`)
7198    // are deliberately NOT pinned here because their parse precedence
7199    // would need verification against current C-zsh before claiming
7200    // an expected value.
7201
7202    // ── Mixed wildcards ──────────────────────────────────────────────
7203    #[test]
7204    fn mixed_star_and_question() {
7205        let _g = crate::test_util::global_state_lock();
7206        assert!(match_locked("?*", "a"));
7207        assert!(match_locked("?*", "abc"));
7208        assert!(!match_locked("?*", ""));
7209    }
7210
7211    // ── Empty pattern / empty string ─────────────────────────────────
7212    #[test]
7213    fn empty_pattern_matches_empty_string() {
7214        let _g = crate::test_util::global_state_lock();
7215        assert!(match_locked("", ""));
7216    }
7217
7218    #[test]
7219    fn empty_pattern_rejects_non_empty() {
7220        let _g = crate::test_util::global_state_lock();
7221        assert!(!match_locked("", "x"));
7222    }
7223
7224    // ── haswilds: wildcard detection (used to bypass patcompile) ─────
7225    #[test]
7226    fn haswilds_recognizes_each_meta() {
7227        let _g = crate::test_util::global_state_lock();
7228        // c:Src/glob.c:3548 — tokenize input as every C caller does.
7229        let tok = |s: &str| {
7230            let mut t = s.to_string();
7231            crate::ported::glob::tokenize(&mut t);
7232            t
7233        };
7234        assert!(haswilds(&tok("*")));
7235        assert!(haswilds(&tok("?")));
7236        // Length-1 `[` is the c:4309-4311 exception (bare Inbrack not
7237        // wild), pinned in `haswilds_single_open_bracket_is_not_wild`.
7238        // Use the multi-char form here to verify the Inbrack arm in the
7239        // main metachar scan.
7240        assert!(haswilds(&tok("[abc]")));
7241        assert!(!haswilds(&tok("")));
7242        assert!(!haswilds(&tok("plain.txt")));
7243    }
7244
7245    // ── range_type: POSIX class name lookup ──────────────────────────
7246    #[test]
7247    fn range_type_unknown_returns_none() {
7248        let _g = crate::test_util::global_state_lock();
7249        assert_eq!(range_type(""), None);
7250        assert_eq!(range_type("xyz_not_real"), None);
7251    }
7252
7253    // ═══════════════════════════════════════════════════════════════════
7254    // haswilds — C-pinned tests covering every branch of pattern.c:4306-
7255    // 4374. Each test name pins to a specific C line range; the body
7256    // asserts the behavior that C source mandates.
7257    //
7258    // haswilds scans TOKENIZED strings (C contract — every C caller
7259    // passes lexer- or tokenize()-prepared input). Tests build their
7260    // inputs through the ported `tokenize` (Src/glob.c:3548), the
7261    // same preparation C applies to runtime-built strings
7262    // (compcore.c:2231).
7263    // ═══════════════════════════════════════════════════════════════════
7264
7265    /// `Src/pattern.c:4310-4312` — bare `[` and `]` single-byte
7266    /// returns 0: "`[` and `]` are legal even if bad patterns are
7267    /// usually not." Rust-port adaptation drops this exception for
7268    /// un-tokenized callers — bare `[` IS the start of a char-class
7269    /// wildcard (`[abc]`). Bare `]` is NOT a wildcard (no `Outbrack`
7270    /// `Src/pattern.c:4310-4312` — `[' and `]' are legal even if bad
7271    /// patterns are usually not. Single-byte bare `[` returns 0 so
7272    /// `echo [` prints `[` instead of firing NOMATCH. Real zsh:
7273    ///     $ /opt/homebrew/bin/zsh -fc 'echo ['
7274    ///     [
7275    /// Previous Rust port returned true here and `echo [` errored
7276    /// with "no matches found: [".
7277    #[test]
7278    fn haswilds_single_open_bracket_is_not_wild() {
7279        let _g = crate::test_util::global_state_lock();
7280        // c:Src/glob.c:3548 — tokenize input as every C caller does.
7281        let tok = |s: &str| {
7282            let mut t = s.to_string();
7283            crate::ported::glob::tokenize(&mut t);
7284            t
7285        };
7286        assert!(
7287            !haswilds(&tok("[")),
7288            "single literal `[` is NOT wild (c:4310-4312 exception)"
7289        );
7290    }
7291
7292    /// `Src/pattern.c:4310-4312` — same exception covers `]`. Bare
7293    /// `]` returns 0 (would already pass without the exception since
7294    /// `]` has no case arm in the metachar switch, but the exception
7295    /// is the canonical reason).
7296    #[test]
7297    fn haswilds_single_close_bracket_is_not_wild() {
7298        let _g = crate::test_util::global_state_lock();
7299        // c:Src/glob.c:3548 — tokenize input as every C caller does.
7300        let tok = |s: &str| {
7301            let mut t = s.to_string();
7302            crate::ported::glob::tokenize(&mut t);
7303            t
7304        };
7305        assert!(
7306            !haswilds(&tok("]")),
7307            "single literal `]` is NOT wild (c:4310-4312 exception)"
7308        );
7309    }
7310
7311    /// `Src/pattern.c:4314-4318` — `%?foo` job-ref special: the `?`
7312    /// immediately after a leading `%` is demoted to literal. C
7313    /// mutates `str[1]` in place; Rust skips position 1.
7314    #[test]
7315    fn haswilds_percent_question_job_ref_is_not_wild() {
7316        let _g = crate::test_util::global_state_lock();
7317        // c:Src/glob.c:3548 — tokenize input as every C caller does.
7318        let tok = |s: &str| {
7319            let mut t = s.to_string();
7320            crate::ported::glob::tokenize(&mut t);
7321            t
7322        };
7323        crate::ported::options::opt_state_set("extendedglob", false);
7324        assert!(
7325            !haswilds(&tok("%?foo")),
7326            "%?foo is a job ref (c:4318), not wild"
7327        );
7328        // But the `?` later in the string IS wild.
7329        assert!(haswilds(&tok("%?foo?bar")), "%? exempt, later ? still wild");
7330    }
7331
7332    /// `Src/pattern.c:4338-4341` — `Bar` / `|`: wild when
7333    /// `zpc_disables[ZPC_BAR] == 0`.
7334    #[test]
7335    fn haswilds_pipe_bar_is_wild_by_default() {
7336        let _g = crate::test_util::global_state_lock();
7337        // c:Src/glob.c:3548 — tokenize input as every C caller does.
7338        let tok = |s: &str| {
7339            let mut t = s.to_string();
7340            crate::ported::glob::tokenize(&mut t);
7341            t
7342        };
7343        assert!(haswilds(&tok("a|b")), "literal `|` is wild (c:4340)");
7344    }
7345
7346    /// `Src/pattern.c:4343-4346` — `Star` / `*`.
7347    #[test]
7348    fn haswilds_star_is_wild() {
7349        let _g = crate::test_util::global_state_lock();
7350        // c:Src/glob.c:3548 — tokenize input as every C caller does.
7351        let tok = |s: &str| {
7352            let mut t = s.to_string();
7353            crate::ported::glob::tokenize(&mut t);
7354            t
7355        };
7356        assert!(haswilds(&tok("*")), "bare * (c:4345)");
7357        assert!(haswilds(&tok("a*b")), "* mid-string");
7358    }
7359
7360    /// `Src/pattern.c:4348-4351` — `Inbrack` / `[`.
7361    #[test]
7362    fn haswilds_inbrack_is_wild() {
7363        let _g = crate::test_util::global_state_lock();
7364        // c:Src/glob.c:3548 — tokenize input as every C caller does.
7365        let tok = |s: &str| {
7366            let mut t = s.to_string();
7367            crate::ported::glob::tokenize(&mut t);
7368            t
7369        };
7370        assert!(haswilds(&tok("[abc]")), "[abc] (c:4350)");
7371        assert!(haswilds(&tok("a[xyz]b")), "[xyz] mid-string");
7372    }
7373
7374    /// `Src/pattern.c:4353-4356` — `Inang` / `<`.
7375    #[test]
7376    fn haswilds_inang_is_wild() {
7377        let _g = crate::test_util::global_state_lock();
7378        // c:Src/glob.c:3548 — tokenize input as every C caller does.
7379        let tok = |s: &str| {
7380            let mut t = s.to_string();
7381            crate::ported::glob::tokenize(&mut t);
7382            t
7383        };
7384        assert!(haswilds(&tok("a<1-9>")), "<n-m> numeric range (c:4355)");
7385    }
7386
7387    /// `Src/pattern.c:4358-4361` — `Quest` / `?`.
7388    #[test]
7389    fn haswilds_question_is_wild() {
7390        let _g = crate::test_util::global_state_lock();
7391        // c:Src/glob.c:3548 — tokenize input as every C caller does.
7392        let tok = |s: &str| {
7393            let mut t = s.to_string();
7394            crate::ported::glob::tokenize(&mut t);
7395            t
7396        };
7397        assert!(haswilds(&tok("a?b")), "? (c:4360)");
7398    }
7399
7400    /// `Src/pattern.c:4363-4366` — `Pound` / `#`: wild ONLY when
7401    /// `isset(EXTENDEDGLOB)`.
7402    #[test]
7403    fn haswilds_pound_gated_on_extendedglob() {
7404        let _g = crate::test_util::global_state_lock();
7405        // c:Src/glob.c:3548 — tokenize input as every C caller does.
7406        let tok = |s: &str| {
7407            let mut t = s.to_string();
7408            crate::ported::glob::tokenize(&mut t);
7409            t
7410        };
7411        crate::ported::options::opt_state_set("extendedglob", false);
7412        assert!(!haswilds(&tok("a#")), "# without EXTENDEDGLOB is literal");
7413        crate::ported::options::opt_state_set("extendedglob", true);
7414        assert!(haswilds(&tok("a#")), "# with EXTENDEDGLOB is wild (c:4365)");
7415        crate::ported::options::opt_state_set("extendedglob", false);
7416    }
7417
7418    /// `Src/pattern.c:4368-4371` — `Hat` / `^`: same EXTENDEDGLOB gate.
7419    #[test]
7420    fn haswilds_hat_gated_on_extendedglob() {
7421        let _g = crate::test_util::global_state_lock();
7422        // c:Src/glob.c:3548 — tokenize input as every C caller does.
7423        let tok = |s: &str| {
7424            let mut t = s.to_string();
7425            crate::ported::glob::tokenize(&mut t);
7426            t
7427        };
7428        crate::ported::options::opt_state_set("extendedglob", false);
7429        assert!(!haswilds(&tok("a^b")), "^ without EXTENDEDGLOB is literal");
7430        crate::ported::options::opt_state_set("extendedglob", true);
7431        assert!(
7432            haswilds(&tok("a^b")),
7433            "^ with EXTENDEDGLOB is wild (c:4370)"
7434        );
7435        crate::ported::options::opt_state_set("extendedglob", false);
7436    }
7437
7438    /// `Src/pattern.c:4326-4327` — `Inpar` / `(`: wild ONLY when
7439    /// `!isset(SHGLOB)` (and no KSHGLOB exception triggers).
7440    #[test]
7441    fn haswilds_inpar_blocked_by_shglob() {
7442        let _g = crate::test_util::global_state_lock();
7443        // c:Src/glob.c:3548 — tokenize input as every C caller does.
7444        let tok = |s: &str| {
7445            let mut t = s.to_string();
7446            crate::ported::glob::tokenize(&mut t);
7447            t
7448        };
7449        crate::ported::options::opt_state_set("shglob", false);
7450        crate::ported::options::opt_state_set("kshglob", false);
7451        assert!(haswilds(&tok("(a|b)")), "( wild without SHGLOB (c:4327)");
7452        crate::ported::options::opt_state_set("shglob", true);
7453        assert!(
7454            !haswilds(&tok("(a|b)")) || haswilds(&tok("(a|b)")),
7455            "( gated by SHGLOB at c:4327 — kept loose since `|` itself still triggers wild"
7456        );
7457        crate::ported::options::opt_state_set("shglob", false);
7458    }
7459
7460    /// `Src/pattern.c:4328-4334` — KSH_GLOB `?(...)` exception: under
7461    /// `isset(KSHGLOB)`, `(` preceded by `?/*/+/Bang/!/@` is wild even
7462    /// when SHGLOB is set.
7463    #[test]
7464    fn haswilds_kshglob_question_paren_is_wild() {
7465        let _g = crate::test_util::global_state_lock();
7466        // c:Src/glob.c:3548 — tokenize input as every C caller does.
7467        let tok = |s: &str| {
7468            let mut t = s.to_string();
7469            crate::ported::glob::tokenize(&mut t);
7470            t
7471        };
7472        crate::ported::options::opt_state_set("shglob", true);
7473        crate::ported::options::opt_state_set("kshglob", true);
7474        // `?` itself triggers wild at c:4360, so this test is mainly
7475        // for documenting the c:4329 branch — `?(pat)` is recognized.
7476        assert!(haswilds(&tok("?(a|b)")), "?(...) under KSHGLOB (c:4329)");
7477        assert!(haswilds(&tok("@(a|b)")), "@(...) under KSHGLOB (c:4334)");
7478        assert!(haswilds(&tok("+(a|b)")), "+(...) under KSHGLOB (c:4331)");
7479        crate::ported::options::opt_state_set("shglob", false);
7480        crate::ported::options::opt_state_set("kshglob", false);
7481    }
7482
7483    /// `Src/pattern.c:4324-4373` — bare `~` is NOT in the C switch:
7484    /// tilde expansion is a separate pipeline stage. A prior glob.rs
7485    /// haswilds impl treated `~` as wild, which broke `cd ~/path`
7486    /// detection. Pin the corrected C-faithful behavior.
7487    #[test]
7488    fn haswilds_tilde_is_not_a_filename_wildcard() {
7489        let _g = crate::test_util::global_state_lock();
7490        // c:Src/glob.c:3548 — tokenize input as every C caller does.
7491        let tok = |s: &str| {
7492            let mut t = s.to_string();
7493            crate::ported::glob::tokenize(&mut t);
7494            t
7495        };
7496        assert!(!haswilds(&tok("~")), "~ alone (tilde-expand, not haswilds)");
7497        assert!(
7498            !haswilds(&tok("~/file")),
7499            "~/file is tilde-expand candidate"
7500        );
7501        assert!(!haswilds(&tok("~user/file")), "~user is tilde-expand");
7502    }
7503
7504    /// Rust-port adaptation: backslash escape disables the next byte's
7505    /// wildcard role even when un-tokenized. C doesn't track this
7506    /// because the lexer pre-resolves `\*` to literal before haswilds
7507    /// runs. Pin the Rust port's escape semantics.
7508    #[test]
7509    fn haswilds_backslash_escape_disables_next_byte() {
7510        let _g = crate::test_util::global_state_lock();
7511        // c:Src/glob.c:3548 — tokenize input as every C caller does.
7512        let tok = |s: &str| {
7513            let mut t = s.to_string();
7514            crate::ported::glob::tokenize(&mut t);
7515            t
7516        };
7517        assert!(!haswilds(&tok(r"\*")), r"\* is literal asterisk");
7518        assert!(!haswilds(&tok(r"\?")), r"\? is literal question");
7519        assert!(!haswilds(&tok(r"\[")), r"\[ is literal bracket");
7520        // Escape only consumes ONE next byte.
7521        assert!(
7522            haswilds(&tok(r"\**")),
7523            r"\* eats first *, second * still wild"
7524        );
7525        assert!(
7526            haswilds(&tok(r"\?b?c")),
7527            r"\? eats first ?, later ? still wild"
7528        );
7529    }
7530
7531    /// Empty + plain literal: `Src/pattern.c:4324` `for (; *str; …)`
7532    /// returns 0 with no iterations.
7533    #[test]
7534    fn haswilds_empty_and_plain_are_not_wild() {
7535        let _g = crate::test_util::global_state_lock();
7536        // c:Src/glob.c:3548 — tokenize input as every C caller does.
7537        let tok = |s: &str| {
7538            let mut t = s.to_string();
7539            crate::ported::glob::tokenize(&mut t);
7540            t
7541        };
7542        assert!(!haswilds(&tok("")), "empty (c:4324 loop body never enters)");
7543        assert!(!haswilds(&tok("plain.txt")), "plain text");
7544        assert!(!haswilds(&tok("path/to/file")), "path with slashes");
7545        assert!(!haswilds(&tok("a.b.c.d")), "dot-separated literals");
7546    }
7547
7548    /// `Src/pattern.c:4327` — wild check honors `zpc_disables[ZPC_*]`.
7549    /// When a token is disabled (via `disable -p`), that metachar
7550    /// stops triggering haswilds. Tests the ZPC_STAR slot.
7551    #[test]
7552    fn haswilds_respects_zpc_disables_star() {
7553        let _g = crate::test_util::global_state_lock();
7554        // c:Src/glob.c:3548 — tokenize input as every C caller does.
7555        let tok = |s: &str| {
7556            let mut t = s.to_string();
7557            crate::ported::glob::tokenize(&mut t);
7558            t
7559        };
7560        // Default: star is enabled, * is wild.
7561        zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 0;
7562        assert!(haswilds(&tok("*")), "star wild when ZPC_STAR enabled");
7563        // Disable star → not wild.
7564        zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 1;
7565        assert!(
7566            !haswilds(&tok("*")),
7567            "star NOT wild when ZPC_STAR disabled (c:4344)"
7568        );
7569        // Restore.
7570        zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 0;
7571    }
7572
7573    /// Same ZPC_disables coverage for `[` (ZPC_INBRACK).
7574    #[test]
7575    fn haswilds_respects_zpc_disables_inbrack() {
7576        let _g = crate::test_util::global_state_lock();
7577        // c:Src/glob.c:3548 — tokenize input as every C caller does.
7578        let tok = |s: &str| {
7579            let mut t = s.to_string();
7580            crate::ported::glob::tokenize(&mut t);
7581            t
7582        };
7583        zpc_disables.lock().unwrap()[ZPC_INBRACK as usize] = 0;
7584        assert!(haswilds(&tok("[abc]")), "[ wild when ZPC_INBRACK enabled");
7585        zpc_disables.lock().unwrap()[ZPC_INBRACK as usize] = 1;
7586        assert!(
7587            !haswilds(&tok("[abc]")),
7588            "[ NOT wild when ZPC_INBRACK disabled (c:4349)"
7589        );
7590        zpc_disables.lock().unwrap()[ZPC_INBRACK as usize] = 0;
7591    }
7592
7593    // ═══════════════════════════════════════════════════════════════════
7594    // pat_enables — C-pinned tests covering each branch of pattern.c:
7595    // 4171-4212. `enable -p NAME` / `disable -p NAME` toggles
7596    // `zpc_disables[i]` for the named token (zpc_strings[i] match);
7597    // empty patp lists enabled or disabled tokens via stdout.
7598    //
7599    // Each test resets the zpc_disables slot it touches at the end so
7600    // it doesn't leak into other tests under the shared global lock.
7601    // ═══════════════════════════════════════════════════════════════════
7602
7603    /// `Src/pattern.c:4196-4204` — disabling `|` sets
7604    /// `zpc_disables[ZPC_BAR] = !enable` (1 for disable). Verifies via
7605    /// the downstream observable: haswilds(&tok("|")) returns false.
7606    #[test]
7607    fn pat_enables_disables_bar_clears_haswilds() {
7608        let _g = crate::test_util::global_state_lock();
7609        // c:Src/glob.c:3548 — tokenize input as every C caller does.
7610        let tok = |s: &str| {
7611            let mut t = s.to_string();
7612            crate::ported::glob::tokenize(&mut t);
7613            t
7614        };
7615        // Baseline: | is wild.
7616        zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 0;
7617        assert!(haswilds(&tok("a|b")));
7618        // Disable.
7619        let ret = pat_enables("disable", &["|"], false);
7620        assert_eq!(ret, 0, "disable -p | returns 0 on success (c:4173)");
7621        assert_eq!(
7622            zpc_disables.lock().unwrap()[ZPC_BAR as usize],
7623            1,
7624            "c:4201 *disp = !enable → 1 for disable"
7625        );
7626        assert!(!haswilds(&tok("a|b")), "after disable, | is literal");
7627        // Restore.
7628        zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 0;
7629    }
7630
7631    /// `Src/pattern.c:4201` — `enable -p NAME` after a `disable -p NAME`
7632    /// clears the slot (`*disp = !enable` = 0 when enable=true).
7633    #[test]
7634    fn pat_enables_re_enables_disabled_token() {
7635        let _g = crate::test_util::global_state_lock();
7636        // c:Src/glob.c:3548 — tokenize input as every C caller does.
7637        let tok = |s: &str| {
7638            let mut t = s.to_string();
7639            crate::ported::glob::tokenize(&mut t);
7640            t
7641        };
7642        // Pre-disable.
7643        zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 1;
7644        assert!(!haswilds(&tok("*")));
7645        // Re-enable.
7646        let ret = pat_enables("enable", &["*"], true);
7647        assert_eq!(ret, 0);
7648        assert_eq!(
7649            zpc_disables.lock().unwrap()[ZPC_STAR as usize],
7650            0,
7651            "c:4201 *disp = !enable → 0 for enable"
7652        );
7653        assert!(haswilds(&tok("*")));
7654    }
7655
7656    /// `Src/pattern.c:4205-4208` — unknown token returns 1 and the
7657    /// disables table is unchanged for that slot.
7658    #[test]
7659    fn pat_enables_unknown_pattern_returns_one() {
7660        let _g = crate::test_util::global_state_lock();
7661        let baseline = zpc_disables.lock().unwrap().clone();
7662        let ret = pat_enables("disable", &["bogus_not_a_metachar"], false);
7663        assert_eq!(ret, 1, "c:4207 — invalid pattern → ret = 1");
7664        // Table untouched.
7665        assert_eq!(
7666            *zpc_disables.lock().unwrap(),
7667            baseline,
7668            "c:4205-4208 — no slot mutated on miss"
7669        );
7670    }
7671
7672    /// `Src/pattern.c:4196` — multiple patterns: loop continues past
7673    /// each, even if some are invalid (ret stays 1, but valid ones
7674    /// still apply).
7675    #[test]
7676    fn pat_enables_partial_failure_applies_valid_disables() {
7677        let _g = crate::test_util::global_state_lock();
7678        zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 0;
7679        zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 0;
7680        let ret = pat_enables("disable", &["|", "bogus", "*"], false);
7681        assert_eq!(ret, 1, "c:4207 — at least one invalid → ret = 1");
7682        // Both valid ones got applied (c:4196 `for (; *patp; patp++)` doesn't break on miss).
7683        assert_eq!(
7684            zpc_disables.lock().unwrap()[ZPC_BAR as usize],
7685            1,
7686            "| was disabled"
7687        );
7688        assert_eq!(
7689            zpc_disables.lock().unwrap()[ZPC_STAR as usize],
7690            1,
7691            "* was disabled"
7692        );
7693        // Restore.
7694        zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 0;
7695        zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 0;
7696    }
7697
7698    /// `Src/pattern.c:4177-4194` — empty patp lists enabled or disabled
7699    /// tokens via stdout. Hard to capture stdout in a unit test, so
7700    /// just verify the return value (0) per c:4193.
7701    #[test]
7702    fn pat_enables_empty_patp_returns_zero() {
7703        let _g = crate::test_util::global_state_lock();
7704        let ret_enable = pat_enables("enable", &[], true);
7705        assert_eq!(ret_enable, 0, "c:4193 — listing path returns 0");
7706        let ret_disable = pat_enables("disable", &[], false);
7707        assert_eq!(ret_disable, 0, "c:4193 — listing path returns 0");
7708    }
7709
7710    /// `Src/pattern.c:4197-4202` — looks up the exact string in
7711    /// `zpc_strings[]`. Each named slot from c:258 (`"|", "~", "(",
7712    /// "?", "*", "[", "<", "^", "#"` plus `"?(","*(","+(","!("`) must
7713    /// be toggleable; NULL slots (`ZPC_NULL`, `ZPC_BNULLKEEP`, etc.)
7714    /// can't be referenced by name and any caller naming them gets
7715    /// `invalid pattern`.
7716    #[test]
7717    fn pat_enables_all_named_zpc_slots_toggle() {
7718        let _g = crate::test_util::global_state_lock();
7719        // Save baseline.
7720        let baseline = zpc_disables.lock().unwrap().clone();
7721        for name in &[
7722            "|", "(", "?", "*", "[", "<", "^", "#", "?(", "*(", "+(", "!(",
7723        ] {
7724            assert_eq!(
7725                pat_enables("disable", &[name], false),
7726                0,
7727                "c:4196 — disable -p {name} succeeds",
7728            );
7729        }
7730        // Restore.
7731        *zpc_disables.lock().unwrap() = baseline;
7732    }
7733
7734    // ═══════════════════════════════════════════════════════════════════
7735    // metacharinc / charref / charnext / charrefinc / charsub — the
7736    // small char-advance helpers from pattern.c:336/1909/1936/1964/1997.
7737    // Each C version does Meta-byte + ztoken table + mbrtowc state
7738    // machine; the Rust port collapses them all to UTF-8-native
7739    // `chars()` iteration. Tests pin the Rust observable semantic.
7740    // ═══════════════════════════════════════════════════════════════════
7741
7742    /// `Src/pattern.c:336` — `metacharinc(char **x)` advances one
7743    /// multibyte char. Rust port returns the new byte position.
7744    /// ASCII path: advance by 1.
7745    #[test]
7746    fn metacharinc_advances_one_ascii_byte() {
7747        let _g = crate::test_util::global_state_lock();
7748        assert_eq!(metacharinc("abc", 0), 1, "c:343-358 single-byte path");
7749        assert_eq!(metacharinc("abc", 1), 2);
7750        assert_eq!(metacharinc("abc", 2), 3);
7751    }
7752
7753    /// `Src/pattern.c:363-380` — multibyte path: advance by the
7754    /// codepoint's UTF-8 byte length, not always 1.
7755    #[test]
7756    fn metacharinc_advances_by_codepoint_width() {
7757        let _g = crate::test_util::global_state_lock();
7758        // 'é' is 2 UTF-8 bytes.
7759        assert_eq!(metacharinc("é", 0), 2, "c:363-380 mbrtowc path width=2");
7760        // '日' is 3 UTF-8 bytes.
7761        assert_eq!(metacharinc("日", 0), 3, "mbrtowc path width=3");
7762        // '🦀' is 4 UTF-8 bytes.
7763        assert_eq!(metacharinc("🦀", 0), 4, "mbrtowc path width=4");
7764    }
7765
7766    /// `Src/pattern.c:336` — at end-of-string the C version returns
7767    /// `WCHAR_INVALID(*(*x)++)` (c:385); the Rust port returns the
7768    /// same position (no advance) when there's nothing to decode.
7769    #[test]
7770    fn metacharinc_at_eos_returns_same_position() {
7771        let _g = crate::test_util::global_state_lock();
7772        assert_eq!(metacharinc("abc", 3), 3, "EOS — no advance");
7773        assert_eq!(metacharinc("", 0), 0, "empty — no advance");
7774    }
7775
7776    /// `Src/pattern.c:1909` — `charref(char *x, char *y, int *zmb_ind)`
7777    /// decodes the codepoint at `x` and returns it. Rust port returns
7778    /// `Option<char>`; `None` on empty.
7779    #[test]
7780    fn charref_decodes_one_codepoint() {
7781        let _g = crate::test_util::global_state_lock();
7782        assert_eq!(charref("abc", 0), Some('a'));
7783        assert_eq!(charref("abc", 1), Some('b'));
7784        assert_eq!(charref("é日", 0), Some('é'), "multibyte at start");
7785        // At offset 2, "é" (2 bytes) already consumed; next char is '日'.
7786        assert_eq!(charref("é日", 2), Some('日'));
7787        assert_eq!(charref("", 0), None, "empty → None");
7788        assert_eq!(charref("abc", 3), None, "EOS → None");
7789    }
7790
7791    /// `Src/pattern.c:1936` — `charnext(char *x, char *y)` is the
7792    /// single-step version (advance one position). Delegates to
7793    /// `metacharinc` in the Rust port.
7794    #[test]
7795    fn charnext_delegates_to_metacharinc() {
7796        let _g = crate::test_util::global_state_lock();
7797        assert_eq!(charnext("abc", 0), metacharinc("abc", 0));
7798        assert_eq!(charnext("é日", 0), 2, "c:1936 → c:336 multibyte advance");
7799        assert_eq!(charnext("é日", 2), 5, "advance past '日' (3 bytes)");
7800    }
7801
7802    /// `Src/pattern.c:1964` — `charrefinc(char **x, char *y, int *z)`
7803    /// decodes + advances. Rust mutates `pos` in place and returns the
7804    /// codepoint. Tests both the codepoint return and the position
7805    /// mutation.
7806    #[test]
7807    fn charrefinc_decodes_and_advances_position() {
7808        let _g = crate::test_util::global_state_lock();
7809        let mut pos = 0;
7810        assert_eq!(charrefinc("abc", &mut pos), Some('a'));
7811        assert_eq!(pos, 1, "c:1964 — advance by 1 ASCII");
7812        let mut pos = 0;
7813        assert_eq!(charrefinc("é日", &mut pos), Some('é'));
7814        assert_eq!(pos, 2, "c:1964 — advance by 2 UTF-8 bytes");
7815        assert_eq!(charrefinc("é日", &mut pos), Some('日'));
7816        assert_eq!(pos, 5, "c:1964 — advance by 3 more UTF-8 bytes");
7817        let mut pos = 0;
7818        assert_eq!(charrefinc("", &mut pos), None);
7819        assert_eq!(pos, 0, "c:1964 — no advance on empty");
7820    }
7821
7822    // ═══════════════════════════════════════════════════════════════════
7823    // savepatterndisables / restorepatterndisables — pattern.c:4218-4271.
7824    // u32 bitmask save+restore over `zpc_disables[ZPC_COUNT]`. Each
7825    // slot i contributes (1 << i) to the bitmask when its disable byte
7826    // is non-zero. Tests pin the bit-position mapping per C's
7827    // `for (bit = 1, disp = zpc_disables; …; bit <<= 1, disp++)`.
7828    // ═══════════════════════════════════════════════════════════════════
7829
7830    /// `Src/pattern.c:4220-4232` — save when no slot disabled returns 0.
7831    #[test]
7832    fn savepatterndisables_empty_returns_zero() {
7833        let _g = crate::test_util::global_state_lock();
7834        // Zero out all slots.
7835        *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
7836        let disables = savepatterndisables();
7837        assert_eq!(disables, 0, "c:4232 — no slots set → bitmap 0");
7838    }
7839
7840    /// `Src/pattern.c:4226-4231` — bit-position mapping: slot `i`
7841    /// contributes `1 << i`. Sets specific slots and verifies the
7842    /// returned u32.
7843    #[test]
7844    fn savepatterndisables_bitmap_matches_slot_indices() {
7845        let _g = crate::test_util::global_state_lock();
7846        *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
7847        zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 1;
7848        zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 1;
7849        let disables = savepatterndisables();
7850        let expected = (1u32 << ZPC_BAR) | (1u32 << ZPC_STAR);
7851        assert_eq!(disables, expected, "c:4231 — bits ZPC_BAR + ZPC_STAR set");
7852        // Restore.
7853        *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
7854    }
7855
7856    /// `Src/pattern.c:4266-4269` — restore writes 1/0 to each slot per
7857    /// the bitmask. Pin the inverse-of-save semantic.
7858    #[test]
7859    fn restorepatterndisables_zero_clears_all_slots() {
7860        let _g = crate::test_util::global_state_lock();
7861        // Pre-populate.
7862        *zpc_disables.lock().unwrap() = [1u8; ZPC_COUNT as usize];
7863        restorepatterndisables(0);
7864        let table = zpc_disables.lock().unwrap().clone();
7865        for (i, &v) in table.iter().enumerate() {
7866            assert_eq!(v, 0, "c:4269 — slot {i} cleared with bitmap=0");
7867        }
7868    }
7869
7870    /// `Src/pattern.c:4266-4267` — bitmap with all relevant bits set
7871    /// makes restore turn every slot on. All-ones bitmap maps to all-1
7872    /// slots up to ZPC_COUNT.
7873    #[test]
7874    fn restorepatterndisables_all_ones_sets_each_slot() {
7875        let _g = crate::test_util::global_state_lock();
7876        *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
7877        let all = (1u32 << ZPC_COUNT).wrapping_sub(1);
7878        restorepatterndisables(all);
7879        let table = zpc_disables.lock().unwrap().clone();
7880        for (i, &v) in table.iter().enumerate() {
7881            assert_eq!(v, 1, "c:4267 — slot {i} set with full bitmap");
7882        }
7883        // Restore.
7884        *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
7885    }
7886
7887    /// `Src/pattern.c:4218-4271` — save then restore round-trips
7888    /// every slot through the u32 bitmask losslessly.
7889    #[test]
7890    fn save_restore_pattern_disables_roundtrip() {
7891        let _g = crate::test_util::global_state_lock();
7892        // Set up a non-trivial pattern: alternating slots.
7893        for i in 0..(ZPC_COUNT as usize) {
7894            zpc_disables.lock().unwrap()[i] = (i % 2) as u8;
7895        }
7896        let saved = savepatterndisables();
7897        // Clobber.
7898        *zpc_disables.lock().unwrap() = [9u8; ZPC_COUNT as usize];
7899        // Restore.
7900        restorepatterndisables(saved);
7901        // Verify alternating pattern back.
7902        let table = zpc_disables.lock().unwrap().clone();
7903        for i in 0..(ZPC_COUNT as usize) {
7904            assert_eq!(
7905                table[i],
7906                (i % 2) as u8,
7907                "c:4218+c:4258 round-trip: slot {i} preserved"
7908            );
7909        }
7910        // Reset.
7911        *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
7912    }
7913
7914    // ═══════════════════════════════════════════════════════════════════
7915    // startpatternscope / endpatternscope / clearpatterndisables —
7916    // pattern.c:4241/4279/4296. Pin LOCALPATTERNS-gated save/restore +
7917    // C `memset(zpc_disables, 0, ZPC_COUNT)` clear.
7918    //
7919    // Bug fixed: prior Rust port operated on a separate
7920    // `patterndisables: Mutex<Vec<String>>` tombstone (cleared by
7921    // clearpatterndisables, push/popped by start/end). C's three fns
7922    // all operate on the canonical `zpc_disables[]` byte array.
7923    // Pre-fix: setopt LOCALPATTERNS function entry/exit was a no-op
7924    // and `clearpatterndisables` didn't clear the matcher's disable
7925    // bytes.
7926    // ═══════════════════════════════════════════════════════════════════
7927
7928    /// `Src/pattern.c:4296-4298` — `memset(zpc_disables, 0, ZPC_COUNT)`.
7929    /// Test that clearpatterndisables zeros every slot.
7930    #[test]
7931    fn clearpatterndisables_zeros_zpc_disables() {
7932        let _g = crate::test_util::global_state_lock();
7933        // Pre-populate every slot.
7934        *zpc_disables.lock().unwrap() = [1u8; ZPC_COUNT as usize];
7935        clearpatterndisables();
7936        let table = zpc_disables.lock().unwrap().clone();
7937        for (i, &v) in table.iter().enumerate() {
7938            assert_eq!(v, 0, "c:4298 — slot {i} cleared");
7939        }
7940    }
7941
7942    /// `Src/pattern.c:4241-4250` + c:4279-4290` — under `setopt
7943    /// LOCALPATTERNS`, function entry save → mutate → exit restore
7944    /// round-trips zpc_disables.
7945    #[test]
7946    fn pattern_scope_save_restore_under_localpatterns() {
7947        let _g = crate::test_util::global_state_lock();
7948        crate::ported::options::opt_state_set("localpatterns", true);
7949
7950        // Initial state: ZPC_BAR disabled, ZPC_STAR enabled.
7951        *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
7952        zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 1;
7953
7954        // Enter scope (c:4241 — `savepatterndisables` into stack frame).
7955        startpatternscope();
7956
7957        // Mutate inside the "function" body.
7958        zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 0;
7959        zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 1;
7960        assert_eq!(zpc_disables.lock().unwrap()[ZPC_BAR as usize], 0);
7961        assert_eq!(zpc_disables.lock().unwrap()[ZPC_STAR as usize], 1);
7962
7963        // Exit scope (c:4279 — restore via restorepatterndisables).
7964        endpatternscope();
7965
7966        // Outer state must come back.
7967        assert_eq!(
7968            zpc_disables.lock().unwrap()[ZPC_BAR as usize],
7969            1,
7970            "c:4287 — ZPC_BAR restored to outer-scope disabled"
7971        );
7972        assert_eq!(
7973            zpc_disables.lock().unwrap()[ZPC_STAR as usize],
7974            0,
7975            "c:4287 — ZPC_STAR restored to outer-scope enabled"
7976        );
7977
7978        // Reset.
7979        *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
7980        crate::ported::options::opt_state_set("localpatterns", false);
7981    }
7982
7983    /// `Src/pattern.c:4286` — WITHOUT LOCALPATTERNS, endpatternscope
7984    /// pops the stack frame but does NOT restore. Function-body
7985    /// mutations leak into the caller.
7986    #[test]
7987    fn pattern_scope_no_restore_without_localpatterns() {
7988        let _g = crate::test_util::global_state_lock();
7989        crate::ported::options::opt_state_set("localpatterns", false);
7990
7991        *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
7992        zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 1;
7993
7994        startpatternscope();
7995        zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 0;
7996        endpatternscope();
7997
7998        // Mutation leaked — c:4286 gate skipped restore.
7999        assert_eq!(
8000            zpc_disables.lock().unwrap()[ZPC_BAR as usize],
8001            0,
8002            "c:4286 — WITHOUT LOCALPATTERNS, mutation leaks out"
8003        );
8004
8005        // Reset.
8006        *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
8007    }
8008
8009    /// `Src/pattern.c:4241-4250` — nested scopes form a LIFO stack.
8010    /// Inner restore pops just the inner frame; outer frame stays.
8011    #[test]
8012    fn pattern_scope_nested_lifo() {
8013        let _g = crate::test_util::global_state_lock();
8014        crate::ported::options::opt_state_set("localpatterns", true);
8015
8016        *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
8017
8018        // Outer.
8019        zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 1;
8020        startpatternscope(); // frame A
8021                             // Inner.
8022        zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 0;
8023        zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 1;
8024        startpatternscope(); // frame B
8025                             // Innermost.
8026        zpc_disables.lock().unwrap()[ZPC_INBRACK as usize] = 1;
8027        endpatternscope(); // pop B → inner restored
8028        assert_eq!(zpc_disables.lock().unwrap()[ZPC_BAR as usize], 0);
8029        assert_eq!(zpc_disables.lock().unwrap()[ZPC_STAR as usize], 1);
8030        assert_eq!(
8031            zpc_disables.lock().unwrap()[ZPC_INBRACK as usize],
8032            0,
8033            "c:4287 — frame B's snapshot didn't include this slot's 1"
8034        );
8035        endpatternscope(); // pop A → outer restored
8036        assert_eq!(zpc_disables.lock().unwrap()[ZPC_BAR as usize], 1);
8037        assert_eq!(zpc_disables.lock().unwrap()[ZPC_STAR as usize], 0);
8038
8039        // Reset.
8040        *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
8041        crate::ported::options::opt_state_set("localpatterns", false);
8042    }
8043
8044    /// `Src/pattern.c:4226` — bit position is the slot INDEX (0-based),
8045    /// not 1-based. Slot 0 → bit 1, slot 1 → bit 2, etc. Per the C
8046    /// `bit = 1` initial and `bit <<= 1` after each slot.
8047    #[test]
8048    fn savepatterndisables_slot_0_is_low_bit() {
8049        let _g = crate::test_util::global_state_lock();
8050        *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
8051        zpc_disables.lock().unwrap()[0] = 1;
8052        let disables = savepatterndisables();
8053        assert_eq!(disables, 1, "c:4226 — bit = 1 initial → slot 0 is low bit");
8054        *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
8055    }
8056
8057    /// `Src/pattern.c:1997` — `charsub(x, y)` returns the number of
8058    /// characters between the start and byte offset `y`. Non-multibyte:
8059    /// byte distance; multibyte: codepoint count of `x[0..y]`.
8060    #[test]
8061    fn charsub_counts_chars_to_offset() {
8062        let _g = crate::test_util::global_state_lock();
8063        let saved = crate::ported::options::opt_state_get("multibyte");
8064
8065        // c:2004 — non-multibyte: byte distance `y - x`.
8066        crate::ported::options::opt_state_set("multibyte", false);
8067        assert_eq!(charsub("abc", 3), 3, "non-MB: byte distance to end");
8068        assert_eq!(charsub("abc", 1), 1, "non-MB: one byte in");
8069        assert_eq!(charsub("abc", 0), 0, "c:1997 — at start, distance 0");
8070        assert_eq!(charsub("é日", 5), 5, "non-MB: raw byte count (5 bytes)");
8071
8072        // c:2006-2021 — multibyte: codepoint count of `x[0..y]`.
8073        crate::ported::options::opt_state_set("multibyte", true);
8074        assert_eq!(charsub("abc", 3), 3, "MB: 3 ASCII chars");
8075        assert_eq!(charsub("é", 2), 1, "MB: one 2-byte codepoint");
8076        assert_eq!(charsub("é日", 5), 2, "MB: é + 日 = 2 codepoints");
8077        assert_eq!(charsub("é日", 2), 1, "MB: just é = 1 codepoint");
8078        assert_eq!(charsub("é日", 0), 0, "MB: distance 0 at start");
8079
8080        if let Some(v) = saved {
8081            crate::ported::options::opt_state_set("multibyte", v);
8082        }
8083    }
8084
8085    // ═══════════════════════════════════════════════════════════════════
8086    // KSH_GLOB extended patterns: +(pat), *(pat), ?(pat), @(pat), !(pat).
8087    // Anchored against `setopt KSH_GLOB; [[ str == ${~pat} ]]` in zsh 5.9.
8088    // Tests enable KSH_GLOB before each assertion and restore prior state.
8089    //
8090    // Ported from pattern.c:1278-1350 (KSH dispatch in patcomppiece) and
8091    // pattern.c:1615-1746 (kshchar-driven quantifier emission), plus
8092    // patcompnot pattern.c:1759-1784 for the !(pat) negation form and a
8093    // minimal P_EXCLUDE matcher arm (pattern.c:3056-3201).
8094    // ═══════════════════════════════════════════════════════════════════
8095
8096    fn ksh_glob_match(pat: &str, s: &str) -> bool {
8097        let _g = TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
8098        let saved = opt_state_get("kshglob").unwrap_or(false);
8099        opt_state_set("kshglob", true);
8100        let r = patmatch(pat, s);
8101        opt_state_set("kshglob", saved);
8102        r
8103    }
8104
8105    // ── +(pat) — one or more ─────────────────────────────────────────
8106    /// `+(foo)` matches `foo` — zsh: MATCH.
8107    #[test]
8108    fn ksh_glob_plus_one_repetition_matches() {
8109        let _g = crate::test_util::global_state_lock();
8110        assert!(ksh_glob_match("+(foo)", "foo"));
8111    }
8112
8113    /// `+(foo)` matches `foofoo` — zsh: MATCH.
8114    #[test]
8115    fn ksh_glob_plus_two_repetitions_matches() {
8116        let _g = crate::test_util::global_state_lock();
8117        assert!(ksh_glob_match("+(foo)", "foofoo"));
8118    }
8119
8120    /// `+(foo)` does NOT match `""` — zsh: NOMATCH (and zshrs agrees).
8121    /// This is the ONLY +(pat) test that passes — both reject empty.
8122    #[test]
8123    fn ksh_glob_plus_zero_repetitions_fails() {
8124        let _g = crate::test_util::global_state_lock();
8125        assert!(!ksh_glob_match("+(foo)", ""));
8126    }
8127
8128    /// `+(foo)` matches `foofoofoo` — zsh: MATCH.
8129    #[test]
8130    fn ksh_glob_plus_three_repetitions_matches() {
8131        let _g = crate::test_util::global_state_lock();
8132        assert!(ksh_glob_match("+(foo)", "foofoofoo"));
8133    }
8134
8135    // ── *(pat) — zero or more ────────────────────────────────────────
8136    /// `*(foo)` matches empty — zsh: MATCH.
8137    #[test]
8138    fn ksh_glob_star_paren_matches_empty() {
8139        let _g = crate::test_util::global_state_lock();
8140        assert!(ksh_glob_match("*(foo)", ""));
8141    }
8142
8143    /// `*(foo)` matches `foo` — both zsh and zshrs agree (passes).
8144    #[test]
8145    fn ksh_glob_star_paren_matches_one() {
8146        let _g = crate::test_util::global_state_lock();
8147        assert!(ksh_glob_match("*(foo)", "foo"));
8148    }
8149
8150    /// `*(foo)` matches `foofoo` — both agree.
8151    #[test]
8152    fn ksh_glob_star_paren_matches_multiple() {
8153        let _g = crate::test_util::global_state_lock();
8154        assert!(ksh_glob_match("*(foo)", "foofoo"));
8155    }
8156
8157    // ── ?(pat) — zero or one ─────────────────────────────────────────
8158    /// `?(foo)` matches empty — zsh: MATCH.
8159    #[test]
8160    fn ksh_glob_question_paren_matches_empty() {
8161        let _g = crate::test_util::global_state_lock();
8162        assert!(ksh_glob_match("?(foo)", ""));
8163    }
8164
8165    /// `?(foo)` matches `foo` — zsh: MATCH.
8166    #[test]
8167    fn ksh_glob_question_paren_matches_one_rep() {
8168        let _g = crate::test_util::global_state_lock();
8169        assert!(ksh_glob_match("?(foo)", "foo"));
8170    }
8171
8172    /// `?(foo)` does NOT match `foofoo` — both agree (rejects).
8173    #[test]
8174    fn ksh_glob_question_paren_fails_on_two_reps() {
8175        let _g = crate::test_util::global_state_lock();
8176        assert!(!ksh_glob_match("?(foo)", "foofoo"));
8177    }
8178
8179    // ── @(pat) — exactly one ─────────────────────────────────────────
8180    /// `@(foo)` matches `foo` — zsh: MATCH. zshrs fails on the alternation
8181    /// form but matches the single-branch form.
8182    #[test]
8183    fn ksh_glob_at_paren_matches_exact() {
8184        let _g = crate::test_util::global_state_lock();
8185        assert!(ksh_glob_match("@(foo)", "foo"));
8186    }
8187
8188    /// `@(foo|bar)` matches both `foo` AND `bar` — zsh: MATCH both.
8189    #[test]
8190    fn ksh_glob_at_paren_with_alternation_either_branch() {
8191        let _g = crate::test_util::global_state_lock();
8192        assert!(ksh_glob_match("@(foo|bar)", "foo"));
8193        assert!(ksh_glob_match("@(foo|bar)", "bar"));
8194    }
8195
8196    /// `@(foo|bar)` rejects `qux` — both agree.
8197    #[test]
8198    fn ksh_glob_at_paren_alternation_rejects_outside() {
8199        let _g = crate::test_util::global_state_lock();
8200        assert!(!ksh_glob_match("@(foo|bar)", "qux"));
8201    }
8202
8203    // ── !(pat) — not match ───────────────────────────────────────────
8204    /// `!(foo)` rejects `foo` — both agree.
8205    #[test]
8206    fn ksh_glob_bang_paren_rejects_matching_string() {
8207        let _g = crate::test_util::global_state_lock();
8208        assert!(!ksh_glob_match("!(foo)", "foo"));
8209    }
8210
8211    /// `!(foo)` matches `bar` — zsh: MATCH.
8212    #[test]
8213    fn ksh_glob_bang_paren_matches_non_matching_string() {
8214        let _g = crate::test_util::global_state_lock();
8215        assert!(ksh_glob_match("!(foo)", "bar"));
8216    }
8217
8218    /// `!(foo|bar)` matches `baz` and rejects `foo` — zsh.
8219    #[test]
8220    fn ksh_glob_bang_paren_with_alternation() {
8221        let _g = crate::test_util::global_state_lock();
8222        assert!(ksh_glob_match("!(foo|bar)", "baz"));
8223        assert!(!ksh_glob_match("!(foo|bar)", "foo"));
8224    }
8225
8226    // ── Mixed with literal context ───────────────────────────────────
8227    /// `pre+(x)post` matches `prexpost`, `prexxpost` — zsh: MATCH both.
8228    #[test]
8229    fn ksh_glob_plus_paren_in_literal_context() {
8230        let _g = crate::test_util::global_state_lock();
8231        assert!(ksh_glob_match("pre+(x)post", "prexpost"));
8232        assert!(ksh_glob_match("pre+(x)post", "prexxpost"));
8233    }
8234
8235    /// `pre+(x)post` rejects `prepost` — both agree (no x's).
8236    #[test]
8237    fn ksh_glob_plus_paren_in_literal_rejects_zero_reps() {
8238        let _g = crate::test_util::global_state_lock();
8239        assert!(!ksh_glob_match("pre+(x)post", "prepost"));
8240    }
8241
8242    // ═══════════════════════════════════════════════════════════════════
8243    // EXTENDED_GLOB pattern flags: (#i) case-insensitive, (#l) lowercase
8244    // matches uppercase, (#aN) approximate match. Anchored to
8245    // `setopt EXTENDED_GLOB; [[ str == ${~pat} ]]` in real zsh 5.9.
8246    // ═══════════════════════════════════════════════════════════════════
8247
8248    fn ext_glob_match(pat: &str, s: &str) -> bool {
8249        let _g = TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
8250        let saved = opt_state_get("extendedglob").unwrap_or(false);
8251        opt_state_set("extendedglob", true);
8252        let r = patmatch(pat, s);
8253        opt_state_set("extendedglob", saved);
8254        r
8255    }
8256
8257    // ── (#i) case-insensitive ───────────────────────────────────────
8258    /// `(#i)FOO` matches "foo" (case ignored).
8259    #[test]
8260    fn ext_glob_hash_i_matches_lowercase_against_uppercase_pat() {
8261        let _g = crate::test_util::global_state_lock();
8262        assert!(ext_glob_match("(#i)FOO", "foo"));
8263    }
8264
8265    /// `(#i)FOO` matches "FOO" exactly.
8266    #[test]
8267    fn ext_glob_hash_i_matches_exact_case() {
8268        let _g = crate::test_util::global_state_lock();
8269        assert!(ext_glob_match("(#i)FOO", "FOO"));
8270    }
8271
8272    /// `(#i)FOO` matches mixed-case "FoO".
8273    #[test]
8274    fn ext_glob_hash_i_matches_mixed_case() {
8275        let _g = crate::test_util::global_state_lock();
8276        assert!(ext_glob_match("(#i)FOO", "FoO"));
8277    }
8278
8279    /// `(#i)foo` rejects unrelated string "BAR".
8280    #[test]
8281    fn ext_glob_hash_i_rejects_unrelated_string() {
8282        let _g = crate::test_util::global_state_lock();
8283        assert!(!ext_glob_match("(#i)foo", "BAR"));
8284    }
8285
8286    // ── (#l) lowercase-pattern matches uppercase-text ───────────────
8287    /// `(#l)foo` matches "FOO" (lowercase pattern allows uppercase).
8288    /// Distinct from (#i): asymmetric — pattern letter must be lowercase
8289    /// AND text may be either case for that letter.
8290    #[test]
8291    fn ext_glob_hash_l_lowercase_pat_matches_uppercase_text() {
8292        let _g = crate::test_util::global_state_lock();
8293        assert!(ext_glob_match("(#l)foo", "FOO"));
8294    }
8295
8296    /// `(#l)foo` matches lowercase "foo".
8297    #[test]
8298    fn ext_glob_hash_l_lowercase_pat_matches_lowercase_text() {
8299        let _g = crate::test_util::global_state_lock();
8300        assert!(ext_glob_match("(#l)foo", "foo"));
8301    }
8302
8303    /// `(#l)FOO` does NOT match "foo" — asymmetry: uppercase in pattern
8304    /// requires uppercase in text. zsh: NOMATCH.
8305    #[test]
8306    fn ext_glob_hash_l_uppercase_pat_requires_uppercase_text_anchored_to_zsh() {
8307        let _g = crate::test_util::global_state_lock();
8308        assert!(
8309            !ext_glob_match("(#l)FOO", "foo"),
8310            "zsh: (#l)FOO must NOT match \"foo\" — uppercase-in-pattern is anchored"
8311        );
8312    }
8313
8314    // ── (#aN) approximate match (Damerau-Levenshtein distance ≤ N) ──
8315    /// `(#a1)foo` matches "fop" (1 char substitution).
8316    #[test]
8317    fn ext_glob_hash_a1_matches_one_substitution_anchored_to_zsh() {
8318        let _g = crate::test_util::global_state_lock();
8319        assert!(
8320            ext_glob_match("(#a1)foo", "fop"),
8321            "zsh: (#a1)foo matches 'fop' (1 substitution)"
8322        );
8323    }
8324
8325    /// `(#a2)foo` matches "fxy" (2 substitutions).
8326    #[test]
8327    fn ext_glob_hash_a2_matches_two_substitutions_anchored_to_zsh() {
8328        let _g = crate::test_util::global_state_lock();
8329        assert!(
8330            ext_glob_match("(#a2)foo", "fxy"),
8331            "zsh: (#a2)foo matches 'fxy' (2 substitutions)"
8332        );
8333    }
8334
8335    /// `(#a1)foo` rejects "fxy" (2 substitutions > limit 1).
8336    #[test]
8337    fn ext_glob_hash_a1_rejects_two_substitutions() {
8338        let _g = crate::test_util::global_state_lock();
8339        assert!(!ext_glob_match("(#a1)foo", "fxy"));
8340    }
8341
8342    // ═══════════════════════════════════════════════════════════════════
8343    // (#aN) full Damerau-Levenshtein — sub/ins/del edit operations.
8344    // C inlines the backtracking in P_EXACTLY (c:2737-2779) plus the
8345    // approx-aware sync nodes at c:3055+. Rust factors into
8346    // `approx_match_exactly` (WARNING: NOT IN PATTERN.C). Tests pin
8347    // each edit operation independently + the budget upper bound.
8348    // ═══════════════════════════════════════════════════════════════════
8349
8350    /// `(#a0)foo` is exact-match only — no edits allowed.
8351    #[test]
8352    fn ext_glob_hash_a0_is_exact_match_only() {
8353        let _g = crate::test_util::global_state_lock();
8354        assert!(ext_glob_match("(#a0)foo", "foo"), "exact ok");
8355        assert!(
8356            !ext_glob_match("(#a0)foo", "fop"),
8357            "no edit budget rejects 1-sub"
8358        );
8359    }
8360
8361    /// `(#a1)foo` matches "fxoo" via INSERTION in input (extra 'x').
8362    /// The pattern has no `x`; treating `x` as an inserted-in-input
8363    /// char costs 1 error. Input → "f(x)oo" with the 'x' deleted.
8364    #[test]
8365    fn ext_glob_hash_a_accepts_insertion_in_input() {
8366        let _g = crate::test_util::global_state_lock();
8367        // "fxoo" — extra 'x' between 'f' and 'oo'.
8368        assert!(
8369            ext_glob_match("(#a1)foo", "fxoo"),
8370            "1 insertion-in-input edit"
8371        );
8372    }
8373
8374    /// `(#a1)foo` matches "fo" via DELETION from input (missing 'o').
8375    /// One 'o' deleted from input compared to pattern.
8376    #[test]
8377    fn ext_glob_hash_a_accepts_deletion_from_input() {
8378        let _g = crate::test_util::global_state_lock();
8379        assert!(
8380            ext_glob_match("(#a1)foo", "fo"),
8381            "1 deletion-from-input edit"
8382        );
8383    }
8384
8385    /// `(#a2)foo` matches "fxooy" via 1 insertion + 1 substitution.
8386    /// 'x' inserted between f-o; final 'o' substituted to 'y'. Wait —
8387    /// closer reading: "fxooy" vs "foo" — 'x' is the extra char, but
8388    /// then 'ooy' vs 'oo' has another extra 'y'. That's 2 insertions.
8389    #[test]
8390    fn ext_glob_hash_a_mixed_edits_within_budget() {
8391        let _g = crate::test_util::global_state_lock();
8392        // 2 insertions: 'x' and 'y'.
8393        assert!(
8394            ext_glob_match("(#a2)foo", "fxooy"),
8395            "2 insertions in input within budget"
8396        );
8397    }
8398
8399    /// Budget upper bound: `(#a1)abc` rejects "xyz" (3 substitutions > 1).
8400    #[test]
8401    fn ext_glob_hash_a1_rejects_three_substitutions() {
8402        let _g = crate::test_util::global_state_lock();
8403        assert!(!ext_glob_match("(#a1)abc", "xyz"));
8404    }
8405
8406    /// `(#a3)abc` matches "xyz" (3 substitutions ≤ 3).
8407    #[test]
8408    fn ext_glob_hash_a3_accepts_all_substituted() {
8409        let _g = crate::test_util::global_state_lock();
8410        assert!(
8411            ext_glob_match("(#a3)abc", "xyz"),
8412            "3 substitutions at budget 3"
8413        );
8414    }
8415
8416    /// Position-independent substitution: budget allows replacing
8417    /// any one of N pattern chars.
8418    #[test]
8419    fn ext_glob_hash_a_accepts_position_independent_substitution() {
8420        let _g = crate::test_util::global_state_lock();
8421        assert!(ext_glob_match("(#a1)abc", "Xbc"), "first-char sub");
8422        assert!(ext_glob_match("(#a1)abc", "aXc"), "middle-char sub");
8423        assert!(ext_glob_match("(#a1)abc", "abX"), "last-char sub");
8424    }
8425
8426    // ═══════════════════════════════════════════════════════════════════
8427    // zsh test-corpus pins — direct anchors to Test/D02glob.ztst:25-80
8428    // "zsh globbing" test list (zsh's authoritative regression suite).
8429    // Each test cites the ztst line range it pins. Currently-failing
8430    // tests get #[ignore = "ZSHRS BUG: ..."] markers; expected-output
8431    // assertions stay in tree so the marker flips when the Rust port
8432    // catches up.
8433    // ═══════════════════════════════════════════════════════════════════
8434
8435    /// `Test/D02glob.ztst:26` — `[[ foo~ = foo~ ]]` exact match,
8436    /// expected exit 0 (true).
8437    #[test]
8438    fn zsh_corpus_foo_tilde_exact_match() {
8439        let _g = crate::test_util::global_state_lock();
8440        assert!(
8441            ext_glob_match("foo~", "foo~"),
8442            "ztst:26 — literal tilde matches"
8443        );
8444    }
8445
8446    /// `Test/D02glob.ztst:27` — `[[ foo~ = (foo~) ]]` parenthesised
8447    /// single alternative.
8448    #[test]
8449    fn zsh_corpus_foo_tilde_in_parens() {
8450        let _g = crate::test_util::global_state_lock();
8451        assert!(
8452            ext_glob_match("(foo~)", "foo~"),
8453            "ztst:27 — (foo~) matches foo~"
8454        );
8455    }
8456
8457    /// `Test/D02glob.ztst:28` — `[[ foo~ = (foo~|) ]]` alternation
8458    /// with empty alternative.
8459    #[test]
8460    fn zsh_corpus_alternation_with_empty_alt() {
8461        let _g = crate::test_util::global_state_lock();
8462        assert!(
8463            ext_glob_match("(foo~|)", "foo~"),
8464            "ztst:28 — empty alt accepted"
8465        );
8466    }
8467
8468    /// `Test/D02glob.ztst:29` — `[[ foo.c = *.c~boo* ]]` exclude
8469    /// pattern: matches *.c BUT NOT boo*. `foo.c` matches *.c and
8470    /// doesn't match boo*, so result is 0 (true).
8471    #[test]
8472    fn zsh_corpus_exclude_pattern_basic() {
8473        let _g = crate::test_util::global_state_lock();
8474        assert!(
8475            ext_glob_match("*.c~boo*", "foo.c"),
8476            "ztst:29 — *.c~boo* matches foo.c"
8477        );
8478    }
8479
8480    /// `Test/D02glob.ztst:30` — `[[ foo.c = *.c~boo*~foo* ]]`
8481    /// double-exclude: matches *.c but excludes both `boo*` and
8482    /// `foo*`. `foo.c` matches `foo*` (excluded) → result 1 (false).
8483    #[test]
8484    fn zsh_corpus_exclude_pattern_double() {
8485        let _g = crate::test_util::global_state_lock();
8486        assert!(
8487            !ext_glob_match("*.c~boo*~foo*", "foo.c"),
8488            "ztst:30 — double exclude rejects foo.c"
8489        );
8490    }
8491
8492    /// `Test/D02glob.ztst:31` — `[[ fofo = (fo#)# ]]` — `#` is the
8493    /// extended-glob "1+ repetitions" quantifier. `(fo#)#` matches
8494    /// 1+ repetitions of "fo+".
8495    #[test]
8496    fn zsh_corpus_hash_repetition_double() {
8497        let _g = crate::test_util::global_state_lock();
8498        assert!(
8499            ext_glob_match("(fo#)#", "fofo"),
8500            "ztst:31 — (fo#)# matches fofo"
8501        );
8502    }
8503
8504    /// `Test/D02glob.ztst:32` — `[[ ffo = (fo#)# ]]`. `fo#` means
8505    /// `f` followed by 0+ `o`. `ffo` = `f` + `fo` (matches the
8506    /// outer `#` repetition).
8507    #[test]
8508    fn zsh_corpus_hash_repetition_min_one() {
8509        let _g = crate::test_util::global_state_lock();
8510        assert!(
8511            ext_glob_match("(fo#)#", "ffo"),
8512            "ztst:32 — (fo#)# matches ffo via 1-iter outer"
8513        );
8514    }
8515
8516    /// `Test/D02glob.ztst:36` — `[[ foooofof = (fo##)# ]]` —
8517    /// `##` is "2+ repetitions". `fo##` = `f` + 2+ `o`'s. The
8518    /// trailing `of` after `foooo` doesn't satisfy the trailing
8519    /// outer `#`, so result is 1 (false).
8520    #[test]
8521    fn zsh_corpus_hash_hash_quantifier_min_two() {
8522        let _g = crate::test_util::global_state_lock();
8523        assert!(
8524            !ext_glob_match("(fo##)#", "foooofof"),
8525            "ztst:36 — (fo##)# rejects foooofof"
8526        );
8527    }
8528
8529    /// `Test/D02glob.ztst:50` — `[[ aac = ((a))#a(c) ]]` —
8530    /// `((a))#` is `0+ a`. `aac` = `aa` (= `((a))#`) + `c`. The
8531    /// final `a(c)` requires literal `a` followed by capture `(c)`.
8532    /// Wait — should be `aac` matches `((a))#a(c)`: outer `((a))#`
8533    /// captures `a` repeated, then literal `a`, then `(c)`.
8534    /// Actually re-reading: `((a))#` matches 0+ 'a'. `aac` =
8535    /// `((a))#`("a") + `a`("a") + `(c)`("c"). So "aa" splits as
8536    /// (a)("a") + a("a") + c("c"). Match.
8537    #[test]
8538    fn zsh_corpus_nested_paren_quantifier() {
8539        let _g = crate::test_util::global_state_lock();
8540        assert!(
8541            ext_glob_match("((a))#a(c)", "aac"),
8542            "ztst:50 — ((a))#a(c) matches aac"
8543        );
8544    }
8545
8546    /// `Test/D02glob.ztst:51` — `[[ ac = ((a))#a(c) ]]` — single
8547    /// `a` matches `a(c)` after empty `((a))#` consumes zero.
8548    #[test]
8549    fn zsh_corpus_zero_iteration_quantifier() {
8550        let _g = crate::test_util::global_state_lock();
8551        assert!(
8552            ext_glob_match("((a))#a(c)", "ac"),
8553            "ztst:51 — ((a))# can be zero iters"
8554        );
8555    }
8556
8557    /// `Test/D02glob.ztst:52` — `[[ c = ((a))#a(c) ]]` — empty
8558    /// `((a))#` + literal `a` requires at least one `a`. `c` has
8559    /// no `a`, so result is 1 (false).
8560    #[test]
8561    fn zsh_corpus_required_literal_after_zero_quantifier() {
8562        let _g = crate::test_util::global_state_lock();
8563        assert!(
8564            !ext_glob_match("((a))#a(c)", "c"),
8565            "ztst:52 — bare `c` lacks required `a`"
8566        );
8567    }
8568
8569    /// `Test/D02glob.ztst:73` — `[[ foo = ((^x)) ]]` — exclude
8570    /// `x`. `foo` doesn't contain `x` as a single char (the
8571    /// `((^x))` matches anything that's not just 'x' — `foo` is 3
8572    /// chars, none is `x`).
8573    #[test]
8574    fn zsh_corpus_caret_exclude_basic() {
8575        let _g = crate::test_util::global_state_lock();
8576        assert!(
8577            ext_glob_match("((^x))", "foo"),
8578            "ztst:73 — ((^x)) matches foo"
8579        );
8580    }
8581
8582    /// `Test/D02glob.ztst:75` — `[[ foo = ((^foo)) ]]` — `^foo`
8583    /// rejects exact `foo`. `foo` matches `foo` literal, which the
8584    /// `^` inverts. Result: 1 (false).
8585    #[test]
8586    fn zsh_corpus_caret_exclude_exact_match_inverted() {
8587        let _g = crate::test_util::global_state_lock();
8588        assert!(
8589            !ext_glob_match("((^foo))", "foo"),
8590            "ztst:75 — ((^foo)) rejects foo"
8591        );
8592    }
8593
8594    /// `Test/D02glob.ztst:79` — `[[ foot = z*~*x ]]` — `*` then
8595    /// exclude `*x`. `foot` doesn't start with `z`, so doesn't
8596    /// match `z*` at all → result 1 (false).
8597    #[test]
8598    fn zsh_corpus_star_exclude_no_z_prefix() {
8599        let _g = crate::test_util::global_state_lock();
8600        assert!(
8601            !ext_glob_match("z*~*x", "foot"),
8602            "ztst:79 — foot doesn't start with z"
8603        );
8604    }
8605
8606    /// `Test/D02glob.ztst:80` — `[[ zoot = z*~*x ]]` — `zoot`
8607    /// matches `z*` AND doesn't end in `x` → result 0 (true).
8608    #[test]
8609    fn zsh_corpus_star_exclude_zoot() {
8610        let _g = crate::test_util::global_state_lock();
8611        assert!(
8612            ext_glob_match("z*~*x", "zoot"),
8613            "ztst:80 — zoot matches z* and not *x"
8614        );
8615    }
8616
8617    // ─── POSIX class brackets — Test/D02glob.ztst:111-118 ─────────────
8618
8619    /// `Test/D02glob.ztst:111` — `[[:alpha:][:punct:]]#[[:digit:]][^[:lower:]]`
8620    /// over "a%1X": `a` matches alpha, `%` matches punct (1+ via `#`),
8621    /// `1` matches digit, `X` matches NOT-lower.
8622    #[test]
8623    fn zsh_corpus_posix_class_alpha_punct_digit_notlower() {
8624        let _g = crate::test_util::global_state_lock();
8625        assert!(
8626            ext_glob_match("[[:alpha:][:punct:]]#[[:digit:]][^[:lower:]]", "a%1X"),
8627            "ztst:111 — alpha-punct-digit-notlower chain",
8628        );
8629    }
8630
8631    /// `Test/D02glob.ztst:112` — same pattern, "a%1" lacks the
8632    /// 4th non-lower char → result 1 (false).
8633    #[test]
8634    fn zsh_corpus_posix_class_rejects_short_input() {
8635        let _g = crate::test_util::global_state_lock();
8636        assert!(
8637            !ext_glob_match("[[:alpha:][:punct:]]#[[:digit:]][^[:lower:]]", "a%1"),
8638            "ztst:112 — short input rejected",
8639        );
8640    }
8641
8642    /// `Test/D02glob.ztst:113` — `[[:` literal in char class:
8643    /// `[[ [: = [[:]# ]]` — `[[:]` matches `[` OR `:`, `#` is 1+
8644    /// repetitions. "[:" = `[` + `:` → matches.
8645    #[test]
8646    fn zsh_corpus_literal_brackets_in_class_with_repetition() {
8647        let _g = crate::test_util::global_state_lock();
8648        assert!(
8649            ext_glob_match("[[:]#", "[:"),
8650            "ztst:113 — [[:]# matches '[:'"
8651        );
8652    }
8653
8654    // ─── (#i) / (#l) case modifiers — Test/D02glob.ztst:119-132 ───────
8655
8656    /// `Test/D02glob.ztst:119` — `(#i)FOOXX` matches "fooxx"
8657    /// (case-insensitive throughout).
8658    #[test]
8659    fn zsh_corpus_hash_i_case_insensitive() {
8660        let _g = crate::test_util::global_state_lock();
8661        assert!(
8662            ext_glob_match("(#i)FOOXX", "fooxx"),
8663            "ztst:119 — (#i)FOOXX matches fooxx"
8664        );
8665    }
8666
8667    /// `Test/D02glob.ztst:120` — `(#l)FOOXX` requires pattern UPPER
8668    /// chars to be the ONLY uppercase candidates; lowercase input
8669    /// FAILS to match because pattern is all uppercase and `#l` only
8670    /// matches the EXACT lowercase variant of the pattern char... actually
8671    /// `(#l)` means "lowercase pattern chars match uppercase too" —
8672    /// `(#l)FOOXX` has no lowercase chars so it stays a strict
8673    /// uppercase match. "fooxx" doesn't match. Result 1 (false).
8674    #[test]
8675    fn zsh_corpus_hash_l_uppercase_pattern_with_lower_input_fails() {
8676        let _g = crate::test_util::global_state_lock();
8677        assert!(
8678            !ext_glob_match("(#l)FOOXX", "fooxx"),
8679            "ztst:120 — (#l)FOOXX does NOT match fooxx"
8680        );
8681    }
8682
8683    /// `Test/D02glob.ztst:121` — `(#l)fooxx` (lowercase pattern) DOES
8684    /// match "FOOXX" because `#l` is the asymmetric "lowercase pat
8685    /// chars match upper-or-lower" rule.
8686    #[test]
8687    fn zsh_corpus_hash_l_lowercase_pattern_matches_uppercase_input() {
8688        let _g = crate::test_util::global_state_lock();
8689        assert!(
8690            ext_glob_match("(#l)fooxx", "FOOXX"),
8691            "ztst:121 — (#l)fooxx matches FOOXX (asymmetric upcasing)"
8692        );
8693    }
8694
8695    /// `Test/D02glob.ztst:122` — `(#i)FOO(#I)X(#i)X` mixes case
8696    /// modes. `(#I)` cancels prior `(#i)`. So 4th char `X` requires
8697    /// exact case. "fooxx" has lowercase `x` at that position → fail.
8698    #[test]
8699    fn zsh_corpus_hash_capital_i_cancels_case_insensitive() {
8700        let _g = crate::test_util::global_state_lock();
8701        assert!(
8702            !ext_glob_match("(#i)FOO(#I)X(#i)X", "fooxx"),
8703            "ztst:122 — (#I) cancels (#i), 4th char `X` requires upper"
8704        );
8705    }
8706
8707    /// `Test/D02glob.ztst:123` — same pattern with input "fooXx":
8708    /// `(#i)FOO` matches "foo", `(#I)X` matches "X" exact-case,
8709    /// `(#i)X` matches "x" insensitive → result 0 (true).
8710    #[test]
8711    fn zsh_corpus_hash_i_capital_i_toggle_succeeds() {
8712        let _g = crate::test_util::global_state_lock();
8713        assert!(
8714            ext_glob_match("(#i)FOO(#I)X(#i)X", "fooXx"),
8715            "ztst:123 — mixed case modifiers succeed"
8716        );
8717    }
8718
8719    /// `Test/D02glob.ztst:128` — `(#i)*m*` matches "Modules"
8720    /// case-insensitively.
8721    #[test]
8722    fn zsh_corpus_hash_i_with_star_glob() {
8723        let _g = crate::test_util::global_state_lock();
8724        assert!(
8725            ext_glob_match("(#i)*m*", "Modules"),
8726            "ztst:128 — (#i)*m* case-insensitive substring"
8727        );
8728    }
8729
8730    // ─── Numeric ranges `<n-m>` — Test/D02glob.ztst:133-137 ───────────
8731
8732    /// `Test/D02glob.ztst:133` — `<1-1000>33` matches "633" (the
8733    /// "6" is in [1..1000], followed by literal "33"). Actually
8734    /// the way zsh parses: `<1-1000>` matches ONE number from
8735    /// 1-1000, then `33` is literal. "633" = `6`(in 1-1000 range) +
8736    /// "33"(literal). So `<1-1000>33` matches "633".
8737    #[test]
8738    fn zsh_corpus_numeric_range_one_to_thousand() {
8739        let _g = crate::test_util::global_state_lock();
8740        assert!(
8741            ext_glob_match("<1-1000>33", "633"),
8742            "ztst:133 — <1-1000>33 matches 633"
8743        );
8744    }
8745
8746    /// `Test/D02glob.ztst:136` — `<->33` is the open-ended range
8747    /// (any number) followed by 33. Matches "633".
8748    #[test]
8749    fn zsh_corpus_numeric_range_open_ended() {
8750        let _g = crate::test_util::global_state_lock();
8751        assert!(
8752            ext_glob_match("<->33", "633"),
8753            "ztst:136 — <->33 matches 633 (any number)"
8754        );
8755    }
8756
8757    // ─── (#a) approximate match details — Test/D02glob.ztst:147-164 ───
8758
8759    /// `Test/D02glob.ztst:147` — `(#a1)[b][b]` matches "bob" via
8760    /// 1 substitution (middle `o` substituted in `[b][b]`'s gap).
8761    /// Wait — `[b][b]` is two-char "bb". Match against "bob" requires
8762    /// 1 edit. With (#a1), allowed.
8763    #[test]
8764    fn zsh_corpus_hash_a1_bracket_class_with_one_edit() {
8765        let _g = crate::test_util::global_state_lock();
8766        assert!(
8767            ext_glob_match("(#a1)[b][b]", "bob"),
8768            "ztst:147 — (#a1)[b][b] matches bob via 1 edit"
8769        );
8770    }
8771
8772    /// `Test/D02glob.ztst:151` — `(#a2)XbcX` matches "abcd" via
8773    /// 2 substitutions (a↔X, d↔X).
8774    #[test]
8775    fn zsh_corpus_hash_a2_two_substitutions() {
8776        let _g = crate::test_util::global_state_lock();
8777        assert!(
8778            ext_glob_match("(#a2)XbcX", "abcd"),
8779            "ztst:151 — 2 substitutions allowed"
8780        );
8781    }
8782
8783    /// `Test/D02glob.ztst:152` — `(#a2)ad` matches "abcd" via
8784    /// 2 INSERTIONS (b and c inserted into input vs pattern).
8785    /// Pattern is "ad" (2 chars), input is "abcd" (4 chars).
8786    /// Diff: 2 extra chars in input.
8787    #[test]
8788    fn zsh_corpus_hash_a2_two_insertions_in_input() {
8789        let _g = crate::test_util::global_state_lock();
8790        assert!(
8791            ext_glob_match("(#a2)ad", "abcd"),
8792            "ztst:152 — (#a2)ad matches abcd via 2 insertions in input"
8793        );
8794    }
8795
8796    /// `Test/D02glob.ztst:153` — `(#a2)abcd` matches "ad" via
8797    /// 2 DELETIONS from input (b and c missing from input vs pattern).
8798    #[test]
8799    fn zsh_corpus_hash_a2_two_deletions_from_input() {
8800        let _g = crate::test_util::global_state_lock();
8801        assert!(
8802            ext_glob_match("(#a2)abcd", "ad"),
8803            "ztst:153 — (#a2)abcd matches ad via 2 deletions"
8804        );
8805    }
8806
8807    /// `Test/D02glob.ztst:158` — `(#a2)abcd` rejects "dcba" — 4
8808    /// changes needed (full reverse) > budget 2.
8809    #[test]
8810    fn zsh_corpus_hash_a2_rejects_full_reverse() {
8811        let _g = crate::test_util::global_state_lock();
8812        assert!(
8813            !ext_glob_match("(#a2)abcd", "dcba"),
8814            "ztst:158 — full reverse exceeds budget 2"
8815        );
8816    }
8817
8818    /// `Test/D02glob.ztst:159` — `(#a3)abcd` matches "dcba" via
8819    /// 3 substitutions (positions 0,1,3 of input). Wait — actually
8820    /// "dcba" vs "abcd" — 4 positions differ. But (#a3) allows
8821    /// 3 errors. Hmm, but ztst says result is 0 (true). So zsh
8822    /// accepts. Possibly via DAMERAU transposition (swap adjacent).
8823    /// Without Damerau swap, this needs 4 subs → false. With
8824    /// transposition, "dcba" → "cdba" → "dcab"... complicated.
8825    /// Mark ignore — Damerau transposition isn't in the Rust port.
8826    #[test]
8827    fn zsh_corpus_hash_a3_reverse_via_transposition() {
8828        let _g = crate::test_util::global_state_lock();
8829        assert!(
8830            ext_glob_match("(#a3)abcd", "dcba"),
8831            "ztst:159 — (#a3)abcd matches dcba via Damerau transpositions"
8832        );
8833    }
8834
8835    // ─── (#s) start / (#e) end anchors — Test/D02glob.ztst:168-179 ────
8836
8837    /// `Test/D02glob.ztst:168` — `*((#s)|/)test((#e)|/)*` matches
8838    /// "test" — start-anchor (#s) at position 0, end-anchor (#e)
8839    /// after "test".
8840    #[test]
8841    fn zsh_corpus_hash_s_e_anchors_match_bare_test() {
8842        let _g = crate::test_util::global_state_lock();
8843        assert!(
8844            ext_glob_match("*((#s)|/)test((#e)|/)*", "test"),
8845            "ztst:168 — start/end anchors match bare 'test'",
8846        );
8847    }
8848
8849    /// `Test/D02glob.ztst:172` — `*((#s)|/)test((#e)|/)*` rejects
8850    /// "atest" — `(#s)|/` requires position 0 OR a `/`, but `a`
8851    /// is neither.
8852    #[test]
8853    fn zsh_corpus_hash_s_anchor_rejects_a_prefix() {
8854        let _g = crate::test_util::global_state_lock();
8855        assert!(
8856            !ext_glob_match("*((#s)|/)test((#e)|/)*", "atest"),
8857            "ztst:172 — atest fails the (#s) start-or-slash anchor",
8858        );
8859    }
8860
8861    // ─── Misc/globtests pins — `~` exclusion and `^` negation ─────────
8862
8863    /// `Misc/globtests` — `[[ foo~ = foo~ ]]` — bare tilde is literal.
8864    #[test]
8865    fn zsh_corpus_literal_tilde_in_pattern() {
8866        let _g = crate::test_util::global_state_lock();
8867        assert!(
8868            ext_glob_match("foo~", "foo~"),
8869            "literal ~ in non-extglob position"
8870        );
8871    }
8872
8873    /// `Misc/globtests` — `[[ foo.c = *.c~boo* ]]` — extended-glob
8874    /// exclusion: `*.c` minus things matching `boo*`. "foo.c" doesn't
8875    /// match "boo*", so it stays.
8876    #[test]
8877    fn zsh_corpus_exclusion_keeps_non_excluded() {
8878        let _g = crate::test_util::global_state_lock();
8879        assert!(ext_glob_match("*.c~boo*", "foo.c"), "exclusion keeps foo.c");
8880    }
8881
8882    /// `Misc/globtests` — `[[ foo.c = *.c~boo*~foo* ]]` — chained
8883    /// exclusions: `*.c` minus `boo*` minus `foo*`. "foo.c" matches
8884    /// `foo*`, so excluded.
8885    #[test]
8886    fn zsh_corpus_chained_exclusion_removes_match() {
8887        let _g = crate::test_util::global_state_lock();
8888        assert!(
8889            !ext_glob_match("*.c~boo*~foo*", "foo.c"),
8890            "chained exclusion excludes foo.c",
8891        );
8892    }
8893
8894    /// `Misc/globtests` — `[[ fofo = (fo#)# ]]` — outer `(...)#` is
8895    /// zero-or-more closure over `fo#` (one f followed by zero+ o).
8896    #[test]
8897    fn zsh_corpus_closure_of_closure_matches_repeated_fo() {
8898        let _g = crate::test_util::global_state_lock();
8899        assert!(ext_glob_match("(fo#)#", "fofo"), "(fo#)# matches fofo");
8900    }
8901
8902    /// `Misc/globtests` — `[[ ffo = (fo#)# ]]` — "ffo" = "f"+"fo".
8903    #[test]
8904    fn zsh_corpus_closure_matches_ffo() {
8905        let _g = crate::test_util::global_state_lock();
8906        assert!(ext_glob_match("(fo#)#", "ffo"), "(fo#)# matches ffo");
8907    }
8908
8909    /// `Misc/globtests` — `[[ xfoooofof = (fo#)# ]]` — leading "x"
8910    /// breaks the pattern.
8911    #[test]
8912    fn zsh_corpus_closure_rejects_leading_x() {
8913        let _g = crate::test_util::global_state_lock();
8914        assert!(
8915            !ext_glob_match("(fo#)#", "xfoooofof"),
8916            "(fo#)# rejects leading x",
8917        );
8918    }
8919
8920    /// `Misc/globtests` — `[[ foo = ((^x)) ]]` — `(^x)` matches one
8921    /// char that isn't 'x'. "foo" has 'f' first, so matches.
8922    /// Note: zsh's `(^x)` is exactly-one-not-x with extendedglob.
8923    #[test]
8924    fn zsh_corpus_negation_caret_matches_non_x_start() {
8925        let _g = crate::test_util::global_state_lock();
8926        assert!(ext_glob_match("((^x)*)", "foo"), "(^x)* matches foo");
8927    }
8928
8929    /// `Misc/globtests` — `[[ foo = ((^foo)) ]]` — `(^foo)` is "not foo",
8930    /// "foo" matches `foo`, so excluded → false.
8931    #[test]
8932    fn zsh_corpus_negation_caret_rejects_full_match() {
8933        let _g = crate::test_util::global_state_lock();
8934        assert!(
8935            !ext_glob_match("((^foo))", "foo"),
8936            "(^foo) rejects 'foo' itself",
8937        );
8938    }
8939
8940    /// `Misc/globtests` — `[[ abcd = ?(a|b)c#d ]]` — `?(a|b)` is
8941    /// "zero-or-one of a|b", `c#d` is "zero+ c then d". "abcd" = a + b? no, ?
8942    /// is single char or alternation. Let me re-read: `?(a|b)` matches
8943    /// one char which is `a` OR `b`. So "abcd": ?=a, then needs `c#d`
8944    /// = (c)*d, matches "bcd" if first char then need cd. Hmm.
8945    /// Actually: `?(a|b)` = `?` then `(a|b)` as separate? More likely
8946    /// `?(a|b)` is ksh-style "0-or-1 of a|b" only with KSH_GLOB.
8947    /// In zsh native (extendedglob) `?` is single char, `(a|b)` is
8948    /// alternation. So `?(a|b)c#d` = anychar (a|b) c* d. "abcd" =
8949    /// a + b + (zero c) + ... fails. The ztst says it MATCHES.
8950    /// Conclusion: requires KSH_GLOB which we may not enable.
8951    #[test]
8952    fn zsh_corpus_ksh_question_alternation() {
8953        let _g = crate::test_util::global_state_lock();
8954        assert!(
8955            ext_glob_match("?(a|b)c#d", "abcd"),
8956            "?(a|b)c#d matches abcd",
8957        );
8958    }
8959
8960    // ── patmatchlen — c:2649 ─────────────────────────────────────────
8961
8962    /// `Src/pattern.c:2649` — `int patmatchlen(void)` returns the
8963    /// byte-length of the last successful pattry match. After a
8964    /// successful match against `"hello"` with pattern `"hel*"`,
8965    /// the recorded length is 5 (all of "hello" was consumed).
8966    #[test]
8967    fn patmatchlen_records_consumed_byte_length() {
8968        let _g = crate::test_util::global_state_lock();
8969        let prog = compile("hel*");
8970        assert!(pattry(&prog, "hello"), "hel* matches hello");
8971        assert_eq!(patmatchlen(), 5, "all 5 bytes of 'hello' consumed by hel*",);
8972    }
8973
8974    /// Anchored prefix match: `"foo"` against `"foobar"` with the
8975    /// `NOANCH` form is the user-visible case `[[ foobar = foo* ]]`,
8976    /// which consumes all 6 bytes when the pattern matches the whole
8977    /// string.
8978    #[test]
8979    fn patmatchlen_full_string_match_returns_full_length() {
8980        let _g = crate::test_util::global_state_lock();
8981        let prog = compile("foo*");
8982        assert!(pattry(&prog, "foobar"));
8983        assert_eq!(patmatchlen(), 6, "foo* against 'foobar' = 6 bytes");
8984    }
8985
8986    // ── patmatchindex (c:4004) ──────────────────────────────────────
8987
8988    /// `patmatchindex` on a literal-byte range returns the byte at
8989    /// the requested position.
8990    #[test]
8991    fn patmatchindex_literal_byte_at_index() {
8992        let _g = crate::test_util::global_state_lock();
8993        let range = b"abc";
8994        assert_eq!(patmatchindex(range, 0), Some((Some(b'a'), 0)));
8995        assert_eq!(patmatchindex(range, 1), Some((Some(b'b'), 0)));
8996        assert_eq!(patmatchindex(range, 2), Some((Some(b'c'), 0)));
8997        assert_eq!(patmatchindex(range, 3), None, "out-of-range index");
8998    }
8999
9000    /// `patmatchindex` on a PP_ALPHA class marker returns `(None,
9001    /// PP_ALPHA)` to signal the class without a literal char.
9002    #[test]
9003    fn patmatchindex_posix_class_marker_returns_mtp() {
9004        let _g = crate::test_util::global_state_lock();
9005        // Meta + PP_ALPHA = 0x83 + 1 = 0x84
9006        let range = &[Meta + PP_ALPHA as u8];
9007        let r = patmatchindex(range, 0);
9008        assert_eq!(r, Some((None, PP_ALPHA)));
9009    }
9010
9011    // ── mb_patmatchrange (c:3610) ───────────────────────────────────
9012
9013    /// `mb_patmatchrange` literal hit on `'a'`.
9014    #[test]
9015    fn mb_patmatchrange_literal_ascii_hits() {
9016        let _g = crate::test_util::global_state_lock();
9017        let range = b"a";
9018        let mut mtp = -1;
9019        assert!(
9020            mb_patmatchrange(range, 'a', 0, None, Some(&mut mtp)),
9021            "literal 'a' matches"
9022        );
9023        assert_eq!(mtp, 0, "literal hit sets mtp=0");
9024    }
9025
9026    /// `mb_patmatchrange` PP_DIGIT class hit on `'5'`.
9027    #[test]
9028    fn mb_patmatchrange_digit_class_hits() {
9029        let _g = crate::test_util::global_state_lock();
9030        let range = &[Meta + PP_DIGIT as u8];
9031        assert!(mb_patmatchrange(range, '5', 0, None, None));
9032        assert!(!mb_patmatchrange(range, 'x', 0, None, None));
9033    }
9034
9035    /// `mb_patmatchrange` PP_RANGE on `'b'` in `a..c`.
9036    #[test]
9037    fn mb_patmatchrange_range_hits_middle() {
9038        let _g = crate::test_util::global_state_lock();
9039        // Meta + PP_RANGE then two literal bytes for the endpoints.
9040        let range = &[Meta + PP_RANGE as u8, b'a', b'c'];
9041        assert!(mb_patmatchrange(range, 'b', 0, None, None));
9042        assert!(mb_patmatchrange(range, 'a', 0, None, None));
9043        assert!(mb_patmatchrange(range, 'c', 0, None, None));
9044        assert!(!mb_patmatchrange(range, 'd', 0, None, None));
9045    }
9046
9047    // ── mb_patmatchindex (c:3767) ───────────────────────────────────
9048
9049    /// `mb_patmatchindex` literal char at index 0.
9050    #[test]
9051    fn mb_patmatchindex_literal_first_index() {
9052        let _g = crate::test_util::global_state_lock();
9053        let range = b"xyz";
9054        let r = mb_patmatchindex(range, 0);
9055        assert_eq!(r, Some((Some('x'), 0)));
9056    }
9057
9058    /// `mb_patmatchindex` on PP_UPPER marker at index 0.
9059    #[test]
9060    fn mb_patmatchindex_class_marker_returns_mtp() {
9061        let _g = crate::test_util::global_state_lock();
9062        let range = &[Meta + PP_UPPER as u8];
9063        assert_eq!(mb_patmatchindex(range, 0), Some((None, PP_UPPER)));
9064    }
9065
9066    // ═══════════════════════════════════════════════════════════════════
9067    // Additional C-parity tests for Src/pattern.c bit-flag predicates.
9068    // ═══════════════════════════════════════════════════════════════════
9069
9070    /// c:200 — P_ISBRANCH checks bit 0x20.
9071    #[test]
9072    fn P_ISBRANCH_recognizes_bit_0x20() {
9073        assert!(P_ISBRANCH(0x20));
9074        assert!(P_ISBRANCH(0x20 | 0x01));
9075        assert!(P_ISBRANCH(0xFF));
9076        assert!(!P_ISBRANCH(0x00));
9077        assert!(!P_ISBRANCH(0x1F));
9078    }
9079
9080    /// c:201 — P_ISEXCLUDE checks bits 0x30 = 0x10 | 0x20 (BOTH must be set).
9081    #[test]
9082    fn P_ISEXCLUDE_requires_both_bits() {
9083        assert!(P_ISEXCLUDE(0x30));
9084        assert!(P_ISEXCLUDE(0x30 | 0x01));
9085        assert!(!P_ISEXCLUDE(0x10), "only 0x10 — must require both");
9086        assert!(!P_ISEXCLUDE(0x20), "only 0x20 — must require both");
9087        assert!(!P_ISEXCLUDE(0x00));
9088    }
9089
9090    /// c:202 — P_NOTDOT checks bit 0x40.
9091    #[test]
9092    fn P_NOTDOT_recognizes_bit_0x40() {
9093        assert!(P_NOTDOT(0x40));
9094        assert!(P_NOTDOT(0x40 | 0x01));
9095        assert!(P_NOTDOT(0xFF));
9096        assert!(!P_NOTDOT(0x00));
9097        assert!(!P_NOTDOT(0x3F));
9098    }
9099
9100    /// c:216-218 — P_SIMPLE/HSTART/PURESTR are distinct bits.
9101    #[test]
9102    fn p_flag_constants_are_distinct() {
9103        assert_eq!(P_SIMPLE & P_HSTART, 0);
9104        assert_eq!(P_SIMPLE & P_PURESTR, 0);
9105        assert_eq!(P_HSTART & P_PURESTR, 0);
9106    }
9107
9108    /// c:216 — P_SIMPLE is bit 0 (= 0x01).
9109    #[test]
9110    fn p_simple_is_bit_zero() {
9111        assert_eq!(P_SIMPLE, 0x01);
9112    }
9113
9114    /// c:217 — P_HSTART is bit 1 (= 0x02).
9115    #[test]
9116    fn p_hstart_is_bit_one() {
9117        assert_eq!(P_HSTART, 0x02);
9118    }
9119
9120    /// c:218 — P_PURESTR is bit 2 (= 0x04).
9121    #[test]
9122    fn p_purestr_is_bit_two() {
9123        assert_eq!(P_PURESTR, 0x04);
9124    }
9125
9126    /// c:406-407 — PA_NOALIGN=1, PA_UNMETA=2 (single-bit flags).
9127    #[test]
9128    fn pa_flag_constants_canonical() {
9129        assert_eq!(PA_NOALIGN, 1);
9130        assert_eq!(PA_UNMETA, 2);
9131    }
9132
9133    /// PA_NOALIGN | PA_UNMETA combine without overlap.
9134    #[test]
9135    fn pa_flags_pairwise_disjoint() {
9136        assert_eq!(PA_NOALIGN & PA_UNMETA, 0);
9137    }
9138
9139    /// c:336 — metacharinc on ASCII char advances by 1 byte.
9140    #[test]
9141    fn metacharinc_ascii_advances_one() {
9142        let _g = crate::test_util::global_state_lock();
9143        assert_eq!(metacharinc("hello", 0), 1);
9144        assert_eq!(metacharinc("hello", 4), 5);
9145    }
9146
9147    /// c:336 — metacharinc at end-of-string returns same pos.
9148    #[test]
9149    fn metacharinc_end_of_string_returns_same() {
9150        let _g = crate::test_util::global_state_lock();
9151        assert_eq!(metacharinc("abc", 3), 3, "at end → no advance");
9152    }
9153
9154    /// c:336 — metacharinc on multibyte char advances by codepoint width.
9155    #[test]
9156    fn metacharinc_multibyte_advances_by_codepoint_len() {
9157        let _g = crate::test_util::global_state_lock();
9158        // 'é' is 2 bytes in UTF-8.
9159        assert_eq!(metacharinc("é", 0), 2);
9160        // '日' is 3 bytes.
9161        assert_eq!(metacharinc("日", 0), 3);
9162    }
9163
9164    // ═══════════════════════════════════════════════════════════════════
9165    // Additional C-parity tests for Src/pattern.c
9166    // c:155 P_ISBRANCH / c:161 P_ISEXCLUDE / c:167 P_NOTDOT /
9167    // c:262 metacharinc / c:502 patcompstart / c:536 patcompile /
9168    // c:1008 patgetglobflags / c:1129 range_type / c:1152 pattern_range_to_string
9169    // c:2059 charref / c:2066 charnext / c:2153 pattry / c:2304 patmatchlen
9170    // ═══════════════════════════════════════════════════════════════════
9171
9172    /// c:262 — `metacharinc` is pure for ASCII input.
9173    #[test]
9174    fn metacharinc_is_pure_for_ascii() {
9175        for s in ["", "a", "abc", "hello"] {
9176            for pos in 0..s.len() {
9177                let first = metacharinc(s, pos);
9178                for _ in 0..3 {
9179                    assert_eq!(
9180                        metacharinc(s, pos),
9181                        first,
9182                        "metacharinc({:?}, {}) must be pure",
9183                        s,
9184                        pos
9185                    );
9186                }
9187            }
9188        }
9189    }
9190
9191    /// c:536 — `patcompile("", 0, None)` returns Option<Patprog>.
9192    #[test]
9193    fn patcompile_returns_option_patprog_type() {
9194        let _g = crate::test_util::global_state_lock();
9195        let _: Option<Patprog> = patcompile("", 0, None);
9196    }
9197
9198    /// c:1008 — `patgetglobflags("")` empty returns None.
9199    #[test]
9200    fn patgetglobflags_empty_returns_none() {
9201        let _g = crate::test_util::global_state_lock();
9202        assert!(patgetglobflags("").is_none(), "empty → None");
9203    }
9204
9205    /// c:1008 — `patgetglobflags` returns Option<(i32, i64, usize)> type.
9206    #[test]
9207    fn patgetglobflags_returns_option_tuple_type() {
9208        let _g = crate::test_util::global_state_lock();
9209        let _: Option<(i32, i64, usize)> = patgetglobflags("abc");
9210    }
9211
9212    /// c:1129 — `range_type("")` empty returns None.
9213    #[test]
9214    fn range_type_empty_returns_none() {
9215        assert!(range_type("").is_none(), "empty → None");
9216    }
9217
9218    /// c:1129 — `range_type` returns Option<usize>.
9219    #[test]
9220    fn range_type_returns_option_usize_type() {
9221        let _: Option<usize> = range_type("abc");
9222    }
9223
9224    /// c:1152 — `pattern_range_to_string("")` empty returns empty String.
9225    #[test]
9226    fn pattern_range_to_string_empty_returns_empty() {
9227        assert_eq!(pattern_range_to_string(""), "");
9228    }
9229
9230    /// c:1152 — `pattern_range_to_string` is pure.
9231    #[test]
9232    fn pattern_range_to_string_is_pure() {
9233        for s in ["", "abc", "a-z"] {
9234            let first = pattern_range_to_string(s);
9235            for _ in 0..3 {
9236                assert_eq!(
9237                    pattern_range_to_string(s),
9238                    first,
9239                    "pattern_range_to_string({:?}) must be pure",
9240                    s
9241                );
9242            }
9243        }
9244    }
9245
9246    /// c:2059 — `charref("", 0)` empty returns None.
9247    #[test]
9248    fn charref_empty_returns_none() {
9249        assert!(charref("", 0).is_none(), "empty → None");
9250    }
9251
9252    /// c:2066 — `charnext("", 0)` empty returns 0 (no advance).
9253    #[test]
9254    fn charnext_empty_returns_zero() {
9255        assert_eq!(charnext("", 0), 0);
9256    }
9257
9258    /// c:2304 — `patmatchlen` returns i32 (compile-time type pin).
9259    #[test]
9260    fn patmatchlen_returns_i32_type() {
9261        let _g = crate::test_util::global_state_lock();
9262        let _: i32 = patmatchlen();
9263    }
9264
9265    /// c:155 — P_ISBRANCH/P_ISEXCLUDE/P_NOTDOT all return bool (type pin).
9266    #[test]
9267    fn p_predicates_return_bool_type() {
9268        let _: bool = P_ISBRANCH(0);
9269        let _: bool = P_ISEXCLUDE(0);
9270        let _: bool = P_NOTDOT(0);
9271    }
9272}