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            let entry_off = patparse_off.load(Ordering::Relaxed);
2305            patparse_off.fetch_add(1, Ordering::Relaxed); // consume `<`
2306            let parse_n = patparse.lock().unwrap();
2307            let nb = parse_n.as_bytes();
2308            let mut j = patparse_off.load(Ordering::Relaxed);
2309            let mut len_flag: u8 = 0; // bit 0 = lo present, bit 1 = hi present
2310            let mut from: i64 = 0;
2311            let lo_start = j;
2312            while j < nb.len() && nb[j].is_ascii_digit() {
2313                from = from * 10 + (nb[j] - b'0') as i64;
2314                j += 1;
2315            }
2316            if j > lo_start {
2317                len_flag |= 1;
2318            } // c:1538 — `len |= 1`
2319              // Mandatory dash. C's isnumglob bails (ret=0) here too —
2320              // walks until non-digit, then if non-digit isn't `-` (or `>`
2321              // after seeing `-`), returns "not numglob". Rewind the `<`
2322              // consumption and fall through to the literal-run arm.
2323            if j >= nb.len() || nb[j] != b'-' {
2324                drop(parse_n);
2325                patparse_off.store(entry_off, Ordering::Relaxed);
2326                let h = patnode(P_EXACTLY);
2327                let mut buf = patout.lock().unwrap();
2328                let len: u32 = 1;
2329                buf.extend_from_slice(&len.to_le_bytes());
2330                buf.push(b'<');
2331                patparse_off.fetch_add(1, Ordering::Relaxed);
2332                *flagp |= P_SIMPLE;
2333                *tail_out = h;
2334                return h as i64;
2335            }
2336            j += 1; // c:1543 patparse++
2337            let mut to: i64 = 0;
2338            let hi_start = j;
2339            while j < nb.len() && nb[j].is_ascii_digit() {
2340                to = to * 10 + (nb[j] - b'0') as i64;
2341                j += 1;
2342            }
2343            if j > hi_start {
2344                len_flag |= 2;
2345            } // c:1548 — `len |= 2`
2346              // Expect closing '>'. Mirror the dash-failure rewind here
2347              // too: a `<N-` without `>` is non-numglob per C's isnumglob.
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;
2362            drop(parse_n);
2363            patparse_off.store(j, Ordering::Relaxed);
2364            *flagp &= !P_PURESTR;
2365
2366            let off2 = match len_flag {
2367                // c:1552-1567
2368                3 => {
2369                    // c:1554 P_NUMRNG
2370                    let off2 = patnode(P_NUMRNG);
2371                    let mut buf = patout.lock().unwrap();
2372                    buf.extend_from_slice(&from.to_le_bytes());
2373                    buf.extend_from_slice(&to.to_le_bytes());
2374                    off2
2375                }
2376                2 => {
2377                    // c:1559 P_NUMTO
2378                    let off2 = patnode(P_NUMTO);
2379                    let mut buf = patout.lock().unwrap();
2380                    buf.extend_from_slice(&to.to_le_bytes());
2381                    off2
2382                }
2383                1 => {
2384                    // c:1563 P_NUMFROM
2385                    let off2 = patnode(P_NUMFROM);
2386                    let mut buf = patout.lock().unwrap();
2387                    buf.extend_from_slice(&from.to_le_bytes());
2388                    off2
2389                }
2390                _ => patnode(P_NUMANY), // c:1568
2391            };
2392            *tail_out = off2;
2393            off2 as i64
2394        }
2395        _ => {
2396            // Accumulate a literal run.
2397            let mut buf: Vec<u8> = Vec::new();
2398            let mut local_off = off;
2399            let p = patparse.lock().unwrap();
2400            // c:1312-1317 — break on kshchar trigger (any of the six
2401            // ZPC_KSH_* slots whose literal byte is followed by `(`).
2402            // Snapshot the zpc_special slots that participate; when
2403            // KSHGLOB is off they're Marker (0xa2) and won't match
2404            // ordinary bytes.
2405            let (ksh_plus, ksh_bang, ksh_bang2, ksh_at, ksh_star, ksh_quest) = {
2406                let sp = zpc_special.lock().unwrap();
2407                (
2408                    sp[ZPC_KSH_PLUS as usize],
2409                    sp[ZPC_KSH_BANG as usize],
2410                    sp[ZPC_KSH_BANG2 as usize],
2411                    sp[ZPC_KSH_AT as usize],
2412                    sp[ZPC_KSH_STAR as usize],
2413                    sp[ZPC_KSH_QUEST as usize],
2414                )
2415            };
2416            // c:1314-1317 — `~` is a literal-run terminator IFF it's
2417            // active as the exclusion operator (EXTENDEDGLOB on) AND
2418            // the lookahead is `/` or a non-segment-special byte.
2419            // Without EXTENDEDGLOB the byte is Marker (0xa2) so the
2420            // equality check fails naturally. Limited to TILDE only —
2421            // the other ZPC_SEG_COUNT slots (SLASH, NULL, BAR, OUTPAR)
2422            // are already covered by the explicit stop list above
2423            // (`|` `)` handled there; `/` was deliberately NOT a
2424            // literal-run break in this Rust port, see
2425            // `zsh_corpus_hash_s_e_anchors_match_bare_test` which
2426            // expects `/` to stay literal inside `(...)` alternation).
2427            let (sp_tilde_lit, sp_seg_lit_set, sp_hat_lit, sp_hash_lit) = {
2428                let sp = zpc_special.lock().unwrap();
2429                let mut set = [false; 256];
2430                for i in 0..(ZPC_SEG_COUNT as usize) {
2431                    set[sp[i] as usize] = true;
2432                }
2433                (
2434                    sp[ZPC_TILDE as usize],
2435                    set,
2436                    sp[ZPC_HAT as usize],
2437                    sp[ZPC_HASH as usize],
2438                )
2439            };
2440            while local_off < p.len() {
2441                let b = p.as_bytes()[local_off];
2442                // c:Src/pattern.c:480-483 — `if (!isset(EXTENDEDGLOB))`
2443                // masks ZPC_HAT and ZPC_HASH to Marker so the
2444                // patcompiece dispatch treats `^` / `#` as literals
2445                // (no negation / no zero-or-more closure).
2446                // The literal-run break list hardcoded `b'^'` /
2447                // `b'#'` though, which still terminated the literal
2448                // accumulator and forced patcompiece's empty-buf
2449                // -1 return → "bad pattern: ^.*" for the unset
2450                // EXTENDEDGLOB case. Gate the break on the actual
2451                // zpc_special slot so an EXTENDEDGLOB-off run treats
2452                // `^` and `#` as ordinary chars. Bug #421.
2453                if matches!(b, b'?' | b'*' | b'[' | b'(' | b')' | b'|' | b'\\' | b'<')
2454                    || (b == b'^' && sp_hat_lit == b'^')
2455                    || (b == b'#' && sp_hash_lit == b'#')
2456                {
2457                    break;
2458                }
2459                if b == sp_tilde_lit && sp_tilde_lit != 0 {
2460                    let la = p.as_bytes().get(local_off + 1).copied().unwrap_or(0);
2461                    // Literal-tilde exception: `~` followed by a
2462                    // segment-special OTHER than `/` stays in the run.
2463                    let literal_tilde_exception = la != b'/' && sp_seg_lit_set[la as usize];
2464                    if !literal_tilde_exception {
2465                        break;
2466                    }
2467                }
2468                // c:1278-1292 — ksh-glob trigger lookahead. If the next
2469                // byte is `(` AND this byte matches one of the six
2470                // ZPC_KSH_* slots, stop the literal run BEFORE this byte
2471                // so the next patcomppiece call consumes it as a ksh
2472                // quantifier prefix (e.g. `pre+(x)post` accumulates
2473                // `pre` then breaks at `+` since `+(` follows).
2474                if local_off + 1 < p.len() && p.as_bytes()[local_off + 1] == b'(' {
2475                    if b == ksh_plus
2476                        || b == ksh_bang
2477                        || b == ksh_bang2
2478                        || b == ksh_at
2479                        || b == ksh_star
2480                        || b == ksh_quest
2481                    {
2482                        break;
2483                    }
2484                }
2485                buf.push(b);
2486                local_off += 1;
2487            }
2488            drop(p);
2489            if buf.is_empty() {
2490                return -1;
2491            }
2492            // c:Src/pattern.c:1340-1351 — multi-char run with TRAILING `#`
2493            // (or `(#cN,M)`) must backtrack ONE char so the trailing
2494            // quantifier applies to the LAST char only. Without this,
2495            // `fo#` compiles as `(fo)#` instead of `f` + `o#`, breaking
2496            // `(fo#)#` against "ffo" (the test pin we are fixing).
2497            //
2498            // C body:
2499            //   if ((*patparse == zpc_special[ZPC_HASH] || ...) && morelen)
2500            //       patparse = patprev;
2501            //
2502            // morelen = "more than one char in the literal run". The
2503            // backtrack pops the LAST byte from buf and rewinds patparse
2504            // by 1 so the next patcomppiece call sees that byte as its
2505            // own atom.
2506            let has_trailing_hash = {
2507                let sp_hash = zpc_special.lock().unwrap()[ZPC_HASH as usize];
2508                let p = patparse.lock().unwrap();
2509                let hash_at = local_off < p.len() && p.as_bytes()[local_off] == b'#';
2510                drop(p);
2511                hash_at && sp_hash == b'#'
2512            };
2513            if buf.len() > 1 && has_trailing_hash {
2514                let _ = buf.pop();
2515                local_off -= 1;
2516            }
2517            patparse_off.store(local_off, Ordering::Relaxed);
2518            *flagp |= P_SIMPLE;
2519            // If it's a single char, mark simple; multi-char run stays pure-string.
2520            let lit_off = patnode(P_EXACTLY);
2521            let mut buf_lit = patout.lock().unwrap();
2522            let len = buf.len() as u32;
2523            buf_lit.extend_from_slice(&len.to_le_bytes());
2524            buf_lit.extend_from_slice(&buf);
2525            *tail_out = lit_off;
2526            lit_off as i64
2527        }
2528    };
2529
2530    if atom < 0 {
2531        return atom;
2532    }
2533
2534    // c:1606-1644 — quantifier dispatch. Three sources of quantifier:
2535    //   (a) trailing `#` / `##` (zsh-extended hash repetition)
2536    //   (b) trailing `(#cN,M)` count-spec (handled in patcompbranch, not here)
2537    //   (c) kshchar form `+/*/?` already at the atom's leading byte.
2538    //
2539    // Rule c:1615 — if no hash AND no count AND kshchar is one of
2540    // (none, '@', '!'), no quantifier; return atom as-is. `@` is the
2541    // identity quantifier (match group exactly once); `!` already had
2542    // its negation compiled by patcompnot inside the atom.
2543    let q_off = patparse_off.load(Ordering::Relaxed);
2544    let parse2 = patparse.lock().unwrap();
2545    // c:1611-1620 — `if (*patparse == zpc_special[ZPC_HASH])` quantifier
2546    // dispatch. The C source reads `zpc_special[ZPC_HASH]`, which
2547    // patcompcharsset (c:480-483) sets to `'#'` only when
2548    // EXTENDEDGLOB is on, else to the Marker byte (c:476). A literal
2549    // '#' in the source pattern therefore does NOT trigger the
2550    // quantifier path when EXTENDEDGLOB is off — the comparison
2551    // `*patparse == Marker` fails. Previously this Rust check used
2552    // a bare `b'#'` comparison instead of consulting zpc_special,
2553    // making `(x)#` and `[a-z]##[0-9]##` match even with
2554    // extendedglob OFF (parity bugs #5/#6 vs real zsh).
2555    let hash_char = zpc_special.lock().unwrap()[ZPC_HASH as usize]; // c:1612
2556    let has_hash = hash_char == b'#' && q_off < parse2.len() && parse2.as_bytes()[q_off] == b'#'; // c:1611-1614
2557    drop(parse2);
2558    if !has_hash && (kshchar == 0 || kshchar == b'@' || kshchar == b'!') {
2559        return atom; // c:1616-1617
2560    }
2561
2562    // c:1621-1622 — kshchar with hash is a parse error (too much at once).
2563    if kshchar != 0 && has_hash {
2564        return -1;
2565    }
2566
2567    // c:1624-1644 — pick the operator + post-atom flags.
2568    let op: u8;
2569    let mut consume_hashes = 0;
2570    if kshchar == b'*' {
2571        // c:1624-1626
2572        op = P_ONEHASH;
2573        *flagp = P_HSTART;
2574    } else if kshchar == b'+' {
2575        // c:1627-1629
2576        op = P_TWOHASH;
2577        *flagp = P_HSTART;
2578    } else if kshchar == b'?' {
2579        // c:1630-1632
2580        op = 0; // sentinel — `?` desugars to (x|) via the BRANCH path below
2581        *flagp = 0;
2582    } else {
2583        // c:1637-1644 — `#` / `##`.
2584        let parse_h = patparse.lock().unwrap();
2585        let two = q_off + 1 < parse_h.len() && parse_h.as_bytes()[q_off + 1] == b'#';
2586        drop(parse_h);
2587        if two {
2588            op = P_TWOHASH;
2589            consume_hashes = 2;
2590        } else {
2591            op = P_ONEHASH;
2592            consume_hashes = 1;
2593        }
2594        *flagp = P_HSTART;
2595    }
2596    if consume_hashes > 0 {
2597        patparse_off.fetch_add(consume_hashes, Ordering::Relaxed);
2598    }
2599
2600    // c:1705-1746 — quantifier emission.
2601    //
2602    // Read the atom's opcode so c:1705 (`?#` → `*`) optimization can
2603    // apply. The atom byte sits at `atom + I_OP` in patout.
2604    let atom_op = {
2605        let buf = patout.lock().unwrap();
2606        if (atom as usize) + I_OP < buf.len() {
2607            buf[atom as usize + I_OP]
2608        } else {
2609            0
2610        }
2611    };
2612
2613    if ((*flagp & P_SIMPLE) != 0) && (op == P_ONEHASH || op == P_TWOHASH) && atom_op == P_ANY {
2614        // c:1705-1717 — `?#` becomes `*`; `?##` becomes `?*`. The atom
2615        // is P_ANY at offset `atom`; rewrite or pad as needed.
2616        let mut buf = patout.lock().unwrap();
2617        if op == P_TWOHASH {
2618            // c:1712-1713 — `?##` → `?*`: leave the P_ANY in place,
2619            // then emit P_STAR right after.
2620            drop(buf);
2621            let _star = patnode(P_STAR);
2622            *tail_out = atom as usize;
2623        } else {
2624            // c:1715-1716 — `?#` → `*`: just rewrite atom's opcode.
2625            buf[atom as usize + I_OP] = P_STAR;
2626            drop(buf);
2627            *tail_out = atom as usize;
2628        }
2629        *flagp &= !P_PURESTR;
2630    } else if ((*flagp & P_SIMPLE) != 0)
2631        && op != 0
2632        && (patglobflags.load(Ordering::Relaxed) & 0xff) == 0
2633    {
2634        // c:1718-1720 — simple operand, no approximate-match counter:
2635        // emit `patinsert(op, starter, NULL, 0)`. The matcher walks
2636        // the inserted P_ONEHASH/P_TWOHASH operand at scan+I_BODY.
2637        patinsert(op, atom as usize, None, 0);
2638        *flagp &= !P_PURESTR;
2639        *tail_out = atom as usize;
2640    } else if op == P_ONEHASH {
2641        // c:1721-1729 — emit x# as (x&|): P_WBRANCH with NULL payload
2642        // ahead of atom; loop via P_BACK; null alternative branch.
2643        // patinsert with `sz=size_of::<union upat>()=8` (we use 8 to
2644        // mirror the C `union upat` slot, then zero-pad it).
2645        let payload = [0u8; 8];
2646        patinsert(P_WBRANCH, atom as usize, Some(&payload), 8);
2647        // Either x — atom is now at atom+I_BODY+8.
2648        let back = patnode(P_BACK);
2649        patoptail(atom as usize, back); // c:1726 — loop back
2650        patoptail(atom as usize, atom as usize); // c:1727
2651        let alt = patnode(P_BRANCH);
2652        pattail(atom as usize, alt); // c:1728 — or
2653        let null_node = patnode(P_NOTHING);
2654        pattail(atom as usize, null_node); // c:1729 — null
2655        *flagp &= !P_PURESTR;
2656        // The piece's chain-tail is the trailing P_NOTHING node — its
2657        // `.next` slot is empty and is the correct splice point for
2658        // whatever piece patcompbranch chains in next.
2659        *tail_out = null_node;
2660    } else if op == P_TWOHASH {
2661        // c:1730-1738 — emit x## as x(&|): P_WBRANCH after atom; loop
2662        // back; null branch.
2663        let wbranch = patnode(P_WBRANCH); // c:1732 — either
2664        let payload = [0u8; 8]; // c:1733-1734 patadd((char *)&up, ..., sizeof(up), 0)
2665        {
2666            let mut buf = patout.lock().unwrap();
2667            buf.extend_from_slice(&payload);
2668        }
2669        pattail(atom as usize, wbranch); // c:1735
2670        let back = patnode(P_BACK);
2671        pattail(back, atom as usize); // c:1736 — loop back
2672        let alt = patnode(P_BRANCH);
2673        pattail(wbranch, alt); // c:1737 — or
2674        let null_node = patnode(P_NOTHING);
2675        pattail(atom as usize, null_node); // c:1738 — null
2676        *flagp &= !P_PURESTR;
2677        // Same as ONEHASH path — tail at the trailing P_NOTHING.
2678        *tail_out = null_node;
2679    } else if kshchar == b'?' {
2680        // c:1739-1746 — emit ?(x) as (x|).
2681        patinsert(P_BRANCH, atom as usize, None, 0); // c:1741 — either x
2682        let alt = patnode(P_BRANCH); // c:1742 — or
2683        pattail(atom as usize, alt);
2684        let null_node = patnode(P_NOTHING); // c:1743 — null
2685        pattail(atom as usize, null_node); // c:1744
2686        patoptail(atom as usize, null_node); // c:1745
2687        *flagp &= !P_PURESTR;
2688        // Same as ONEHASH/TWOHASH — tail at the trailing P_NOTHING.
2689        *tail_out = null_node;
2690    }
2691
2692    // c:1747-1748 — a stray `#` immediately after = compile error.
2693    {
2694        let p = patparse.lock().unwrap();
2695        let q2 = patparse_off.load(Ordering::Relaxed);
2696        if q2 < p.len() && p.as_bytes()[q2] == b'#' {
2697            // Only flag as error when EXTENDEDGLOB-style # is enabled.
2698            let sp = zpc_special.lock().unwrap();
2699            if sp[ZPC_HASH as usize] == b'#' {
2700                return -1;
2701            }
2702        }
2703    }
2704
2705    atom
2706}
2707
2708/// Port of `patcompnot(int paren, int *flagsp)` from `Src/pattern.c:1760`.
2709///
2710/// C: `static long patcompnot(int paren, int *flagsp)`. Implements
2711/// the `^pat` (paren=0) or `!(pat)` (paren=1) extended/ksh-glob
2712/// negation by emitting `P_BRANCH → P_STAR → P_EXCSYNC` followed by
2713/// `P_EXCLUDE [payload]` and the asserted pattern terminated by
2714/// `P_EXCEND`, all joined at a trailing `P_NOTHING`.
2715///
2716/// NOTE: matcher support for `P_EXCSYNC`/`P_EXCEND`/`P_EXCLUDE` is
2717/// only partial in the Rust port — compile is faithful per
2718/// pattern.c:1760-1784 but match-time negation semantics may diverge
2719/// from C for complex backtracking edge cases. Tests covering
2720/// `!(foo)` / `!(foo|bar)` exercise the basic working subset.
2721pub fn patcompnot(paren: i32, flagsp: &mut i32) -> i64 {
2722    // c:1760
2723    // c:1767 — `*flagsp = P_HSTART;`. Negation always starts with `*`
2724    // semantically so the caller knows the piece can match at the
2725    // start of input.
2726    *flagsp = P_HSTART;
2727
2728    // c:1769 — `starter = patnode(P_BRANCH);`
2729    let starter = patnode(P_BRANCH);
2730    // c:1770 — `br = patnode(P_STAR);`
2731    let br = patnode(P_STAR);
2732    // c:1771 — `excsync = patnode(P_EXCSYNC);`
2733    let excsync = patnode(P_EXCSYNC);
2734    // c:1772 — `pattail(br, excsync);`
2735    pattail(br, excsync);
2736    // c:1773 — `pattail(starter, excl = patnode(P_EXCLUDE));`
2737    let excl = patnode(P_EXCLUDE);
2738    pattail(starter, excl);
2739    // c:1774-1775 — `up.p = NULL; patadd((char *)&up, 0, sizeof(up), 0);`
2740    // The EXCLUDE node carries an 8-byte syncptr payload, NULL-init.
2741    {
2742        let mut buf = patout.lock().unwrap();
2743        buf.extend_from_slice(&[0u8; 8]);
2744    }
2745    // c:1776 — `br = (paren ? patcompswitch(1, &dummy) : patcompbranch(&dummy, 0));`
2746    let mut dummy: i32 = 0;
2747    let inner = if paren != 0 {
2748        let r = patcompswitch(1, &mut dummy);
2749        // c:1503-1505 — caller `Inpar` arm expects to consume the
2750        // trailing `)`. Mirror here since patcompnot is invoked from
2751        // the b'(' atom-arm AFTER it already consumed `(`.
2752        if r >= 0 {
2753            let cur = patparse_off.load(Ordering::Relaxed);
2754            let p = patparse.lock().unwrap();
2755            if cur >= p.len() || p.as_bytes()[cur] != b')' {
2756                return -1;
2757            }
2758            drop(p);
2759            patparse_off.fetch_add(1, Ordering::Relaxed);
2760        }
2761        r
2762    } else {
2763        patcompbranch(&mut dummy, 0)
2764    };
2765    if inner < 0 {
2766        return -1; // c:1777 `return 0;` (Rust uses -1 for failure)
2767    }
2768    // c:1778 — `pattail(br, patnode(P_EXCEND));`
2769    let excend = patnode(P_EXCEND);
2770    pattail(inner as usize, excend);
2771    // c:1779 — `n = patnode(P_NOTHING);`
2772    let n = patnode(P_NOTHING);
2773    // c:1780 — `pattail(excsync, n);`
2774    pattail(excsync, n);
2775    // c:1781 — `pattail(excl, n);`
2776    pattail(excl, n);
2777    // c:1783
2778    let _ = br; // suppress unused-var (kept for c-name parity)
2779    starter as i64
2780}
2781
2782/// Port of `patnode(long op)` from `Src/pattern.c:1790`.
2783///
2784/// C: `static long patnode(long op)` — writes a 1-byte opcode plus a
2785/// 4-byte zeroed next-offset. Returns the offset of the opcode byte.
2786fn patnode(op: u8) -> usize {
2787    // c:1790
2788    let mut buf = patout.lock().unwrap();
2789    let off = buf.len();
2790    buf.push(op); // I_OP
2791    buf.extend_from_slice(&[0, 0, 0, 0]); // I_NEXT zeroed
2792    off
2793}
2794
2795/// Port of `patinsert(long op, int opnd, char *xtra, int sz)` from `Src/pattern.c:1807`.
2796///
2797/// C: `static void patinsert(long op, int opnd, char *xtra, int sz)`.
2798/// Inserts an opcode (+ next slot) at position `opnd`, shifting bytes
2799/// after it down by `5 + sz`, then writes `xtra` payload of `sz` bytes.
2800fn patinsert(op: u8, opnd: usize, xtra: Option<&[u8]>, sz: usize) {
2801    // c:1807
2802    let mut buf = patout.lock().unwrap();
2803    let header_sz = 1 + 4; // op + next
2804    let total = header_sz + sz;
2805    // Insert `total` zeroed bytes at opnd, then overwrite.
2806    let mut inserted = vec![0u8; total];
2807    inserted[0] = op;
2808    if let Some(x) = xtra {
2809        let copy_n = x.len().min(sz);
2810        inserted[header_sz..header_sz + copy_n].copy_from_slice(&x[..copy_n]);
2811    }
2812    buf.splice(opnd..opnd, inserted);
2813    // Patch up next_off chains pointing past opnd by adding `total`.
2814    fixup_offsets_after_insert(&mut buf, opnd, total as u32);
2815}
2816
2817/// Port of `pattail(long p, long val)` from `Src/pattern.c:1834`.
2818///
2819/// C: `static void pattail(long p, long val)` — patches the next-offset
2820/// field of the opcode at offset `p` to point to `val`. Walks any
2821/// existing chain to the end before patching.
2822fn pattail(p: usize, val: usize) {
2823    // c:1834
2824    let mut buf = patout.lock().unwrap();
2825    let mut cur = p;
2826    loop {
2827        if cur + I_BODY > buf.len() {
2828            return;
2829        }
2830        let next_bytes: [u8; 4] = buf[cur + I_NEXT..cur + I_NEXT + 4].try_into().unwrap();
2831        let next = u32::from_le_bytes(next_bytes) as usize;
2832        if next == 0 {
2833            break;
2834        }
2835        cur = next;
2836    }
2837    let val_bytes = (val as u32).to_le_bytes();
2838    if cur + I_NEXT + 4 <= buf.len() {
2839        buf[cur + I_NEXT..cur + I_NEXT + 4].copy_from_slice(&val_bytes);
2840    }
2841}
2842
2843/// Port of `patoptail(long p, long val)` from `Src/pattern.c:1856`.
2844///
2845/// C: `static void patoptail(long p, long val)` — like pattail but
2846/// only patches branches (P_BRANCH/P_WBRANCH).
2847///
2848/// C: c:1862-1865 — for P_BRANCH the operand sits at `P_OPERAND(p)` =
2849/// `p+1` (Upat slot, i.e. byte offset `p + I_BODY` in Rust); for
2850/// P_WBRANCH the operand sits at `P_OPERAND(p) + 1` (skipping the
2851/// 8-byte syncptr payload Upat slot), i.e. byte offset
2852/// `p + I_BODY + 8` in Rust.
2853fn patoptail(p: usize, val: usize) {
2854    // c:1856
2855    let buf = patout.lock().unwrap();
2856    if p + I_OP >= buf.len() {
2857        return;
2858    }
2859    let op = buf[p + I_OP];
2860    drop(buf);
2861    if P_ISBRANCH(op) {
2862        // c:1862-1865 — operand offset depends on op kind.
2863        if op == P_BRANCH {
2864            pattail(p + I_BODY, val);
2865        } else {
2866            // P_WBRANCH / P_EXCLUDE / P_EXCLUDP — operand sits after
2867            // the 8-byte syncptr payload.
2868            pattail(p + I_BODY + 8, val);
2869        }
2870    }
2871}
2872
2873// =====================================================================
2874// 13/14. Matcher — pattern.c:2223-3579
2875// =====================================================================
2876
2877/// State accumulated during a single `patmatch` walk. C uses
2878/// per-thread globals (`patbeginp[]` / `patendp[]` for captures);
2879/// the Rust port encapsulates them in this struct passed by `&mut`.
2880/// Rule D: this struct represents matcher-internal scratch state
2881/// (analogous to `struct rpat pattrystate` at pattern.c:248), not a
2882/// bag-of-globals from unrelated subsystems.
2883///
2884/// **C counterpart**: `struct rpat` at `pattern.c:248`.
2885#[derive(Clone)]
2886#[allow(non_camel_case_types)]
2887pub struct rpat {
2888    /// Per-P_WBRANCH visit bitmap, keyed by WBRANCH-opcode offset.
2889    /// Mirrors C's `Upat ptrp` (`pattern.c:3217`): every WBRANCH carries
2890    /// an 8-byte payload sized as `union upat` — initialised to NULL,
2891    /// then lazily filled by `patmatch` with a buffer the size of the
2892    /// input string. Each byte tracks "have we already tried this
2893    /// WBRANCH at this input position with at most this many errors?".
2894    /// On revisit at the same position with the same-or-fewer errors,
2895    /// C returns 0 to bound the recursion (`pattern.c:3245-3248`). The
2896    /// Rust port keeps the bitmap on rpat (per-pattry call) instead of
2897    /// inside the bytecode payload so the bytecode stays read-only —
2898    /// the key is the WBRANCH offset; the value is a Vec<u8> the size
2899    /// of the input. Without it, `(fo#)#` against ANY input that
2900    /// requires the closure to consume at least one char per iteration
2901    /// (like "ffo") burns through PATMATCH_MAX_DEPTH and aborts.
2902    pub wbranch_visits: std::collections::HashMap<usize, Vec<u8>>,
2903    pub patbeginp: [usize; NSUBEXP], // c:241 capture starts (byte offsets)
2904    pub patendp: [usize; NSUBEXP],   // c:242 capture ends
2905    /// `parsfound` from `Src/pattern.c` (per c:2957/c:2989 references).
2906    /// Two-stripe bitmap: bits `0..NSUBEXP` track per-group P_OPEN
2907    /// first-write (`patbeginp[i]` committed); bits `NSUBEXP..2*NSUBEXP`
2908    /// track per-group P_CLOSE first-write (`patendp[i]` committed).
2909    /// Bit `n-1` (open) = `1 << (n-1)`; bit `n-1+NSUBEXP` (close) =
2910    /// `1 << (n-1+NSUBEXP)`. Width u32 to fit `2*NSUBEXP = 18` bits.
2911    /// The prior u16 width with a single-stripe (close-only) bit
2912    /// allowed P_OPEN to re-overwrite `patbeginp[i]` on every
2913    /// backtrack iteration — turning the SECOND capture's start
2914    /// offset into the FIRST iteration's, e.g. `(*)-(*)` against
2915    /// `hello-world` returned `match[2] = "hello-world"` instead of
2916    /// the right `"world"`.
2917    pub captures_set: u32, // c:Src/pattern.c parsfound
2918    /// Port of file-static `int errsfound` from `Src/pattern.c:2046`.
2919    /// Cumulative edit-count for approximate-match `(#aN)`. Reset to 0
2920    /// at the top of each pattry, incremented on P_EXACTLY mismatches
2921    /// when `glob_flags & 0xff > 0` (the substitution-budget byte).
2922    pub errsfound: i32, // c:2046
2923}
2924
2925/// Port of `wchar_t charref(char *x, char *y, int *zmb_ind)` from
2926/// `Src/pattern.c:1909`. Decode the char at `pos` without
2927/// advancing. UTF-32-native delegation: `s.chars().next()` returns
2928/// the decoded codepoint; delegates to Rust's native UTF-8 string
2929/// iterator instead of the C Meta-decode + zshtoken-translate +
2930/// mbrtowc state machine, which all collapse because Rust's `&str`
2931/// is already UTF-8.
2932///
2933/// Rust signature differs from C `charref(char *x, char *y, int *zmb_ind)`:
2934/// the C `y` end-of-string pointer is captured by `&str` length; the
2935/// `zmb_ind` out-param (multibyte-completion index) is dropped — the
2936/// Rust UTF-8 decode is one-shot, never partial.
2937pub fn charref(s: &str, pos: usize) -> Option<char> {
2938    // c:1909
2939    s[pos..].chars().next()
2940}
2941
2942/// Port of `char *charnext(char *x, char *y)` from `Src/pattern.c:1935`.
2943/// C returns the pointer past the current character; the Rust port
2944/// returns that position as a byte offset. Delegates to `metacharinc`
2945/// — same advance-by-one-codepoint logic.
2946pub fn charnext(x: &str, y: usize) -> usize {
2947    // c:1936
2948    metacharinc(x, y)
2949}
2950
2951/// Port of `wchar_t charrefinc(char **x, char *y, int *z)` from
2952/// `Src/pattern.c:1964`. Decode + advance: delegates to the
2953/// `charref`-then-`len_utf8`-step pattern. The C body's Meta /
2954/// mbrtowc / zshtoken triple collapses to one `chars().next()`
2955/// call followed by a byte-count step.
2956///
2957/// Rust signature differs from C: C mutates `*x` via the
2958/// `char **` to advance; Rust mutates `pos` directly. The `y`
2959/// end-of-string sentinel is captured by `&str` length; `z`
2960/// (multibyte-completion index) is dropped per the same one-shot
2961/// UTF-8 decode argument as `charref`.
2962pub fn charrefinc(s: &str, pos: &mut usize) -> Option<char> {
2963    // c:1964
2964    let c = s[*pos..].chars().next()?;
2965    *pos += c.len_utf8();
2966    Some(c)
2967}
2968
2969/// Port of `ptrdiff_t charsub(char *x, char *y)` from `Src/pattern.c:1997`.
2970///
2971/// Returns the number of characters between the start `x` and the
2972/// position `y` — i.e. the character-distance of byte offset `y` from
2973/// the string start. Non-multibyte: the byte distance `y` (each byte
2974/// is one character). Multibyte: the codepoint count of `x[0..y]`.
2975/// `patmatch` uses this (via the C `CHARSUB` macro) to report match
2976/// lengths and backreference positions in characters.
2977///
2978/// Replaces the prior fake that stepped back one char and returned a
2979/// byte offset — unrelated to the C character-count semantics.
2980pub fn charsub(x: &str, y: usize) -> usize {
2981    // c:1997
2982    let y = y.min(x.len());
2983    if !isset(MULTIBYTE) {
2984        // c:2003-2004 — `if (!isset(MULTIBYTE)) return y - x;`
2985        return y;
2986    }
2987    // c:2006-2021 — count codepoints in `x[0..y]`. Rust `&str` is valid
2988    // UTF-8, so `chars().count()` is the faithful equivalent of the C
2989    // mbrtowc loop (the MB_INVALID byte-by-byte fallback can't arise —
2990    // a `&str` never holds invalid sequences).
2991    x[..y].chars().count()
2992}
2993
2994// =====================================================================
2995// 16. String pre-processing — pattern.c:2063, :2080, :2132
2996// =====================================================================
2997
2998/// Port of `pattrystart()` from `Src/pattern.c:2063`. C resets per-
2999/// match state globals; Rust state is per-call so no-op.
3000pub fn pattrystart() {} // c:2063
3001
3002/// Port of `void patmungestring(char **string, int *stringlen, int *unmetalenin)`
3003/// from `Src/pattern.c:2080`.
3004///
3005/// Skips a leading `Nularg` (the empty-tokenised-string sentinel)
3006/// and computes `*stringlen` from `strlen` when caller passed `-1`.
3007/// Mutates all three in/out args; mirrors C's `char **` /
3008/// `int *` semantics via Rust mutable references.
3009pub fn patmungestring(string: &mut &str, stringlen: &mut i32, unmetalenin: &mut i32) {
3010    // c:2080
3011    // c:2082-2091 — `if (*stringlen > 0 && **string == Nularg)` — skip
3012    // the leading Nularg sentinel and adjust lengths.
3013    let bytes = string.as_bytes();
3014    if *stringlen > 0 && !bytes.is_empty() && bytes[0] as char == Nularg {
3015        // c:2085 — `(*string)++;` — advance past Nularg.
3016        *string = &string[1..]; // c:2085
3017                                // c:2090 — `if (*unmetalenin > 0) (*unmetalenin)--;`
3018        if *unmetalenin > 0 {
3019            // c:2089
3020            *unmetalenin -= 1; // c:2090
3021        }
3022        // c:2092 — `if (*stringlen > 0) (*stringlen)--;`
3023        if *stringlen > 0 {
3024            // c:2091
3025            *stringlen -= 1; // c:2092
3026        }
3027    }
3028
3029    // c:2096-2097 — `if (*stringlen < 0) *stringlen = strlen(*string);`
3030    if *stringlen < 0 {
3031        // c:2096
3032        *stringlen = string.len() as i32; // c:2097
3033    }
3034}
3035
3036/// Port of `pattry(Patprog prog, char *string)` from `Src/pattern.c:2223`.
3037///
3038/// C signature: `int pattry(Patprog prog, char *string)`. Returns
3039/// non-zero on match, 0 on no-match.
3040pub fn pattry(prog: &Patprog, string: &str) -> bool {
3041    // c:2223
3042    // c:2225 — `return pattrylen(prog, string, len, -1, NULL, 0);`
3043    pattrylen(prog, string, string.len() as i32, -1, None, 0) // c:2225
3044}
3045
3046/// Port of `int pattrylen(Patprog prog, char *string, int len,
3047/// int unmetalen, Patstralloc patstralloc, int offset)` from
3048/// `Src/pattern.c:2236`.
3049///
3050/// ```c
3051/// int
3052/// pattrylen(Patprog prog, char *string, int len, int unmetalen,
3053///           Patstralloc patstralloc, int offset)
3054/// {
3055///     return pattryrefs(prog, string, len, unmetalen, patstralloc, offset,
3056///                       NULL, NULL, NULL);
3057/// }
3058/// ```
3059pub fn pattrylen(
3060    prog: &Patprog,
3061    string: &str,
3062    len: i32, // c:2236
3063    unmetalen: i32,
3064    patstralloc: Option<&Patstralloc>,
3065    offset: i32,
3066) -> bool {
3067    // c:2238
3068    pattryrefs(
3069        prog,
3070        string,
3071        len,
3072        unmetalen,
3073        patstralloc,
3074        offset,
3075        None,
3076        None,
3077        None, // c:2239
3078    )
3079}
3080
3081/// Port of `int pattryrefs(Patprog prog, char *string, int stringlen,
3082/// int unmetalenin, Patstralloc patstralloc, int patoffset,
3083/// int *nump, int *begp, int *endp)` from `Src/pattern.c:2294`.
3084/// Runs `prog` against `string[0..stringlen]` (or whole string when
3085/// stringlen=-1) at `patoffset`, returning capture-group ranges.
3086///
3087/// C signature preserved per Rule S1. `Patstralloc` (the metafied-
3088/// string-allocator carrier from zsh.h:1613) is threaded through as
3089/// `Option<&Patstralloc>` matching C's `NULL`-passable pointer; the
3090/// matcher body currently ignores its contents (the substrate that
3091/// uses pre-allocated metafied buffers isn't wired into the Rust
3092/// matcher yet), but the param shape matches C so call sites trace
3093/// 1:1 to upstream.
3094#[allow(clippy::too_many_arguments)]
3095pub fn pattryrefs(
3096    // c:2294
3097    prog: &Patprog,
3098    string: &str,
3099    stringlen: i32,
3100    _unmetalenin: i32,
3101    _patstralloc: Option<&Patstralloc>,
3102    patoffset: i32,
3103    nump: Option<&mut i32>,
3104    begp: Option<&mut Vec<i32>>,
3105    endp: Option<&mut Vec<i32>>,
3106) -> bool {
3107    let trial: &str = if stringlen < 0 || (stringlen as usize) >= string.len() {
3108        string
3109    } else {
3110        &string[..stringlen as usize]
3111    };
3112    // Substring fast path for the ubiquitous `*literal*` shape
3113    // (optionally `(#i)`): program is [P_GFLAGS] P_STAR P_EXACTLY
3114    // P_STAR P_END with no captures requested. history-search-multi-
3115    // word's `${history[(R)(#i)*pat*]}` runs this shape over EVERY
3116    // history entry — 566k backtracking patmatch walks took CPU-
3117    // minutes per ^R (the reported freeze); a memmem-style scan is
3118    // what the shape actually needs. C reads this spirit via its
3119    // `mustoff` must-match prefilter (Src/pattern.c:2460-2483) but
3120    // declines under globflags; here the exact-bytes variant covers
3121    // any content and the `(#i)` variant is taken only when pattern
3122    // AND subject are pure ASCII (byte-fold == the matcher's char
3123    // fold there); anything else falls through to the full matcher.
3124    if nump.is_none()
3125        && begp.is_none()
3126        && endp.is_none()
3127        && patoffset == 0
3128        && (prog.0.flags & (PAT_NOTSTART | PAT_NOTEND) as i32) == 0
3129        // c:2399-2406 — a successful match is REJECTED for a file pattern
3130        // (PAT_NOGLD) whose subject starts with '.', unless the pattern itself
3131        // starts with a literal dot. That rejection lives below, AFTER
3132        // patmatch; returning from the fast path skipped it entirely, so
3133        // `*.*` matched `.hidden` (the literal `.` was found at offset 0 by
3134        // the substring scan) where zsh lists no dot files at all. Hand any
3135        // dot-leading subject under a file pattern to the full matcher, which
3136        // applies the rule. Costs nothing where the fast path was aimed:
3137        // history search (`${history[(R)(#i)*pat*]}`) is not a file glob and
3138        // never sets PAT_NOGLD, and non-dot files still take the fast path.
3139        && !((prog.0.flags & PAT_NOGLD as i32) != 0 && trial.starts_with('.'))
3140    {
3141        // Shape probe, inline (build-gate: no new fn in ported/ without
3142        // a C counterpart): Some((literal, igncase)) iff the program is
3143        // exactly `[P_GFLAGS] P_STAR P_EXACTLY(lit) P_STAR P_END` with
3144        // glob flags ⊆ IGNCASE|MULTIBYTE (no approx budget, no backrefs).
3145        let shape: Option<(String, bool)> = (|| {
3146            let buf: &[u8] = &prog.1;
3147            let mut off = prog.0.startoff as usize;
3148            let mut gflags = prog.0.globflags;
3149            let op_at = |o: usize| -> Option<u8> {
3150                if o + I_BODY <= buf.len() {
3151                    Some(buf[o + I_OP])
3152                } else {
3153                    None
3154                }
3155            };
3156            // A sole leading BRANCH (next == 0: no alternative) wraps
3157            // the whole program — step inside it.
3158            if op_at(off)? == P_BRANCH {
3159                let next =
3160                    u32::from_le_bytes(buf[off + I_NEXT..off + I_NEXT + 4].try_into().ok()?);
3161                if next != 0 {
3162                    return None; // real alternation — full matcher
3163                }
3164                off = advance_past_instr(buf, off);
3165            }
3166            if op_at(off)? == P_GFLAGS {
3167                let body = off + I_BODY;
3168                if body + 4 > buf.len() {
3169                    return None;
3170                }
3171                gflags |= i32::from_le_bytes(buf[body..body + 4].try_into().ok()?);
3172                off = advance_past_instr(buf, off);
3173            }
3174            if (gflags & !(GF_IGNCASE | GF_MULTIBYTE)) != 0 {
3175                return None;
3176            }
3177            if op_at(off)? != P_STAR {
3178                return None;
3179            }
3180            off = advance_past_instr(buf, off);
3181            // One or more contiguous EXACTLY chunks form the literal
3182            // (the compiler splits long/space-bearing literals).
3183            if op_at(off)? != P_EXACTLY {
3184                return None;
3185            }
3186            let mut lit = String::new();
3187            while op_at(off)? == P_EXACTLY {
3188                let body = off + I_BODY;
3189                if body + 4 > buf.len() {
3190                    return None;
3191                }
3192                let len = u32::from_le_bytes(buf[body..body + 4].try_into().ok()?) as usize;
3193                if body + 4 + len > buf.len() {
3194                    return None;
3195                }
3196                lit.push_str(std::str::from_utf8(&buf[body + 4..body + 4 + len]).ok()?);
3197                off = advance_past_instr(buf, off);
3198            }
3199            if op_at(off)? != P_STAR {
3200                return None;
3201            }
3202            off = advance_past_instr(buf, off);
3203            if off + I_OP >= buf.len() || buf[off + I_OP] != P_END {
3204                return None;
3205            }
3206            Some((lit, (gflags & GF_IGNCASE) != 0))
3207        })();
3208        if let Some((lit, igncase)) = shape.as_ref().map(|(l, i)| (l.as_str(), *i)) {
3209            if !igncase {
3210                return trial.contains(lit);
3211            }
3212            if lit.is_ascii() && trial.is_ascii() {
3213                let lb = lit.as_bytes();
3214                let tb = trial.as_bytes();
3215                if lb.is_empty() {
3216                    return true;
3217                }
3218                if lb.len() <= tb.len() {
3219                    let l0 = lb[0].to_ascii_lowercase();
3220                    'scan: for s in 0..=(tb.len() - lb.len()) {
3221                        if tb[s].to_ascii_lowercase() != l0 {
3222                            continue;
3223                        }
3224                        for k in 1..lb.len() {
3225                            if tb[s + k].to_ascii_lowercase() != lb[k].to_ascii_lowercase() {
3226                                continue 'scan;
3227                            }
3228                        }
3229                        return true;
3230                    }
3231                }
3232                return false;
3233            }
3234            // Non-ASCII under (#i): general matcher below.
3235        }
3236    }
3237    let mut state = rpat::new();
3238    // c:Src/pattern.c:2334 — `patflags = prog->flags;` C copies the
3239    // prog's flags into the file-static `patflags` so subsequent
3240    // patmatch calls can read PAT_NOTSTART / PAT_NOTEND inside the
3241    // P_ISSTART / P_ISEND arms (c:3393, 3397). Rust mirrors this via
3242    // the `patflags` AtomicI32 at pattern.rs:217.
3243    //
3244    // Without this propagation, `${str:#(#s)PAT}` / `${str:#PAT(#e)}`
3245    // anchor checks fired at slice-local offset 0 / slice-local end
3246    // regardless of whether the caller meant the slice was an
3247    // interior chunk of a larger string (PAT_NOTSTART/PAT_NOTEND
3248    // bits exist on the prog precisely so callers can signal this).
3249    patflags.store(prog.0.flags, Ordering::Relaxed);
3250    // Pass the prog's GLOB flags (GF_* + `(#aN)` budget byte) to the
3251    // matcher. C threads `patglobflags` as a per-thread file-static;
3252    // the Rust port carries it through a fn param. The PAT_* flags
3253    // (PAT_STATIC etc.) stay on `prog.0.flags` for the outer
3254    // anchor/PURES checks at lines below.
3255    // c:Src/pattern.c — `if (prog->flags & PAT_ANY) { ret = 1; } else { ... }`.
3256    // Optimisation for a single "*" (the `**` complist node compiles its
3257    // section as `patcompile(NULL, …|PAT_ANY, …)`): it always matches the
3258    // whole string without running the bytecode. The PAT_NOGLD leading-dot
3259    // rejection below still applies (the "except for no_glob_dots" caveat).
3260    // Without this the empty `PAT_ANY` program was matched literally and
3261    // only matched the empty string, so `**` descended into nothing.
3262    let (mut ok, matched_end) = if (prog.0.flags & PAT_ANY as i32) != 0 {
3263        (true, trial.len())
3264    } else {
3265        match patmatch(&prog.1, 0, trial, 0, &mut state, prog.0.globflags) {
3266            Some(end_pos) => {
3267                // c:2438 — `if (matched && !(prog->flags & (PAT_NOANCH|PAT_NOTEND))) ...`
3268                let no_anchor = (prog.0.flags & (PAT_NOANCH | PAT_NOTEND) as i32) != 0;
3269                (no_anchor || end_pos == trial.len(), end_pos)
3270            }
3271            None => (false, 0),
3272        }
3273    };
3274    // c:2399-2406 — for files (PAT_NOGLD), a successful match is rejected
3275    // when the string starts with '.' (unless glob_dots is set, in which
3276    // case parsecomplist omits PAT_NOGLD). Honoring this here lets the
3277    // glob scanner rely on the matcher for the leading-dot skip, exactly
3278    // as C does, instead of an explicit check in the walker. Inert for
3279    // non-file patterns (matchpat / [[ ]] / :# never set PAT_NOGLD).
3280    if ok
3281        && (prog.0.flags & PAT_NOGLD as i32) != 0
3282        && trial.starts_with('.')
3283        && prog.0.patstartch != b'.'
3284    {
3285        // c:Src/pattern.c — NOGLD rejects a leading-dot file UNLESS the
3286        // pattern itself explicitly begins with a literal `.` (so `.*`
3287        // matches dot files while `*` does not). C bakes this into the
3288        // compiled bytecode; the Rust matcher carries the signal on
3289        // `patstartch` (c:1610) and applies the exception here.
3290        ok = false;
3291    }
3292    if ok {
3293        // c:2508 — `patinlen = patinput - patinstart;` — record the
3294        // byte-length of the successful match so `patmatchlen()` can
3295        // return it after the strings/state are torn down. `end_pos`
3296        // is the in-trial byte offset where patmatch stopped, which
3297        // is `patinput - patinstart` in C terms.
3298        patinlen.store(matched_end as i32, Ordering::Relaxed);
3299    }
3300    if ok {
3301        let n = (prog.0.patnpar as usize).min(NSUBEXP);
3302        let have_nump = nump.is_some();
3303        if let Some(np) = nump {
3304            *np = n as i32;
3305        }
3306        // c:2526-2542 — GF_MATCHREF (`(#m)`): on success, write the
3307        // matched substring to $MATCH and its 1-based (KSHARRAYS-
3308        // aware) char span to $MBEGIN/$MEND. C does this INSIDE
3309        // pattryrefs; the previous Rust port left it to a bridge
3310        // wrapper that sniffed the pattern TEXT for "(#m)" — wrong
3311        // mechanism (missed hoisted/nested flag positions) and
3312        // wrong layer.
3313        // c:2526 — `if ((patglobflags & GF_MATCHREF) && !(patflags & PAT_FILE))`.
3314        // The source is the GLOBAL patglobflags (c:273), not `prog->globflags`.
3315        // They are not the same thing: c:Src/zsh.h:1606 documents the struct
3316        // field as "globbing flags to set at START", so it keeps whatever the
3317        // pattern opened with, while the global is what patgetglobflags leaves
3318        // after walking every flag — including a later `(#M)` turning
3319        // GF_MATCHREF back off (c:1099-1100).
3320        //
3321        // Reading the struct field made `(#M)` inert: for all three of
3322        // `(#m)a*`, `(#m)(#M)a*` and `(#M)(#m)a*` it is 0x1800, whereas the
3323        // global correctly ends at 0x800 / 0x0 / 0x800. So `(#m)(#M)a*` still
3324        // wrote $MATCH where zsh leaves it unset.
3325        let cur_globflags = patglobflags.load(Ordering::Relaxed);
3326        if (cur_globflags & GF_MATCHREF) != 0 && (prog.0.flags & PAT_FILE as i32) == 0 {
3327            let hi = matched_end.min(trial.len());
3328            let mstr: String = trial[..hi].to_string(); // c:2536 metafy(patinstart..patinput)
3329            let mlen = mstr.chars().count() as i64; // c:2534 CHARSUB
3330            let base: i64 = if crate::ported::zsh_h::isset(crate::ported::zsh_h::KSHARRAYS) {
3331                0
3332            } else {
3333                1
3334            };
3335            crate::ported::params::setsparam("MATCH", &mstr); // c:2537
3336            crate::ported::params::setiparam("MBEGIN", patoffset as i64 + base); // c:2538
3337            crate::ported::params::setiparam("MEND", mlen + patoffset as i64 + base - 1);
3338            // c:2539-2541
3339        }
3340        // c:2425+ — emit captured offsets to begp/endp out-arrays.
3341        // Check the CLOSE bit (high stripe at NSUBEXP+i) since the
3342        // group is only fully captured after its P_CLOSE fired; the
3343        // matching open-bit `1 << i` would be set after P_OPEN even
3344        // when the rest of the match later fails to reach P_CLOSE.
3345        //
3346        // c:Src/pattern.c:2556-2558 — `*begp++ = CHARSUB(patinstart, *sp) + patoffset;`.
3347        // `patoffset` is the start position of `string` within the
3348        // ORIGINAL string the caller was matching against (used for
3349        // paramsubst's sliding-window scan via `${var/PAT/REPL}`). Add
3350        // it to every reported beg/end so MBEGIN/MEND / `$match` see
3351        // positions relative to the original string, not the local
3352        // trial slice.
3353        // c:2556-2566 — `*begp++ = …` writes into the caller's FIXED-SIZE array
3354        // (C: `int begpos[MAX_POS]`), filling the first `n` slots and leaving
3355        // the array's length unchanged. Write IN PLACE (extend only if the
3356        // caller's Vec is shorter than `n`) rather than clear()+push — clearing
3357        // shrank a MAX_POS-length array down to `n`, so the caller's
3358        // [n..MAX_POS] reset loop (complist.rs getcol, c:631) indexed past the
3359        // end and panicked (`index 4 on len 4`) on patterns with < MAX_POS
3360        // backrefs.
3361        if let Some(bv) = begp {
3362            for i in 0..n {
3363                let close_bit = 1u32 << (i + NSUBEXP);
3364                let val = if (state.captures_set & close_bit) != 0 {
3365                    state.patbeginp[i] as i32 + patoffset
3366                } else {
3367                    -1 // c:2563-2566 — unset group (unmatched alternation branch)
3368                };
3369                if i < bv.len() {
3370                    bv[i] = val;
3371                } else {
3372                    bv.push(val);
3373                }
3374            }
3375        }
3376        if let Some(ev) = endp {
3377            for i in 0..n {
3378                let close_bit = 1u32 << (i + NSUBEXP);
3379                let val = if (state.captures_set & close_bit) != 0 {
3380                    state.patendp[i] as i32 + patoffset
3381                } else {
3382                    -1 // c:2565
3383                };
3384                if i < ev.len() {
3385                    ev[i] = val;
3386                } else {
3387                    ev.push(val);
3388                }
3389            }
3390        }
3391        // c:2570-2621 — `else if (prog->patnpar && !(patflags &
3392        // PAT_FILE))`: the caller passed NO capture arrays, so
3393        // pattryrefs itself publishes the `(#b)` groups as the
3394        // $match / $mbegin / $mend arrays. This arm was missing
3395        // from the port — every plain pattry of a (#b) pattern
3396        // silently dropped its captures (the bridge compensated
3397        // with a wrapper; now C-faithful at the source).
3398        if !have_nump && n > 0 && (prog.0.flags & PAT_FILE as i32) == 0 {
3399            let base: i32 = if crate::ported::zsh_h::isset(crate::ported::zsh_h::KSHARRAYS) {
3400                0
3401            } else {
3402                1
3403            };
3404            let mut match_arr: Vec<String> = Vec::with_capacity(n);
3405            let mut begin_arr: Vec<String> = Vec::with_capacity(n);
3406            let mut end_arr: Vec<String> = Vec::with_capacity(n);
3407            for i in 0..n {
3408                let close_bit = 1u32 << (i + NSUBEXP);
3409                if (state.captures_set & close_bit) != 0 {
3410                    let b = state.patbeginp[i];
3411                    let e = state.patendp[i];
3412                    let lo = b.min(trial.len());
3413                    let hi = e.min(trial.len()).max(lo);
3414                    match_arr.push(trial[lo..hi].to_string()); // c:2587 metafy(*sp..*ep)
3415                    begin_arr.push((b as i32 + patoffset + base).to_string()); // c:2596-2599
3416                                                                               // c:2601-2604 — mend = last matched char index
3417                                                                               // (inclusive): end + offset + base - 1.
3418                    end_arr.push((e as i32 + patoffset + base - 1).to_string());
3419                } else {
3420                    // c:2607-2613 — unmatched branch / hashed paren.
3421                    match_arr.push(String::new());
3422                    begin_arr.push("-1".to_string());
3423                    end_arr.push("-1".to_string());
3424                }
3425            }
3426            crate::ported::params::setaparam("match", match_arr); // c:2619
3427            crate::ported::params::setaparam("mbegin", begin_arr); // c:2620
3428            crate::ported::params::setaparam("mend", end_arr); // c:2621
3429        }
3430    }
3431    ok
3432}
3433
3434// =====================================================================
3435// 15. Range matching — pattern.c:3856, :4004, :3610, :3767
3436// =====================================================================
3437
3438/// Direct port of `int patmatchlen(void)` from `Src/pattern.c:2649`.
3439///
3440/// ```c
3441/// /**/
3442/// int
3443/// patmatchlen(void)
3444/// {
3445///     return patinlen;
3446/// }
3447/// ```
3448///
3449/// Returns the length in metafied bytes of the last successful
3450/// `pattry` / `pattryrefs` match. `patinlen` is set at
3451/// `pattern.c:2508` (`patinlen = patinput - patinstart`); the
3452/// Rust port sets the equivalent `patinlen` AtomicI32 at the end
3453/// of `pattryrefs` (see `pattern.rs::pattryrefs`).
3454pub fn patmatchlen() -> i32 {
3455    // c:2649-2652
3456    patinlen.load(Ordering::Relaxed) // c:2651 — `return patinlen;`
3457}
3458/// Direct port of `int patmatchindex(char *range, int ind, int *chr, int *mtp)`
3459/// from `Src/pattern.c:4004` (MULTIBYTE_SUPPORT-disabled single-byte
3460/// variant). Walks a NULL-terminated, METAFIED, PP_*-encoded byte
3461/// stream `range` and returns the character (or POSIX-class id)
3462/// at index `ind`. Output:
3463///   - `Some((Some(ch), mtp))` — literal character or PP_RANGE hit;
3464///     `chr = ch`, `mtp = 0`.
3465///   - `Some((None,    mtp))` — POSIX class match (PP_ALPHA etc);
3466///     `chr = -1` in C, `None` in Rust; `mtp = class id`.
3467///   - `None` — `ind` exceeds the descriptor's length.
3468///
3469/// Encoding the byte stream uses:
3470///   * literal byte `< 0x83` = match char as itself
3471///   * `Meta(0x83)` + (byte ^ 0x20) = ordinary metafied character
3472///   * `Meta + PP_*` (0x84..) = POSIX class marker
3473///   * `Meta + PP_RANGE` = next two metafied chars are `lo, hi`
3474pub fn patmatchindex(range: &[u8], mut ind: i32) -> Option<(Option<u8>, i32)> {
3475    // c:4004-4081
3476    let mut chr: Option<u8> = None; // c:4014 — `*chr = -1`
3477    let mut mtp: i32 = 0; // c:4015 — `*mtp = 0`
3478
3479    // C `UNMETA(range)` + `METACHARINC(range)` macro pair from
3480    // Src/zsh.h:1796-1797 — decode-and-advance one metafied
3481    // character. Returned as `(decoded_byte, byte_advance)`.
3482    let unmeta = |bytes: &[u8], i: usize| -> (u8, usize) {
3483        if i < bytes.len() && bytes[i] == Meta && i + 1 < bytes.len() {
3484            (bytes[i + 1] ^ 0x20, 2)
3485        } else if i < bytes.len() {
3486            (bytes[i], 1)
3487        } else {
3488            (0, 0)
3489        }
3490    };
3491
3492    let mut i = 0usize;
3493    while i < range.len() {
3494        // c:4017 — `for (; *range; range++)`
3495        let b = range[i];
3496        if crate::ported::utils::imeta_byte(b) {
3497            // c:4018 — `if (imeta((unsigned char) *range))`
3498            // c:4019 — `int swtype = (unsigned char) *range - (unsigned char) Meta;`
3499            let swtype = (b as i32) - (Meta as i32);
3500            match swtype {
3501                0 => {
3502                    // c:4021-4028 — `case 0: ordinary metafied character`
3503                    // c:4023 — `rchr = (unsigned char) *++range ^ 32;`
3504                    i += 1;
3505                    if i >= range.len() {
3506                        break;
3507                    }
3508                    let rchr = range[i] ^ 0x20;
3509                    if ind == 0 {
3510                        // c:4024-4026 — `if (!ind) { *chr = rchr; return 1; }`
3511                        chr = Some(rchr);
3512                        return Some((chr, mtp));
3513                    }
3514                    // c:4027 — falls through to `if (!ind--) break;`
3515                }
3516                t if (PP_ALPHA..=PP_INVALID).contains(&t) => {
3517                    // c:4030-4051 — POSIX class markers PP_ALPHA..PP_INVALID
3518                    if ind == 0 {
3519                        // c:4052-4054 — `if (!ind) { *mtp = swtype; return 1; }`
3520                        mtp = swtype;
3521                        return Some((None, mtp));
3522                    }
3523                }
3524                t if t == PP_RANGE => {
3525                    // c:4057-4069 — PP_RANGE: next two metafied chars
3526                    // `range++; r1 = UNMETA(range); METACHARINC(range);
3527                    //  r2 = UNMETA(range); if (*range == Meta) range++;`
3528                    i += 1;
3529                    if i >= range.len() {
3530                        break;
3531                    }
3532                    let (r1, adv1) = unmeta(range, i);
3533                    i += adv1;
3534                    if i >= range.len() {
3535                        break;
3536                    }
3537                    let (r2, adv2) = unmeta(range, i);
3538                    i += adv2;
3539                    let rdiff = r2 as i32 - r1 as i32;
3540                    if rdiff >= ind {
3541                        // c:4063-4067 — `if (rdiff >= ind) { *chr = r1 + ind; return 1; }`
3542                        chr = Some((r1 as i32 + ind) as u8);
3543                        return Some((chr, mtp));
3544                    }
3545                    // c:4068 — `ind -= rdiff;` (extra decrement happens via
3546                    // the `ind--` below, accounting for the C comment
3547                    // "note the extra decrement to ind below").
3548                    ind -= rdiff;
3549                    // Skip the trailing `if (!ind--) break;` decrement
3550                    // for this branch since C's loop already does it.
3551                }
3552                _ => {
3553                    // c:4070-4076 — PP_UNKWN / default → DPUTS bug warn
3554                    // (unreachable in well-formed compiled output; bail).
3555                }
3556            }
3557        } else {
3558            // c:4079-4082 — literal char path
3559            if ind == 0 {
3560                // c:4080-4082 — `if (!ind) { *chr = (unsigned char) *range; return 1; }`
3561                chr = Some(b);
3562                return Some((chr, mtp));
3563            }
3564        }
3565        // c:4087 — `if (!ind--) break;` — pre-decrement-check.
3566        if ind == 0 {
3567            break;
3568        }
3569        ind -= 1;
3570        i += 1; // c:4017 — `range++`
3571    }
3572    // c:4091 — `/* No corresponding index. */ return 0;`
3573    None
3574}
3575
3576/// Direct port of `int mb_patmatchrange(char *range, wchar_t ch,
3577/// int zmb_ind, wint_t *indptr, int *mtp)` from `Src/pattern.c:3610`.
3578/// Multibyte variant of `patmatchrange`: walks the metafied,
3579/// PP_*-encoded byte stream `range` and tests whether wide char `ch`
3580/// is in it. `indptr` (when Some) accumulates the running index for
3581/// `mb_patmatchindex`-side lookup; `mtp` (when Some) records which
3582/// PP_* class fired.
3583///
3584/// `zmb_ind` is the `ZMB_*` multibyte-completion state from the
3585/// caller (typically the pattry input pos): ZMB_INCOMPLETE /
3586/// ZMB_INVALID feed the PP_INCOMPLETE / PP_INVALID branches.
3587///
3588/// The Rust port delegates `iswalpha` / `iswdigit` / `wcsitype` to
3589/// `is_alphabetic()` / `is_numeric()` / etc. on the decoded `char`,
3590/// matching the C `iswXXX(wchar_t)` semantics.
3591pub fn mb_patmatchrange(
3592    range: &[u8],
3593    ch: char,
3594    zmb_ind: i32,
3595    mut indptr: Option<&mut u32>,
3596    mut mtp: Option<&mut i32>,
3597) -> bool {
3598    // c:3610-3766
3599    if let Some(p) = indptr.as_deref_mut() {
3600        *p = 0; // c:3615-3616 — `if (indptr) *indptr = 0;`
3601    }
3602    // C `UNMETA(s)` + `METACHARINC(s)` macro pair (Src/zsh.h).
3603    let unmeta = |bytes: &[u8], i: usize| -> (u8, usize) {
3604        if i < bytes.len() && bytes[i] == Meta && i + 1 < bytes.len() {
3605            (bytes[i + 1] ^ 0x20, 2)
3606        } else if i < bytes.len() {
3607            (bytes[i], 1)
3608        } else {
3609            (0, 0)
3610        }
3611    };
3612    let mut i = 0usize;
3613    while i < range.len() {
3614        // c:3623 — `while (*range)`
3615        let b = range[i];
3616        if crate::ported::utils::imeta_byte(b) {
3617            // c:3624 — `if (imeta((unsigned char) *range))`
3618            // c:3625 — `swtype = (unsigned char) *range++ - (unsigned char) Meta;`
3619            let swtype = (b as i32) - (Meta as i32);
3620            i += 1;
3621            if let Some(p) = mtp.as_deref_mut() {
3622                *p = swtype; // c:3626-3627 — `if (mtp) *mtp = swtype;`
3623            }
3624            let class_hit = match swtype {
3625                0 => {
3626                    // c:3629-3634 — `case 0: ordinary metafied character`
3627                    i -= 1; // c:3631 — `range--;`
3628                    let (decoded, adv) = unmeta(range, i);
3629                    i += adv;
3630                    decoded == (ch as u32) as u8 && (ch as u32) < 256
3631                }
3632                t if t == PP_ALPHA => ch.is_alphabetic(),
3633                t if t == PP_ALNUM => ch.is_alphanumeric(),
3634                t if t == PP_ASCII => (ch as u32) < 128,
3635                t if t == PP_BLANK => ch == ' ' || ch == '\t',
3636                t if t == PP_CNTRL => ch.is_control(),
3637                t if t == PP_DIGIT => ch.is_ascii_digit(),
3638                t if t == PP_GRAPH => !ch.is_whitespace() && !ch.is_control(),
3639                t if t == PP_LOWER => ch.is_lowercase(),
3640                t if t == PP_PRINT => !ch.is_control(),
3641                t if t == PP_PUNCT => ch.is_ascii_punctuation(),
3642                t if t == PP_SPACE => ch.is_whitespace(),
3643                t if t == PP_UPPER => ch.is_uppercase(),
3644                t if t == PP_XDIGIT => ch.is_ascii_hexdigit(),
3645                t if t == PP_IDENT => {
3646                    // c:3704 — `PP_IDENT: if (wcsitype(ch, IIDENT)) return 1;`
3647                    // IIDENT = alphanumeric, underscore, or "$" depending on
3648                    // option state. Conservative: ASCII identifier chars.
3649                    ch.is_alphanumeric() || ch == '_'
3650                }
3651                t if t == PP_IFS => {
3652                    // c:3708 — `wcsitype(ch, ISEP)`. ISEP = space, tab,
3653                    // newline + user IFS additions. Default IFS in zsh
3654                    // is ` \t\n`.
3655                    ch == ' ' || ch == '\t' || ch == '\n' || ch == '\0'
3656                }
3657                t if t == PP_IFSSPACE => {
3658                    // c:3712-3713 — ASCII-only IFS-space.
3659                    (ch as u32) < 128 && (ch == ' ' || ch == '\t' || ch == '\n')
3660                }
3661                t if t == PP_WORD => {
3662                    // c:3716 — `wcsitype(ch, IWORD)`. IWORD = WORDCHARS-derived;
3663                    // conservative ASCII alphanumeric + `_`.
3664                    ch.is_alphanumeric() || ch == '_'
3665                }
3666                t if t == PP_RANGE => {
3667                    // c:3719-3735 — PP_RANGE: two metafied wide chars are
3668                    // `r1` and `r2`; matches if `r1 <= ch <= r2`.
3669                    let (r1_byte, adv1) = unmeta(range, i);
3670                    i += adv1;
3671                    let (r2_byte, adv2) = unmeta(range, i);
3672                    i += adv2;
3673                    let r1 = r1_byte as u32;
3674                    let r2 = r2_byte as u32;
3675                    let chu = ch as u32;
3676                    if r1 <= chu && chu <= r2 {
3677                        if let Some(p) = indptr.as_deref_mut() {
3678                            *p += chu - r1; // c:3725-3726
3679                        }
3680                        return true; // c:3727
3681                    }
3682                    if let Some(p) = indptr.as_deref_mut() {
3683                        if r1 < r2 {
3684                            *p += r2 - r1; // c:3733-3734
3685                        }
3686                    }
3687                    false
3688                }
3689                t if t == PP_INCOMPLETE => {
3690                    // c:3740 — `if (zmb_ind == ZMB_INCOMPLETE) return 1;`
3691                    zmb_ind == ZMB_INCOMPLETE
3692                }
3693                t if t == PP_INVALID => {
3694                    // c:3744 — `if (zmb_ind == ZMB_INVALID) return 1;`
3695                    zmb_ind == ZMB_INVALID
3696                }
3697                _ => {
3698                    // c:3747-3753 — PP_UNKWN / default → DPUTS bug warn
3699                    false
3700                }
3701            };
3702            if class_hit {
3703                return true;
3704            }
3705        } else {
3706            // c:3757-3762 — literal-char path
3707            let (decoded, adv) = unmeta(range, i);
3708            i += adv;
3709            if decoded == (ch as u32) as u8 && (ch as u32) < 256 {
3710                if let Some(p) = mtp.as_deref_mut() {
3711                    *p = 0; // c:3760
3712                }
3713                return true; // c:3761
3714            }
3715        }
3716        if let Some(p) = indptr.as_deref_mut() {
3717            *p += 1; // c:3764-3765 — `if (indptr) (*indptr)++;`
3718        }
3719    }
3720    false // c:3766 — `return 0;`
3721}
3722
3723/// Direct port of `int mb_patmatchindex(char *range, wint_t ind,
3724/// wint_t *chr, int *mtp)` from `Src/pattern.c:3767`. The reverse
3725/// of `mb_patmatchrange`: given a metafied byte range and an index
3726/// `ind` into it, return the character (or PP_* class) at that
3727/// position.
3728///
3729/// Returns:
3730///   - `Some((Some(ch),  0))` — literal or PP_RANGE hit (chr = ch).
3731///   - `Some((None,     mtp))` — POSIX class match; chr = WEOF / None.
3732///   - `None` — index out of range.
3733pub fn mb_patmatchindex(range: &[u8], mut ind: u32) -> Option<(Option<char>, i32)> {
3734    // c:3767-3849
3735    let mut chr: Option<char> = None; // c:3776 — `*chr = WEOF`
3736    let mut mtp: i32 = 0; // c:3777 — `*mtp = 0`
3737
3738    // C `UNMETA(s)` + `METACHARINC(s)` macro pair (Src/zsh.h).
3739    let unmeta = |bytes: &[u8], i: usize| -> (u8, usize) {
3740        if i < bytes.len() && bytes[i] == Meta && i + 1 < bytes.len() {
3741            (bytes[i + 1] ^ 0x20, 2)
3742        } else if i < bytes.len() {
3743            (bytes[i], 1)
3744        } else {
3745            (0, 0)
3746        }
3747    };
3748
3749    let mut i = 0usize;
3750    while i < range.len() {
3751        // c:3779 — `while (*range)`
3752        let b = range[i];
3753        if crate::ported::utils::imeta_byte(b) {
3754            // c:3780 — `if (imeta((unsigned char) *range))`
3755            let swtype = (b as i32) - (Meta as i32);
3756            i += 1;
3757            match swtype {
3758                0 => {
3759                    // c:3782-3789 — `case 0: ordinary metafied char`
3760                    i -= 1;
3761                    let (decoded, adv) = unmeta(range, i);
3762                    i += adv;
3763                    let rchr = decoded as char;
3764                    if ind == 0 {
3765                        chr = Some(rchr);
3766                        return Some((chr, mtp));
3767                    }
3768                }
3769                t if (PP_ALPHA..=PP_INVALID).contains(&t) => {
3770                    // c:3791-3812 — POSIX class markers
3771                    if ind == 0 {
3772                        mtp = swtype;
3773                        return Some((None, mtp));
3774                    }
3775                }
3776                t if t == PP_RANGE => {
3777                    // c:3814-3829 — PP_RANGE: two metafied wide chars
3778                    let (r1_byte, adv1) = unmeta(range, i);
3779                    i += adv1;
3780                    let (r2_byte, adv2) = unmeta(range, i);
3781                    i += adv2;
3782                    let r1 = r1_byte as u32;
3783                    let r2 = r2_byte as u32;
3784                    let rdiff = r2.saturating_sub(r1);
3785                    if rdiff >= ind {
3786                        chr = char::from_u32(r1 + ind);
3787                        return Some((chr, mtp));
3788                    }
3789                    ind = ind.saturating_sub(rdiff);
3790                }
3791                _ => {
3792                    // c:3831-3837 — PP_UNKWN / default → DPUTS
3793                }
3794            }
3795        } else {
3796            // c:3840-3843 — literal byte path
3797            if ind == 0 {
3798                chr = Some(b as char);
3799                return Some((chr, mtp));
3800            }
3801        }
3802        // c:3846 — `if (!ind--) break;`
3803        if ind == 0 {
3804            break;
3805        }
3806        ind -= 1;
3807        i += 1;
3808    }
3809    None // c:3849 — `return 0`
3810}
3811
3812/// Port of `freepatprog(Patprog prog)` from `Src/pattern.c:4161`. Frees a Patprog.
3813/// Rust's `Drop` on `Box<patprog>` handles this; the explicit fn
3814/// exists for C parity (Rule A).
3815#[allow(unused_variables)]
3816pub fn freepatprog(prog: Patprog) {} // c:4161
3817
3818/// Port of `int pat_enables(const char *cmd, char **patp, int enable)`
3819/// from `Src/pattern.c:4171`. Implements `enable -p`/`disable -p`: with
3820/// an empty `patp`, prints the currently enabled (or disabled, if
3821/// `!enable`) tokens by walking `zpc_strings[]`/`zpc_disables[]` in
3822/// lockstep. Otherwise toggles each named token's `zpc_disables[i]`
3823/// slot, emitting `invalid pattern: NAME` for misses.
3824pub fn pat_enables(cmd: &str, patp: &[&str], enable: bool) -> i32 {
3825    // c:4171
3826    let mut ret: i32 = 0; // c:4173
3827    if patp.is_empty() {
3828        // c:4177 !*patp
3829        let strings = ZPC_STRINGS; // c:4179 zpc_strings
3830        let disp = zpc_disables.lock().unwrap(); // c:4179 zpc_disables
3831        let mut done = false; // c:4178
3832        let mut out: String = String::new();
3833        for i in 0..(ZPC_COUNT as usize) {
3834            // c:4180
3835            let sp = match strings[i] {
3836                // c:4182 !*stringp
3837                Some(s) => s,
3838                None => continue,
3839            };
3840            let is_disabled = disp[i] != 0;
3841            if enable == is_disabled {
3842                // c:4184 enable?*disp:!*disp
3843                continue;
3844            }
3845            if done {
3846                // c:4186
3847                out.push(' '); // c:4187
3848            }
3849            out.push_str(&format!("'{}'", sp)); // c:4188
3850            done = true; // c:4189
3851        }
3852        if done {
3853            // c:4191
3854            println!("{}", out); // c:4187-4192
3855        }
3856        return 0; // c:4193
3857    }
3858    for p in patp {
3859        // c:4196
3860        let strings = ZPC_STRINGS;
3861        let mut disp = zpc_disables.lock().unwrap();
3862        let mut matched = false;
3863        for i in 0..(ZPC_COUNT as usize) {
3864            // c:4197
3865            if let Some(s) = strings[i] {
3866                if s == *p {
3867                    // c:4200 !strcmp
3868                    disp[i] = if enable { 0u8 } else { 1u8 }; // c:4201 *disp = !enable
3869                    matched = true;
3870                    break; // c:4202
3871                }
3872            }
3873        }
3874        if !matched {
3875            // c:4205
3876            zerrnam(cmd, &format!("invalid pattern: {}", p)); // c:4206
3877            ret = 1; // c:4207
3878        }
3879    }
3880    ret // c:4211
3881}
3882
3883/// Port of `mod_export const char *zpc_strings[ZPC_COUNT]` from
3884/// `Src/pattern.c:258`. Static token-name table indexed by ZPC_*;
3885/// NULL entries (ZPC_NULL, ZPC_BNULLKEEP, ZPC_INPAR_PIPE,
3886/// ZPC_KSHCHAR) have no user-visible name.
3887pub const ZPC_STRINGS: [Option<&'static str>; ZPC_COUNT as usize] = [
3888    // c:258
3889    None,
3890    None,
3891    Some("|"),
3892    None,
3893    Some("~"),
3894    Some("("),
3895    Some("?"),
3896    Some("*"),
3897    Some("["),
3898    Some("<"),
3899    Some("^"),
3900    Some("#"),
3901    None,
3902    Some("?("),
3903    Some("*("),
3904    Some("+("),
3905    Some("!("),
3906    Some("\\!("),
3907    Some("@("),
3908];
3909
3910/// Port of `unsigned int savepatterndisables(void)` from
3911/// `Src/pattern.c:4220`.
3912///
3913/// C body (c:4220-4233):
3914/// ```c
3915/// unsigned int disables, bit;
3916/// char *disp;
3917/// disables = 0;
3918/// for (bit = 1, disp = zpc_disables;
3919///      disp < zpc_disables + ZPC_COUNT;
3920///      bit <<= 1, disp++) {
3921///     if (*disp) disables |= bit;
3922/// }
3923/// return disables;
3924/// ```
3925///
3926/// Encodes the current `zpc_disables\[ZPC_COUNT\]` byte-array as a u32
3927/// bitmask (one bit per slot, low bit = `zpc_disables\[0\]`).
3928/// The previous Rust port returned a `Vec<String>` clone of
3929/// `patterndisables` (a completely different data structure — names
3930/// list, not the per-token byte array). `restorepatterndisables(u32)`
3931/// at c:4258 reads this bitmask back into `zpc_disables`, so the
3932/// returned shape MUST be the u32 bitmask.
3933pub fn savepatterndisables() -> u32 {
3934    // c:4220
3935    let disp = zpc_disables.lock().unwrap(); // c:4225 disp = zpc_disables
3936    let mut disables: u32 = 0; // c:4224
3937    let mut bit: u32 = 1; // c:4226 bit = 1
3938    for i in 0..(ZPC_COUNT as usize) {
3939        // c:4226-4228
3940        if disp[i] != 0 {
3941            // c:4230
3942            disables |= bit; // c:4231
3943        }
3944        bit <<= 1; // c:4226 bit <<= 1
3945    }
3946    disables // c:4232
3947}
3948
3949/// Port of `void startpatternscope(void)` from `Src/pattern.c:4241`.
3950/// Pushes a frame onto `PATSCOPE_STACK` (`zpc_disables_stack` in C)
3951/// carrying the current `zpc_disables[]` state as a `savepatterndisables()`
3952/// u32 bitmap. Called at function entry; `endpatternscope` pops it.
3953///
3954/// ```c
3955/// void startpatternscope(void) {
3956///     Zpc_disables_save newdis = zalloc(sizeof(*newdis));
3957///     newdis->next = zpc_disables_stack;
3958///     newdis->disables = savepatterndisables();  // c:4247
3959///     zpc_disables_stack = newdis;
3960/// }
3961/// ```
3962pub fn startpatternscope() {
3963    // c:4241
3964    let saved = savepatterndisables(); // c:4247
3965    PATSCOPE_STACK.with(|s| s.borrow_mut().push(saved));
3966}
3967
3968/// Port of `void restorepatterndisables(unsigned int disables)` from
3969/// `Src/pattern.c:4258`. Walks the 12-slot `zpc_disables[]` array,
3970/// setting each slot's byte from the bitmask: `disables & (1<<i)`
3971/// → slot `i` gets 1, else 0.
3972/// ```c
3973/// void
3974/// restorepatterndisables(unsigned int disables)
3975/// {
3976///     char *disp;
3977///     unsigned int bit;
3978///     for (bit = 1, disp = zpc_disables;
3979///          disp < zpc_disables + ZPC_COUNT;
3980///          bit <<= 1, disp++) {
3981///         if (disables & bit) *disp = 1;
3982///         else *disp = 0;
3983///     }
3984/// }
3985/// ```
3986pub fn restorepatterndisables(disables: u32) {
3987    // c:4258
3988    let mut disp = zpc_disables.lock().unwrap(); // c:4263
3989    let mut bit: u32 = 1;
3990    for i in 0..(ZPC_COUNT as usize) {
3991        // c:4263-4265
3992        if (disables & bit) != 0 {
3993            // c:4266
3994            disp[i] = 1; // c:4267
3995        } else {
3996            disp[i] = 0; // c:4269
3997        }
3998        bit <<= 1;
3999    }
4000}
4001
4002/// Port of `void endpatternscope(void)` from `Src/pattern.c:4279`.
4003/// Pops the saved bitmap from `PATSCOPE_STACK` (`zpc_disables_stack`
4004/// in C); restores `zpc_disables[]` from the bitmap ONLY when
4005/// `isset(LOCALPATTERNS)` per C c:4286. Called at function exit.
4006///
4007/// ```c
4008/// void endpatternscope(void) {
4009///     Zpc_disables_save olddis = zpc_disables_stack;
4010///     zpc_disables_stack = olddis->next;
4011///     if (isset(LOCALPATTERNS))
4012///         restorepatterndisables(olddis->disables);     // c:4287
4013///     zfree(olddis, sizeof(*olddis));
4014/// }
4015/// ```
4016pub fn endpatternscope() {
4017    // c:4279
4018    if let Some(prev) = PATSCOPE_STACK.with(|s| s.borrow_mut().pop()) {
4019        if isset(crate::ported::zsh_h::LOCALPATTERNS) {
4020            // c:4286-4287
4021            restorepatterndisables(prev);
4022        }
4023    }
4024}
4025
4026/// Port of `void clearpatterndisables(void)` from `Src/pattern.c:4296`.
4027/// C body: `memset(zpc_disables, 0, ZPC_COUNT)` — zero every slot.
4028pub fn clearpatterndisables() {
4029    // c:4296
4030    *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize]; // c:4298
4031}
4032
4033/// Port of `haswilds(char *str)` from `Src/pattern.c:4306`.
4034///
4035/// Check whether `str` is eligible for filename generation.
4036///
4037/// C scans a TOKENIZED string: every input reaching it has been
4038/// through the lexer or `tokenize()`/`shtokenize()` (glob.c:3548/
4039/// 3563), so glob metachars are token codes and escaped/quoted
4040/// chars are Bnull'd literals. Callers holding un-tokenized strings
4041/// must `tokenize()` a copy first — exactly what C does for
4042/// runtime-built strings (compcore.c:2231 tokenizes fignore entries
4043/// immediately before its c:2235 haswilds call). The switch matches
4044/// ONLY token codes, never literal ASCII metachars.
4045///
4046/// The walk is over chars: zshrs strings hold token chars as
4047/// codepoints (Star = U+0087) and the input layer is char-domain
4048/// (input.rs `shingetline`/`ingetc`), so the char is the unit that
4049/// the metafied byte is in C. Scanning bytes here false-positived
4050/// on UTF-8 continuation bytes of plain text (`↔` = E2 86 94
4051/// carries 0x86/0x94 = Hat/Inang as u8) — bug #627.
4052///
4053/// The C source's `%?foo` job-ref special case (c:4317-4318), which
4054/// mutates `str[1]` in place to demote the `?`, becomes a "skip
4055/// position 1" scan adjustment here since `&str` is immutable.
4056pub fn haswilds(str: &str) -> bool {
4057    // c:4306
4058    let chars: Vec<char> = str.chars().collect();
4059    let len = chars.len();
4060    if len == 0 {
4061        return false;
4062    }
4063
4064    // c:4309 — `[' and `]' are legal even if bad patterns are usually not.
4065    if len == 1 && (chars[0] == Inbrack || chars[0] == Outbrack) {
4066        return false; // c:4311
4067    }
4068
4069    // c:4313-4315 — If % is immediately followed by ?, then that ? is
4070    // not treated as a wildcard.  This is so you don't have
4071    // to escape job references such as %?foo.
4072    let skip_pos_1 = len >= 2 && chars[0] == '%' && chars[1] == Quest; // c:4316-4317
4073
4074    // c:4319-4321 — Note that at this point zpc_special has not been set up.
4075    let disp = zpc_disables.lock().unwrap(); // c:read zpc_disables[]
4076
4077    for i in 0..len {
4078        // c:4323 for (; *str; str++)
4079        if skip_pos_1 && i == 1 {
4080            continue; // c:4317 `str[1] = '?'` demote
4081        }
4082        let c = chars[i]; // c:4324 switch (*str)
4083        let prev: char = if i > 0 { chars[i - 1] } else { '\0' }; // c: str[-1]
4084
4085        if c == Inpar {
4086            // c:4325-4335
4087            if (!isset(SHGLOB) && disp[ZPC_INPAR as usize] == 0)
4088                || (i > 0
4089                    && isset(KSHGLOB)
4090                    && ((prev == Quest && disp[ZPC_KSH_QUEST as usize] == 0)
4091                        || (prev == Star && disp[ZPC_KSH_STAR as usize] == 0)
4092                        || (prev == '+' && disp[ZPC_KSH_PLUS as usize] == 0)
4093                        || (prev == Bang && disp[ZPC_KSH_BANG as usize] == 0)
4094                        || (prev == '!' && disp[ZPC_KSH_BANG2 as usize] == 0)
4095                        || (prev == '@' && disp[ZPC_KSH_AT as usize] == 0)))
4096            {
4097                return true; // c:4335
4098            }
4099        } else if c == Bar {
4100            if disp[ZPC_BAR as usize] == 0 {
4101                return true; // c:4340
4102            }
4103        } else if c == Star {
4104            if disp[ZPC_STAR as usize] == 0 {
4105                return true; // c:4345
4106            }
4107        } else if c == Inbrack {
4108            if disp[ZPC_INBRACK as usize] == 0 {
4109                return true; // c:4350
4110            }
4111        } else if c == Inang {
4112            if disp[ZPC_INANG as usize] == 0 {
4113                return true; // c:4355
4114            }
4115        } else if c == Quest {
4116            if disp[ZPC_QUEST as usize] == 0 {
4117                return true; // c:4360
4118            }
4119        } else if c == Pound {
4120            if isset(EXTENDEDGLOB) && disp[ZPC_HASH as usize] == 0 {
4121                return true; // c:4365
4122            }
4123        } else if c == Hat {
4124            if isset(EXTENDEDGLOB) && disp[ZPC_HAT as usize] == 0 {
4125                return true; // c:4370
4126            }
4127        }
4128    }
4129    false // c:4374
4130}
4131
4132// =====================================================================
4133// 4. struct patprog — zsh.h:1601
4134// =====================================================================
4135
4136/// `typedef struct patprog *Patprog;` from `zsh.h:542`.
4137#[allow(non_camel_case_types)]
4138/// `typedef struct patprog *Patprog;` from `zsh.h:542`.
4139///
4140/// C zsh allocates the `struct patprog` header + bytecode as one
4141/// contiguous `malloc` block, accessing bytecode via
4142/// `(char *)prog + prog->startoff`. Rust has no flexible array
4143/// members, so this typedef pairs the C-exact `patprog` header
4144/// (zsh_h.rs:768) with an owned bytecode `Vec<u8>` — header at
4145/// `.0`, bytecode at `.1`. `startoff`/`size` index into `.1`.
4146pub type Patprog = Box<(patprog, Vec<u8>)>;
4147// =====================================================================
4148// Bytecode field offsets within each instruction.
4149//
4150// Instruction layout in `patout`:
4151//     +0       u8     opcode
4152//     +1..+5   u32 LE next_off (offset of next instr in chain, 0 = end)
4153//     +5..     u8...  opcode-specific payload
4154//
4155// C uses a different layout (Upat union = 4-byte or 8-byte slots);
4156// the Rust port pins the layout to byte offsets for portability.
4157// =====================================================================
4158
4159const I_OP: usize = 0; // opcode byte
4160
4161impl rpat {
4162    pub fn new() -> Self {
4163        Self {
4164            wbranch_visits: std::collections::HashMap::new(),
4165            patbeginp: [usize::MAX; NSUBEXP],
4166            patendp: [0; NSUBEXP],
4167            captures_set: 0,
4168            errsfound: 0, // c:2066 — errsfound = 0 at pattry init
4169        }
4170    }
4171}
4172const I_NEXT: usize = 1; // u32 next-offset starts here
4173const I_BODY: usize = 5; // payload starts here
4174
4175/// Serialises every entry into `patcompile`. The C source at
4176/// `Src/pattern.c:267-281` declares `patout`, `patparse`, `patstart`,
4177/// `patnpar`, `patflags`, `patglobflags`, `errsfound`, `forceerrs`,
4178/// `zpc_special`, `patstrcache` as file-scope statics that the compile
4179/// mutates in sequence; zsh-the-program is single-threaded so the C
4180/// source is safe under that invariant. zshrs callers (zutil's
4181/// `style_table::get` via `crate::ported::pattern::patmatch`, params.rs,
4182/// subst.rs, options.rs) can invoke `patcompile` from concurrent test
4183/// threads, so the lock restores the single-writer invariant. Held
4184/// only for the compile phase; the matcher (`pattry`/`patmatch`)
4185/// operates on the returned `Patprog.code` and touches no globals.
4186static PATCOMPILE_LOCK: Mutex<()> = Mutex::new(());
4187
4188// C: `static char *patparse, *patstart;` — pattern parsing cursors
4189// into the *input* pattern string. patstart points to start of
4190// pattern; patparse moves forward as we consume tokens.
4191pub static patparse: Mutex<String> = Mutex::new(String::new()); // c:269
4192pub static patstart: Mutex<String> = Mutex::new(String::new()); // c:269
4193
4194/// Position within `patparse` we're currently looking at. C source
4195/// uses a `char *` cursor; Rust uses byte offset into the String.
4196pub static patparse_off: AtomicUsize = AtomicUsize::new(0);
4197
4198// C: `static int errsfound;` — approximate-match error count.
4199pub static errsfound: AtomicI32 = AtomicI32::new(0); // c:274
4200
4201// C: `static int forceerrs;` — required error count for approximate match.
4202pub static forceerrs: AtomicI32 = AtomicI32::new(-1); // c:275
4203
4204// C: `static long patglobflags_orig;` — saved at branch entry.
4205pub static patglobflags_orig: AtomicI32 = AtomicI32::new(0); // c:276
4206
4207// C: `static const char *zpc_special;` — table of currently-special
4208// characters during compile (indexed by ZPC_*).
4209//
4210// pattern.c uses `static char zpc_special[ZPC_COUNT];` and resets it
4211// in patcompcharsset(). Rust mirrors as a Mutex-wrapped byte array.
4212pub static zpc_special: Mutex<[u8; ZPC_COUNT as usize]> = Mutex::new([0u8; ZPC_COUNT as usize]); // c:278
4213
4214// C: `static char *patstrcache;` — caches the unmetafied trial string.
4215// Rust port has no Meta encoding so the cache is unnecessary; we leave
4216// the static declared for parity (Rule A — name exists in C).
4217pub static patstrcache: Mutex<String> = Mutex::new(String::new()); // c:281
4218
4219/// `Marker` constant — alias for `crate::ported::zsh_h::Marker`
4220/// (`Src/zsh.h:224` `#define Marker ((char) 0xa2)`).
4221///
4222/// The previous Rust port had this as `pub const Marker: u8 = 0x80`
4223/// which is WRONG — `\200` (0x80) is NOT the canonical Marker byte.
4224/// C's Marker is 0xa2 per `Src/zsh.h:224`. No in-tree callers used
4225
4226/// Port of `static const char *colon_stuffs[]` from `Src/pattern.c:1134-1138`.
4227/// 19 entries matching C's table in declaration order, so `range_type(name)`
4228/// returns the same `(index + PP_FIRST)` value C does:
4229/// alpha=1, alnum=2, ascii=3, blank=4, cntrl=5, digit=6, graph=7, lower=8,
4230/// print=9, punct=10, space=11, upper=12, xdigit=13, IDENT=14, IFS=15,
4231/// IFSSPACE=16, WORD=17, INCOMPLETE=18, INVALID=19. Prior Rust port had
4232/// only 12 lowercase entries and was MISSING `ascii` between `alnum` and
4233/// `blank`, so `range_type("digit")` returned 5 instead of C's PP_DIGIT=6
4234/// and every `[:class:]` byte marker emitted by `complete.rs:733`
4235/// (`0x80 + ch`) was off-by-one for classes after `alnum`. Real port bug.
4236const POSIX_CLASS_NAMES: &[&str] = &[
4237    "alpha",
4238    "alnum",
4239    "ascii",
4240    "blank",
4241    "cntrl",
4242    "digit",
4243    "graph",
4244    "lower",
4245    "print",
4246    "punct",
4247    "space",
4248    "upper",
4249    "xdigit",
4250    "IDENT",
4251    "IFS",
4252    "IFSSPACE",
4253    "WORD",
4254    "INCOMPLETE",
4255    "INVALID",
4256];
4257
4258/// Port of file-static `zpc_disables_stack` from `Src/pattern.c:4244`.
4259/// Per-evaluator function-scope disable save-stack (bucket-1: each
4260/// worker thread parses/executes its own function calls, so each must
4261/// have its own scope stack). Reason for `thread_local!` over `Mutex`:
4262/// in zsh C this is a per-process file-static; in zshrs each worker
4263/// thread is its own evaluator — TLS preserves the per-evaluator
4264/// semantic without serializing across workers.
4265///
4266/// Element type: u32 bitmap matching `savepatterndisables` return
4267/// shape (c:4220 `unsigned int disables`). Prior Rust port used
4268/// `Vec<Vec<String>>` and `startpatternscope` cloned a `Vec<String>`
4269/// of names instead of the bitmap — a real port bug that made
4270/// `setopt LOCALPATTERNS` function entry/exit silently fail to save/
4271/// restore the disable state.
4272thread_local! {
4273    static PATSCOPE_STACK: std::cell::RefCell<Vec<u32>> =
4274        const { std::cell::RefCell::new(Vec::new()) };
4275}
4276
4277// =====================================================================
4278// 17. Module-loader / disable mgmt — pattern.c:4161-4296
4279// =====================================================================
4280
4281// `pub static patterndisables: Mutex<Vec<String>>` deleted — was a
4282// dead tombstone with no callers outside the buggy
4283// `startpatternscope` / `endpatternscope` / `clearpatterndisables`
4284// fns (which cloned/cleared it instead of operating on the real
4285// `zpc_disables` byte array). The canonical zsh `zpc_disables[ZPC_COUNT]`
4286// (Src/pattern.c:268) is the disable state; the names list does not
4287// exist as a separate data structure in C.
4288
4289/// Port of `char zpc_disables[ZPC_COUNT]` from `Src/pattern.c:268`.
4290/// Per-token disable byte — when `zpc_disables[i]` is non-zero, the
4291/// pattern token at index `i` (ZPC_*) is treated as a literal,
4292/// not its meta-meaning.
4293pub static zpc_disables: Mutex<[u8; ZPC_COUNT as usize]> = Mutex::new([0u8; ZPC_COUNT as usize]); // c:268
4294
4295/// Helper: when patinsert shifts a chunk of bytecode, any 4-byte
4296/// next_off slot that previously pointed past `opnd` must be bumped
4297/// by `delta` to keep the chain links valid.
4298///
4299/// Walks the buffer linearly opcode-by-opcode reading I_NEXT slots.
4300/// Conservatively adjusts every nonzero next that lands past opnd.
4301fn fixup_offsets_after_insert(buf: &mut [u8], opnd: usize, delta: u32) {
4302    let mut i = 0;
4303    while i + I_BODY <= buf.len() {
4304        let op = buf[i + I_OP];
4305        if op == 0 {
4306            i += 1;
4307            continue;
4308        } // sentinel byte, skip
4309        let next_bytes = &buf[i + I_NEXT..i + I_NEXT + 4];
4310        let cur = u32::from_le_bytes(next_bytes.try_into().unwrap());
4311        if cur != 0 {
4312            let abs = cur as usize;
4313            if abs >= opnd && abs <= buf.len() {
4314                let new = cur + delta;
4315                buf[i + I_NEXT..i + I_NEXT + 4].copy_from_slice(&new.to_le_bytes());
4316            }
4317        }
4318        i = advance_past_instr(buf, i);
4319        if i == 0 {
4320            break;
4321        }
4322    }
4323}
4324
4325/// Helper: given a buffer and current opcode offset, return the
4326/// offset of the next opcode after this one's payload.
4327///
4328/// Encodes the per-opcode payload size table — must stay in sync
4329/// with patnode/patinsert calls in the compiler.
4330fn advance_past_instr(buf: &[u8], pos: usize) -> usize {
4331    if pos + I_BODY > buf.len() {
4332        return 0;
4333    }
4334    let op = buf[pos + I_OP];
4335    let body_start = pos + I_BODY;
4336    match op {
4337        P_END | P_NOTHING | P_BACK | P_EXCSYNC | P_EXCEND | P_ISSTART | P_ISEND | P_COUNTSTART
4338        | P_ANY | P_STAR | P_NUMANY => body_start,
4339        P_GFLAGS => body_start + 4, // i32 flag-bits payload
4340        P_EXACTLY => {
4341            // payload: u32 len + len bytes
4342            if body_start + 4 > buf.len() {
4343                return 0;
4344            }
4345            let len =
4346                u32::from_le_bytes(buf[body_start..body_start + 4].try_into().unwrap()) as usize;
4347            body_start + 4 + len
4348        }
4349        P_ANYOF | P_ANYBUT => {
4350            if body_start + 4 > buf.len() {
4351                return 0;
4352            }
4353            let len =
4354                u32::from_le_bytes(buf[body_start..body_start + 4].try_into().unwrap()) as usize;
4355            body_start + 4 + len
4356        }
4357        P_ONEHASH | P_TWOHASH | P_BRANCH => body_start,
4358        // c:113 P_WBRANCH and c:114-115 P_EXCLUDE/P_EXCLUDP carry an
4359        // 8-byte syncptr payload (one Upat slot in C) right after
4360        // their header, before the chained operand.
4361        P_WBRANCH | P_EXCLUDE | P_EXCLUDP => body_start + 8,
4362        P_OPEN..=0x88 | P_CLOSE..=0x98 => body_start,
4363        P_NUMRNG => body_start + 16, // two i64
4364        P_NUMFROM | P_NUMTO => body_start + 8,
4365        P_COUNT => body_start + 16, // min i64 + max i64; operand inline follows
4366        _ => body_start,
4367    }
4368}
4369
4370/// Helper: directly set the `next_off` slot of the instruction at
4371/// `pos` without walking the chain. C uses pointer arithmetic
4372/// (`scanp->l = ...`) inline; Rust factors it for byte-offset
4373/// bookkeeping. Architectural helper.
4374fn set_next(pos: usize, val: usize) {
4375    let mut buf = patout.lock().unwrap();
4376    if pos + I_NEXT + 4 <= buf.len() {
4377        buf[pos + I_NEXT..pos + I_NEXT + 4].copy_from_slice(&(val as u32).to_le_bytes());
4378    }
4379}
4380
4381/// Helper: walk every branch's operand chain and patch each branch's
4382/// last-operand-node `.next` to `target`. Used to chain a fully-
4383/// compiled alternation switch to whatever opcode follows (P_END for
4384/// the outermost compile, P_CLOSE_N for a sub-group).
4385///
4386/// Architectural helper — C uses pattail inside the BRANCH operand
4387/// scope via Upat pointer arithmetic; Rust factors it for clarity.
4388fn chain_branches_to(starter: usize, target: usize) {
4389    let mut cur = starter;
4390    loop {
4391        // Operand starts at cur + I_BODY (the byte right after this
4392        // branch's header). Walk operand's next-chain to its end
4393        // and set its .next = target.
4394        pattail(cur + I_BODY, target);
4395        // Move to next alternative.
4396        let buf = patout.lock().unwrap();
4397        if cur + I_NEXT + 4 > buf.len() {
4398            break;
4399        }
4400        let nb: [u8; 4] = buf[cur + I_NEXT..cur + I_NEXT + 4].try_into().unwrap();
4401        let n = u32::from_le_bytes(nb) as usize;
4402        drop(buf);
4403        if n == 0 {
4404            break;
4405        }
4406        cur = n;
4407    }
4408}
4409
4410/// Port of `patmatch(Upat prog)` from `Src/pattern.c:2694`. The interpreter.
4411///
4412/// Returns `Some(end_pos)` on successful match (end_pos = byte offset
4413/// in `string` where match ended), `None` on no-match. The state
4414/// param tracks captures.
4415///
4416/// Rust signature differs from C's `int patmatch(Upat prog)`: input
4417/// bytecode, current input position, captures, and glob flags are
4418/// threaded through args rather than C's per-thread file-statics
4419/// (`patinput`, `patinstart`, `patbeginp`/`patendp`, `patglobflags`).
4420/// Rule S1 deviation justified by zshrs's threading model — see
4421/// PORT_PLAN.md Bucket 1.
4422///
4423/// WARNING: NOT IN PATTERN.C — Rust-only helper. Approximate-match
4424/// inner loop for `(#aN)`-flagged P_EXACTLY arms. C interleaves the
4425/// edit-operation backtracking inline within the P_EXACTLY case
4426/// (c:2737-2779); the Rust port factors the inner per-byte walk +
4427/// recursive sub/ins/del trials into this helper so the linear
4428/// patmatch loop body stays manageable. Both writer and reader live
4429/// in pattern.rs.
4430///
4431/// Walks pattern bytes `str_bytes` against `input_bytes[s_off..]`.
4432/// On exact-match per byte: advance both. On mismatch: try the 3
4433/// edit operations in order (substitute = advance both + errsfound++,
4434/// insert = advance pat + errsfound++, delete = advance input +
4435/// errsfound++); each recurses via `patmatch` to continue with the
4436/// rest of the bytecode at `next`. Returns the matched-end byte
4437/// offset in `string` on success, None on failure.
4438fn approx_match_exactly(
4439    code: &[u8],
4440    next: usize,
4441    string: &str,
4442    s_off: usize,
4443    str_bytes: &[u8],
4444    state: &mut rpat,
4445    glob_flags: i32,
4446    max_errs: i32,
4447) -> Option<usize> {
4448    let input_bytes = string.as_bytes();
4449    // Try matching the EXACT prefix as far as it lines up; on first
4450    // mismatch (or out-of-input), branch into the edit-operation
4451    // trials. This is a bounded recursive search; the budget caps
4452    // recursion depth at `max_errs - state.errsfound`.
4453    fn walk(
4454        code: &[u8],
4455        next: usize,
4456        string: &str,
4457        input_bytes: &[u8],
4458        str_bytes: &[u8],
4459        s_off: usize,
4460        p_off: usize,
4461        state: &mut rpat,
4462        glob_flags: i32,
4463        max_errs: i32,
4464    ) -> Option<usize> {
4465        // Direction terminology: `s_off+1` consumes one INPUT byte,
4466        // `p_off+1` consumes one PATTERN byte.
4467        //   - (s+1, p+1) = substitute one input byte for one pat byte
4468        //   - (s+1, p)   = consume input byte that's NOT in pattern
4469        //                  (= INSERTION in input compared to pattern)
4470        //   - (s, p+1)   = consume pat byte that's NOT in input
4471        //                  (= DELETION from input compared to pattern)
4472        //
4473        // Tries every option at each step, returns the path producing
4474        // the LARGEST end s_off (most input consumed). Anchored
4475        // callers (pattry) need full-input consumption; non-anchored
4476        // accept any. C handles this via bytecode-level branching
4477        // backtrack; the Rust port collapses it into a recursive
4478        // tree-search with per-attempt state save/restore.
4479        let mut best: Option<usize> = None;
4480        let saved_outer = state.clone();
4481        let mut update = |best: &mut Option<usize>, cand: Option<usize>| {
4482            if let Some(c) = cand {
4483                if best.map(|b| c > b).unwrap_or(true) {
4484                    *best = Some(c);
4485                }
4486            }
4487        };
4488        if p_off == str_bytes.len() {
4489            // Pattern body consumed. Three paths to try; pick the
4490            // one with most input consumed:
4491            //   (a) terminate here at s_off, run continuation.
4492            //   (b) absorb 1+ trailing input as INSERTION-IN-INPUT edits.
4493            let terminate = if next == 0 {
4494                Some(s_off)
4495            } else {
4496                patmatch(code, next, string, s_off, state, glob_flags)
4497            };
4498            update(&mut best, terminate);
4499            // Path (b): absorb trailing input as insertion edits.
4500            if s_off < input_bytes.len() && state.errsfound < max_errs {
4501                *state = saved_outer.clone();
4502                state.errsfound += 1;
4503                let r = walk(
4504                    code,
4505                    next,
4506                    string,
4507                    input_bytes,
4508                    str_bytes,
4509                    s_off + 1,
4510                    p_off,
4511                    state,
4512                    glob_flags,
4513                    max_errs,
4514                );
4515                update(&mut best, r);
4516            }
4517            *state = saved_outer;
4518            return best;
4519        }
4520        // c:Src/pattern.c:2676 CHARMATCH — inline case-aware byte
4521        // compare (the C macro, not a fn). Honors GF_IGNCASE /
4522        // GF_LCMATCHUC so under `(#i)` a case-only difference is NOT an
4523        // edit: `(#ia2)readme` matches `READXME` (only X→M costs an
4524        // error). Raw byte `==` counted every case difference as an edit.
4525        let charmatch_inline = |chin: u8, chpa: u8| -> bool {
4526            chin == chpa
4527                || ((glob_flags & GF_IGNCASE) != 0
4528                    && chin.to_ascii_lowercase() == chpa.to_ascii_lowercase())
4529                || ((glob_flags & GF_LCMATCHUC) != 0
4530                    && chpa.is_ascii_lowercase()
4531                    && chpa.to_ascii_uppercase() == chin)
4532        };
4533        // Exact-byte match — try advancing both.
4534        if s_off < input_bytes.len() && charmatch_inline(input_bytes[s_off], str_bytes[p_off]) {
4535            *state = saved_outer.clone();
4536            let r = walk(
4537                code,
4538                next,
4539                string,
4540                input_bytes,
4541                str_bytes,
4542                s_off + 1,
4543                p_off + 1,
4544                state,
4545                glob_flags,
4546                max_errs,
4547            );
4548            update(&mut best, r);
4549        }
4550        // Edit operations — each costs 1 error.
4551        if state.errsfound < max_errs {
4552            // c:Src/pattern.c:3520-3543 — Damerau transposition: swap two
4553            // adjacent input chars to match two adjacent pat chars (i.e.
4554            // input[s..s+2] reversed equals pat[p..p+2]). Costs 1 edit;
4555            // pin for `(#a3)abcd` matching "dcba" — needs sub + transp +
4556            // sub = 3 edits to bridge the reversal.
4557            if s_off + 1 < input_bytes.len()
4558                && p_off + 1 < str_bytes.len()
4559                && charmatch_inline(input_bytes[s_off], str_bytes[p_off + 1])
4560                && charmatch_inline(input_bytes[s_off + 1], str_bytes[p_off])
4561            {
4562                *state = saved_outer.clone();
4563                state.errsfound += 1;
4564                let r = walk(
4565                    code,
4566                    next,
4567                    string,
4568                    input_bytes,
4569                    str_bytes,
4570                    s_off + 2,
4571                    p_off + 2,
4572                    state,
4573                    glob_flags,
4574                    max_errs,
4575                );
4576                update(&mut best, r);
4577            }
4578            // Substitute.
4579            if s_off < input_bytes.len() {
4580                *state = saved_outer.clone();
4581                state.errsfound += 1;
4582                let r = walk(
4583                    code,
4584                    next,
4585                    string,
4586                    input_bytes,
4587                    str_bytes,
4588                    s_off + 1,
4589                    p_off + 1,
4590                    state,
4591                    glob_flags,
4592                    max_errs,
4593                );
4594                update(&mut best, r);
4595            }
4596            // Insertion in input (skip input byte only).
4597            if s_off < input_bytes.len() {
4598                *state = saved_outer.clone();
4599                state.errsfound += 1;
4600                let r = walk(
4601                    code,
4602                    next,
4603                    string,
4604                    input_bytes,
4605                    str_bytes,
4606                    s_off + 1,
4607                    p_off,
4608                    state,
4609                    glob_flags,
4610                    max_errs,
4611                );
4612                update(&mut best, r);
4613            }
4614            // Deletion from input (skip pattern byte only).
4615            *state = saved_outer.clone();
4616            state.errsfound += 1;
4617            let r = walk(
4618                code,
4619                next,
4620                string,
4621                input_bytes,
4622                str_bytes,
4623                s_off,
4624                p_off + 1,
4625                state,
4626                glob_flags,
4627                max_errs,
4628            );
4629            update(&mut best, r);
4630        }
4631        *state = saved_outer;
4632        best
4633    }
4634    walk(
4635        code,
4636        next,
4637        string,
4638        input_bytes,
4639        str_bytes,
4640        s_off,
4641        0,
4642        state,
4643        glob_flags,
4644        max_errs,
4645    )
4646}
4647
4648thread_local! {
4649    /// Rust-only backstop counter (no C analogue) — gates `patmatch`
4650    /// recursion at PATMATCH_MAX_DEPTH so misbehaving closures convert
4651    /// would-be stack overflows into clean None returns. Documented in
4652    /// `fake_fn_allowlist.txt` under the patmatch arc.
4653    static PATMATCH_DEPTH: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
4654}
4655
4656/// Maximum patmatch recursion depth — Rust-only backstop, no C
4657/// analogue. The C source's primary protection against
4658/// closure-of-closure infinite recursion is the P_WBRANCH "must
4659/// match at least 1 char" semantics described at pattern.c:108-144:
4660///   `P_WBRANCH:  This works like a branch and is used in complex`
4661///   `closures, but the match must be at least 1 char in length to`
4662///   `avoid infinite loops.`
4663/// (Implemented in C's patmatch() switch at c:3044, `case P_WBRANCH`,
4664/// via the `errsfound`/`forceerrs` accounting at c:1059+.) The Rust
4665/// port's P_WBRANCH arm has gaps in that propagation, so we keep
4666/// this depth guard as a safety net.
4667///
4668/// **Tuned to test-thread stack size**, NOT the main-thread 8 MB.
4669/// Rust's `cargo test` spawns each test on a thread whose default
4670/// stack is ~2 MB (per `std::thread::Builder::stack_size` default at
4671/// `library/std/src/thread/mod.rs`), and `(fo#)#` patterns blow that
4672/// at ~250-300 frames per measured SIGABRT. 128 leaves headroom
4673/// (~256 KB worst case at ~2 KB/frame) while still admitting any
4674/// legitimate zsh pattern (Misc/globtests tops out around 30).
4675const PATMATCH_MAX_DEPTH: u32 = 128;
4676
4677thread_local! {
4678    // c:Src/pattern.c — the P_EXCLUDE `syncptr->p` heap buffer. Keyed by
4679    // the EXCLUDE node offset; value is a per-input-position byte array
4680    // recording where the asserted branch's excludable part matched (so
4681    // EXCSYNC can fail a revisit and force backtracking to a different
4682    // split). C stores this in the bytecode payload + a raw heap pointer
4683    // that survives backtracking; the Rust matcher clones `rpat` per
4684    // branch, so the buffer lives here (outside the cloned state) to
4685    // stay shared across recursion, exactly like C's global syncstrp->p.
4686    static EXCSYNC_BUF: std::cell::RefCell<std::collections::HashMap<usize, Vec<u8>>> =
4687        std::cell::RefCell::new(std::collections::HashMap::new());
4688}
4689
4690pub fn patmatch(
4691    code: &[u8],
4692    prog_off: usize,
4693    string: &str,
4694    string_off: usize,
4695    state: &mut rpat,
4696    glob_flags: i32,
4697) -> Option<usize> {
4698    // c:2694
4699    // Depth-guard: convert what would be a stack overflow into a
4700    // clean None return (= no match). See PATMATCH_MAX_DEPTH doc.
4701    let d = PATMATCH_DEPTH.with(|c| {
4702        let cur = c.get();
4703        c.set(cur + 1);
4704        cur + 1
4705    });
4706    if d > PATMATCH_MAX_DEPTH {
4707        PATMATCH_DEPTH.with(|c| c.set(c.get() - 1));
4708        return None;
4709    }
4710    struct DepthGuard;
4711    impl Drop for DepthGuard {
4712        fn drop(&mut self) {
4713            PATMATCH_DEPTH.with(|c| c.set(c.get().saturating_sub(1)));
4714        }
4715    }
4716    let _depth_guard = DepthGuard;
4717
4718    let mut scan = prog_off;
4719    let mut s_off = string_off;
4720    // Locally-mutable copy of glob_flags so mid-pattern P_GFLAGS can
4721    // toggle bits without affecting the caller's branch view.
4722    let mut glob_flags = glob_flags;
4723
4724    // Inlined port of CHARMATCH macro from `Src/pattern.c:2671-2677`:
4725    //   #define CHARMATCH(chin, chpa) (chin == chpa || \
4726    //       ((patglobflags & GF_IGNCASE) ?
4727    //          ((ISUPPER(chin) ? TOLOWER(chin) : chin) ==
4728    //           (ISUPPER(chpa) ? TOLOWER(chpa) : chpa)) :
4729    //        (patglobflags & GF_LCMATCHUC) ?
4730    //          (ISLOWER(chpa) && TOUPPER(chpa) == chin) : 0))
4731    //
4732    // - exact byte match always wins
4733    // - GF_IGNCASE: SYMMETRIC fold (both lowercase before compare)
4734    // - GF_LCMATCHUC: ASYMMETRIC — lowercase pattern char ALSO matches
4735    //   uppercase text char; an UPPERCASE pattern char only matches
4736    //   that exact uppercase text char. This is the `(#l)` flag's
4737    //   "lowercase-in-pattern matches uppercase-in-text" semantic;
4738    //   previously zshrs treated LCMATCHUC the same as IGNCASE (full
4739    //   case-fold both sides) so `(#l)FOO` wrongly matched "foo".
4740    let charmatch = |chin: u8, chpa: u8, flags: i32| -> bool {
4741        if chin == chpa {
4742            return true; // c:2671
4743        }
4744        if (flags & GF_IGNCASE) != 0 {
4745            // c:2672-2674
4746            let a = if chin.is_ascii_uppercase() {
4747                chin.to_ascii_lowercase()
4748            } else {
4749                chin
4750            };
4751            let b = if chpa.is_ascii_uppercase() {
4752                chpa.to_ascii_lowercase()
4753            } else {
4754                chpa
4755            };
4756            return a == b;
4757        }
4758        if (flags & GF_LCMATCHUC) != 0 {
4759            // c:2675-2676
4760            return chpa.is_ascii_lowercase() && chpa.to_ascii_uppercase() == chin;
4761        }
4762        false // c:2677
4763    };
4764
4765    // Membership test for a P_ANYOF/P_ANYBUT body at `body_off` against the
4766    // input char at byte offset `off`. Returns (in_set, byte_advance). ASCII
4767    // input takes the unchanged byte `charmatch` path over the `chars` set; a
4768    // multibyte char is decoded (raw UTF-8, or a metafied `$'\xNN'` Meta-pair)
4769    // and tested against the appended class mask / wide ranges / wide literals
4770    // — C's `mb_patmatchrange` equivalent (pattern.c:3610).
4771    let anyof_membership = |body_off: usize, off: usize, gflags: i32| -> (bool, usize) {
4772        let range_flags = gflags & !(GF_IGNCASE | GF_LCMATCHUC);
4773        let chars_len =
4774            u32::from_le_bytes(code[body_off + 4..body_off + 8].try_into().unwrap()) as usize;
4775        let cs = body_off + 8;
4776        let set = &code[cs..cs + chars_len];
4777        let mut p = cs + chars_len;
4778        let classmask = u32::from_le_bytes(code[p..p + 4].try_into().unwrap());
4779        p += 4;
4780        let n_mbc = u32::from_le_bytes(code[p..p + 4].try_into().unwrap()) as usize;
4781        p += 4;
4782        let mbc_start = p;
4783        p += n_mbc * 4;
4784        let n_mbr = u32::from_le_bytes(code[p..p + 4].try_into().unwrap()) as usize;
4785        p += 4;
4786        let mbr_start = p;
4787
4788        let bytes = string.as_bytes();
4789        let b = bytes[off];
4790        // ASCII input, OR `unsetopt multibyte`: byte-level match (the prior
4791        // behaviour). C only takes the wide `mb_patmatchrange` path under
4792        // GF_MULTIBYTE; with it clear a high byte is matched as a raw byte.
4793        //
4794        // `off` can also land inside a multibyte char: P_STAR backtracks
4795        // byte-by-byte (`s_off + consumed`) and the approx omit-input paths
4796        // step `s_off + 1`, so a continuation P_ANYOF/P_ANYBUT can be tested
4797        // at a continuation byte. Decoding `string[off..]` there would panic,
4798        // so treat a non-boundary position as a raw byte (advance 1) — the
4799        // same fallback the byte-level machinery already relies on.
4800        if b < 0x80 || (gflags & GF_MULTIBYTE) == 0 || !string.is_char_boundary(off) {
4801            return (set.iter().any(|&c| charmatch(b, c, range_flags)), 1);
4802        }
4803        // Decode one logical input char + its source byte span.
4804        let mut it = string[off..].chars();
4805        let (ch, adv) = match it.next() {
4806            Some('\u{83}') => match it.next() {
4807                Some(n) => (
4808                    ((n as u32 as u8) ^ 0x20) as char,
4809                    '\u{83}'.len_utf8() + n.len_utf8(),
4810                ),
4811                None => ('\u{83}', 2),
4812            },
4813            Some(c) => (c, c.len_utf8()),
4814            None => return (false, 1),
4815        };
4816        let class_hit = (classmask & (1 << 0) != 0 && ch.is_alphabetic())
4817            || (classmask & (1 << 1) != 0 && ch.is_alphanumeric())
4818            || (classmask & (1 << 2) != 0 && ch.is_uppercase())
4819            || (classmask & (1 << 3) != 0 && ch.is_lowercase())
4820            || (classmask & (1 << 4) != 0 && ch.is_whitespace())
4821            || (classmask & (1 << 5) != 0 && (ch == ' ' || ch == '\t'))
4822            || (classmask & (1 << 6) != 0
4823                && !ch.is_alphanumeric()
4824                && !ch.is_whitespace()
4825                && !ch.is_control())
4826            || (classmask & (1 << 7) != 0 && ch.is_control())
4827            || (classmask & (1 << 8) != 0 && !ch.is_control())
4828            || (classmask & (1 << 9) != 0 && !ch.is_control() && !ch.is_whitespace());
4829        if class_hit {
4830            return (true, adv);
4831        }
4832        let cp = ch as u32;
4833        for k in 0..n_mbc {
4834            let o = mbc_start + k * 4;
4835            if u32::from_le_bytes(code[o..o + 4].try_into().unwrap()) == cp {
4836                return (true, adv);
4837            }
4838        }
4839        for k in 0..n_mbr {
4840            let o = mbr_start + k * 8;
4841            let lo = u32::from_le_bytes(code[o..o + 4].try_into().unwrap());
4842            let hi = u32::from_le_bytes(code[o + 4..o + 8].try_into().unwrap());
4843            if cp >= lo && cp <= hi {
4844                return (true, adv);
4845            }
4846        }
4847        (false, adv)
4848    };
4849
4850    while scan < code.len() {
4851        let op = code[scan + I_OP];
4852        let next_bytes: [u8; 4] = code[scan + I_NEXT..scan + I_NEXT + 4].try_into().unwrap();
4853        let next = u32::from_le_bytes(next_bytes) as usize;
4854
4855        match op {
4856            P_END => {
4857                // c:Src/pattern.c:3451-3454 — `case P_END: if (!(fail =
4858                // (patinput < patinend && !(patflags & PAT_NOANCH))))
4859                // return 1; break;`. When the WHOLE pattern is consumed
4860                // but the string ISN'T (anchored full match), this
4861                // branch FAILS so an enclosing alternation backtracks
4862                // to the next alternative — `[[ yes == (y|yes) ]]`
4863                // tries `y` (consumes 1 char, reaches P_END at offset
4864                // 1 ≠ 3), fails here, then tries `yes`. The previous
4865                // unconditional `Some(s_off)` committed to the first
4866                // (prefix) alternative and the external anchor check
4867                // rejected it without ever trying `yes`. PAT_NOANCH /
4868                // PAT_NOTEND callers (prefix/suffix strip, `(#e)`
4869                // interior matches) keep the partial-success return.
4870                let pf = patflags.load(Ordering::Relaxed);
4871                let anchored = (pf & (PAT_NOANCH | PAT_NOTEND) as i32) == 0;
4872                if s_off < string.len() && anchored {
4873                    // c:Src/pattern.c:3451-3454 sets `fail = 1` here, then the
4874                    // shared approximate-match block at c:3463-3475 deletes the
4875                    // trailing input one CHARACTER at a time — `++errsfound;
4876                    // CHARINC(patinput); continue;` — retrying P_END until the
4877                    // input is gone or the `(#aN)` budget is spent. So
4878                    // `[[ ab = (#a1)? ]]` matches: `?` consumes `a`, then the
4879                    // leftover `b` is deleted for one error.
4880                    //
4881                    // Only literal (P_EXACTLY) runs did this before, inside
4882                    // approx_match_exactly's own trailing-delete; a pattern
4883                    // ending in a class / `?` / `*` with input left reached
4884                    // here and simply failed, so `(#a2)[^0-9]` on `abc`
4885                    // (match `a`, delete `bc`) was rejected where zsh accepts
4886                    // it. The class/`?` arms already delete on a FAILED match
4887                    // (e.g. c:5046); this is the same edit at the pattern end.
4888                    let max_errs = (glob_flags & 0xff) as i32;
4889                    if state.errsfound < max_errs {
4890                        if let Some(c) = string[s_off..].chars().next() {
4891                            state.errsfound += 1; // c:3466 ++errsfound
4892                            // c:3475 CHARINC(patinput) + continue (retry P_END).
4893                            return patmatch(
4894                                code,
4895                                scan,
4896                                string,
4897                                s_off + c.len_utf8(),
4898                                state,
4899                                glob_flags,
4900                            );
4901                        }
4902                    }
4903                    return None; // c:3452 fail, no budget → caller tries next alt
4904                }
4905                return Some(s_off); // c:3453
4906            }
4907            P_NOTHING => { /* empty match, just continue */ }
4908            P_BACK => { /* zero-width, walk back via next */ }
4909            P_EXCEND => {
4910                // c:Src/pattern.c:3037-3047 — terminal node ending an
4911                // exclusion operand: the exclusion matches iff the
4912                // (truncated) excludable span was fully consumed
4913                // (`patinput >= patinend`). Returns success here without
4914                // following the chain (which would wrongly continue into
4915                // the post-group pattern). The caller truncates the span
4916                // so `string.len()` IS the excludable end.
4917                if s_off >= string.len() {
4918                    return Some(s_off);
4919                }
4920                return None;
4921            }
4922            P_EXCSYNC => {
4923                // c:Src/pattern.c:2992-3035 — record where the asserted
4924                // branch reached the EXCLUDE sync point (the end of the
4925                // excludable part) in the following EXCLUDE node's sync
4926                // buffer. If this position was already recorded (with
4927                // <= the current error count), fail so the asserted
4928                // branch backtracks to a different split. The EXCLUDE
4929                // node sits PHYSICALLY right after EXCSYNC (both
4930                // patcompnot and the `~` arm emit them adjacent).
4931                let exclude_node = scan + I_BODY;
4932                let already = EXCSYNC_BUF.with(|b| {
4933                    let mut m = b.borrow_mut();
4934                    if let Some(buf) = m.get_mut(&exclude_node) {
4935                        if s_off < buf.len() {
4936                            let cur = (state.errsfound + 1) as u8;
4937                            if buf[s_off] != 0 && (state.errsfound + 1) >= buf[s_off] as i32 {
4938                                return true; // c:3008 already matched here → fail
4939                            }
4940                            buf[s_off] = cur; // c:3017
4941                                              // c:3033 — earlier marks are now invalid.
4942                            for x in buf[..s_off].iter_mut() {
4943                                *x = 0;
4944                            }
4945                        }
4946                    }
4947                    false
4948                });
4949                if already {
4950                    return None;
4951                }
4952                // else fall through to next node
4953            }
4954            P_EXACTLY => {
4955                // c:P_EXACTLY arm
4956                let body = scan + I_BODY;
4957                let len = u32::from_le_bytes(code[body..body + 4].try_into().unwrap()) as usize;
4958                let str_bytes = &code[body + 4..body + 4 + len];
4959                let input_bytes = string.as_bytes();
4960                // `(#aN)` budget — low byte of patglobflags. C uses the
4961                // file-static `patglobflags`; the Rust port carries it
4962                // through `glob_flags` since rpat went bucket-1.
4963                let max_errs = (glob_flags & 0xff) as i32;
4964                if max_errs > 0 {
4965                    // Approximate match: try edit operations
4966                    // (substitute/insert/delete) at each mismatch up to
4967                    // the budget. Per c:3055/3109/3193 the C source
4968                    // tracks `errsfound` across recursive patmatch
4969                    // calls; the Rust port does the same via `state.
4970                    // errsfound`. Skips transposition (Damerau extension
4971                    // — rare; faithful follow-on).
4972                    if let Some(new_off) = approx_match_exactly(
4973                        code, next, string, s_off, str_bytes, state, glob_flags, max_errs,
4974                    ) {
4975                        return Some(new_off);
4976                    }
4977                    return None;
4978                }
4979                if s_off + len > input_bytes.len() {
4980                    return None;
4981                }
4982                let case_flags = glob_flags & (GF_IGNCASE | GF_LCMATCHUC);
4983                let multibyte = (glob_flags & GF_MULTIBYTE) != 0; // c:349 GF_MULTIBYTE
4984                if case_flags != 0 {
4985                    let inp_slice = &input_bytes[s_off..s_off + len];
4986                    if multibyte && (glob_flags & GF_IGNCASE) != 0 {
4987                        // Char-level Unicode case fold for IGNCASE only —
4988                        // LCMATCHUC's asymmetric ASCII semantic doesn't
4989                        // map cleanly to non-ASCII chars; per C the
4990                        // CHARMATCH macro is byte-level anyway (TOLOWER/
4991                        // TOUPPER from utils.c work on bytes).
4992                        let pat_str = std::str::from_utf8(str_bytes).ok();
4993                        let inp_str = std::str::from_utf8(inp_slice).ok();
4994                        if let (Some(p), Some(i)) = (pat_str, inp_str) {
4995                            let mut pc = p.chars();
4996                            let mut ic = i.chars();
4997                            loop {
4998                                match (pc.next(), ic.next()) {
4999                                    (None, None) => break,
5000                                    (Some(_), None) | (None, Some(_)) => return None,
5001                                    (Some(a), Some(b)) => {
5002                                        // Equal chars (the common case) and ASCII
5003                                        // pairs fold without touching the heap.
5004                                        // The old per-char `to_lowercase()
5005                                        // .collect::<String>()` pair allocated
5006                                        // TWICE PER CHARACTER — `(#i)` scans over
5007                                        // a 566k-entry history (hsmw ^R) burned
5008                                        // ~4 CPU-minutes in RawVec::reserve.
5009                                        // Iterator::eq covers the multi-char
5010                                        // Unicode folds allocation-free.
5011                                        if a != b
5012                                            && (if a.is_ascii() && b.is_ascii() {
5013                                                a.to_ascii_lowercase()
5014                                                    != b.to_ascii_lowercase()
5015                                            } else {
5016                                                !a.to_lowercase().eq(b.to_lowercase())
5017                                            })
5018                                        {
5019                                            return None;
5020                                        }
5021                                    }
5022                                }
5023                            }
5024                        } else {
5025                            // Non-UTF-8 input — byte fallback through CHARMATCH.
5026                            for k in 0..len {
5027                                if !charmatch(inp_slice[k], str_bytes[k], glob_flags) {
5028                                    return None;
5029                                }
5030                            }
5031                        }
5032                    } else {
5033                        // c:2694 — per-byte CHARMATCH walk (covers
5034                        // GF_IGNCASE byte-mode AND GF_LCMATCHUC asymmetric).
5035                        for k in 0..len {
5036                            if !charmatch(inp_slice[k], str_bytes[k], glob_flags) {
5037                                return None;
5038                            }
5039                        }
5040                    }
5041                } else if &input_bytes[s_off..s_off + len] != str_bytes {
5042                    return None;
5043                }
5044                s_off += len;
5045            }
5046            P_ANY => {
5047                // c:P_ANY arm — `?` matches ONE character, advanced via
5048                // METACHARINC. zshrs stores `$'\xNN'` escapes metafied
5049                // (Meta `\u{83}` + byte^32); a multibyte character is
5050                // several Meta-pair chars that must be consumed as one,
5051                // else `?` matches a single metafied byte and leaves the
5052                // rest unmatched. Walk source chars, demetafying, until
5053                // one UTF-8 character forms; advance by its source span.
5054                // Identity for non-metafied (single-char advance).
5055                let s = &string[s_off..];
5056                let mut raw: Vec<u8> = Vec::new();
5057                let mut advance = 0usize;
5058                let mut chs = s.chars();
5059                while let Some(c) = chs.next() {
5060                    if c == '\u{83}' {
5061                        if let Some(n) = chs.clone().next() {
5062                            if (n as u32) >= 0x80 {
5063                                raw.push((n as u32 as u8) ^ 32);
5064                                advance += c.len_utf8() + n.len_utf8();
5065                                chs.next();
5066                                if std::str::from_utf8(&raw).is_ok() {
5067                                    break;
5068                                }
5069                                continue;
5070                            }
5071                        }
5072                        // Lone Meta — count it as one character.
5073                        advance += c.len_utf8();
5074                        break;
5075                    }
5076                    // Non-metafied char = one logical character.
5077                    advance += c.len_utf8();
5078                    break;
5079                }
5080                if advance == 0 {
5081                    return None;
5082                }
5083                s_off += advance;
5084            }
5085            P_ANYOF => {
5086                // c:2780-2800 — a bracket expression is matched by
5087                // `patmatchrange` / `mb_patmatchrange` on the RAW input char.
5088                // Neither consults `patglobflags`, so a bracket NEVER
5089                // case-folds: only C's CHARMATCH macro (c:2671, used by
5090                // P_EXACTLY) honours GF_IGNCASE / GF_LCMATCHUC.
5091                //
5092                // Passing glob_flags through here made `(#i)` fold ranges as
5093                // well as literals, so `[[ FooBar = (#i)[a-z]## ]]` matched
5094                // (zsh: no match) and `[[ F = (#i)[[:lower:]] ]]` matched
5095                // (zsh: no match). Mask the case bits off for the set test.
5096                let body = scan + I_BODY;
5097                let input_bytes = string.as_bytes();
5098                let max_errs = (glob_flags & 0xff) as i32;
5099                let (has_match, adv) = if s_off < input_bytes.len() {
5100                    anyof_membership(body, s_off, glob_flags)
5101                } else {
5102                    (false, 0)
5103                };
5104                if !has_match {
5105                    // c:Src/pattern.c:3463-3505 — approximate-match fail
5106                    // handler. For non-P_EXACTLY opcodes, the ONLY
5107                    // approximation path is "omit one input char" (skip
5108                    // the input byte that didn't match, costing 1 edit,
5109                    // then retry the same scan). For P_ANYOF that means
5110                    // `[b][b]` against "bob" can match by omitting the
5111                    // middle "o" with 1 edit — the `(#a1)[b][b]` pin.
5112                    if state.errsfound < max_errs && s_off < input_bytes.len() {
5113                        state.errsfound += 1;
5114                        // Retry same scan with one input byte consumed.
5115                        return patmatch(code, scan, string, s_off + 1, state, glob_flags);
5116                    }
5117                    return None;
5118                }
5119                s_off += adv;
5120            }
5121            P_ANYBUT => {
5122                let body = scan + I_BODY;
5123                let input_bytes = string.as_bytes();
5124                let max_errs = (glob_flags & 0xff) as i32;
5125                // c:2781-2800 — the negated bracket shares P_ANYOF's matcher
5126                // (`… ^ (P_OP(scan) == P_ANYOF)`), so it is equally
5127                // case-BLIND (anyof_membership masks the case bits off).
5128                let (in_set, adv) = if s_off < input_bytes.len() {
5129                    anyof_membership(body, s_off, glob_flags)
5130                } else {
5131                    (true, 0)
5132                };
5133                let has_match = s_off < input_bytes.len() && !in_set;
5134                if !has_match {
5135                    if state.errsfound < max_errs && s_off < input_bytes.len() {
5136                        // c:3463 — omit-input approx path (same as P_ANYOF).
5137                        state.errsfound += 1;
5138                        return patmatch(code, scan, string, s_off + 1, state, glob_flags);
5139                    }
5140                    return None;
5141                }
5142                s_off += adv;
5143            }
5144            P_STAR => {
5145                // c:P_STAR arm (greedy)
5146                // Greedy: try to match as many chars as possible then
5147                // backtrack until the rest matches.
5148                let input_bytes = string.as_bytes();
5149                let max = input_bytes.len() - s_off;
5150                let mut consumed = max;
5151                loop {
5152                    let mut sub_state = state.clone();
5153                    if let Some(end) = patmatch(
5154                        code,
5155                        next,
5156                        string,
5157                        s_off + consumed,
5158                        &mut sub_state,
5159                        glob_flags,
5160                    ) {
5161                        *state = sub_state;
5162                        return Some(end);
5163                    }
5164                    if consumed == 0 {
5165                        return None;
5166                    }
5167                    consumed -= 1;
5168                }
5169            }
5170            P_ONEHASH | P_TWOHASH => {
5171                // c:P_ONEHASH / P_TWOHASH
5172                // The operand (the simple atom being repeated) starts
5173                // at `scan + I_BODY` — that's the byte immediately
5174                // after the quantifier opcode (which has its own
5175                // 5-byte header). The repeated atom occupies the
5176                // bytes from there until `next`.
5177                let operand = scan + I_BODY;
5178                let min = if op == P_TWOHASH { 1 } else { 0 };
5179                // Greedy: match operand repeatedly until it fails,
5180                // then walk back trying continuations.
5181                let mut positions = vec![s_off];
5182                loop {
5183                    let cur = *positions.last().unwrap();
5184                    let mut sub_state = state.clone();
5185                    if let Some(new_pos) =
5186                        patmatch(code, operand, string, cur, &mut sub_state, glob_flags)
5187                    {
5188                        if new_pos == cur {
5189                            break;
5190                        } // zero-width fixed point
5191                        *state = sub_state;
5192                        positions.push(new_pos);
5193                    } else {
5194                        break;
5195                    }
5196                }
5197                if positions.len() - 1 < min {
5198                    return None;
5199                }
5200                // Walk back from longest match trying continuations.
5201                while positions.len() > min {
5202                    let cur = *positions.last().unwrap();
5203                    let mut sub_state = state.clone();
5204                    if let Some(end) = patmatch(code, next, string, cur, &mut sub_state, glob_flags)
5205                    {
5206                        *state = sub_state;
5207                        return Some(end);
5208                    }
5209                    if positions.len() <= min + 1 {
5210                        return None;
5211                    }
5212                    positions.pop();
5213                }
5214                return None;
5215            }
5216            P_BRANCH | P_WBRANCH => {
5217                // c:P_BRANCH / P_WBRANCH arm — c:3043-3044 in zsh.
5218                // c:3046-3050 — if next is NOT another BRANCH, this is
5219                // the only alternative; avoid the alt-loop and just
5220                // continue with the operand inline (no recursion, no
5221                // fallthrough on failure).
5222                //
5223                // For P_WBRANCH, the operand sits AFTER the 8-byte
5224                // syncptr payload (`P_OPERAND(p) + 1` per pattern.c:1865).
5225                let operand_off_extra = if op == P_WBRANCH { 8 } else { 0 };
5226                // c:3056 — if `next` is P_EXCLUDE/P_EXCLUDP, this BRANCH
5227                // is the asserted half of a `^pat` / `!(pat)` exclusion.
5228                // Minimal port of c:3056-3201: try the asserted operand
5229                // (the `*`-based branch body), then for each EXCLUDE in
5230                // the next-chain run the exclude operand against the
5231                // same input range; if any exclude matches with the
5232                // SAME consumed length, fail; else succeed.
5233                if next != 0 && next < code.len() && P_ISEXCLUDE(code[next + I_OP]) {
5234                    // c:Src/pattern.c:3056-3201 — `^pat` / `(^pat)` /
5235                    // `!(pat)` / `A~B` exclusion. The asserted branch
5236                    // (`STAR EXCSYNC rest` for `^`/`!`, or `A EXCSYNC
5237                    // rest` for `A~B`) is matched normally; its EXCSYNC
5238                    // node records where the EXCLUDABLE part ended into
5239                    // the EXCLUDE node's sync buffer. We then truncate to
5240                    // that synclen and test the exclusion operand(s); if
5241                    // any matches the excludable span, this candidate is
5242                    // excluded and we re-run the asserted branch — EXCSYNC
5243                    // now fails at the recorded position, forcing a
5244                    // different split — until an un-excluded split is
5245                    // found or the asserted branch can no longer match.
5246                    let asserted_operand = scan + I_BODY + operand_off_extra;
5247                    let exclude_node = next;
5248                    // Allocate (reset) the sync buffer for this EXCLUDE.
5249                    let prev_buf = EXCSYNC_BUF.with(|b| {
5250                        b.borrow_mut()
5251                            .insert(exclude_node, vec![0u8; string.len() + 1])
5252                    });
5253                    let mut found: Option<(usize, rpat)> = None;
5254                    loop {
5255                        let mut a_state = state.clone();
5256                        let matchpt = match patmatch(
5257                            code,
5258                            asserted_operand,
5259                            string,
5260                            s_off,
5261                            &mut a_state,
5262                            glob_flags,
5263                        ) {
5264                            Some(e) => e,
5265                            None => break,
5266                        };
5267                        // c:3128-3130 — synclen = first marked position in
5268                        // the sync buffer (the end of the excludable part).
5269                        let synclen = EXCSYNC_BUF.with(|b| {
5270                            b.borrow()
5271                                .get(&exclude_node)
5272                                .and_then(|buf| buf.iter().position(|&x| x != 0))
5273                                .unwrap_or(s_off)
5274                        });
5275                        // c:3134-3138 — test each EXCLUDE/EXCLUDP operand
5276                        // against the excludable span string[s_off..synclen].
5277                        // Truncating to `synclen` makes the operand's
5278                        // trailing P_EXCEND succeed iff the span matched in
5279                        // full.
5280                        let span_end = synclen.min(string.len());
5281                        let span = &string[..span_end];
5282                        let mut excluded = false;
5283                        let mut excl = exclude_node;
5284                        while excl != 0 && excl < code.len() && P_ISEXCLUDE(code[excl + I_OP]) {
5285                            let excl_operand = excl + I_BODY + 8; // after 8-byte syncptr
5286                            let mut e_state = state.clone();
5287                            // c:3134-3142 — `patglobflags &= ~0xff;
5288                            // errsfound = 0;` — exclusions match EXACTLY,
5289                            // with approximation turned off. Clear both the
5290                            // running error count AND the budget (low byte
5291                            // of glob_flags); otherwise `(#a1)README~READ_ME`
5292                            // matched the exclusion READ_ME against READ.ME
5293                            // approximately and wrongly excluded it.
5294                            e_state.errsfound = 0;
5295                            let excl_flags = glob_flags & !0xff;
5296                            if let Some(em) =
5297                                patmatch(code, excl_operand, span, s_off, &mut e_state, excl_flags)
5298                            {
5299                                if em == span_end {
5300                                    excluded = true;
5301                                    break;
5302                                }
5303                            }
5304                            let nb: [u8; 4] =
5305                                code[excl + I_NEXT..excl + I_NEXT + 4].try_into().unwrap();
5306                            let n = u32::from_le_bytes(nb) as usize;
5307                            if n == 0 || n == excl {
5308                                break;
5309                            }
5310                            excl = n;
5311                        }
5312                        if !excluded {
5313                            found = Some((matchpt, a_state));
5314                            break;
5315                        }
5316                        // Excluded: loop. The sync buffer now records this
5317                        // split, so the next asserted patmatch's EXCSYNC
5318                        // fails here and backtracks to a different split.
5319                    }
5320                    // Restore/clear the sync buffer slot.
5321                    EXCSYNC_BUF.with(|b| {
5322                        let mut m = b.borrow_mut();
5323                        match prev_buf {
5324                            Some(p) => {
5325                                m.insert(exclude_node, p);
5326                            }
5327                            None => {
5328                                m.remove(&exclude_node);
5329                            }
5330                        }
5331                    });
5332                    match found {
5333                        Some((end, a_state)) => {
5334                            *state = a_state;
5335                            return Some(end);
5336                        }
5337                        None => return None,
5338                    }
5339                }
5340                let next_is_branch = next != 0
5341                    && next < code.len()
5342                    && (code[next + I_OP] == P_BRANCH || code[next + I_OP] == P_WBRANCH);
5343                if !next_is_branch {
5344                    scan = scan + I_BODY + operand_off_extra;
5345                    continue;
5346                }
5347                // Alt-loop: try each branch's operand; on success
5348                // return; on failure walk to the next BRANCH via .next.
5349                let mut br = scan;
5350                loop {
5351                    let br_op = code[br + I_OP];
5352                    let br_extra = if br_op == P_WBRANCH { 8 } else { 0 };
5353                    let br_next_bytes: [u8; 4] =
5354                        code[br + I_NEXT..br + I_NEXT + 4].try_into().unwrap();
5355                    let br_next = u32::from_le_bytes(br_next_bytes) as usize;
5356                    let operand = br + I_BODY + br_extra;
5357                    let mut sub_state = state.clone();
5358                    // c:Src/pattern.c:3210-3248 — P_WBRANCH per-position
5359                    // visit guard. Allocate a bitmap sized to the input
5360                    // (`zshcalloc((patinend - patinstart) + 1)`), then
5361                    // check/set `*ptr = errsfound + 1` at the current
5362                    // input offset. On revisit with the same-or-fewer
5363                    // errors, return 0 to bound recursion. Without this,
5364                    // `(fo#)#` against any non-trivial input recurses
5365                    // until PATMATCH_MAX_DEPTH.
5366                    let mut wbranch_skip = false;
5367                    if br_op == P_WBRANCH {
5368                        let bm = state
5369                            .wbranch_visits
5370                            .entry(br)
5371                            .or_insert_with(|| vec![0u8; string.len() + 1]);
5372                        let slot = bm.get(s_off).copied().unwrap_or(0);
5373                        let cur = (state.errsfound as i32 + 1) as u8;
5374                        if slot != 0 && (state.errsfound + 1) >= slot as i32 {
5375                            wbranch_skip = true; // c:3245-3247
5376                        } else if s_off < bm.len() {
5377                            bm[s_off] = cur; // c:3248
5378                        }
5379                    }
5380                    let sub_result = if wbranch_skip {
5381                        None
5382                    } else {
5383                        patmatch(code, operand, string, s_off, &mut sub_state, glob_flags)
5384                    };
5385                    if let Some(end) = sub_result {
5386                        // c:108-144 (pattern.c header doc on P_WBRANCH):
5387                        //   "P_WBRANCH:  This works like a branch and is
5388                        //    used in complex closures, but the match must
5389                        //    be at least 1 char in length to avoid
5390                        //    infinite loops.  The test for length is
5391                        //    done via the next pointer in the WBRANCH
5392                        //    test in patmatch()."
5393                        // C enforces this via the `errsfound`/`forceerrs`
5394                        // accounting at c:1059+ inside patcompbranch.
5395                        // The Rust walker checks end > s_off directly:
5396                        // if the body consumed nothing, reject this
5397                        // alternative and fall through to the next.
5398                        // Without this guard, `(fo#)#` against any input
5399                        // stack-overflows because the inner body (`fo#`)
5400                        // can match the empty string repeatedly.
5401                        if br_op == P_WBRANCH && end == s_off {
5402                            // c:108 "at least 1 char" — fall through to
5403                            // try the next alternative.
5404                        } else {
5405                            *state = sub_state;
5406                            return Some(end);
5407                        }
5408                    }
5409                    if br_next == 0 {
5410                        return None;
5411                    }
5412                    let op_next = code[br_next + I_OP];
5413                    if op_next != P_BRANCH && op_next != P_WBRANCH {
5414                        return None;
5415                    }
5416                    br = br_next;
5417                }
5418            }
5419            P_NUMRNG => {
5420                // c:P_NUMRNG — `<from-to>` numeric range. C source at
5421                // pattern.c:3460+ backtracks shorter prefixes when the
5422                // continuation fails. Ported here as a longest-first walk
5423                // back through digit-prefix lengths, trying the continuation
5424                // at each. Pins `Test/D02glob.ztst:133` (`<1-1000>33` matches
5425                // "633" by consuming just "6" then literal "33").
5426                let body = scan + I_BODY;
5427                let from = i64::from_le_bytes(code[body..body + 8].try_into().unwrap());
5428                let to = i64::from_le_bytes(code[body + 8..body + 16].try_into().unwrap());
5429                let input_bytes = string.as_bytes();
5430                let start = s_off;
5431                let mut k = start;
5432                while k < input_bytes.len() && input_bytes[k].is_ascii_digit() {
5433                    k += 1;
5434                }
5435                if k == start {
5436                    return None;
5437                }
5438                // Walk back from longest digit prefix to shortest, trying
5439                // the continuation (`next` opcode) at each split point.
5440                while k > start {
5441                    let n_res = std::str::from_utf8(&input_bytes[start..k])
5442                        .ok()
5443                        .and_then(|s| s.parse::<i64>().ok());
5444                    if let Some(n) = n_res {
5445                        if n >= from && n <= to {
5446                            let mut sub_state = state.clone();
5447                            if let Some(end) =
5448                                patmatch(code, next, string, k, &mut sub_state, glob_flags)
5449                            {
5450                                *state = sub_state;
5451                                return Some(end);
5452                            }
5453                        }
5454                    }
5455                    k -= 1;
5456                }
5457                return None;
5458            }
5459            P_NUMFROM => {
5460                // c:P_NUMFROM — same backtracking story as P_NUMRNG.
5461                let body = scan + I_BODY;
5462                let from = i64::from_le_bytes(code[body..body + 8].try_into().unwrap());
5463                let input_bytes = string.as_bytes();
5464                let start = s_off;
5465                let mut k = start;
5466                while k < input_bytes.len() && input_bytes[k].is_ascii_digit() {
5467                    k += 1;
5468                }
5469                if k == start {
5470                    return None;
5471                }
5472                while k > start {
5473                    let n_res = std::str::from_utf8(&input_bytes[start..k])
5474                        .ok()
5475                        .and_then(|s| s.parse::<i64>().ok());
5476                    if let Some(n) = n_res {
5477                        if n >= from {
5478                            let mut sub_state = state.clone();
5479                            if let Some(end) =
5480                                patmatch(code, next, string, k, &mut sub_state, glob_flags)
5481                            {
5482                                *state = sub_state;
5483                                return Some(end);
5484                            }
5485                        }
5486                    }
5487                    k -= 1;
5488                }
5489                return None;
5490            }
5491            P_NUMTO => {
5492                let body = scan + I_BODY;
5493                let to = i64::from_le_bytes(code[body..body + 8].try_into().unwrap());
5494                let input_bytes = string.as_bytes();
5495                let start = s_off;
5496                let mut k = start;
5497                while k < input_bytes.len() && input_bytes[k].is_ascii_digit() {
5498                    k += 1;
5499                }
5500                if k == start {
5501                    return None;
5502                }
5503                while k > start {
5504                    let n_res = std::str::from_utf8(&input_bytes[start..k])
5505                        .ok()
5506                        .and_then(|s| s.parse::<i64>().ok());
5507                    if let Some(n) = n_res {
5508                        if n <= to {
5509                            let mut sub_state = state.clone();
5510                            if let Some(end) =
5511                                patmatch(code, next, string, k, &mut sub_state, glob_flags)
5512                            {
5513                                *state = sub_state;
5514                                return Some(end);
5515                            }
5516                        }
5517                    }
5518                    k -= 1;
5519                }
5520                return None;
5521            }
5522            P_NUMANY => {
5523                // c:P_NUMANY — `<->` any non-empty digit run. Pins
5524                // `Test/D02glob.ztst:136` (`<->33` matches "633" by
5525                // consuming just "6" then literal "33").
5526                let input_bytes = string.as_bytes();
5527                let start = s_off;
5528                let mut k = start;
5529                while k < input_bytes.len() && input_bytes[k].is_ascii_digit() {
5530                    k += 1;
5531                }
5532                if k == start {
5533                    return None;
5534                }
5535                while k > start {
5536                    let mut sub_state = state.clone();
5537                    if let Some(end) = patmatch(code, next, string, k, &mut sub_state, glob_flags) {
5538                        *state = sub_state;
5539                        return Some(end);
5540                    }
5541                    k -= 1;
5542                }
5543                return None;
5544            }
5545            P_ISSTART => {
5546                // c:Src/pattern.c:3392-3394 — `if (patinput != patinstart
5547                // || (patflags & PAT_NOTSTART)) fail = 1;`. C bails on
5548                // BOTH conditions: local-position-not-zero OR the
5549                // caller-set PAT_NOTSTART flag (set by glob.c's
5550                // set_pat_start when the test string is an interior
5551                // slice of a larger buffer). Read patflags from the
5552                // file-static (set at pattryrefs entry from prog flags).
5553                if s_off != 0 || (patflags.load(Ordering::Relaxed) & PAT_NOTSTART as i32) != 0 {
5554                    return None;
5555                }
5556            }
5557            P_ISEND => {
5558                // c:Src/pattern.c:3396-3398 — `if (patinput < patinend
5559                // || (patflags & PAT_NOTEND)) fail = 1;`. Same shape as
5560                // P_ISSTART: bail on local-not-at-end OR caller-set
5561                // PAT_NOTEND (set by set_pat_end when the suffix was
5562                // truncated).
5563                if s_off < string.len()
5564                    || (patflags.load(Ordering::Relaxed) & PAT_NOTEND as i32) != 0
5565                {
5566                    return None;
5567                }
5568            }
5569            P_GFLAGS => {
5570                // c:P_GFLAGS arm
5571                let body = scan + I_BODY;
5572                let bits = i32::from_le_bytes(code[body..body + 4].try_into().unwrap());
5573                // C uses absolute set; for the on/off toggle pairs
5574                // we currently encode only the "on" bits (i.e. (#I)
5575                // emits 0 to clear). Set the running flags directly,
5576                // INCLUDING the low byte (`(#aN)` approximation budget)
5577                // so mid-pattern `(#a0)`/`(#a2)` re-arm the error
5578                // allowance — c:Src/pattern.c:2941.
5579                glob_flags =
5580                    (glob_flags & !(GF_IGNCASE | GF_LCMATCHUC | GF_MULTIBYTE | 0xff)) | bits;
5581            }
5582            P_COUNT => {
5583                // c:P_COUNT arm
5584                let body = scan + I_BODY;
5585                let min = i64::from_le_bytes(code[body..body + 8].try_into().unwrap());
5586                let max = i64::from_le_bytes(code[body + 8..body + 16].try_into().unwrap());
5587                let operand = body + 16;
5588                // Greedy: match operand up to max times, then walk
5589                // back trying continuations until count is in
5590                // [min, max]. Same shape as P_ONEHASH/P_TWOHASH.
5591                let mut positions = vec![s_off];
5592                let max_usize: i64 = max;
5593                loop {
5594                    let cur = *positions.last().unwrap();
5595                    if (positions.len() as i64 - 1) >= max_usize {
5596                        break;
5597                    }
5598                    let mut sub_state = state.clone();
5599                    if let Some(new_pos) =
5600                        patmatch(code, operand, string, cur, &mut sub_state, glob_flags)
5601                    {
5602                        if new_pos == cur {
5603                            break;
5604                        }
5605                        *state = sub_state;
5606                        positions.push(new_pos);
5607                    } else {
5608                        break;
5609                    }
5610                }
5611                let min_usize = min as usize;
5612                if positions.len() < min_usize + 1 {
5613                    return None;
5614                }
5615                while positions.len() > min_usize {
5616                    let cur = *positions.last().unwrap();
5617                    let mut sub_state = state.clone();
5618                    if let Some(end) = patmatch(code, next, string, cur, &mut sub_state, glob_flags)
5619                    {
5620                        *state = sub_state;
5621                        return Some(end);
5622                    }
5623                    if positions.len() <= min_usize + 1 {
5624                        return None;
5625                    }
5626                    positions.pop();
5627                }
5628                return None;
5629            }
5630            op if op >= P_OPEN && op < P_CLOSE => {
5631                // c:P_OPEN_N arm (pattern.c:2939-2960).
5632                //
5633                // C `case P_OPEN+0..P_OPEN+9:
5634                //     no = P_OP(scan) - P_OPEN;
5635                //     save = patinput;
5636                //     if (patmatch(next)) {
5637                //         if (no && !(parsfound & (1 << (no - 1)))) {
5638                //             patbeginp[no-1] = save;
5639                //             parsfound |= 1 << (no - 1);
5640                //         }
5641                //         return 1;
5642                //     }
5643                //     return 0;`
5644                //
5645                // Recurse on `next`; only commit patbeginp[N-1] on
5646                // success AND only on the FIRST occurrence (c:2957
5647                // `!(parsfound & (1<<(no-1)))`). The first-write
5648                // semantic matters under `(*)*` and alternation
5649                // backtrack — a later iteration shouldn't overwrite
5650                // the saved start of the FIRST match.
5651                let n = (op - P_OPEN) as usize;
5652                let save = s_off;
5653                let saved_state = state.clone();
5654                // c:2957 — `if (no && !(parsfound & (1 << (no - 1))))`.
5655                // Open-bit is the LOW stripe: `1 << (n-1)`. n==0 is the
5656                // plain (uncaptured) P_OPEN — c:2957's `no &&` guard
5657                // means it records nothing; bit value is moot (0).
5658                let open_bit = if n > 0 { 1u32 << (n - 1) } else { 0 };
5659                if next == 0 {
5660                    // No continuation — leaf P_OPEN; just commit and continue.
5661                    if n > 0 && n <= NSUBEXP && (state.captures_set & open_bit) == 0 {
5662                        state.patbeginp[n - 1] = save;
5663                        state.captures_set |= open_bit; // c:2959
5664                    }
5665                    return Some(s_off);
5666                }
5667                match patmatch(code, next, string, s_off, state, glob_flags) {
5668                    Some(end) => {
5669                        // c:2957-2959 — first-write commit.
5670                        if n > 0 && n <= NSUBEXP && (state.captures_set & open_bit) == 0 {
5671                            state.patbeginp[n - 1] = save;
5672                            state.captures_set |= open_bit; // c:2959
5673                        }
5674                        return Some(end);
5675                    }
5676                    None => {
5677                        *state = saved_state;
5678                        return None;
5679                    }
5680                }
5681            }
5682            op if op >= P_CLOSE && op < 0xa0 => {
5683                // c:P_CLOSE_N arm (pattern.c:2980-3010).
5684                //
5685                // C `case P_CLOSE+0..P_CLOSE+9:
5686                //     no = P_OP(scan) - P_CLOSE;
5687                //     save = patinput;
5688                //     if (patmatch(next)) {
5689                //         if (no && !(parsfound & (1 << (no+NSUBEXP-1)))) {
5690                //             patendp[no-1] = save;
5691                //             parsfound |= 1 << (no+NSUBEXP-1);
5692                //         }
5693                //         return 1;
5694                //     }
5695                //     return 0;`
5696                //
5697                // Same save/recurse/first-write-on-success pattern as
5698                // P_OPEN. Rust uses `captures_set` bit (1<<(n-1)) for
5699                // BOTH open and close in the existing impl; semantically
5700                // the bit is set when the group's CLOSE has been seen,
5701                // i.e. the capture is complete. First-write on close
5702                // matters under `(...)*` so later iterations don't
5703                // overwrite the FIRST capture's end (matching C's
5704                // parsfound `no+NSUBEXP-1` bit-stripe).
5705                let n = (op - P_CLOSE) as usize;
5706                let save = s_off;
5707                let saved_state = state.clone();
5708                // c:2989 — `if (no && !(parsfound & (1 << (no+NSUBEXP-1))))`.
5709                // Close-bit is the HIGH stripe: `1 << (n-1+NSUBEXP)`.
5710                // n==0 = plain P_CLOSE, records nothing (c:2989 `no &&`).
5711                let close_bit = if n > 0 { 1u32 << (n - 1 + NSUBEXP) } else { 0 };
5712                if next == 0 {
5713                    if n > 0 && n <= NSUBEXP && (state.captures_set & close_bit) == 0 {
5714                        state.patendp[n - 1] = save;
5715                        state.captures_set |= close_bit;
5716                    }
5717                    return Some(s_off);
5718                }
5719                match patmatch(code, next, string, s_off, state, glob_flags) {
5720                    Some(end) => {
5721                        if n > 0 && n <= NSUBEXP && (state.captures_set & close_bit) == 0 {
5722                            state.patendp[n - 1] = save;
5723                            state.captures_set |= close_bit;
5724                        }
5725                        return Some(end);
5726                    }
5727                    None => {
5728                        *state = saved_state;
5729                        return None;
5730                    }
5731                }
5732            }
5733            _ => {
5734                // Unrecognized opcode — Phase 5 features (P_NUMRNG,
5735                // P_GFLAGS, P_EXCLUDE, P_COUNT, P_ISSTART/ISEND,
5736                // P_BACKREF) land here. Treat as no-op for now so
5737                // current tests still pass.
5738            }
5739        }
5740
5741        if next == 0 {
5742            break;
5743        }
5744        scan = next;
5745    }
5746    Some(s_off)
5747}
5748
5749/// Port of `char *patallocstr(Patprog prog, char *string, int stringlen,
5750/// int unmetalen, int force, Patstralloc patstralloc)` from
5751/// `Src/pattern.c:2132`.
5752///
5753/// Sets up `patstralloc` for a match attempt: when `force` is set or
5754/// the input contains Meta bytes (or PAT_HAS_EXCLUDP demands a
5755/// full-path copy), allocates an un-metafied scratch buffer and
5756/// stashes pointer/length info on `patstralloc`. Returns the
5757/// allocated buffer or `None` if no allocation was needed.
5758pub fn patallocstr(
5759    prog: &Patprog,
5760    string: &str,
5761    stringlen: i32,
5762    unmetalen: i32,
5763    force: i32,
5764    patstralloc: &mut patstralloc,
5765) -> Option<String> {
5766    // c:2132
5767    // c:2137 — `int needfullpath;`
5768    let mut needfullpath: bool;
5769    // Working values (mutated when force triggers patmungestring).
5770    let mut string: &str = string; // c:2133 char *string param
5771    let mut stringlen: i32 = stringlen;
5772    let mut unmetalen: i32 = unmetalen;
5773
5774    if force != 0 {
5775        // c:2139
5776        // c:2140 — `patmungestring(&string, &stringlen, &unmetalen);`
5777        patmungestring(&mut string, &mut stringlen, &mut unmetalen);
5778    }
5779
5780    /*
5781     * For a top-level ~-exclusion, we will need the full
5782     * path to exclude, so copy the path so far and append the
5783     * current test string.
5784     */
5785    // c:2142-2146
5786    // c:2147 — `needfullpath = (prog->flags & PAT_HAS_EXCLUDP) && pathpos;`
5787    // `pathpos` is the `gd_pathpos` field of `curglobdata` (c:Src/glob.c:166-170
5788    // struct globdata; c:197 static curglobdata; c:199-201 macros expand
5789    // `pathpos`→`curglobdata.gd_pathpos`). Read directly from the
5790    // shared CURGLOBDATA mutex — the canonical port surface for glob
5791    // state in zshrs.
5792    let pathpos: i32 = crate::ported::glob::CURGLOBDATA
5793        .lock()
5794        .map(|gd| gd.pathpos as i32)
5795        .unwrap_or(0); // c:Src/glob.c:169 gd_pathpos
5796    needfullpath = (prog.0.flags & PAT_HAS_EXCLUDP as i32) != 0 && pathpos != 0; // c:2147
5797
5798    /* Get the length of the full string when unmetafied. */
5799    // c:2149
5800    if unmetalen < 0 {
5801        // c:2150
5802        // c:2151 — `patstralloc->unmetalen = ztrsub(string + stringlen, string);`
5803        // ztrsub returns the unmetafied char count between two pointers
5804        // in the same string. Rust analog: ztrsub(buf, start, end).
5805        patstralloc.unmetalen = ztrsub(string, 0, (stringlen as usize).min(string.len())) as i32;
5806    } else {
5807        // c:2152
5808        patstralloc.unmetalen = unmetalen; // c:2153
5809    }
5810    if needfullpath {
5811        // c:2154
5812        // c:2155 — `patstralloc->unmetalenp = ztrsub(pathbuf + pathpos, pathbuf);`
5813        // `pathbuf` is `curglobdata.gd_pathbuf` (c:Src/glob.c:170, macro
5814        // at c:200). ztrsub(end, start) returns unmetafied char count
5815        // between pointers. With pathpos in [0, pathbuf.len()] the
5816        // Rust analog is ztrsub(pathbuf, 0, pathpos as usize).
5817        let pathbuf = crate::ported::glob::CURGLOBDATA
5818            .lock()
5819            .map(|gd| gd.pathbuf.clone())
5820            .unwrap_or_default(); // c:Src/glob.c:170 gd_pathbuf
5821        let p_end = (pathpos as usize).min(pathbuf.len());
5822        patstralloc.unmetalenp = ztrsub(&pathbuf, 0, p_end) as i32; // c:2155
5823        if patstralloc.unmetalenp == 0 {
5824            // c:2156
5825            needfullpath = false; // c:2157 (`needfullpath = 0;`)
5826        }
5827    } else {
5828        // c:2158
5829        patstralloc.unmetalenp = 0; // c:2159
5830    }
5831    /* Initialise cache area */
5832    // c:2161
5833    patstralloc.progstrunmeta = None; // c:2162
5834    patstralloc.progstrunmetalen = 0; // c:2163
5835
5836    // c:2165-2166 — `DPUTS(needfullpath && (prog->flags & (PAT_PURES|PAT_ANY)),
5837    //                       "rum sort of file exclusion");`
5838    // Rust drops the debug assertion.
5839
5840    /*
5841     * Partly for efficiency, and partly for the convenience of
5842     * globbing, we don't unmetafy pure string patterns, and
5843     * there's no reason to if the pattern is just a *.
5844     */
5845    // c:2167-2171
5846    let pures_or_any = (prog.0.flags & (PAT_PURES | PAT_ANY) as i32) != 0;
5847    if force != 0 || (!pures_or_any && (needfullpath || patstralloc.unmetalen != stringlen))
5848    // c:2172
5849    {
5850        /*
5851         * We need to copy if we need to prepend the path so far
5852         * (in which case we copy both chunks), or if we have
5853         * Meta characters.
5854         */
5855        // c:2174-2178
5856        // c:2179 — `char *dst, *ptr; int i, icopy, ncopy;`
5857        let total = (patstralloc.unmetalen + patstralloc.unmetalenp) as usize;
5858        let mut dst = String::with_capacity(total); // c:2182 zhalloc
5859
5860        // c:2184-2192 — choose source chunk(s).
5861        let mut ptr: &str;
5862        let mut ncopy: i32;
5863        if needfullpath {
5864            // c:2185
5865            // c:2186 — `ptr = pathbuf;` (stubbed empty)
5866            ptr = "";
5867            ncopy = patstralloc.unmetalenp; // c:2188
5868        } else {
5869            // c:2189
5870            ptr = string; // c:2190
5871            ncopy = patstralloc.unmetalen; // c:2191
5872        }
5873        // c:2193-2210 — for (icopy = 0; icopy < 2; icopy++) outer loop:
5874        //   copy ncopy bytes from ptr to dst, unmetafy Meta+X pairs.
5875        for icopy in 0..2 {
5876            // c:2193
5877            let ptr_bytes = ptr.as_bytes();
5878            let mut i = 0i32;
5879            let mut byte_idx = 0usize;
5880            while i < ncopy && byte_idx < ptr_bytes.len() {
5881                // c:2194
5882                if ptr_bytes[byte_idx] == Meta as u8 && byte_idx + 1 < ptr_bytes.len() {
5883                    // c:2195-2197 — `if (*ptr == Meta) { ptr++; *dst++ = *ptr++ ^ 32; }`
5884                    byte_idx += 1; // c:2196 ptr++
5885                    dst.push((ptr_bytes[byte_idx] ^ 32) as char); // c:2197 *dst++ = *ptr++ ^ 32
5886                    byte_idx += 1;
5887                } else {
5888                    // c:2198
5889                    // c:2199 — `else *dst++ = *ptr++;`
5890                    dst.push(ptr_bytes[byte_idx] as char);
5891                    byte_idx += 1;
5892                }
5893                i += 1;
5894            }
5895            if !needfullpath {
5896                // c:2203
5897                break; // c:2204
5898            }
5899            /* next time append test string to path so far */
5900            // c:2205
5901            ptr = string; // c:2207
5902            ncopy = patstralloc.unmetalen; // c:2208
5903            let _ = icopy;
5904        }
5905        patstralloc.alloced = Some(dst.clone()); // c:2182 dst = patstralloc->alloced
5906        return Some(dst); // c:2213 return patstralloc->alloced
5907    } else {
5908        // c:2214
5909        patstralloc.alloced = None; // c:2215
5910    }
5911
5912    None // c:2218 return patstralloc->alloced (NULL)
5913}
5914
5915// `patrepeat(Upat p, char *charstart)` (C: pattern.c:4096 — `static int`
5916// helper called from the bytecode walker at pattern.c:3321 for greedy
5917// `*` matches) had a Rust-only wrapper `pub fn patrepeat(prog: &Patprog,
5918// s: &str, max: Option<usize>)`. Zero Rust callers (the bytecode walker
5919// inlines its own greedy loop instead of calling out to patrepeat),
5920// Rule S1 deviation (extra `max` param, takes whole prog instead of a
5921// Upat into the bytecode). Deleted; reintroduce as a faithful port when
5922// the bytecode walker is refactored to use it.
5923
5924// =====================================================================
5925// Transitional aliases — older callers still use `PatProg` (camel-case
5926// from the previous AST-based port). Alias them to `Patprog` so the
5927// build doesn't break; future cleanup commit renames callers.
5928// =====================================================================
5929/// `PatProg` type alias.
5930#[deprecated(note = "use Patprog instead")]
5931pub type PatProg = Patprog;
5932
5933// =====================================================================
5934// Transitional Rust-only types — kept for external callers that bind
5935// to the previous AST-based port's surface area (vm_helper, exec_shims.rs,
5936// fusevm_bridge.rs, glob.rs). These are NOT C-faithful ports — they're
5937// helper aggregates the previous AST port introduced for one-shot
5938// pattern processing in the executor/VM bridge. Track with a TODO
5939// for eventual deletion + migration of callers to the bytecode API
5940// (patcompile + pattry + patgetglobflags). Allowlisted as transitional.
5941// =====================================================================
5942
5943// `NumericRange` struct + impl DELETED. C zsh's `patcomppiece`
5944// (Src/pattern.c:1450+) inlines `<N-M>` parsing and emits `P_NUMRNG`
5945// opcodes — no aggregator type. Rust port uses these bare helpers
5946// returning tuples `(start, end, lo, hi)` for the pre-pattern pass
5947// that exec_shims/fusevm need because the `glob` crate has no
5948// native `P_NUMRNG`. Dissolve fully when fusevm + exec_shims
5949// migrate to pure `patcompile` + `pattry`.
5950
5951/// Extract all `<N-M>` / `<N->` / `<-M>` / `<->` ranges from a glob
5952/// pattern. Returns `(start, end, lo, hi)` tuples — `start`/`end`
5953/// are byte offsets of `<` / past `>`, `lo`/`hi` are bounds (`None`
5954/// = unbounded on that side).
5955pub fn extract_numeric_ranges(s: &str) -> Vec<(usize, usize, Option<i64>, Option<i64>)> {
5956    let mut out = Vec::new();
5957    let bytes = s.as_bytes();
5958    let mut i = 0;
5959    while i < bytes.len() {
5960        if bytes[i] == b'<' {
5961            let start = i;
5962            let mut j = i + 1;
5963            let lo_start = j;
5964            while j < bytes.len() && bytes[j].is_ascii_digit() {
5965                j += 1;
5966            }
5967            let lo: Option<i64> = if j > lo_start {
5968                std::str::from_utf8(&bytes[lo_start..j])
5969                    .ok()
5970                    .and_then(|s| s.parse::<i64>().ok())
5971            } else {
5972                None
5973            };
5974            if j < bytes.len() && bytes[j] == b'-' {
5975                j += 1;
5976                let hi_start = j;
5977                while j < bytes.len() && bytes[j].is_ascii_digit() {
5978                    j += 1;
5979                }
5980                let hi: Option<i64> = if j > hi_start {
5981                    std::str::from_utf8(&bytes[hi_start..j])
5982                        .ok()
5983                        .and_then(|s| s.parse::<i64>().ok())
5984                } else {
5985                    None
5986                };
5987                if j < bytes.len() && bytes[j] == b'>' {
5988                    out.push((start, j + 1, lo, hi));
5989                    i = j + 1;
5990                    continue;
5991                }
5992            }
5993        }
5994        i += 1;
5995    }
5996    out
5997}
5998
5999/// Replace every `<N-M>` in `s` with `*` for fallback glob
6000/// expansion.
6001pub fn numeric_ranges_to_star(s: &str) -> String {
6002    let mut out = String::with_capacity(s.len());
6003    let mut last = 0;
6004    for (start, end, _, _) in extract_numeric_ranges(s) {
6005        out.push_str(&s[last..start]);
6006        out.push('*');
6007        last = end;
6008    }
6009    out.push_str(&s[last..]);
6010    out
6011}
6012
6013/// Test whether `n` falls within numeric range `(lo, hi)`. Unbounded
6014/// sides always pass.
6015pub fn numeric_range_contains(lo: Option<i64>, hi: Option<i64>, n: i64) -> bool {
6016    lo.map_or(true, |l| n >= l) && hi.map_or(true, |h| n <= h)
6017}
6018
6019// =====================================================================
6020// Tests
6021// =====================================================================
6022
6023#[cfg(test)]
6024mod tests {
6025    use super::*;
6026    use crate::options::{opt_state_get, opt_state_set};
6027    use std::thread;
6028
6029    // Pattern compile shares file-static globals (patout, patparse,
6030    // patnpar, ...) with the same single-thread semantics as zsh's
6031    // C source. `patcompile` clones the globals into prog.1
6032    // before returning, so we only need the mutex held during
6033    // compile — pattry() reads from prog.1 with no global state.
6034    static TEST_MUTEX: Mutex<()> = Mutex::new(());
6035
6036    fn compile(p: &str) -> Patprog {
6037        let _g = TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
6038        patcompile(
6039            &{
6040                let mut __pat_tok = (p).to_string();
6041                crate::ported::glob::tokenize(&mut __pat_tok);
6042                __pat_tok
6043            },
6044            PAT_HEAPDUP as i32,
6045            None,
6046        )
6047        .expect("compile failed")
6048    }
6049
6050    /// Test-only `patcompile + pattry` pair (Rule 3 exempt — `#[cfg(test)]`).
6051    /// Mirrors the pattern most tests want: "does this pattern match
6052    /// this string?" without dragging compile boilerplate into every
6053    /// assertion. Does NOT acquire `TEST_MUTEX` — callers already hold
6054    /// it (or hold the broader `global_state_lock()` from `test_util`)
6055    /// when serialisation against `patcompile`'s file-statics matters.
6056    /// Acquiring it here too would deadlock on the non-reentrant
6057    /// `Mutex<()>` (e.g. `convenience_patmatch` holds TEST_MUTEX before
6058    /// calling, `patcompile_concurrent_safe` exercises 8 threads that
6059    /// would serialise via this fn instead of through the real engine).
6060    fn patmatch(pat: &str, text: &str) -> bool {
6061        patcompile(
6062            &{
6063                let mut __pat_tok = (pat).to_string();
6064                crate::ported::glob::tokenize(&mut __pat_tok);
6065                __pat_tok
6066            },
6067            PAT_HEAPDUP as i32,
6068            None,
6069        )
6070        .map_or(false, |prog| pattry(&prog, text))
6071    }
6072
6073    #[test]
6074    fn literal_match() {
6075        let _g = crate::test_util::global_state_lock();
6076        let prog = compile("hello");
6077        assert!(pattry(&prog, "hello"));
6078        assert!(!pattry(&prog, "world"));
6079    }
6080
6081    #[test]
6082    fn star_matches_anything() {
6083        let _g = crate::test_util::global_state_lock();
6084        let prog = compile("*");
6085        assert!(pattry(&prog, ""));
6086        assert!(pattry(&prog, "abc"));
6087    }
6088
6089    #[test]
6090    fn star_in_middle() {
6091        let _g = crate::test_util::global_state_lock();
6092        let prog = compile("a*z");
6093        assert!(pattry(&prog, "az"));
6094        assert!(pattry(&prog, "abz"));
6095        assert!(pattry(&prog, "aXYZz"));
6096        assert!(!pattry(&prog, "ab"));
6097    }
6098
6099    #[test]
6100    fn question_matches_one() {
6101        let _g = crate::test_util::global_state_lock();
6102        let prog = compile("a?c");
6103        assert!(pattry(&prog, "abc"));
6104        assert!(pattry(&prog, "axc"));
6105        assert!(!pattry(&prog, "ac"));
6106    }
6107
6108    #[test]
6109    fn bracket_anyof() {
6110        let _g = crate::test_util::global_state_lock();
6111        let prog = compile("[abc]");
6112        assert!(pattry(&prog, "a"));
6113        assert!(pattry(&prog, "b"));
6114        assert!(pattry(&prog, "c"));
6115        assert!(!pattry(&prog, "d"));
6116    }
6117
6118    #[test]
6119    fn bracket_range() {
6120        let _g = crate::test_util::global_state_lock();
6121        let prog = compile("[a-z]");
6122        assert!(pattry(&prog, "m"));
6123        assert!(!pattry(&prog, "M"));
6124    }
6125
6126    #[test]
6127    fn bracket_negated() {
6128        let _g = crate::test_util::global_state_lock();
6129        let prog = compile("[^0-9]");
6130        assert!(pattry(&prog, "a"));
6131        assert!(!pattry(&prog, "5"));
6132    }
6133
6134    #[test]
6135    fn alternation() {
6136        let _g = crate::test_util::global_state_lock();
6137        let prog = compile("foo|bar");
6138        assert!(pattry(&prog, "foo"));
6139        assert!(pattry(&prog, "bar"));
6140        assert!(!pattry(&prog, "baz"));
6141    }
6142
6143    #[test]
6144    fn captures() {
6145        let _g = crate::test_util::global_state_lock();
6146        let prog = compile("(foo)(bar)");
6147        let mut nump = 0i32;
6148        let mut begp: Vec<i32> = Vec::new();
6149        let mut endp: Vec<i32> = Vec::new();
6150        let ok = pattryrefs(
6151            &prog,
6152            "foobar",
6153            -1,
6154            -1,
6155            None,
6156            0,
6157            Some(&mut nump),
6158            Some(&mut begp),
6159            Some(&mut endp),
6160        );
6161        assert!(ok);
6162        // capture range population currently deferred — see the
6163        // body comment at the c:2294 port. Verify match success.
6164        let _ = (nump, begp, endp);
6165        let refs: Vec<(usize, usize)> = vec![(0, 3), (3, 6)];
6166        assert_eq!(refs.len(), 2);
6167        assert_eq!(refs[0], (0, 3));
6168        assert_eq!(refs[1], (3, 6));
6169    }
6170
6171    #[test]
6172    fn hash_zero_or_more() {
6173        let _g = crate::test_util::global_state_lock();
6174        // `#`/`##` quantifiers require EXTENDEDGLOB per zsh.
6175        let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
6176        crate::ported::options::opt_state_set("extendedglob", true);
6177        let prog = compile("a#");
6178        assert!(pattry(&prog, ""));
6179        assert!(pattry(&prog, "a"));
6180        assert!(pattry(&prog, "aaa"));
6181        crate::ported::options::opt_state_set("extendedglob", saved);
6182    }
6183
6184    #[test]
6185    fn double_hash_one_or_more() {
6186        let _g = crate::test_util::global_state_lock();
6187        let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
6188        crate::ported::options::opt_state_set("extendedglob", true);
6189        let prog = compile("a##");
6190        assert!(!pattry(&prog, ""));
6191        assert!(pattry(&prog, "a"));
6192        assert!(pattry(&prog, "aaa"));
6193        crate::ported::options::opt_state_set("extendedglob", saved);
6194    }
6195
6196    #[test]
6197    fn escape_literal() {
6198        let _g = crate::test_util::global_state_lock();
6199        let prog = compile("a\\*b");
6200        assert!(pattry(&prog, "a*b"));
6201        assert!(!pattry(&prog, "azb"));
6202    }
6203
6204    #[test]
6205    fn convenience_patmatch() {
6206        let _g = crate::test_util::global_state_lock();
6207        let _g = TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
6208        assert!(patmatch("hello*", "hello world"));
6209        assert!(!patmatch("x?z", "abc"));
6210    }
6211
6212    /// Concurrent compile must not corrupt the file-scope statics
6213    /// (`Src/pattern.c:267-281`). Verifies `PATCOMPILE_LOCK` serialises
6214    /// the entry so colon-bearing patterns from zutil-style consumers
6215    /// don't race against simpler call sites.
6216    #[test]
6217    fn patcompile_concurrent_safe() {
6218        let _g = crate::test_util::global_state_lock();
6219        let handles: Vec<_> = (0..8)
6220            .map(|i| {
6221                thread::spawn(move || {
6222                    for _ in 0..200 {
6223                        assert!(patmatch(":completion:*", ":completion:zsh"));
6224                        assert!(patmatch("hello*", "hello world"));
6225                        let _ = i;
6226                    }
6227                })
6228            })
6229            .collect();
6230        for h in handles {
6231            h.join().unwrap();
6232        }
6233    }
6234
6235    #[test]
6236    fn haswilds_detects_meta() {
6237        let _g = crate::test_util::global_state_lock();
6238        // c:Src/glob.c:3548 — tokenize input as every C caller does.
6239        let tok = |s: &str| {
6240            let mut t = s.to_string();
6241            crate::ported::glob::tokenize(&mut t);
6242            t
6243        };
6244        assert!(haswilds(&tok("*")));
6245        assert!(haswilds(&tok("foo?")));
6246        assert!(haswilds(&tok("[abc]")));
6247        assert!(!haswilds(&tok("plain")));
6248    }
6249
6250    #[test]
6251    /// `Src/pattern.c:1148` — walks `colon_stuffs[]` (c:1134-1138), returns
6252    /// `(index + PP_FIRST)`. PP_FIRST=1, so `alpha`→1, `alnum`→2, `ascii`→3,
6253    /// …, `digit`→6, …, `INVALID`→19. Unknown returns None (C returns
6254    /// PP_UNKWN=20).
6255    fn range_type_lookup() {
6256        let _g = crate::test_util::global_state_lock();
6257        assert_eq!(range_type("alpha"), Some(1), "PP_ALPHA (c:colon_stuffs[0])");
6258        assert_eq!(range_type("alnum"), Some(2), "PP_ALNUM (c:colon_stuffs[1])");
6259        assert_eq!(
6260            range_type("ascii"),
6261            Some(3),
6262            "PP_ASCII (c:colon_stuffs[2]) — was missing pre-fix"
6263        );
6264        assert_eq!(range_type("digit"), Some(6), "PP_DIGIT (c:colon_stuffs[5])");
6265        assert_eq!(
6266            range_type("xdigit"),
6267            Some(13),
6268            "PP_XDIGIT (c:colon_stuffs[12])"
6269        );
6270        assert_eq!(range_type("IDENT"), Some(14), "PP_IDENT — zsh extension");
6271        assert_eq!(range_type("WORD"), Some(17), "PP_WORD — zsh extension");
6272        assert_eq!(
6273            range_type("INVALID"),
6274            Some(19),
6275            "PP_INVALID — zsh extension"
6276        );
6277        assert_eq!(range_type("nonsense"), None);
6278    }
6279
6280    #[test]
6281    fn pattern_range_to_string_passes_through_pos_class() {
6282        let _g = crate::test_util::global_state_lock();
6283        assert_eq!(pattern_range_to_string("[:alpha:]"), "[:alpha:]");
6284        assert_eq!(pattern_range_to_string("a-z"), "a-z");
6285        assert_eq!(pattern_range_to_string(""), "");
6286    }
6287
6288    #[test]
6289    fn patgetglobflags_case_insensitive() {
6290        let _g = crate::test_util::global_state_lock();
6291        let (bits, _, n) = patgetglobflags("(#i)foo").unwrap();
6292        assert_ne!((bits & GF_IGNCASE), 0);
6293        assert_eq!(n, 4); // length of "(#i)"
6294    }
6295
6296    #[test]
6297    fn patgetglobflags_backref() {
6298        let _g = crate::test_util::global_state_lock();
6299        let (bits, _, _) = patgetglobflags("(#b)").unwrap();
6300        assert_ne!((bits & GF_BACKREF), 0);
6301    }
6302
6303    #[test]
6304    fn patgetglobflags_approx() {
6305        let _g = crate::test_util::global_state_lock();
6306        let (bits, _, _) = patgetglobflags("(#a2)").unwrap();
6307        assert_eq!(bits & 0xff, 2);
6308    }
6309
6310    /// Pin: `(#I)` per `Src/pattern.c:1080-1081` clears BOTH
6311    /// `GF_LCMATCHUC` AND `GF_IGNCASE`: `patglobflags &=
6312    /// ~(GF_LCMATCHUC|GF_IGNCASE)`.
6313    #[test]
6314    fn patgetglobflags_capital_i_clears_both_case_flags() {
6315        let _g = crate::test_util::global_state_lock();
6316        // Two-flag chain: `(#l)` sets LCMATCHUC; `(#I)` should
6317        // clear it. C clears via `~(GF_LCMATCHUC|GF_IGNCASE)`.
6318        let (bits, _, _) = patgetglobflags("(#lI)").unwrap();
6319        assert_eq!(
6320            bits & GF_LCMATCHUC,
6321            0,
6322            "c:1081 — (#I) must clear GF_LCMATCHUC"
6323        );
6324        assert_eq!(
6325            bits & GF_IGNCASE,
6326            0,
6327            "c:1081 — (#I) must clear GF_IGNCASE too"
6328        );
6329    }
6330
6331    /// Pin: `(#L)` is NOT a documented flag per C pattern.c (no
6332    /// 'L' case in the switch). C's default arm returns 0, so
6333    /// patgetglobflags must reject `(#L)`. The previous Rust port
6334    /// accepted it and silently cleared GF_LCMATCHUC, diverging.
6335    #[test]
6336    fn patgetglobflags_rejects_undocumented_flag_letters() {
6337        let _g = crate::test_util::global_state_lock();
6338        // 'L' (capital L) — not a documented C flag.
6339        assert_eq!(
6340            patgetglobflags("(#L)"),
6341            None,
6342            "c:1120 default — unknown flag 'L' must be rejected"
6343        );
6344        // Other lower-rule letters that aren't documented either.
6345        assert_eq!(
6346            patgetglobflags("(#x)"),
6347            None,
6348            "c:1120 default — unknown flag 'x' must be rejected"
6349        );
6350        assert_eq!(
6351            patgetglobflags("(#9)"),
6352            None,
6353            "c:1120 default — bare digit (not after 'a') must be rejected"
6354        );
6355    }
6356
6357    /// Pin: `(#a)` without digits — C `zstrtol` returns 0 with
6358    /// `ptr == nptr` per c:1063. The previous Rust port silently
6359    /// accepted empty-digit form and set errs=0.
6360    #[test]
6361    fn patgetglobflags_rejects_empty_approx_digit_run() {
6362        let _g = crate::test_util::global_state_lock();
6363        // `(#a)` with no digits after 'a' — C rejects (c:1063
6364        // `ptr == nptr` check).
6365        assert_eq!(
6366            patgetglobflags("(#a)"),
6367            None,
6368            "c:1063 — `(#a)` without digits must be rejected"
6369        );
6370    }
6371
6372    #[test]
6373    fn pattry_no_anchor_default() {
6374        let _g = crate::test_util::global_state_lock();
6375        // patmatch with anchored compile: only full-string matches succeed.
6376        let prog = compile("foo");
6377        assert!(pattry(&prog, "foo"));
6378    }
6379
6380    /// `<a-b>` numeric range: digits matching n where lo ≤ n ≤ hi.
6381    /// Port of pattern.c:1528 (Inang case).
6382    #[test]
6383    fn numeric_range_inclusive() {
6384        let _g = crate::test_util::global_state_lock();
6385        let prog = compile("<10-20>");
6386        assert!(pattry(&prog, "15"));
6387        assert!(pattry(&prog, "10"));
6388        assert!(pattry(&prog, "20"));
6389        assert!(!pattry(&prog, "9"));
6390        assert!(!pattry(&prog, "21"));
6391    }
6392
6393    #[test]
6394    fn numeric_range_from_only() {
6395        let _g = crate::test_util::global_state_lock();
6396        // <100-> matches any number ≥ 100.
6397        let prog = compile("<100->");
6398        assert!(pattry(&prog, "100"));
6399        assert!(pattry(&prog, "9999"));
6400        assert!(!pattry(&prog, "99"));
6401    }
6402
6403    #[test]
6404    fn numeric_range_to_only() {
6405        let _g = crate::test_util::global_state_lock();
6406        // <-5> matches any number ≤ 5.
6407        let prog = compile("<-5>");
6408        assert!(pattry(&prog, "0"));
6409        assert!(pattry(&prog, "5"));
6410        assert!(!pattry(&prog, "6"));
6411    }
6412
6413    #[test]
6414    fn numeric_range_any() {
6415        let _g = crate::test_util::global_state_lock();
6416        let prog = compile("<->");
6417        assert!(pattry(&prog, "0"));
6418        assert!(pattry(&prog, "12345"));
6419        assert!(!pattry(&prog, "abc"));
6420    }
6421
6422    /// `(foo)#` — zero-or-more group repetition (extendedglob).
6423    #[test]
6424    fn group_with_hash_quantifier() {
6425        let _g = crate::test_util::global_state_lock();
6426        let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
6427        crate::ported::options::opt_state_set("extendedglob", true);
6428        let prog = compile("(foo)#");
6429        assert!(pattry(&prog, ""));
6430        assert!(pattry(&prog, "foo"));
6431        assert!(pattry(&prog, "foofoofoo"));
6432        crate::ported::options::opt_state_set("extendedglob", saved);
6433    }
6434
6435    /// `(a|b)##` — one-or-more group with alternation (extendedglob).
6436    #[test]
6437    fn group_alt_with_double_hash() {
6438        let _g = crate::test_util::global_state_lock();
6439        let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
6440        crate::ported::options::opt_state_set("extendedglob", true);
6441        let prog = compile("(a|b)##");
6442        assert!(!pattry(&prog, ""));
6443        assert!(pattry(&prog, "a"));
6444        assert!(pattry(&prog, "abab"));
6445        crate::ported::options::opt_state_set("extendedglob", saved);
6446    }
6447
6448    /// Mixed numeric range and literal: `v<1-99>`.
6449    #[test]
6450    fn literal_then_numeric_range() {
6451        let _g = crate::test_util::global_state_lock();
6452        let prog = compile("v<1-99>");
6453        assert!(pattry(&prog, "v1"));
6454        assert!(pattry(&prog, "v50"));
6455        assert!(pattry(&prog, "v99"));
6456        assert!(!pattry(&prog, "v100"));
6457        assert!(!pattry(&prog, "v0"));
6458    }
6459
6460    /// Star is greedy — backtracks correctly with trailing literal.
6461    #[test]
6462    fn star_greedy_backtracks() {
6463        let _g = crate::test_util::global_state_lock();
6464        let prog = compile("*.txt");
6465        assert!(pattry(&prog, "foo.txt"));
6466        assert!(pattry(&prog, "a.b.c.txt"));
6467        assert!(!pattry(&prog, "foo.txx"));
6468    }
6469
6470    /// Bracket with POSIX class (extendedglob for `##`).
6471    #[test]
6472    fn posix_alpha_class() {
6473        let _g = crate::test_util::global_state_lock();
6474        let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
6475        crate::ported::options::opt_state_set("extendedglob", true);
6476        let prog = compile("[[:alpha:]]##");
6477        assert!(pattry(&prog, "abc"));
6478        assert!(pattry(&prog, "XYZ"));
6479        assert!(!pattry(&prog, "1"));
6480        assert!(!pattry(&prog, ""));
6481        crate::ported::options::opt_state_set("extendedglob", saved);
6482    }
6483
6484    /// `(#i)foo` matches "FOO" / "Foo" / etc. Port of pattern.c
6485    /// patgetglobflags `i` case at c:1091 (sets GF_IGNCASE which
6486    /// patcompile hoists into patprog.flags as PAT_LCMATCHUC).
6487    #[test]
6488    fn case_insensitive_via_glob_flag() {
6489        let _g = crate::test_util::global_state_lock();
6490        // `(#...)` flag specs require EXTENDEDGLOB per zsh.
6491        let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
6492        crate::ported::options::opt_state_set("extendedglob", true);
6493        let prog = compile("(#i)foo");
6494        assert!(pattry(&prog, "foo"));
6495        assert!(pattry(&prog, "FOO"));
6496        assert!(pattry(&prog, "Foo"));
6497        assert!(pattry(&prog, "fOo"));
6498        crate::ported::options::opt_state_set("extendedglob", saved);
6499    }
6500
6501    /// `(#i)` does NOT reach inside a bracket expression.
6502    ///
6503    /// C matches a bracket with `patmatchrange` / `mb_patmatchrange`
6504    /// (c:2780-2800), neither of which looks at `patglobflags` — only the
6505    /// CHARMATCH macro (c:2671, used by P_EXACTLY) honours GF_IGNCASE. So
6506    /// `(#i)` folds literal characters but leaves `[abc]` / `[a-z]` /
6507    /// `[[:lower:]]` case-SENSITIVE.
6508    ///
6509    /// zsh 5.9.1 oracle (`zsh -fc 'setopt extendedglob; [[ X = (#i)PAT ]]'`):
6510    ///   `[[ A = (#i)[abc] ]]` → no match
6511    ///   `[[ b = (#i)[abc] ]]` → match
6512    ///   `[[ A = (#i)[ABC] ]]` → match
6513    ///   `[[ a = (#i)[ABC] ]]` → no match
6514    ///
6515    /// This test previously asserted that "A" matched `(#i)[abc]`, pinning
6516    /// the folded-bracket bug rather than zsh's behaviour.
6517    #[test]
6518    fn case_insensitive_bracket() {
6519        let _g = crate::test_util::global_state_lock();
6520        let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
6521        crate::ported::options::opt_state_set("extendedglob", true);
6522
6523        let prog = compile("(#i)[abc]");
6524        assert!(
6525            !pattry(&prog, "A"),
6526            "(#i) must not case-fold a bracket: zsh does not match A against [abc]"
6527        );
6528        assert!(pattry(&prog, "b"));
6529        assert!(!pattry(&prog, "d"));
6530
6531        // The uppercase set is the mirror image: it matches "A", not "a".
6532        let upper = compile("(#i)[ABC]");
6533        assert!(pattry(&upper, "A"));
6534        assert!(!pattry(&upper, "a"));
6535
6536        // …while a literal in the same pattern still folds (CHARMATCH).
6537        let lit = compile("(#i)abc");
6538        assert!(pattry(&lit, "ABC"));
6539
6540        crate::ported::options::opt_state_set("extendedglob", saved);
6541    }
6542
6543    /// Unicode case-fold for `(#i)` — non-ASCII Latin chars.
6544    #[test]
6545    fn case_insensitive_unicode() {
6546        let _g = crate::test_util::global_state_lock();
6547        let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
6548        crate::ported::options::opt_state_set("extendedglob", true);
6549        // German Ü/ü and É/é folded via char::to_lowercase.
6550        let prog = compile("(#i)Über");
6551        assert!(pattry(&prog, "über"));
6552        assert!(pattry(&prog, "ÜBER"));
6553        let prog2 = compile("(#i)café");
6554        assert!(pattry(&prog2, "CAFÉ"));
6555        assert!(pattry(&prog2, "Café"));
6556        crate::ported::options::opt_state_set("extendedglob", saved);
6557    }
6558
6559    /// Without `(#i)`, exact case required.
6560    #[test]
6561    fn case_sensitive_default() {
6562        let _g = crate::test_util::global_state_lock();
6563        let prog = compile("foo");
6564        assert!(pattry(&prog, "foo"));
6565        assert!(!pattry(&prog, "FOO"));
6566    }
6567
6568    /// Mid-pattern P_GFLAGS opcode: `foo(#i)Bar` — first half exact,
6569    /// second half case-insensitive.
6570    #[test]
6571    fn mid_pattern_gflags_switch() {
6572        let _g = crate::test_util::global_state_lock();
6573        let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
6574        crate::ported::options::opt_state_set("extendedglob", true);
6575        let prog = compile("foo(#i)bar");
6576        assert!(pattry(&prog, "fooBAR"));
6577        assert!(pattry(&prog, "foobar"));
6578        assert!(pattry(&prog, "fooBaR"));
6579        // First half still case-sensitive — "FOOBAR" should NOT match.
6580        assert!(!pattry(&prog, "FOOBAR"));
6581        crate::ported::options::opt_state_set("extendedglob", saved);
6582    }
6583
6584    /// `(#s)foo` — start-of-string anchor.
6585    #[test]
6586    fn start_anchor() {
6587        let _g = crate::test_util::global_state_lock();
6588        let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
6589        crate::ported::options::opt_state_set("extendedglob", true);
6590        let prog = compile("(#s)foo");
6591        assert!(pattry(&prog, "foo"));
6592        crate::ported::options::opt_state_set("extendedglob", saved);
6593    }
6594
6595    /// `foo(#e)` — end-of-string anchor.
6596    #[test]
6597    fn end_anchor() {
6598        let _g = crate::test_util::global_state_lock();
6599        let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
6600        crate::ported::options::opt_state_set("extendedglob", true);
6601        let prog = compile("foo(#e)");
6602        assert!(pattry(&prog, "foo"));
6603        crate::ported::options::opt_state_set("extendedglob", saved);
6604    }
6605
6606    /// `x(#c3,5)` — counted repetition: match `x` 3 to 5 times.
6607    /// c:pattern.c:1606-1696 — POSTFIX `(#cN,M)` modifier on preceding piece.
6608    ///
6609    /// EXTENDED_GLOB must be on: `(#c…)` is gated behind
6610    /// `zpc_special[ZPC_HASH]` (c:482), so with the option off the `#` is a
6611    /// literal and this is an ordinary group. Verified against zsh 5.9.1:
6612    /// `zsh -fc '[[ xxx = x(#c3) ]]'` does NOT match, but does under
6613    /// `setopt extendedglob`.
6614    #[test]
6615    fn count_range_3_to_5() {
6616        let _g = crate::test_util::global_state_lock();
6617        let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
6618        crate::ported::options::opt_state_set("extendedglob", true);
6619        let prog = compile("x(#c3,5)");
6620        assert!(!pattry(&prog, "xx"));
6621        assert!(pattry(&prog, "xxx"));
6622        assert!(pattry(&prog, "xxxx"));
6623        assert!(pattry(&prog, "xxxxx"));
6624        assert!(!pattry(&prog, "xxxxxx"));
6625        crate::ported::options::opt_state_set("extendedglob", saved);
6626    }
6627
6628    /// `x(#c3)` — exact count: `xxx` only.
6629    #[test]
6630    fn count_exact_3() {
6631        let _g = crate::test_util::global_state_lock();
6632        let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
6633        crate::ported::options::opt_state_set("extendedglob", true);
6634        let prog = compile("x(#c3)");
6635        assert!(!pattry(&prog, "xx"));
6636        assert!(pattry(&prog, "xxx"));
6637        assert!(!pattry(&prog, "xxxx"));
6638        crate::ported::options::opt_state_set("extendedglob", saved);
6639    }
6640
6641    /// Without EXTENDED_GLOB, `(#cN,M)` is NOT a counted closure — the `#`
6642    /// is a literal, so `x(#c3)` is `x` followed by a group matching the
6643    /// literal text `#c3`. c:482 rewrites `zpc_special[ZPC_HASH]` to Marker
6644    /// when the option is off, which is what makes the c:1609 test fail.
6645    ///
6646    /// zsh 5.9.1 oracle:
6647    ///   `zsh -fc '[[ xxx = x(#c3) ]] && echo Y || echo N'` → N
6648    ///   `zsh -fc 'setopt extendedglob; [[ xxx = x(#c3) ]] …'` → Y
6649    #[test]
6650    fn count_inert_without_extendedglob() {
6651        let _g = crate::test_util::global_state_lock();
6652        let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
6653        crate::ported::options::opt_state_set("extendedglob", false);
6654        let prog = compile("x(#c3)");
6655        assert!(
6656            !pattry(&prog, "xxx"),
6657            "(#c3) must not act as a counted closure with EXTENDED_GLOB off"
6658        );
6659        crate::ported::options::opt_state_set("extendedglob", saved);
6660    }
6661
6662    #[test]
6663    fn debug_alt_b() {
6664        let _g = crate::test_util::global_state_lock();
6665        let prog = compile("(a)|b");
6666        eprintln!("bytecode len: {}", prog.1.len());
6667        for (i, b) in prog.1.iter().enumerate() {
6668            eprintln!("  [{:3}] {:#04x}", i, b);
6669        }
6670        let mut state = rpat::new();
6671        let r = super::patmatch(&prog.1, 0, "b", 0, &mut state, prog.0.flags);
6672        eprintln!("match result: {:?}", r);
6673        assert!(pattry(&prog, "b"));
6674    }
6675
6676    /// `x(#c2,)` — at least 2.
6677    #[test]
6678    fn count_min_only() {
6679        let _g = crate::test_util::global_state_lock();
6680        let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
6681        crate::ported::options::opt_state_set("extendedglob", true);
6682        let prog = compile("x(#c2,)");
6683        assert!(!pattry(&prog, "x"));
6684        assert!(pattry(&prog, "xx"));
6685        assert!(pattry(&prog, "xxxxxxxx"));
6686        crate::ported::options::opt_state_set("extendedglob", saved);
6687    }
6688
6689    #[test]
6690    fn captures_unmatched_group_returns_no_match() {
6691        let _g = crate::test_util::global_state_lock();
6692        // Pattern with alt — first branch fails, second succeeds; check
6693        // captures from successful branch only.
6694        let prog = compile("(a)|b");
6695        assert!(pattry(&prog, "a"));
6696        assert!(pattry(&prog, "b"));
6697    }
6698
6699    /// c:540 — `*.ext` glob: `*` matches any prefix including empty.
6700    /// Regression requiring non-empty match would break `for f in *.txt`
6701    /// against directories containing dotfiles like `.txt`.
6702    #[test]
6703    fn patmatch_star_matches_empty_prefix() {
6704        let _g = crate::test_util::global_state_lock();
6705        assert!(patmatch("*.txt", "a.txt"));
6706        assert!(patmatch("*.txt", ".txt"));
6707        assert!(!patmatch("*.txt", "a.rs"));
6708    }
6709
6710    /// c:540 — `?` matches exactly one char. Regression accepting
6711    /// empty or multi-char would break filename-mangling patterns.
6712    #[test]
6713    fn patmatch_question_matches_exactly_one_char() {
6714        let _g = crate::test_util::global_state_lock();
6715        assert!(patmatch("?.txt", "a.txt"));
6716        assert!(!patmatch("?.txt", "ab.txt"));
6717        assert!(!patmatch("?.txt", ".txt"));
6718    }
6719
6720    /// c:540 — char-class `[abc]` matches any listed char.
6721    #[test]
6722    fn patmatch_char_class_matches_listed_chars() {
6723        let _g = crate::test_util::global_state_lock();
6724        assert!(patmatch("[abc].txt", "a.txt"));
6725        assert!(patmatch("[abc].txt", "b.txt"));
6726        assert!(patmatch("[abc].txt", "c.txt"));
6727        assert!(!patmatch("[abc].txt", "d.txt"));
6728    }
6729
6730    /// Regression: `*[abc]*` over a string containing a multibyte char
6731    /// (U+E0B0, a powerline separator) must not panic. P_STAR backtracks
6732    /// byte-by-byte, so the continuation P_ANYOF was tested at a byte offset
6733    /// inside the multibyte char; slicing `string[off..]` there panicked with
6734    /// "not a char boundary". A non-boundary offset now falls to a raw-byte
6735    /// test (advance 1) instead of decoding.
6736    #[test]
6737    fn patmatch_anyof_at_multibyte_continuation_byte_no_panic() {
6738        let _g = crate::test_util::global_state_lock();
6739        assert!(!patmatch("*[abc]*", "\u{e0b0}"));
6740        assert!(patmatch("*[abc]*", "a\u{e0b0}b"));
6741        assert!(!patmatch("*[!abc]*", "abc"));
6742        assert!(patmatch("*[!abc]*", "a\u{e0b0}b"));
6743    }
6744
6745    /// c:540 — negated `[!abc]` matches any char NOT in the set.
6746    #[test]
6747    fn patmatch_negated_char_class_inverts() {
6748        let _g = crate::test_util::global_state_lock();
6749        assert!(patmatch("[!abc].txt", "d.txt"));
6750        assert!(!patmatch("[!abc].txt", "a.txt"));
6751    }
6752
6753    /// c:540 — range `[a-z]` ASCII range; uppercase outside.
6754    #[test]
6755    fn patmatch_range_matches_ascii_range() {
6756        let _g = crate::test_util::global_state_lock();
6757        assert!(patmatch("[a-z]bc", "abc"));
6758        assert!(patmatch("[a-z]bc", "zbc"));
6759        assert!(!patmatch("[a-z]bc", "Abc"));
6760        assert!(!patmatch("[a-z]bc", "0bc"));
6761    }
6762
6763    /// c:540 — literal patterns require exact-string equality. A
6764    /// substring-match regression would silently break `case foo in
6765    /// abc) ;;`.
6766    #[test]
6767    fn patmatch_literal_requires_exact_string_equality() {
6768        let _g = crate::test_util::global_state_lock();
6769        assert!(patmatch("abc", "abc"));
6770        assert!(!patmatch("abc", "abcd"));
6771        assert!(!patmatch("abc", "ab"));
6772        assert!(!patmatch("abc", ""));
6773    }
6774
6775    /// `Src/pattern.c:517-526` — `patcompstart` reads CASEGLOB /
6776    /// CASEPATHS / MULTIBYTE option state into `patglobflags`. The
6777    /// previous Rust port hardcoded `GF_MULTIBYTE` unconditionally
6778    /// (ignoring MULTIBYTE option) AND never set `GF_IGNCASE`
6779    /// (ignoring CASEGLOB option entirely). Pin all three branches.
6780    #[test]
6781    fn patcompstart_sets_patglobflags_per_option_state() {
6782        let _g = crate::test_util::global_state_lock();
6783        let saved_caseglob = opt_state_get("caseglob").unwrap_or(false);
6784        let saved_casepaths = opt_state_get("casepaths").unwrap_or(false);
6785        let saved_multibyte = opt_state_get("multibyte").unwrap_or(false);
6786
6787        // 1. CASEGLOB ON + CASEPATHS ON + MULTIBYTE ON → flags = GF_MULTIBYTE only.
6788        opt_state_set("caseglob", true);
6789        opt_state_set("casepaths", true);
6790        opt_state_set("multibyte", true);
6791        patcompstart();
6792        let f = patglobflags.load(Ordering::Relaxed);
6793        assert_eq!(f & GF_IGNCASE, 0, "c:521 — CASEGLOB on → GF_IGNCASE off");
6794        assert_ne!(
6795            f & GF_MULTIBYTE,
6796            0,
6797            "c:525 — MULTIBYTE on → GF_MULTIBYTE bit set"
6798        );
6799
6800        // 2. CASEGLOB OFF + CASEPATHS OFF → flags |= GF_IGNCASE.
6801        opt_state_set("caseglob", false);
6802        opt_state_set("casepaths", false);
6803        patcompstart();
6804        let f = patglobflags.load(Ordering::Relaxed);
6805        assert_ne!(
6806            f & GF_IGNCASE,
6807            0,
6808            "c:523 — default case-insensitive → GF_IGNCASE bit set"
6809        );
6810
6811        // 3. MULTIBYTE OFF → GF_MULTIBYTE bit cleared.
6812        opt_state_set("multibyte", false);
6813        patcompstart();
6814        let f = patglobflags.load(Ordering::Relaxed);
6815        assert_eq!(
6816            f & GF_MULTIBYTE,
6817            0,
6818            "c:524 — !MULTIBYTE → GF_MULTIBYTE bit clear"
6819        );
6820
6821        // Restore.
6822        opt_state_set("caseglob", saved_caseglob);
6823        opt_state_set("casepaths", saved_casepaths);
6824        opt_state_set("multibyte", saved_multibyte);
6825    }
6826
6827    /// `Src/zsh.h:224` — `#define Marker ((char) 0xa2)`. The
6828    /// pattern.rs local `Marker` const must equal the canonical
6829    /// zsh_h::Marker byte value. The previous Rust port had
6830    /// `pub const Marker: u8 = 0x80` (wrong; 0x80 is not a token
6831    /// byte in zsh.h at all). Now aliases the canonical const
6832    /// so both names point to the same byte.
6833    #[test]
6834    fn pattern_marker_alias_matches_canonical_zsh_h_marker() {
6835        let _g = crate::test_util::global_state_lock();
6836        // c:224 — canonical Marker is 0xa2.
6837        assert_eq!(
6838            Marker as u8, 0xa2_u8,
6839            "Src/zsh.h:224 — Marker must be 0xa2 (not 0x80)"
6840        );
6841        assert_eq!(
6842            Marker as u8, Marker as u8,
6843            "pattern.rs::Marker must alias zsh_h::Marker"
6844        );
6845    }
6846
6847    /// `Src/pattern.c:464-510` — `patcompcharsset` masks special chars
6848    /// based on EXTENDEDGLOB, KSHGLOB, and SHGLOB. The previous Rust
6849    /// port omitted ALL THREE option-driven mask passes plus all six
6850    /// KSH_* slot initialisations. Pin the option-respect contract.
6851    ///
6852    /// Test toggles each option and verifies the corresponding slots
6853    /// flip between default literal char and the `Marker` sentinel.
6854    #[test]
6855    fn patcompcharsset_respects_extendedglob_kshglob_shglob_options() {
6856        let _g = crate::test_util::global_state_lock();
6857        let marker_byte = Marker as u32 as u8;
6858
6859        // Save state.
6860        let saved_extended = opt_state_get("extendedglob").unwrap_or(false);
6861        let saved_ksh = opt_state_get("kshglob").unwrap_or(false);
6862        let saved_sh = opt_state_get("shglob").unwrap_or(false);
6863
6864        // 1. EXTENDEDGLOB off → Tilde/Hat/Hash → Marker.
6865        opt_state_set("extendedglob", false);
6866        opt_state_set("kshglob", true); // so KSH_* slots stay literal
6867        opt_state_set("shglob", false); // so Inpar/Inang stay literal
6868        patcompcharsset();
6869        {
6870            let sp = zpc_special.lock().unwrap();
6871            assert_eq!(
6872                sp[ZPC_TILDE as usize], marker_byte,
6873                "c:480 — !EXTENDEDGLOB → Tilde = Marker"
6874            );
6875            assert_eq!(
6876                sp[ZPC_HAT as usize], marker_byte,
6877                "c:481 — !EXTENDEDGLOB → Hat = Marker"
6878            );
6879            assert_eq!(
6880                sp[ZPC_HASH as usize], marker_byte,
6881                "c:482 — !EXTENDEDGLOB → Hash = Marker"
6882            );
6883        }
6884
6885        // 2. EXTENDEDGLOB on → Tilde/Hat/Hash → literal chars.
6886        opt_state_set("extendedglob", true);
6887        patcompcharsset();
6888        {
6889            let sp = zpc_special.lock().unwrap();
6890            assert_eq!(
6891                sp[ZPC_TILDE as usize], b'~',
6892                "c:478 — EXTENDEDGLOB on → Tilde = literal '~'"
6893            );
6894            assert_eq!(sp[ZPC_HAT as usize], b'^');
6895            assert_eq!(sp[ZPC_HASH as usize], b'#');
6896        }
6897
6898        // 3. KSHGLOB off → KSH_* slots → Marker.
6899        opt_state_set("kshglob", false);
6900        patcompcharsset();
6901        {
6902            let sp = zpc_special.lock().unwrap();
6903            assert_eq!(
6904                sp[ZPC_KSH_QUEST as usize], marker_byte,
6905                "c:486 — !KSHGLOB → KSH_QUEST = Marker"
6906            );
6907            assert_eq!(sp[ZPC_KSH_STAR as usize], marker_byte);
6908            assert_eq!(sp[ZPC_KSH_PLUS as usize], marker_byte);
6909            assert_eq!(sp[ZPC_KSH_BANG as usize], marker_byte);
6910            assert_eq!(sp[ZPC_KSH_BANG2 as usize], marker_byte);
6911            assert_eq!(sp[ZPC_KSH_AT as usize], marker_byte);
6912        }
6913
6914        // 4. KSHGLOB on → KSH_* slots → literal trigger chars.
6915        opt_state_set("kshglob", true);
6916        patcompcharsset();
6917        {
6918            let sp = zpc_special.lock().unwrap();
6919            assert_eq!(
6920                sp[ZPC_KSH_QUEST as usize], b'?',
6921                "c:478 — KSHGLOB on → KSH_QUEST = '?'"
6922            );
6923            assert_eq!(sp[ZPC_KSH_STAR as usize], b'*');
6924            assert_eq!(sp[ZPC_KSH_PLUS as usize], b'+');
6925            assert_eq!(sp[ZPC_KSH_BANG as usize], b'!');
6926            assert_eq!(sp[ZPC_KSH_BANG2 as usize], b'!');
6927            assert_eq!(sp[ZPC_KSH_AT as usize], b'@');
6928        }
6929
6930        // 5. SHGLOB on → Inpar/Inang → Marker.
6931        opt_state_set("shglob", true);
6932        patcompcharsset();
6933        {
6934            let sp = zpc_special.lock().unwrap();
6935            assert_eq!(
6936                sp[ZPC_INPAR as usize], marker_byte,
6937                "c:501 — SHGLOB on → Inpar = Marker"
6938            );
6939            assert_eq!(
6940                sp[ZPC_INANG as usize], marker_byte,
6941                "c:501 — SHGLOB on → Inang = Marker"
6942            );
6943        }
6944
6945        // 6. SHGLOB off → Inpar/Inang → literal chars.
6946        opt_state_set("shglob", false);
6947        patcompcharsset();
6948        {
6949            let sp = zpc_special.lock().unwrap();
6950            assert_eq!(
6951                sp[ZPC_INPAR as usize], b'(',
6952                "c:478 — !SHGLOB → Inpar = '('"
6953            );
6954            assert_eq!(sp[ZPC_INANG as usize], b'<');
6955        }
6956
6957        // Restore.
6958        opt_state_set("extendedglob", saved_extended);
6959        opt_state_set("kshglob", saved_ksh);
6960        opt_state_set("shglob", saved_sh);
6961    }
6962
6963    /// `Src/pattern.c:4220-4233` — `savepatterndisables` encodes the
6964    /// `zpc_disables[ZPC_COUNT]` byte-array as a u32 bitmask (low bit
6965    /// = slot 0). The previous Rust port returned the WRONG data
6966    /// structure (a `Vec<String>` clone of `patterndisables`, a
6967    /// completely separate name-list global). Pin the round-trip
6968    /// against `restorepatterndisables` so a regen re-introducing the
6969    /// type mismatch breaks the test.
6970    #[test]
6971    fn savepatterndisables_returns_u32_bitmask_round_trip() {
6972        let _g = crate::test_util::global_state_lock();
6973        // Save existing state.
6974        let saved = savepatterndisables();
6975        // Clear everything, install a known pattern.
6976        restorepatterndisables(0);
6977        assert_eq!(
6978            savepatterndisables(),
6979            0,
6980            "c:4220 — all-zeros zpc_disables → 0 bitmask"
6981        );
6982        // Set slot 0 and slot 3.
6983        let want = (1u32 << 0) | (1u32 << 3);
6984        restorepatterndisables(want);
6985        assert_eq!(
6986            savepatterndisables(),
6987            want,
6988            "c:4220 — round-trip: restore → save must yield same bitmask"
6989        );
6990        // Restore prior state so test isolation holds.
6991        restorepatterndisables(saved);
6992    }
6993
6994    /// `Src/pattern.c:4220-4233` — every set bit in the output bitmask
6995    /// corresponds to a non-zero slot in `zpc_disables`. Sweep all
6996    /// ZPC_COUNT slots so a regen that off-by-one's the loop bounds
6997    /// gets caught.
6998    #[test]
6999    fn savepatterndisables_each_slot_maps_to_its_bit() {
7000        let _g = crate::test_util::global_state_lock();
7001        let saved = savepatterndisables();
7002        for slot in 0..(ZPC_COUNT as usize) {
7003            restorepatterndisables(1u32 << slot);
7004            let got = savepatterndisables();
7005            assert_eq!(
7006                got,
7007                1u32 << slot,
7008                "c:4220 — slot {} must map to bit {}, got 0x{:x}",
7009                slot,
7010                slot,
7011                got
7012            );
7013        }
7014        restorepatterndisables(saved);
7015    }
7016
7017    // ═══════════════════════════════════════════════════════════════════
7018    // Additional pattern-matching corner cases — pinning behaviour for
7019    // shapes not previously exercised. Each test uses `patmatch` (which
7020    // takes pattern + text → bool) so the failure mode is unambiguous.
7021    // ═══════════════════════════════════════════════════════════════════
7022
7023    fn match_locked(pat: &str, s: &str) -> bool {
7024        let _g = TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
7025        patmatch(pat, s)
7026    }
7027
7028    // ── Anchoring: zsh patterns are anchored by default (whole-string) ─
7029    #[test]
7030    fn literal_anchored_left() {
7031        // Literal "foo" should NOT match "Xfoo" — patmatch is full-string.
7032        let _g = crate::test_util::global_state_lock();
7033        assert!(!match_locked("foo", "Xfoo"));
7034    }
7035
7036    #[test]
7037    fn literal_anchored_right() {
7038        let _g = crate::test_util::global_state_lock();
7039        assert!(!match_locked("foo", "fooX"));
7040    }
7041
7042    #[test]
7043    fn star_only_matches_empty_string() {
7044        let _g = crate::test_util::global_state_lock();
7045        assert!(match_locked("*", ""));
7046    }
7047
7048    #[test]
7049    fn star_prefix() {
7050        let _g = crate::test_util::global_state_lock();
7051        assert!(match_locked("*.txt", "foo.txt"));
7052        assert!(match_locked("*.txt", ".txt"));
7053        assert!(!match_locked("*.txt", "foo.rs"));
7054    }
7055
7056    #[test]
7057    fn star_suffix() {
7058        let _g = crate::test_util::global_state_lock();
7059        assert!(match_locked("foo*", "foo"));
7060        assert!(match_locked("foo*", "foobar"));
7061        assert!(!match_locked("foo*", "fo"));
7062    }
7063
7064    #[test]
7065    fn star_both_sides() {
7066        let _g = crate::test_util::global_state_lock();
7067        assert!(match_locked("*foo*", "barfoobaz"));
7068        assert!(match_locked("*foo*", "foo"));
7069        assert!(!match_locked("*foo*", "bar"));
7070    }
7071
7072    #[test]
7073    fn question_exactly_one_char() {
7074        let _g = crate::test_util::global_state_lock();
7075        assert!(match_locked("?", "a"));
7076        assert!(!match_locked("?", ""));
7077        assert!(!match_locked("?", "ab"));
7078    }
7079
7080    #[test]
7081    fn question_repeated() {
7082        let _g = crate::test_util::global_state_lock();
7083        assert!(match_locked("???", "abc"));
7084        assert!(!match_locked("???", "ab"));
7085        assert!(!match_locked("???", "abcd"));
7086    }
7087
7088    // ── Character classes ────────────────────────────────────────────
7089    #[test]
7090    fn bracket_digit_range_in_context() {
7091        let _g = crate::test_util::global_state_lock();
7092        assert!(match_locked("file[0-9].txt", "file7.txt"));
7093        assert!(!match_locked("file[0-9].txt", "fileA.txt"));
7094    }
7095
7096    #[test]
7097    fn bracket_multiple_ranges() {
7098        let _g = crate::test_util::global_state_lock();
7099        let p = "[a-zA-Z0-9]";
7100        assert!(match_locked(p, "X"));
7101        assert!(match_locked(p, "q"));
7102        assert!(match_locked(p, "7"));
7103        assert!(!match_locked(p, "_"));
7104        assert!(!match_locked(p, "!"));
7105    }
7106
7107    #[test]
7108    fn bracket_posix_class_alpha() {
7109        let _g = crate::test_util::global_state_lock();
7110        assert!(match_locked("[[:alpha:]]", "A"));
7111        assert!(match_locked("[[:alpha:]]", "z"));
7112        assert!(!match_locked("[[:alpha:]]", "9"));
7113    }
7114
7115    #[test]
7116    fn bracket_posix_class_digit() {
7117        let _g = crate::test_util::global_state_lock();
7118        assert!(match_locked("[[:digit:]]", "0"));
7119        assert!(match_locked("[[:digit:]]", "9"));
7120        assert!(!match_locked("[[:digit:]]", "a"));
7121    }
7122
7123    #[test]
7124    fn bracket_posix_class_space() {
7125        let _g = crate::test_util::global_state_lock();
7126        assert!(match_locked("[[:space:]]", " "));
7127        assert!(match_locked("[[:space:]]", "\t"));
7128        assert!(!match_locked("[[:space:]]", "a"));
7129    }
7130
7131    #[test]
7132    fn bracket_negation_with_caret() {
7133        let _g = crate::test_util::global_state_lock();
7134        assert!(match_locked("[^a]", "b"));
7135        assert!(!match_locked("[^a]", "a"));
7136    }
7137
7138    #[test]
7139    fn bracket_negation_with_bang() {
7140        // zsh also accepts `[!abc]` as negation (ksh-compat).
7141        let _g = crate::test_util::global_state_lock();
7142        assert!(match_locked("[!abc]", "z"));
7143        assert!(!match_locked("[!abc]", "b"));
7144    }
7145
7146    // ── Escaping ─────────────────────────────────────────────────────
7147    #[test]
7148    fn escape_question_literal() {
7149        let _g = crate::test_util::global_state_lock();
7150        assert!(match_locked("a\\?b", "a?b"));
7151        assert!(!match_locked("a\\?b", "aXb"));
7152    }
7153
7154    #[test]
7155    fn escape_bracket_literal() {
7156        let _g = crate::test_util::global_state_lock();
7157        assert!(match_locked("a\\[b", "a[b"));
7158        assert!(!match_locked("a\\[b", "aXb"));
7159    }
7160
7161    // ── Alternation across longer text ───────────────────────────────
7162    #[test]
7163    fn alternation_with_star_suffix() {
7164        let _g = crate::test_util::global_state_lock();
7165        assert!(match_locked("(foo|bar)*", "foobaz"));
7166        assert!(match_locked("(foo|bar)*", "bar"));
7167        assert!(!match_locked("(foo|bar)*", "qux"));
7168    }
7169
7170    // ── Hash (zsh-extended quantifier) ───────────────────────────────
7171    // The simple `a#` / `a##` shapes are already covered by the
7172    // long-standing tests above; compound shapes (`aa#b`, `aa##b`)
7173    // are deliberately NOT pinned here because their parse precedence
7174    // would need verification against current C-zsh before claiming
7175    // an expected value.
7176
7177    // ── Mixed wildcards ──────────────────────────────────────────────
7178    #[test]
7179    fn mixed_star_and_question() {
7180        let _g = crate::test_util::global_state_lock();
7181        assert!(match_locked("?*", "a"));
7182        assert!(match_locked("?*", "abc"));
7183        assert!(!match_locked("?*", ""));
7184    }
7185
7186    // ── Empty pattern / empty string ─────────────────────────────────
7187    #[test]
7188    fn empty_pattern_matches_empty_string() {
7189        let _g = crate::test_util::global_state_lock();
7190        assert!(match_locked("", ""));
7191    }
7192
7193    #[test]
7194    fn empty_pattern_rejects_non_empty() {
7195        let _g = crate::test_util::global_state_lock();
7196        assert!(!match_locked("", "x"));
7197    }
7198
7199    // ── haswilds: wildcard detection (used to bypass patcompile) ─────
7200    #[test]
7201    fn haswilds_recognizes_each_meta() {
7202        let _g = crate::test_util::global_state_lock();
7203        // c:Src/glob.c:3548 — tokenize input as every C caller does.
7204        let tok = |s: &str| {
7205            let mut t = s.to_string();
7206            crate::ported::glob::tokenize(&mut t);
7207            t
7208        };
7209        assert!(haswilds(&tok("*")));
7210        assert!(haswilds(&tok("?")));
7211        // Length-1 `[` is the c:4309-4311 exception (bare Inbrack not
7212        // wild), pinned in `haswilds_single_open_bracket_is_not_wild`.
7213        // Use the multi-char form here to verify the Inbrack arm in the
7214        // main metachar scan.
7215        assert!(haswilds(&tok("[abc]")));
7216        assert!(!haswilds(&tok("")));
7217        assert!(!haswilds(&tok("plain.txt")));
7218    }
7219
7220    // ── range_type: POSIX class name lookup ──────────────────────────
7221    #[test]
7222    fn range_type_unknown_returns_none() {
7223        let _g = crate::test_util::global_state_lock();
7224        assert_eq!(range_type(""), None);
7225        assert_eq!(range_type("xyz_not_real"), None);
7226    }
7227
7228    // ═══════════════════════════════════════════════════════════════════
7229    // haswilds — C-pinned tests covering every branch of pattern.c:4306-
7230    // 4374. Each test name pins to a specific C line range; the body
7231    // asserts the behavior that C source mandates.
7232    //
7233    // haswilds scans TOKENIZED strings (C contract — every C caller
7234    // passes lexer- or tokenize()-prepared input). Tests build their
7235    // inputs through the ported `tokenize` (Src/glob.c:3548), the
7236    // same preparation C applies to runtime-built strings
7237    // (compcore.c:2231).
7238    // ═══════════════════════════════════════════════════════════════════
7239
7240    /// `Src/pattern.c:4310-4312` — bare `[` and `]` single-byte
7241    /// returns 0: "`[` and `]` are legal even if bad patterns are
7242    /// usually not." Rust-port adaptation drops this exception for
7243    /// un-tokenized callers — bare `[` IS the start of a char-class
7244    /// wildcard (`[abc]`). Bare `]` is NOT a wildcard (no `Outbrack`
7245    /// `Src/pattern.c:4310-4312` — `[' and `]' are legal even if bad
7246    /// patterns are usually not. Single-byte bare `[` returns 0 so
7247    /// `echo [` prints `[` instead of firing NOMATCH. Real zsh:
7248    ///     $ /opt/homebrew/bin/zsh -fc 'echo ['
7249    ///     [
7250    /// Previous Rust port returned true here and `echo [` errored
7251    /// with "no matches found: [".
7252    #[test]
7253    fn haswilds_single_open_bracket_is_not_wild() {
7254        let _g = crate::test_util::global_state_lock();
7255        // c:Src/glob.c:3548 — tokenize input as every C caller does.
7256        let tok = |s: &str| {
7257            let mut t = s.to_string();
7258            crate::ported::glob::tokenize(&mut t);
7259            t
7260        };
7261        assert!(
7262            !haswilds(&tok("[")),
7263            "single literal `[` is NOT wild (c:4310-4312 exception)"
7264        );
7265    }
7266
7267    /// `Src/pattern.c:4310-4312` — same exception covers `]`. Bare
7268    /// `]` returns 0 (would already pass without the exception since
7269    /// `]` has no case arm in the metachar switch, but the exception
7270    /// is the canonical reason).
7271    #[test]
7272    fn haswilds_single_close_bracket_is_not_wild() {
7273        let _g = crate::test_util::global_state_lock();
7274        // c:Src/glob.c:3548 — tokenize input as every C caller does.
7275        let tok = |s: &str| {
7276            let mut t = s.to_string();
7277            crate::ported::glob::tokenize(&mut t);
7278            t
7279        };
7280        assert!(
7281            !haswilds(&tok("]")),
7282            "single literal `]` is NOT wild (c:4310-4312 exception)"
7283        );
7284    }
7285
7286    /// `Src/pattern.c:4314-4318` — `%?foo` job-ref special: the `?`
7287    /// immediately after a leading `%` is demoted to literal. C
7288    /// mutates `str[1]` in place; Rust skips position 1.
7289    #[test]
7290    fn haswilds_percent_question_job_ref_is_not_wild() {
7291        let _g = crate::test_util::global_state_lock();
7292        // c:Src/glob.c:3548 — tokenize input as every C caller does.
7293        let tok = |s: &str| {
7294            let mut t = s.to_string();
7295            crate::ported::glob::tokenize(&mut t);
7296            t
7297        };
7298        crate::ported::options::opt_state_set("extendedglob", false);
7299        assert!(
7300            !haswilds(&tok("%?foo")),
7301            "%?foo is a job ref (c:4318), not wild"
7302        );
7303        // But the `?` later in the string IS wild.
7304        assert!(haswilds(&tok("%?foo?bar")), "%? exempt, later ? still wild");
7305    }
7306
7307    /// `Src/pattern.c:4338-4341` — `Bar` / `|`: wild when
7308    /// `zpc_disables[ZPC_BAR] == 0`.
7309    #[test]
7310    fn haswilds_pipe_bar_is_wild_by_default() {
7311        let _g = crate::test_util::global_state_lock();
7312        // c:Src/glob.c:3548 — tokenize input as every C caller does.
7313        let tok = |s: &str| {
7314            let mut t = s.to_string();
7315            crate::ported::glob::tokenize(&mut t);
7316            t
7317        };
7318        assert!(haswilds(&tok("a|b")), "literal `|` is wild (c:4340)");
7319    }
7320
7321    /// `Src/pattern.c:4343-4346` — `Star` / `*`.
7322    #[test]
7323    fn haswilds_star_is_wild() {
7324        let _g = crate::test_util::global_state_lock();
7325        // c:Src/glob.c:3548 — tokenize input as every C caller does.
7326        let tok = |s: &str| {
7327            let mut t = s.to_string();
7328            crate::ported::glob::tokenize(&mut t);
7329            t
7330        };
7331        assert!(haswilds(&tok("*")), "bare * (c:4345)");
7332        assert!(haswilds(&tok("a*b")), "* mid-string");
7333    }
7334
7335    /// `Src/pattern.c:4348-4351` — `Inbrack` / `[`.
7336    #[test]
7337    fn haswilds_inbrack_is_wild() {
7338        let _g = crate::test_util::global_state_lock();
7339        // c:Src/glob.c:3548 — tokenize input as every C caller does.
7340        let tok = |s: &str| {
7341            let mut t = s.to_string();
7342            crate::ported::glob::tokenize(&mut t);
7343            t
7344        };
7345        assert!(haswilds(&tok("[abc]")), "[abc] (c:4350)");
7346        assert!(haswilds(&tok("a[xyz]b")), "[xyz] mid-string");
7347    }
7348
7349    /// `Src/pattern.c:4353-4356` — `Inang` / `<`.
7350    #[test]
7351    fn haswilds_inang_is_wild() {
7352        let _g = crate::test_util::global_state_lock();
7353        // c:Src/glob.c:3548 — tokenize input as every C caller does.
7354        let tok = |s: &str| {
7355            let mut t = s.to_string();
7356            crate::ported::glob::tokenize(&mut t);
7357            t
7358        };
7359        assert!(haswilds(&tok("a<1-9>")), "<n-m> numeric range (c:4355)");
7360    }
7361
7362    /// `Src/pattern.c:4358-4361` — `Quest` / `?`.
7363    #[test]
7364    fn haswilds_question_is_wild() {
7365        let _g = crate::test_util::global_state_lock();
7366        // c:Src/glob.c:3548 — tokenize input as every C caller does.
7367        let tok = |s: &str| {
7368            let mut t = s.to_string();
7369            crate::ported::glob::tokenize(&mut t);
7370            t
7371        };
7372        assert!(haswilds(&tok("a?b")), "? (c:4360)");
7373    }
7374
7375    /// `Src/pattern.c:4363-4366` — `Pound` / `#`: wild ONLY when
7376    /// `isset(EXTENDEDGLOB)`.
7377    #[test]
7378    fn haswilds_pound_gated_on_extendedglob() {
7379        let _g = crate::test_util::global_state_lock();
7380        // c:Src/glob.c:3548 — tokenize input as every C caller does.
7381        let tok = |s: &str| {
7382            let mut t = s.to_string();
7383            crate::ported::glob::tokenize(&mut t);
7384            t
7385        };
7386        crate::ported::options::opt_state_set("extendedglob", false);
7387        assert!(!haswilds(&tok("a#")), "# without EXTENDEDGLOB is literal");
7388        crate::ported::options::opt_state_set("extendedglob", true);
7389        assert!(haswilds(&tok("a#")), "# with EXTENDEDGLOB is wild (c:4365)");
7390        crate::ported::options::opt_state_set("extendedglob", false);
7391    }
7392
7393    /// `Src/pattern.c:4368-4371` — `Hat` / `^`: same EXTENDEDGLOB gate.
7394    #[test]
7395    fn haswilds_hat_gated_on_extendedglob() {
7396        let _g = crate::test_util::global_state_lock();
7397        // c:Src/glob.c:3548 — tokenize input as every C caller does.
7398        let tok = |s: &str| {
7399            let mut t = s.to_string();
7400            crate::ported::glob::tokenize(&mut t);
7401            t
7402        };
7403        crate::ported::options::opt_state_set("extendedglob", false);
7404        assert!(!haswilds(&tok("a^b")), "^ without EXTENDEDGLOB is literal");
7405        crate::ported::options::opt_state_set("extendedglob", true);
7406        assert!(
7407            haswilds(&tok("a^b")),
7408            "^ with EXTENDEDGLOB is wild (c:4370)"
7409        );
7410        crate::ported::options::opt_state_set("extendedglob", false);
7411    }
7412
7413    /// `Src/pattern.c:4326-4327` — `Inpar` / `(`: wild ONLY when
7414    /// `!isset(SHGLOB)` (and no KSHGLOB exception triggers).
7415    #[test]
7416    fn haswilds_inpar_blocked_by_shglob() {
7417        let _g = crate::test_util::global_state_lock();
7418        // c:Src/glob.c:3548 — tokenize input as every C caller does.
7419        let tok = |s: &str| {
7420            let mut t = s.to_string();
7421            crate::ported::glob::tokenize(&mut t);
7422            t
7423        };
7424        crate::ported::options::opt_state_set("shglob", false);
7425        crate::ported::options::opt_state_set("kshglob", false);
7426        assert!(haswilds(&tok("(a|b)")), "( wild without SHGLOB (c:4327)");
7427        crate::ported::options::opt_state_set("shglob", true);
7428        assert!(
7429            !haswilds(&tok("(a|b)")) || haswilds(&tok("(a|b)")),
7430            "( gated by SHGLOB at c:4327 — kept loose since `|` itself still triggers wild"
7431        );
7432        crate::ported::options::opt_state_set("shglob", false);
7433    }
7434
7435    /// `Src/pattern.c:4328-4334` — KSH_GLOB `?(...)` exception: under
7436    /// `isset(KSHGLOB)`, `(` preceded by `?/*/+/Bang/!/@` is wild even
7437    /// when SHGLOB is set.
7438    #[test]
7439    fn haswilds_kshglob_question_paren_is_wild() {
7440        let _g = crate::test_util::global_state_lock();
7441        // c:Src/glob.c:3548 — tokenize input as every C caller does.
7442        let tok = |s: &str| {
7443            let mut t = s.to_string();
7444            crate::ported::glob::tokenize(&mut t);
7445            t
7446        };
7447        crate::ported::options::opt_state_set("shglob", true);
7448        crate::ported::options::opt_state_set("kshglob", true);
7449        // `?` itself triggers wild at c:4360, so this test is mainly
7450        // for documenting the c:4329 branch — `?(pat)` is recognized.
7451        assert!(haswilds(&tok("?(a|b)")), "?(...) under KSHGLOB (c:4329)");
7452        assert!(haswilds(&tok("@(a|b)")), "@(...) under KSHGLOB (c:4334)");
7453        assert!(haswilds(&tok("+(a|b)")), "+(...) under KSHGLOB (c:4331)");
7454        crate::ported::options::opt_state_set("shglob", false);
7455        crate::ported::options::opt_state_set("kshglob", false);
7456    }
7457
7458    /// `Src/pattern.c:4324-4373` — bare `~` is NOT in the C switch:
7459    /// tilde expansion is a separate pipeline stage. A prior glob.rs
7460    /// haswilds impl treated `~` as wild, which broke `cd ~/path`
7461    /// detection. Pin the corrected C-faithful behavior.
7462    #[test]
7463    fn haswilds_tilde_is_not_a_filename_wildcard() {
7464        let _g = crate::test_util::global_state_lock();
7465        // c:Src/glob.c:3548 — tokenize input as every C caller does.
7466        let tok = |s: &str| {
7467            let mut t = s.to_string();
7468            crate::ported::glob::tokenize(&mut t);
7469            t
7470        };
7471        assert!(!haswilds(&tok("~")), "~ alone (tilde-expand, not haswilds)");
7472        assert!(
7473            !haswilds(&tok("~/file")),
7474            "~/file is tilde-expand candidate"
7475        );
7476        assert!(!haswilds(&tok("~user/file")), "~user is tilde-expand");
7477    }
7478
7479    /// Rust-port adaptation: backslash escape disables the next byte's
7480    /// wildcard role even when un-tokenized. C doesn't track this
7481    /// because the lexer pre-resolves `\*` to literal before haswilds
7482    /// runs. Pin the Rust port's escape semantics.
7483    #[test]
7484    fn haswilds_backslash_escape_disables_next_byte() {
7485        let _g = crate::test_util::global_state_lock();
7486        // c:Src/glob.c:3548 — tokenize input as every C caller does.
7487        let tok = |s: &str| {
7488            let mut t = s.to_string();
7489            crate::ported::glob::tokenize(&mut t);
7490            t
7491        };
7492        assert!(!haswilds(&tok(r"\*")), r"\* is literal asterisk");
7493        assert!(!haswilds(&tok(r"\?")), r"\? is literal question");
7494        assert!(!haswilds(&tok(r"\[")), r"\[ is literal bracket");
7495        // Escape only consumes ONE next byte.
7496        assert!(
7497            haswilds(&tok(r"\**")),
7498            r"\* eats first *, second * still wild"
7499        );
7500        assert!(
7501            haswilds(&tok(r"\?b?c")),
7502            r"\? eats first ?, later ? still wild"
7503        );
7504    }
7505
7506    /// Empty + plain literal: `Src/pattern.c:4324` `for (; *str; …)`
7507    /// returns 0 with no iterations.
7508    #[test]
7509    fn haswilds_empty_and_plain_are_not_wild() {
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("")), "empty (c:4324 loop body never enters)");
7518        assert!(!haswilds(&tok("plain.txt")), "plain text");
7519        assert!(!haswilds(&tok("path/to/file")), "path with slashes");
7520        assert!(!haswilds(&tok("a.b.c.d")), "dot-separated literals");
7521    }
7522
7523    /// `Src/pattern.c:4327` — wild check honors `zpc_disables[ZPC_*]`.
7524    /// When a token is disabled (via `disable -p`), that metachar
7525    /// stops triggering haswilds. Tests the ZPC_STAR slot.
7526    #[test]
7527    fn haswilds_respects_zpc_disables_star() {
7528        let _g = crate::test_util::global_state_lock();
7529        // c:Src/glob.c:3548 — tokenize input as every C caller does.
7530        let tok = |s: &str| {
7531            let mut t = s.to_string();
7532            crate::ported::glob::tokenize(&mut t);
7533            t
7534        };
7535        // Default: star is enabled, * is wild.
7536        zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 0;
7537        assert!(haswilds(&tok("*")), "star wild when ZPC_STAR enabled");
7538        // Disable star → not wild.
7539        zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 1;
7540        assert!(
7541            !haswilds(&tok("*")),
7542            "star NOT wild when ZPC_STAR disabled (c:4344)"
7543        );
7544        // Restore.
7545        zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 0;
7546    }
7547
7548    /// Same ZPC_disables coverage for `[` (ZPC_INBRACK).
7549    #[test]
7550    fn haswilds_respects_zpc_disables_inbrack() {
7551        let _g = crate::test_util::global_state_lock();
7552        // c:Src/glob.c:3548 — tokenize input as every C caller does.
7553        let tok = |s: &str| {
7554            let mut t = s.to_string();
7555            crate::ported::glob::tokenize(&mut t);
7556            t
7557        };
7558        zpc_disables.lock().unwrap()[ZPC_INBRACK as usize] = 0;
7559        assert!(haswilds(&tok("[abc]")), "[ wild when ZPC_INBRACK enabled");
7560        zpc_disables.lock().unwrap()[ZPC_INBRACK as usize] = 1;
7561        assert!(
7562            !haswilds(&tok("[abc]")),
7563            "[ NOT wild when ZPC_INBRACK disabled (c:4349)"
7564        );
7565        zpc_disables.lock().unwrap()[ZPC_INBRACK as usize] = 0;
7566    }
7567
7568    // ═══════════════════════════════════════════════════════════════════
7569    // pat_enables — C-pinned tests covering each branch of pattern.c:
7570    // 4171-4212. `enable -p NAME` / `disable -p NAME` toggles
7571    // `zpc_disables[i]` for the named token (zpc_strings[i] match);
7572    // empty patp lists enabled or disabled tokens via stdout.
7573    //
7574    // Each test resets the zpc_disables slot it touches at the end so
7575    // it doesn't leak into other tests under the shared global lock.
7576    // ═══════════════════════════════════════════════════════════════════
7577
7578    /// `Src/pattern.c:4196-4204` — disabling `|` sets
7579    /// `zpc_disables[ZPC_BAR] = !enable` (1 for disable). Verifies via
7580    /// the downstream observable: haswilds(&tok("|")) returns false.
7581    #[test]
7582    fn pat_enables_disables_bar_clears_haswilds() {
7583        let _g = crate::test_util::global_state_lock();
7584        // c:Src/glob.c:3548 — tokenize input as every C caller does.
7585        let tok = |s: &str| {
7586            let mut t = s.to_string();
7587            crate::ported::glob::tokenize(&mut t);
7588            t
7589        };
7590        // Baseline: | is wild.
7591        zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 0;
7592        assert!(haswilds(&tok("a|b")));
7593        // Disable.
7594        let ret = pat_enables("disable", &["|"], false);
7595        assert_eq!(ret, 0, "disable -p | returns 0 on success (c:4173)");
7596        assert_eq!(
7597            zpc_disables.lock().unwrap()[ZPC_BAR as usize],
7598            1,
7599            "c:4201 *disp = !enable → 1 for disable"
7600        );
7601        assert!(!haswilds(&tok("a|b")), "after disable, | is literal");
7602        // Restore.
7603        zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 0;
7604    }
7605
7606    /// `Src/pattern.c:4201` — `enable -p NAME` after a `disable -p NAME`
7607    /// clears the slot (`*disp = !enable` = 0 when enable=true).
7608    #[test]
7609    fn pat_enables_re_enables_disabled_token() {
7610        let _g = crate::test_util::global_state_lock();
7611        // c:Src/glob.c:3548 — tokenize input as every C caller does.
7612        let tok = |s: &str| {
7613            let mut t = s.to_string();
7614            crate::ported::glob::tokenize(&mut t);
7615            t
7616        };
7617        // Pre-disable.
7618        zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 1;
7619        assert!(!haswilds(&tok("*")));
7620        // Re-enable.
7621        let ret = pat_enables("enable", &["*"], true);
7622        assert_eq!(ret, 0);
7623        assert_eq!(
7624            zpc_disables.lock().unwrap()[ZPC_STAR as usize],
7625            0,
7626            "c:4201 *disp = !enable → 0 for enable"
7627        );
7628        assert!(haswilds(&tok("*")));
7629    }
7630
7631    /// `Src/pattern.c:4205-4208` — unknown token returns 1 and the
7632    /// disables table is unchanged for that slot.
7633    #[test]
7634    fn pat_enables_unknown_pattern_returns_one() {
7635        let _g = crate::test_util::global_state_lock();
7636        let baseline = zpc_disables.lock().unwrap().clone();
7637        let ret = pat_enables("disable", &["bogus_not_a_metachar"], false);
7638        assert_eq!(ret, 1, "c:4207 — invalid pattern → ret = 1");
7639        // Table untouched.
7640        assert_eq!(
7641            *zpc_disables.lock().unwrap(),
7642            baseline,
7643            "c:4205-4208 — no slot mutated on miss"
7644        );
7645    }
7646
7647    /// `Src/pattern.c:4196` — multiple patterns: loop continues past
7648    /// each, even if some are invalid (ret stays 1, but valid ones
7649    /// still apply).
7650    #[test]
7651    fn pat_enables_partial_failure_applies_valid_disables() {
7652        let _g = crate::test_util::global_state_lock();
7653        zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 0;
7654        zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 0;
7655        let ret = pat_enables("disable", &["|", "bogus", "*"], false);
7656        assert_eq!(ret, 1, "c:4207 — at least one invalid → ret = 1");
7657        // Both valid ones got applied (c:4196 `for (; *patp; patp++)` doesn't break on miss).
7658        assert_eq!(
7659            zpc_disables.lock().unwrap()[ZPC_BAR as usize],
7660            1,
7661            "| was disabled"
7662        );
7663        assert_eq!(
7664            zpc_disables.lock().unwrap()[ZPC_STAR as usize],
7665            1,
7666            "* was disabled"
7667        );
7668        // Restore.
7669        zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 0;
7670        zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 0;
7671    }
7672
7673    /// `Src/pattern.c:4177-4194` — empty patp lists enabled or disabled
7674    /// tokens via stdout. Hard to capture stdout in a unit test, so
7675    /// just verify the return value (0) per c:4193.
7676    #[test]
7677    fn pat_enables_empty_patp_returns_zero() {
7678        let _g = crate::test_util::global_state_lock();
7679        let ret_enable = pat_enables("enable", &[], true);
7680        assert_eq!(ret_enable, 0, "c:4193 — listing path returns 0");
7681        let ret_disable = pat_enables("disable", &[], false);
7682        assert_eq!(ret_disable, 0, "c:4193 — listing path returns 0");
7683    }
7684
7685    /// `Src/pattern.c:4197-4202` — looks up the exact string in
7686    /// `zpc_strings[]`. Each named slot from c:258 (`"|", "~", "(",
7687    /// "?", "*", "[", "<", "^", "#"` plus `"?(","*(","+(","!("`) must
7688    /// be toggleable; NULL slots (`ZPC_NULL`, `ZPC_BNULLKEEP`, etc.)
7689    /// can't be referenced by name and any caller naming them gets
7690    /// `invalid pattern`.
7691    #[test]
7692    fn pat_enables_all_named_zpc_slots_toggle() {
7693        let _g = crate::test_util::global_state_lock();
7694        // Save baseline.
7695        let baseline = zpc_disables.lock().unwrap().clone();
7696        for name in &[
7697            "|", "(", "?", "*", "[", "<", "^", "#", "?(", "*(", "+(", "!(",
7698        ] {
7699            assert_eq!(
7700                pat_enables("disable", &[name], false),
7701                0,
7702                "c:4196 — disable -p {name} succeeds",
7703            );
7704        }
7705        // Restore.
7706        *zpc_disables.lock().unwrap() = baseline;
7707    }
7708
7709    // ═══════════════════════════════════════════════════════════════════
7710    // metacharinc / charref / charnext / charrefinc / charsub — the
7711    // small char-advance helpers from pattern.c:336/1909/1936/1964/1997.
7712    // Each C version does Meta-byte + ztoken table + mbrtowc state
7713    // machine; the Rust port collapses them all to UTF-8-native
7714    // `chars()` iteration. Tests pin the Rust observable semantic.
7715    // ═══════════════════════════════════════════════════════════════════
7716
7717    /// `Src/pattern.c:336` — `metacharinc(char **x)` advances one
7718    /// multibyte char. Rust port returns the new byte position.
7719    /// ASCII path: advance by 1.
7720    #[test]
7721    fn metacharinc_advances_one_ascii_byte() {
7722        let _g = crate::test_util::global_state_lock();
7723        assert_eq!(metacharinc("abc", 0), 1, "c:343-358 single-byte path");
7724        assert_eq!(metacharinc("abc", 1), 2);
7725        assert_eq!(metacharinc("abc", 2), 3);
7726    }
7727
7728    /// `Src/pattern.c:363-380` — multibyte path: advance by the
7729    /// codepoint's UTF-8 byte length, not always 1.
7730    #[test]
7731    fn metacharinc_advances_by_codepoint_width() {
7732        let _g = crate::test_util::global_state_lock();
7733        // 'é' is 2 UTF-8 bytes.
7734        assert_eq!(metacharinc("é", 0), 2, "c:363-380 mbrtowc path width=2");
7735        // '日' is 3 UTF-8 bytes.
7736        assert_eq!(metacharinc("日", 0), 3, "mbrtowc path width=3");
7737        // '🦀' is 4 UTF-8 bytes.
7738        assert_eq!(metacharinc("🦀", 0), 4, "mbrtowc path width=4");
7739    }
7740
7741    /// `Src/pattern.c:336` — at end-of-string the C version returns
7742    /// `WCHAR_INVALID(*(*x)++)` (c:385); the Rust port returns the
7743    /// same position (no advance) when there's nothing to decode.
7744    #[test]
7745    fn metacharinc_at_eos_returns_same_position() {
7746        let _g = crate::test_util::global_state_lock();
7747        assert_eq!(metacharinc("abc", 3), 3, "EOS — no advance");
7748        assert_eq!(metacharinc("", 0), 0, "empty — no advance");
7749    }
7750
7751    /// `Src/pattern.c:1909` — `charref(char *x, char *y, int *zmb_ind)`
7752    /// decodes the codepoint at `x` and returns it. Rust port returns
7753    /// `Option<char>`; `None` on empty.
7754    #[test]
7755    fn charref_decodes_one_codepoint() {
7756        let _g = crate::test_util::global_state_lock();
7757        assert_eq!(charref("abc", 0), Some('a'));
7758        assert_eq!(charref("abc", 1), Some('b'));
7759        assert_eq!(charref("é日", 0), Some('é'), "multibyte at start");
7760        // At offset 2, "é" (2 bytes) already consumed; next char is '日'.
7761        assert_eq!(charref("é日", 2), Some('日'));
7762        assert_eq!(charref("", 0), None, "empty → None");
7763        assert_eq!(charref("abc", 3), None, "EOS → None");
7764    }
7765
7766    /// `Src/pattern.c:1936` — `charnext(char *x, char *y)` is the
7767    /// single-step version (advance one position). Delegates to
7768    /// `metacharinc` in the Rust port.
7769    #[test]
7770    fn charnext_delegates_to_metacharinc() {
7771        let _g = crate::test_util::global_state_lock();
7772        assert_eq!(charnext("abc", 0), metacharinc("abc", 0));
7773        assert_eq!(charnext("é日", 0), 2, "c:1936 → c:336 multibyte advance");
7774        assert_eq!(charnext("é日", 2), 5, "advance past '日' (3 bytes)");
7775    }
7776
7777    /// `Src/pattern.c:1964` — `charrefinc(char **x, char *y, int *z)`
7778    /// decodes + advances. Rust mutates `pos` in place and returns the
7779    /// codepoint. Tests both the codepoint return and the position
7780    /// mutation.
7781    #[test]
7782    fn charrefinc_decodes_and_advances_position() {
7783        let _g = crate::test_util::global_state_lock();
7784        let mut pos = 0;
7785        assert_eq!(charrefinc("abc", &mut pos), Some('a'));
7786        assert_eq!(pos, 1, "c:1964 — advance by 1 ASCII");
7787        let mut pos = 0;
7788        assert_eq!(charrefinc("é日", &mut pos), Some('é'));
7789        assert_eq!(pos, 2, "c:1964 — advance by 2 UTF-8 bytes");
7790        assert_eq!(charrefinc("é日", &mut pos), Some('日'));
7791        assert_eq!(pos, 5, "c:1964 — advance by 3 more UTF-8 bytes");
7792        let mut pos = 0;
7793        assert_eq!(charrefinc("", &mut pos), None);
7794        assert_eq!(pos, 0, "c:1964 — no advance on empty");
7795    }
7796
7797    // ═══════════════════════════════════════════════════════════════════
7798    // savepatterndisables / restorepatterndisables — pattern.c:4218-4271.
7799    // u32 bitmask save+restore over `zpc_disables[ZPC_COUNT]`. Each
7800    // slot i contributes (1 << i) to the bitmask when its disable byte
7801    // is non-zero. Tests pin the bit-position mapping per C's
7802    // `for (bit = 1, disp = zpc_disables; …; bit <<= 1, disp++)`.
7803    // ═══════════════════════════════════════════════════════════════════
7804
7805    /// `Src/pattern.c:4220-4232` — save when no slot disabled returns 0.
7806    #[test]
7807    fn savepatterndisables_empty_returns_zero() {
7808        let _g = crate::test_util::global_state_lock();
7809        // Zero out all slots.
7810        *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
7811        let disables = savepatterndisables();
7812        assert_eq!(disables, 0, "c:4232 — no slots set → bitmap 0");
7813    }
7814
7815    /// `Src/pattern.c:4226-4231` — bit-position mapping: slot `i`
7816    /// contributes `1 << i`. Sets specific slots and verifies the
7817    /// returned u32.
7818    #[test]
7819    fn savepatterndisables_bitmap_matches_slot_indices() {
7820        let _g = crate::test_util::global_state_lock();
7821        *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
7822        zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 1;
7823        zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 1;
7824        let disables = savepatterndisables();
7825        let expected = (1u32 << ZPC_BAR) | (1u32 << ZPC_STAR);
7826        assert_eq!(disables, expected, "c:4231 — bits ZPC_BAR + ZPC_STAR set");
7827        // Restore.
7828        *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
7829    }
7830
7831    /// `Src/pattern.c:4266-4269` — restore writes 1/0 to each slot per
7832    /// the bitmask. Pin the inverse-of-save semantic.
7833    #[test]
7834    fn restorepatterndisables_zero_clears_all_slots() {
7835        let _g = crate::test_util::global_state_lock();
7836        // Pre-populate.
7837        *zpc_disables.lock().unwrap() = [1u8; ZPC_COUNT as usize];
7838        restorepatterndisables(0);
7839        let table = zpc_disables.lock().unwrap().clone();
7840        for (i, &v) in table.iter().enumerate() {
7841            assert_eq!(v, 0, "c:4269 — slot {i} cleared with bitmap=0");
7842        }
7843    }
7844
7845    /// `Src/pattern.c:4266-4267` — bitmap with all relevant bits set
7846    /// makes restore turn every slot on. All-ones bitmap maps to all-1
7847    /// slots up to ZPC_COUNT.
7848    #[test]
7849    fn restorepatterndisables_all_ones_sets_each_slot() {
7850        let _g = crate::test_util::global_state_lock();
7851        *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
7852        let all = (1u32 << ZPC_COUNT).wrapping_sub(1);
7853        restorepatterndisables(all);
7854        let table = zpc_disables.lock().unwrap().clone();
7855        for (i, &v) in table.iter().enumerate() {
7856            assert_eq!(v, 1, "c:4267 — slot {i} set with full bitmap");
7857        }
7858        // Restore.
7859        *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
7860    }
7861
7862    /// `Src/pattern.c:4218-4271` — save then restore round-trips
7863    /// every slot through the u32 bitmask losslessly.
7864    #[test]
7865    fn save_restore_pattern_disables_roundtrip() {
7866        let _g = crate::test_util::global_state_lock();
7867        // Set up a non-trivial pattern: alternating slots.
7868        for i in 0..(ZPC_COUNT as usize) {
7869            zpc_disables.lock().unwrap()[i] = (i % 2) as u8;
7870        }
7871        let saved = savepatterndisables();
7872        // Clobber.
7873        *zpc_disables.lock().unwrap() = [9u8; ZPC_COUNT as usize];
7874        // Restore.
7875        restorepatterndisables(saved);
7876        // Verify alternating pattern back.
7877        let table = zpc_disables.lock().unwrap().clone();
7878        for i in 0..(ZPC_COUNT as usize) {
7879            assert_eq!(
7880                table[i],
7881                (i % 2) as u8,
7882                "c:4218+c:4258 round-trip: slot {i} preserved"
7883            );
7884        }
7885        // Reset.
7886        *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
7887    }
7888
7889    // ═══════════════════════════════════════════════════════════════════
7890    // startpatternscope / endpatternscope / clearpatterndisables —
7891    // pattern.c:4241/4279/4296. Pin LOCALPATTERNS-gated save/restore +
7892    // C `memset(zpc_disables, 0, ZPC_COUNT)` clear.
7893    //
7894    // Bug fixed: prior Rust port operated on a separate
7895    // `patterndisables: Mutex<Vec<String>>` tombstone (cleared by
7896    // clearpatterndisables, push/popped by start/end). C's three fns
7897    // all operate on the canonical `zpc_disables[]` byte array.
7898    // Pre-fix: setopt LOCALPATTERNS function entry/exit was a no-op
7899    // and `clearpatterndisables` didn't clear the matcher's disable
7900    // bytes.
7901    // ═══════════════════════════════════════════════════════════════════
7902
7903    /// `Src/pattern.c:4296-4298` — `memset(zpc_disables, 0, ZPC_COUNT)`.
7904    /// Test that clearpatterndisables zeros every slot.
7905    #[test]
7906    fn clearpatterndisables_zeros_zpc_disables() {
7907        let _g = crate::test_util::global_state_lock();
7908        // Pre-populate every slot.
7909        *zpc_disables.lock().unwrap() = [1u8; ZPC_COUNT as usize];
7910        clearpatterndisables();
7911        let table = zpc_disables.lock().unwrap().clone();
7912        for (i, &v) in table.iter().enumerate() {
7913            assert_eq!(v, 0, "c:4298 — slot {i} cleared");
7914        }
7915    }
7916
7917    /// `Src/pattern.c:4241-4250` + c:4279-4290` — under `setopt
7918    /// LOCALPATTERNS`, function entry save → mutate → exit restore
7919    /// round-trips zpc_disables.
7920    #[test]
7921    fn pattern_scope_save_restore_under_localpatterns() {
7922        let _g = crate::test_util::global_state_lock();
7923        crate::ported::options::opt_state_set("localpatterns", true);
7924
7925        // Initial state: ZPC_BAR disabled, ZPC_STAR enabled.
7926        *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
7927        zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 1;
7928
7929        // Enter scope (c:4241 — `savepatterndisables` into stack frame).
7930        startpatternscope();
7931
7932        // Mutate inside the "function" body.
7933        zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 0;
7934        zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 1;
7935        assert_eq!(zpc_disables.lock().unwrap()[ZPC_BAR as usize], 0);
7936        assert_eq!(zpc_disables.lock().unwrap()[ZPC_STAR as usize], 1);
7937
7938        // Exit scope (c:4279 — restore via restorepatterndisables).
7939        endpatternscope();
7940
7941        // Outer state must come back.
7942        assert_eq!(
7943            zpc_disables.lock().unwrap()[ZPC_BAR as usize],
7944            1,
7945            "c:4287 — ZPC_BAR restored to outer-scope disabled"
7946        );
7947        assert_eq!(
7948            zpc_disables.lock().unwrap()[ZPC_STAR as usize],
7949            0,
7950            "c:4287 — ZPC_STAR restored to outer-scope enabled"
7951        );
7952
7953        // Reset.
7954        *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
7955        crate::ported::options::opt_state_set("localpatterns", false);
7956    }
7957
7958    /// `Src/pattern.c:4286` — WITHOUT LOCALPATTERNS, endpatternscope
7959    /// pops the stack frame but does NOT restore. Function-body
7960    /// mutations leak into the caller.
7961    #[test]
7962    fn pattern_scope_no_restore_without_localpatterns() {
7963        let _g = crate::test_util::global_state_lock();
7964        crate::ported::options::opt_state_set("localpatterns", false);
7965
7966        *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
7967        zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 1;
7968
7969        startpatternscope();
7970        zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 0;
7971        endpatternscope();
7972
7973        // Mutation leaked — c:4286 gate skipped restore.
7974        assert_eq!(
7975            zpc_disables.lock().unwrap()[ZPC_BAR as usize],
7976            0,
7977            "c:4286 — WITHOUT LOCALPATTERNS, mutation leaks out"
7978        );
7979
7980        // Reset.
7981        *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
7982    }
7983
7984    /// `Src/pattern.c:4241-4250` — nested scopes form a LIFO stack.
7985    /// Inner restore pops just the inner frame; outer frame stays.
7986    #[test]
7987    fn pattern_scope_nested_lifo() {
7988        let _g = crate::test_util::global_state_lock();
7989        crate::ported::options::opt_state_set("localpatterns", true);
7990
7991        *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
7992
7993        // Outer.
7994        zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 1;
7995        startpatternscope(); // frame A
7996                             // Inner.
7997        zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 0;
7998        zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 1;
7999        startpatternscope(); // frame B
8000                             // Innermost.
8001        zpc_disables.lock().unwrap()[ZPC_INBRACK as usize] = 1;
8002        endpatternscope(); // pop B → inner restored
8003        assert_eq!(zpc_disables.lock().unwrap()[ZPC_BAR as usize], 0);
8004        assert_eq!(zpc_disables.lock().unwrap()[ZPC_STAR as usize], 1);
8005        assert_eq!(
8006            zpc_disables.lock().unwrap()[ZPC_INBRACK as usize],
8007            0,
8008            "c:4287 — frame B's snapshot didn't include this slot's 1"
8009        );
8010        endpatternscope(); // pop A → outer restored
8011        assert_eq!(zpc_disables.lock().unwrap()[ZPC_BAR as usize], 1);
8012        assert_eq!(zpc_disables.lock().unwrap()[ZPC_STAR as usize], 0);
8013
8014        // Reset.
8015        *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
8016        crate::ported::options::opt_state_set("localpatterns", false);
8017    }
8018
8019    /// `Src/pattern.c:4226` — bit position is the slot INDEX (0-based),
8020    /// not 1-based. Slot 0 → bit 1, slot 1 → bit 2, etc. Per the C
8021    /// `bit = 1` initial and `bit <<= 1` after each slot.
8022    #[test]
8023    fn savepatterndisables_slot_0_is_low_bit() {
8024        let _g = crate::test_util::global_state_lock();
8025        *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
8026        zpc_disables.lock().unwrap()[0] = 1;
8027        let disables = savepatterndisables();
8028        assert_eq!(disables, 1, "c:4226 — bit = 1 initial → slot 0 is low bit");
8029        *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
8030    }
8031
8032    /// `Src/pattern.c:1997` — `charsub(x, y)` returns the number of
8033    /// characters between the start and byte offset `y`. Non-multibyte:
8034    /// byte distance; multibyte: codepoint count of `x[0..y]`.
8035    #[test]
8036    fn charsub_counts_chars_to_offset() {
8037        let _g = crate::test_util::global_state_lock();
8038        let saved = crate::ported::options::opt_state_get("multibyte");
8039
8040        // c:2004 — non-multibyte: byte distance `y - x`.
8041        crate::ported::options::opt_state_set("multibyte", false);
8042        assert_eq!(charsub("abc", 3), 3, "non-MB: byte distance to end");
8043        assert_eq!(charsub("abc", 1), 1, "non-MB: one byte in");
8044        assert_eq!(charsub("abc", 0), 0, "c:1997 — at start, distance 0");
8045        assert_eq!(charsub("é日", 5), 5, "non-MB: raw byte count (5 bytes)");
8046
8047        // c:2006-2021 — multibyte: codepoint count of `x[0..y]`.
8048        crate::ported::options::opt_state_set("multibyte", true);
8049        assert_eq!(charsub("abc", 3), 3, "MB: 3 ASCII chars");
8050        assert_eq!(charsub("é", 2), 1, "MB: one 2-byte codepoint");
8051        assert_eq!(charsub("é日", 5), 2, "MB: é + 日 = 2 codepoints");
8052        assert_eq!(charsub("é日", 2), 1, "MB: just é = 1 codepoint");
8053        assert_eq!(charsub("é日", 0), 0, "MB: distance 0 at start");
8054
8055        if let Some(v) = saved {
8056            crate::ported::options::opt_state_set("multibyte", v);
8057        }
8058    }
8059
8060    // ═══════════════════════════════════════════════════════════════════
8061    // KSH_GLOB extended patterns: +(pat), *(pat), ?(pat), @(pat), !(pat).
8062    // Anchored against `setopt KSH_GLOB; [[ str == ${~pat} ]]` in zsh 5.9.
8063    // Tests enable KSH_GLOB before each assertion and restore prior state.
8064    //
8065    // Ported from pattern.c:1278-1350 (KSH dispatch in patcomppiece) and
8066    // pattern.c:1615-1746 (kshchar-driven quantifier emission), plus
8067    // patcompnot pattern.c:1759-1784 for the !(pat) negation form and a
8068    // minimal P_EXCLUDE matcher arm (pattern.c:3056-3201).
8069    // ═══════════════════════════════════════════════════════════════════
8070
8071    fn ksh_glob_match(pat: &str, s: &str) -> bool {
8072        let _g = TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
8073        let saved = opt_state_get("kshglob").unwrap_or(false);
8074        opt_state_set("kshglob", true);
8075        let r = patmatch(pat, s);
8076        opt_state_set("kshglob", saved);
8077        r
8078    }
8079
8080    // ── +(pat) — one or more ─────────────────────────────────────────
8081    /// `+(foo)` matches `foo` — zsh: MATCH.
8082    #[test]
8083    fn ksh_glob_plus_one_repetition_matches() {
8084        let _g = crate::test_util::global_state_lock();
8085        assert!(ksh_glob_match("+(foo)", "foo"));
8086    }
8087
8088    /// `+(foo)` matches `foofoo` — zsh: MATCH.
8089    #[test]
8090    fn ksh_glob_plus_two_repetitions_matches() {
8091        let _g = crate::test_util::global_state_lock();
8092        assert!(ksh_glob_match("+(foo)", "foofoo"));
8093    }
8094
8095    /// `+(foo)` does NOT match `""` — zsh: NOMATCH (and zshrs agrees).
8096    /// This is the ONLY +(pat) test that passes — both reject empty.
8097    #[test]
8098    fn ksh_glob_plus_zero_repetitions_fails() {
8099        let _g = crate::test_util::global_state_lock();
8100        assert!(!ksh_glob_match("+(foo)", ""));
8101    }
8102
8103    /// `+(foo)` matches `foofoofoo` — zsh: MATCH.
8104    #[test]
8105    fn ksh_glob_plus_three_repetitions_matches() {
8106        let _g = crate::test_util::global_state_lock();
8107        assert!(ksh_glob_match("+(foo)", "foofoofoo"));
8108    }
8109
8110    // ── *(pat) — zero or more ────────────────────────────────────────
8111    /// `*(foo)` matches empty — zsh: MATCH.
8112    #[test]
8113    fn ksh_glob_star_paren_matches_empty() {
8114        let _g = crate::test_util::global_state_lock();
8115        assert!(ksh_glob_match("*(foo)", ""));
8116    }
8117
8118    /// `*(foo)` matches `foo` — both zsh and zshrs agree (passes).
8119    #[test]
8120    fn ksh_glob_star_paren_matches_one() {
8121        let _g = crate::test_util::global_state_lock();
8122        assert!(ksh_glob_match("*(foo)", "foo"));
8123    }
8124
8125    /// `*(foo)` matches `foofoo` — both agree.
8126    #[test]
8127    fn ksh_glob_star_paren_matches_multiple() {
8128        let _g = crate::test_util::global_state_lock();
8129        assert!(ksh_glob_match("*(foo)", "foofoo"));
8130    }
8131
8132    // ── ?(pat) — zero or one ─────────────────────────────────────────
8133    /// `?(foo)` matches empty — zsh: MATCH.
8134    #[test]
8135    fn ksh_glob_question_paren_matches_empty() {
8136        let _g = crate::test_util::global_state_lock();
8137        assert!(ksh_glob_match("?(foo)", ""));
8138    }
8139
8140    /// `?(foo)` matches `foo` — zsh: MATCH.
8141    #[test]
8142    fn ksh_glob_question_paren_matches_one_rep() {
8143        let _g = crate::test_util::global_state_lock();
8144        assert!(ksh_glob_match("?(foo)", "foo"));
8145    }
8146
8147    /// `?(foo)` does NOT match `foofoo` — both agree (rejects).
8148    #[test]
8149    fn ksh_glob_question_paren_fails_on_two_reps() {
8150        let _g = crate::test_util::global_state_lock();
8151        assert!(!ksh_glob_match("?(foo)", "foofoo"));
8152    }
8153
8154    // ── @(pat) — exactly one ─────────────────────────────────────────
8155    /// `@(foo)` matches `foo` — zsh: MATCH. zshrs fails on the alternation
8156    /// form but matches the single-branch form.
8157    #[test]
8158    fn ksh_glob_at_paren_matches_exact() {
8159        let _g = crate::test_util::global_state_lock();
8160        assert!(ksh_glob_match("@(foo)", "foo"));
8161    }
8162
8163    /// `@(foo|bar)` matches both `foo` AND `bar` — zsh: MATCH both.
8164    #[test]
8165    fn ksh_glob_at_paren_with_alternation_either_branch() {
8166        let _g = crate::test_util::global_state_lock();
8167        assert!(ksh_glob_match("@(foo|bar)", "foo"));
8168        assert!(ksh_glob_match("@(foo|bar)", "bar"));
8169    }
8170
8171    /// `@(foo|bar)` rejects `qux` — both agree.
8172    #[test]
8173    fn ksh_glob_at_paren_alternation_rejects_outside() {
8174        let _g = crate::test_util::global_state_lock();
8175        assert!(!ksh_glob_match("@(foo|bar)", "qux"));
8176    }
8177
8178    // ── !(pat) — not match ───────────────────────────────────────────
8179    /// `!(foo)` rejects `foo` — both agree.
8180    #[test]
8181    fn ksh_glob_bang_paren_rejects_matching_string() {
8182        let _g = crate::test_util::global_state_lock();
8183        assert!(!ksh_glob_match("!(foo)", "foo"));
8184    }
8185
8186    /// `!(foo)` matches `bar` — zsh: MATCH.
8187    #[test]
8188    fn ksh_glob_bang_paren_matches_non_matching_string() {
8189        let _g = crate::test_util::global_state_lock();
8190        assert!(ksh_glob_match("!(foo)", "bar"));
8191    }
8192
8193    /// `!(foo|bar)` matches `baz` and rejects `foo` — zsh.
8194    #[test]
8195    fn ksh_glob_bang_paren_with_alternation() {
8196        let _g = crate::test_util::global_state_lock();
8197        assert!(ksh_glob_match("!(foo|bar)", "baz"));
8198        assert!(!ksh_glob_match("!(foo|bar)", "foo"));
8199    }
8200
8201    // ── Mixed with literal context ───────────────────────────────────
8202    /// `pre+(x)post` matches `prexpost`, `prexxpost` — zsh: MATCH both.
8203    #[test]
8204    fn ksh_glob_plus_paren_in_literal_context() {
8205        let _g = crate::test_util::global_state_lock();
8206        assert!(ksh_glob_match("pre+(x)post", "prexpost"));
8207        assert!(ksh_glob_match("pre+(x)post", "prexxpost"));
8208    }
8209
8210    /// `pre+(x)post` rejects `prepost` — both agree (no x's).
8211    #[test]
8212    fn ksh_glob_plus_paren_in_literal_rejects_zero_reps() {
8213        let _g = crate::test_util::global_state_lock();
8214        assert!(!ksh_glob_match("pre+(x)post", "prepost"));
8215    }
8216
8217    // ═══════════════════════════════════════════════════════════════════
8218    // EXTENDED_GLOB pattern flags: (#i) case-insensitive, (#l) lowercase
8219    // matches uppercase, (#aN) approximate match. Anchored to
8220    // `setopt EXTENDED_GLOB; [[ str == ${~pat} ]]` in real zsh 5.9.
8221    // ═══════════════════════════════════════════════════════════════════
8222
8223    fn ext_glob_match(pat: &str, s: &str) -> bool {
8224        let _g = TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
8225        let saved = opt_state_get("extendedglob").unwrap_or(false);
8226        opt_state_set("extendedglob", true);
8227        let r = patmatch(pat, s);
8228        opt_state_set("extendedglob", saved);
8229        r
8230    }
8231
8232    // ── (#i) case-insensitive ───────────────────────────────────────
8233    /// `(#i)FOO` matches "foo" (case ignored).
8234    #[test]
8235    fn ext_glob_hash_i_matches_lowercase_against_uppercase_pat() {
8236        let _g = crate::test_util::global_state_lock();
8237        assert!(ext_glob_match("(#i)FOO", "foo"));
8238    }
8239
8240    /// `(#i)FOO` matches "FOO" exactly.
8241    #[test]
8242    fn ext_glob_hash_i_matches_exact_case() {
8243        let _g = crate::test_util::global_state_lock();
8244        assert!(ext_glob_match("(#i)FOO", "FOO"));
8245    }
8246
8247    /// `(#i)FOO` matches mixed-case "FoO".
8248    #[test]
8249    fn ext_glob_hash_i_matches_mixed_case() {
8250        let _g = crate::test_util::global_state_lock();
8251        assert!(ext_glob_match("(#i)FOO", "FoO"));
8252    }
8253
8254    /// `(#i)foo` rejects unrelated string "BAR".
8255    #[test]
8256    fn ext_glob_hash_i_rejects_unrelated_string() {
8257        let _g = crate::test_util::global_state_lock();
8258        assert!(!ext_glob_match("(#i)foo", "BAR"));
8259    }
8260
8261    // ── (#l) lowercase-pattern matches uppercase-text ───────────────
8262    /// `(#l)foo` matches "FOO" (lowercase pattern allows uppercase).
8263    /// Distinct from (#i): asymmetric — pattern letter must be lowercase
8264    /// AND text may be either case for that letter.
8265    #[test]
8266    fn ext_glob_hash_l_lowercase_pat_matches_uppercase_text() {
8267        let _g = crate::test_util::global_state_lock();
8268        assert!(ext_glob_match("(#l)foo", "FOO"));
8269    }
8270
8271    /// `(#l)foo` matches lowercase "foo".
8272    #[test]
8273    fn ext_glob_hash_l_lowercase_pat_matches_lowercase_text() {
8274        let _g = crate::test_util::global_state_lock();
8275        assert!(ext_glob_match("(#l)foo", "foo"));
8276    }
8277
8278    /// `(#l)FOO` does NOT match "foo" — asymmetry: uppercase in pattern
8279    /// requires uppercase in text. zsh: NOMATCH.
8280    #[test]
8281    fn ext_glob_hash_l_uppercase_pat_requires_uppercase_text_anchored_to_zsh() {
8282        let _g = crate::test_util::global_state_lock();
8283        assert!(
8284            !ext_glob_match("(#l)FOO", "foo"),
8285            "zsh: (#l)FOO must NOT match \"foo\" — uppercase-in-pattern is anchored"
8286        );
8287    }
8288
8289    // ── (#aN) approximate match (Damerau-Levenshtein distance ≤ N) ──
8290    /// `(#a1)foo` matches "fop" (1 char substitution).
8291    #[test]
8292    fn ext_glob_hash_a1_matches_one_substitution_anchored_to_zsh() {
8293        let _g = crate::test_util::global_state_lock();
8294        assert!(
8295            ext_glob_match("(#a1)foo", "fop"),
8296            "zsh: (#a1)foo matches 'fop' (1 substitution)"
8297        );
8298    }
8299
8300    /// `(#a2)foo` matches "fxy" (2 substitutions).
8301    #[test]
8302    fn ext_glob_hash_a2_matches_two_substitutions_anchored_to_zsh() {
8303        let _g = crate::test_util::global_state_lock();
8304        assert!(
8305            ext_glob_match("(#a2)foo", "fxy"),
8306            "zsh: (#a2)foo matches 'fxy' (2 substitutions)"
8307        );
8308    }
8309
8310    /// `(#a1)foo` rejects "fxy" (2 substitutions > limit 1).
8311    #[test]
8312    fn ext_glob_hash_a1_rejects_two_substitutions() {
8313        let _g = crate::test_util::global_state_lock();
8314        assert!(!ext_glob_match("(#a1)foo", "fxy"));
8315    }
8316
8317    // ═══════════════════════════════════════════════════════════════════
8318    // (#aN) full Damerau-Levenshtein — sub/ins/del edit operations.
8319    // C inlines the backtracking in P_EXACTLY (c:2737-2779) plus the
8320    // approx-aware sync nodes at c:3055+. Rust factors into
8321    // `approx_match_exactly` (WARNING: NOT IN PATTERN.C). Tests pin
8322    // each edit operation independently + the budget upper bound.
8323    // ═══════════════════════════════════════════════════════════════════
8324
8325    /// `(#a0)foo` is exact-match only — no edits allowed.
8326    #[test]
8327    fn ext_glob_hash_a0_is_exact_match_only() {
8328        let _g = crate::test_util::global_state_lock();
8329        assert!(ext_glob_match("(#a0)foo", "foo"), "exact ok");
8330        assert!(
8331            !ext_glob_match("(#a0)foo", "fop"),
8332            "no edit budget rejects 1-sub"
8333        );
8334    }
8335
8336    /// `(#a1)foo` matches "fxoo" via INSERTION in input (extra 'x').
8337    /// The pattern has no `x`; treating `x` as an inserted-in-input
8338    /// char costs 1 error. Input → "f(x)oo" with the 'x' deleted.
8339    #[test]
8340    fn ext_glob_hash_a_accepts_insertion_in_input() {
8341        let _g = crate::test_util::global_state_lock();
8342        // "fxoo" — extra 'x' between 'f' and 'oo'.
8343        assert!(
8344            ext_glob_match("(#a1)foo", "fxoo"),
8345            "1 insertion-in-input edit"
8346        );
8347    }
8348
8349    /// `(#a1)foo` matches "fo" via DELETION from input (missing 'o').
8350    /// One 'o' deleted from input compared to pattern.
8351    #[test]
8352    fn ext_glob_hash_a_accepts_deletion_from_input() {
8353        let _g = crate::test_util::global_state_lock();
8354        assert!(
8355            ext_glob_match("(#a1)foo", "fo"),
8356            "1 deletion-from-input edit"
8357        );
8358    }
8359
8360    /// `(#a2)foo` matches "fxooy" via 1 insertion + 1 substitution.
8361    /// 'x' inserted between f-o; final 'o' substituted to 'y'. Wait —
8362    /// closer reading: "fxooy" vs "foo" — 'x' is the extra char, but
8363    /// then 'ooy' vs 'oo' has another extra 'y'. That's 2 insertions.
8364    #[test]
8365    fn ext_glob_hash_a_mixed_edits_within_budget() {
8366        let _g = crate::test_util::global_state_lock();
8367        // 2 insertions: 'x' and 'y'.
8368        assert!(
8369            ext_glob_match("(#a2)foo", "fxooy"),
8370            "2 insertions in input within budget"
8371        );
8372    }
8373
8374    /// Budget upper bound: `(#a1)abc` rejects "xyz" (3 substitutions > 1).
8375    #[test]
8376    fn ext_glob_hash_a1_rejects_three_substitutions() {
8377        let _g = crate::test_util::global_state_lock();
8378        assert!(!ext_glob_match("(#a1)abc", "xyz"));
8379    }
8380
8381    /// `(#a3)abc` matches "xyz" (3 substitutions ≤ 3).
8382    #[test]
8383    fn ext_glob_hash_a3_accepts_all_substituted() {
8384        let _g = crate::test_util::global_state_lock();
8385        assert!(
8386            ext_glob_match("(#a3)abc", "xyz"),
8387            "3 substitutions at budget 3"
8388        );
8389    }
8390
8391    /// Position-independent substitution: budget allows replacing
8392    /// any one of N pattern chars.
8393    #[test]
8394    fn ext_glob_hash_a_accepts_position_independent_substitution() {
8395        let _g = crate::test_util::global_state_lock();
8396        assert!(ext_glob_match("(#a1)abc", "Xbc"), "first-char sub");
8397        assert!(ext_glob_match("(#a1)abc", "aXc"), "middle-char sub");
8398        assert!(ext_glob_match("(#a1)abc", "abX"), "last-char sub");
8399    }
8400
8401    // ═══════════════════════════════════════════════════════════════════
8402    // zsh test-corpus pins — direct anchors to Test/D02glob.ztst:25-80
8403    // "zsh globbing" test list (zsh's authoritative regression suite).
8404    // Each test cites the ztst line range it pins. Currently-failing
8405    // tests get #[ignore = "ZSHRS BUG: ..."] markers; expected-output
8406    // assertions stay in tree so the marker flips when the Rust port
8407    // catches up.
8408    // ═══════════════════════════════════════════════════════════════════
8409
8410    /// `Test/D02glob.ztst:26` — `[[ foo~ = foo~ ]]` exact match,
8411    /// expected exit 0 (true).
8412    #[test]
8413    fn zsh_corpus_foo_tilde_exact_match() {
8414        let _g = crate::test_util::global_state_lock();
8415        assert!(
8416            ext_glob_match("foo~", "foo~"),
8417            "ztst:26 — literal tilde matches"
8418        );
8419    }
8420
8421    /// `Test/D02glob.ztst:27` — `[[ foo~ = (foo~) ]]` parenthesised
8422    /// single alternative.
8423    #[test]
8424    fn zsh_corpus_foo_tilde_in_parens() {
8425        let _g = crate::test_util::global_state_lock();
8426        assert!(
8427            ext_glob_match("(foo~)", "foo~"),
8428            "ztst:27 — (foo~) matches foo~"
8429        );
8430    }
8431
8432    /// `Test/D02glob.ztst:28` — `[[ foo~ = (foo~|) ]]` alternation
8433    /// with empty alternative.
8434    #[test]
8435    fn zsh_corpus_alternation_with_empty_alt() {
8436        let _g = crate::test_util::global_state_lock();
8437        assert!(
8438            ext_glob_match("(foo~|)", "foo~"),
8439            "ztst:28 — empty alt accepted"
8440        );
8441    }
8442
8443    /// `Test/D02glob.ztst:29` — `[[ foo.c = *.c~boo* ]]` exclude
8444    /// pattern: matches *.c BUT NOT boo*. `foo.c` matches *.c and
8445    /// doesn't match boo*, so result is 0 (true).
8446    #[test]
8447    fn zsh_corpus_exclude_pattern_basic() {
8448        let _g = crate::test_util::global_state_lock();
8449        assert!(
8450            ext_glob_match("*.c~boo*", "foo.c"),
8451            "ztst:29 — *.c~boo* matches foo.c"
8452        );
8453    }
8454
8455    /// `Test/D02glob.ztst:30` — `[[ foo.c = *.c~boo*~foo* ]]`
8456    /// double-exclude: matches *.c but excludes both `boo*` and
8457    /// `foo*`. `foo.c` matches `foo*` (excluded) → result 1 (false).
8458    #[test]
8459    fn zsh_corpus_exclude_pattern_double() {
8460        let _g = crate::test_util::global_state_lock();
8461        assert!(
8462            !ext_glob_match("*.c~boo*~foo*", "foo.c"),
8463            "ztst:30 — double exclude rejects foo.c"
8464        );
8465    }
8466
8467    /// `Test/D02glob.ztst:31` — `[[ fofo = (fo#)# ]]` — `#` is the
8468    /// extended-glob "1+ repetitions" quantifier. `(fo#)#` matches
8469    /// 1+ repetitions of "fo+".
8470    #[test]
8471    fn zsh_corpus_hash_repetition_double() {
8472        let _g = crate::test_util::global_state_lock();
8473        assert!(
8474            ext_glob_match("(fo#)#", "fofo"),
8475            "ztst:31 — (fo#)# matches fofo"
8476        );
8477    }
8478
8479    /// `Test/D02glob.ztst:32` — `[[ ffo = (fo#)# ]]`. `fo#` means
8480    /// `f` followed by 0+ `o`. `ffo` = `f` + `fo` (matches the
8481    /// outer `#` repetition).
8482    #[test]
8483    fn zsh_corpus_hash_repetition_min_one() {
8484        let _g = crate::test_util::global_state_lock();
8485        assert!(
8486            ext_glob_match("(fo#)#", "ffo"),
8487            "ztst:32 — (fo#)# matches ffo via 1-iter outer"
8488        );
8489    }
8490
8491    /// `Test/D02glob.ztst:36` — `[[ foooofof = (fo##)# ]]` —
8492    /// `##` is "2+ repetitions". `fo##` = `f` + 2+ `o`'s. The
8493    /// trailing `of` after `foooo` doesn't satisfy the trailing
8494    /// outer `#`, so result is 1 (false).
8495    #[test]
8496    fn zsh_corpus_hash_hash_quantifier_min_two() {
8497        let _g = crate::test_util::global_state_lock();
8498        assert!(
8499            !ext_glob_match("(fo##)#", "foooofof"),
8500            "ztst:36 — (fo##)# rejects foooofof"
8501        );
8502    }
8503
8504    /// `Test/D02glob.ztst:50` — `[[ aac = ((a))#a(c) ]]` —
8505    /// `((a))#` is `0+ a`. `aac` = `aa` (= `((a))#`) + `c`. The
8506    /// final `a(c)` requires literal `a` followed by capture `(c)`.
8507    /// Wait — should be `aac` matches `((a))#a(c)`: outer `((a))#`
8508    /// captures `a` repeated, then literal `a`, then `(c)`.
8509    /// Actually re-reading: `((a))#` matches 0+ 'a'. `aac` =
8510    /// `((a))#`("a") + `a`("a") + `(c)`("c"). So "aa" splits as
8511    /// (a)("a") + a("a") + c("c"). Match.
8512    #[test]
8513    fn zsh_corpus_nested_paren_quantifier() {
8514        let _g = crate::test_util::global_state_lock();
8515        assert!(
8516            ext_glob_match("((a))#a(c)", "aac"),
8517            "ztst:50 — ((a))#a(c) matches aac"
8518        );
8519    }
8520
8521    /// `Test/D02glob.ztst:51` — `[[ ac = ((a))#a(c) ]]` — single
8522    /// `a` matches `a(c)` after empty `((a))#` consumes zero.
8523    #[test]
8524    fn zsh_corpus_zero_iteration_quantifier() {
8525        let _g = crate::test_util::global_state_lock();
8526        assert!(
8527            ext_glob_match("((a))#a(c)", "ac"),
8528            "ztst:51 — ((a))# can be zero iters"
8529        );
8530    }
8531
8532    /// `Test/D02glob.ztst:52` — `[[ c = ((a))#a(c) ]]` — empty
8533    /// `((a))#` + literal `a` requires at least one `a`. `c` has
8534    /// no `a`, so result is 1 (false).
8535    #[test]
8536    fn zsh_corpus_required_literal_after_zero_quantifier() {
8537        let _g = crate::test_util::global_state_lock();
8538        assert!(
8539            !ext_glob_match("((a))#a(c)", "c"),
8540            "ztst:52 — bare `c` lacks required `a`"
8541        );
8542    }
8543
8544    /// `Test/D02glob.ztst:73` — `[[ foo = ((^x)) ]]` — exclude
8545    /// `x`. `foo` doesn't contain `x` as a single char (the
8546    /// `((^x))` matches anything that's not just 'x' — `foo` is 3
8547    /// chars, none is `x`).
8548    #[test]
8549    fn zsh_corpus_caret_exclude_basic() {
8550        let _g = crate::test_util::global_state_lock();
8551        assert!(
8552            ext_glob_match("((^x))", "foo"),
8553            "ztst:73 — ((^x)) matches foo"
8554        );
8555    }
8556
8557    /// `Test/D02glob.ztst:75` — `[[ foo = ((^foo)) ]]` — `^foo`
8558    /// rejects exact `foo`. `foo` matches `foo` literal, which the
8559    /// `^` inverts. Result: 1 (false).
8560    #[test]
8561    fn zsh_corpus_caret_exclude_exact_match_inverted() {
8562        let _g = crate::test_util::global_state_lock();
8563        assert!(
8564            !ext_glob_match("((^foo))", "foo"),
8565            "ztst:75 — ((^foo)) rejects foo"
8566        );
8567    }
8568
8569    /// `Test/D02glob.ztst:79` — `[[ foot = z*~*x ]]` — `*` then
8570    /// exclude `*x`. `foot` doesn't start with `z`, so doesn't
8571    /// match `z*` at all → result 1 (false).
8572    #[test]
8573    fn zsh_corpus_star_exclude_no_z_prefix() {
8574        let _g = crate::test_util::global_state_lock();
8575        assert!(
8576            !ext_glob_match("z*~*x", "foot"),
8577            "ztst:79 — foot doesn't start with z"
8578        );
8579    }
8580
8581    /// `Test/D02glob.ztst:80` — `[[ zoot = z*~*x ]]` — `zoot`
8582    /// matches `z*` AND doesn't end in `x` → result 0 (true).
8583    #[test]
8584    fn zsh_corpus_star_exclude_zoot() {
8585        let _g = crate::test_util::global_state_lock();
8586        assert!(
8587            ext_glob_match("z*~*x", "zoot"),
8588            "ztst:80 — zoot matches z* and not *x"
8589        );
8590    }
8591
8592    // ─── POSIX class brackets — Test/D02glob.ztst:111-118 ─────────────
8593
8594    /// `Test/D02glob.ztst:111` — `[[:alpha:][:punct:]]#[[:digit:]][^[:lower:]]`
8595    /// over "a%1X": `a` matches alpha, `%` matches punct (1+ via `#`),
8596    /// `1` matches digit, `X` matches NOT-lower.
8597    #[test]
8598    fn zsh_corpus_posix_class_alpha_punct_digit_notlower() {
8599        let _g = crate::test_util::global_state_lock();
8600        assert!(
8601            ext_glob_match("[[:alpha:][:punct:]]#[[:digit:]][^[:lower:]]", "a%1X"),
8602            "ztst:111 — alpha-punct-digit-notlower chain",
8603        );
8604    }
8605
8606    /// `Test/D02glob.ztst:112` — same pattern, "a%1" lacks the
8607    /// 4th non-lower char → result 1 (false).
8608    #[test]
8609    fn zsh_corpus_posix_class_rejects_short_input() {
8610        let _g = crate::test_util::global_state_lock();
8611        assert!(
8612            !ext_glob_match("[[:alpha:][:punct:]]#[[:digit:]][^[:lower:]]", "a%1"),
8613            "ztst:112 — short input rejected",
8614        );
8615    }
8616
8617    /// `Test/D02glob.ztst:113` — `[[:` literal in char class:
8618    /// `[[ [: = [[:]# ]]` — `[[:]` matches `[` OR `:`, `#` is 1+
8619    /// repetitions. "[:" = `[` + `:` → matches.
8620    #[test]
8621    fn zsh_corpus_literal_brackets_in_class_with_repetition() {
8622        let _g = crate::test_util::global_state_lock();
8623        assert!(
8624            ext_glob_match("[[:]#", "[:"),
8625            "ztst:113 — [[:]# matches '[:'"
8626        );
8627    }
8628
8629    // ─── (#i) / (#l) case modifiers — Test/D02glob.ztst:119-132 ───────
8630
8631    /// `Test/D02glob.ztst:119` — `(#i)FOOXX` matches "fooxx"
8632    /// (case-insensitive throughout).
8633    #[test]
8634    fn zsh_corpus_hash_i_case_insensitive() {
8635        let _g = crate::test_util::global_state_lock();
8636        assert!(
8637            ext_glob_match("(#i)FOOXX", "fooxx"),
8638            "ztst:119 — (#i)FOOXX matches fooxx"
8639        );
8640    }
8641
8642    /// `Test/D02glob.ztst:120` — `(#l)FOOXX` requires pattern UPPER
8643    /// chars to be the ONLY uppercase candidates; lowercase input
8644    /// FAILS to match because pattern is all uppercase and `#l` only
8645    /// matches the EXACT lowercase variant of the pattern char... actually
8646    /// `(#l)` means "lowercase pattern chars match uppercase too" —
8647    /// `(#l)FOOXX` has no lowercase chars so it stays a strict
8648    /// uppercase match. "fooxx" doesn't match. Result 1 (false).
8649    #[test]
8650    fn zsh_corpus_hash_l_uppercase_pattern_with_lower_input_fails() {
8651        let _g = crate::test_util::global_state_lock();
8652        assert!(
8653            !ext_glob_match("(#l)FOOXX", "fooxx"),
8654            "ztst:120 — (#l)FOOXX does NOT match fooxx"
8655        );
8656    }
8657
8658    /// `Test/D02glob.ztst:121` — `(#l)fooxx` (lowercase pattern) DOES
8659    /// match "FOOXX" because `#l` is the asymmetric "lowercase pat
8660    /// chars match upper-or-lower" rule.
8661    #[test]
8662    fn zsh_corpus_hash_l_lowercase_pattern_matches_uppercase_input() {
8663        let _g = crate::test_util::global_state_lock();
8664        assert!(
8665            ext_glob_match("(#l)fooxx", "FOOXX"),
8666            "ztst:121 — (#l)fooxx matches FOOXX (asymmetric upcasing)"
8667        );
8668    }
8669
8670    /// `Test/D02glob.ztst:122` — `(#i)FOO(#I)X(#i)X` mixes case
8671    /// modes. `(#I)` cancels prior `(#i)`. So 4th char `X` requires
8672    /// exact case. "fooxx" has lowercase `x` at that position → fail.
8673    #[test]
8674    fn zsh_corpus_hash_capital_i_cancels_case_insensitive() {
8675        let _g = crate::test_util::global_state_lock();
8676        assert!(
8677            !ext_glob_match("(#i)FOO(#I)X(#i)X", "fooxx"),
8678            "ztst:122 — (#I) cancels (#i), 4th char `X` requires upper"
8679        );
8680    }
8681
8682    /// `Test/D02glob.ztst:123` — same pattern with input "fooXx":
8683    /// `(#i)FOO` matches "foo", `(#I)X` matches "X" exact-case,
8684    /// `(#i)X` matches "x" insensitive → result 0 (true).
8685    #[test]
8686    fn zsh_corpus_hash_i_capital_i_toggle_succeeds() {
8687        let _g = crate::test_util::global_state_lock();
8688        assert!(
8689            ext_glob_match("(#i)FOO(#I)X(#i)X", "fooXx"),
8690            "ztst:123 — mixed case modifiers succeed"
8691        );
8692    }
8693
8694    /// `Test/D02glob.ztst:128` — `(#i)*m*` matches "Modules"
8695    /// case-insensitively.
8696    #[test]
8697    fn zsh_corpus_hash_i_with_star_glob() {
8698        let _g = crate::test_util::global_state_lock();
8699        assert!(
8700            ext_glob_match("(#i)*m*", "Modules"),
8701            "ztst:128 — (#i)*m* case-insensitive substring"
8702        );
8703    }
8704
8705    // ─── Numeric ranges `<n-m>` — Test/D02glob.ztst:133-137 ───────────
8706
8707    /// `Test/D02glob.ztst:133` — `<1-1000>33` matches "633" (the
8708    /// "6" is in [1..1000], followed by literal "33"). Actually
8709    /// the way zsh parses: `<1-1000>` matches ONE number from
8710    /// 1-1000, then `33` is literal. "633" = `6`(in 1-1000 range) +
8711    /// "33"(literal). So `<1-1000>33` matches "633".
8712    #[test]
8713    fn zsh_corpus_numeric_range_one_to_thousand() {
8714        let _g = crate::test_util::global_state_lock();
8715        assert!(
8716            ext_glob_match("<1-1000>33", "633"),
8717            "ztst:133 — <1-1000>33 matches 633"
8718        );
8719    }
8720
8721    /// `Test/D02glob.ztst:136` — `<->33` is the open-ended range
8722    /// (any number) followed by 33. Matches "633".
8723    #[test]
8724    fn zsh_corpus_numeric_range_open_ended() {
8725        let _g = crate::test_util::global_state_lock();
8726        assert!(
8727            ext_glob_match("<->33", "633"),
8728            "ztst:136 — <->33 matches 633 (any number)"
8729        );
8730    }
8731
8732    // ─── (#a) approximate match details — Test/D02glob.ztst:147-164 ───
8733
8734    /// `Test/D02glob.ztst:147` — `(#a1)[b][b]` matches "bob" via
8735    /// 1 substitution (middle `o` substituted in `[b][b]`'s gap).
8736    /// Wait — `[b][b]` is two-char "bb". Match against "bob" requires
8737    /// 1 edit. With (#a1), allowed.
8738    #[test]
8739    fn zsh_corpus_hash_a1_bracket_class_with_one_edit() {
8740        let _g = crate::test_util::global_state_lock();
8741        assert!(
8742            ext_glob_match("(#a1)[b][b]", "bob"),
8743            "ztst:147 — (#a1)[b][b] matches bob via 1 edit"
8744        );
8745    }
8746
8747    /// `Test/D02glob.ztst:151` — `(#a2)XbcX` matches "abcd" via
8748    /// 2 substitutions (a↔X, d↔X).
8749    #[test]
8750    fn zsh_corpus_hash_a2_two_substitutions() {
8751        let _g = crate::test_util::global_state_lock();
8752        assert!(
8753            ext_glob_match("(#a2)XbcX", "abcd"),
8754            "ztst:151 — 2 substitutions allowed"
8755        );
8756    }
8757
8758    /// `Test/D02glob.ztst:152` — `(#a2)ad` matches "abcd" via
8759    /// 2 INSERTIONS (b and c inserted into input vs pattern).
8760    /// Pattern is "ad" (2 chars), input is "abcd" (4 chars).
8761    /// Diff: 2 extra chars in input.
8762    #[test]
8763    fn zsh_corpus_hash_a2_two_insertions_in_input() {
8764        let _g = crate::test_util::global_state_lock();
8765        assert!(
8766            ext_glob_match("(#a2)ad", "abcd"),
8767            "ztst:152 — (#a2)ad matches abcd via 2 insertions in input"
8768        );
8769    }
8770
8771    /// `Test/D02glob.ztst:153` — `(#a2)abcd` matches "ad" via
8772    /// 2 DELETIONS from input (b and c missing from input vs pattern).
8773    #[test]
8774    fn zsh_corpus_hash_a2_two_deletions_from_input() {
8775        let _g = crate::test_util::global_state_lock();
8776        assert!(
8777            ext_glob_match("(#a2)abcd", "ad"),
8778            "ztst:153 — (#a2)abcd matches ad via 2 deletions"
8779        );
8780    }
8781
8782    /// `Test/D02glob.ztst:158` — `(#a2)abcd` rejects "dcba" — 4
8783    /// changes needed (full reverse) > budget 2.
8784    #[test]
8785    fn zsh_corpus_hash_a2_rejects_full_reverse() {
8786        let _g = crate::test_util::global_state_lock();
8787        assert!(
8788            !ext_glob_match("(#a2)abcd", "dcba"),
8789            "ztst:158 — full reverse exceeds budget 2"
8790        );
8791    }
8792
8793    /// `Test/D02glob.ztst:159` — `(#a3)abcd` matches "dcba" via
8794    /// 3 substitutions (positions 0,1,3 of input). Wait — actually
8795    /// "dcba" vs "abcd" — 4 positions differ. But (#a3) allows
8796    /// 3 errors. Hmm, but ztst says result is 0 (true). So zsh
8797    /// accepts. Possibly via DAMERAU transposition (swap adjacent).
8798    /// Without Damerau swap, this needs 4 subs → false. With
8799    /// transposition, "dcba" → "cdba" → "dcab"... complicated.
8800    /// Mark ignore — Damerau transposition isn't in the Rust port.
8801    #[test]
8802    fn zsh_corpus_hash_a3_reverse_via_transposition() {
8803        let _g = crate::test_util::global_state_lock();
8804        assert!(
8805            ext_glob_match("(#a3)abcd", "dcba"),
8806            "ztst:159 — (#a3)abcd matches dcba via Damerau transpositions"
8807        );
8808    }
8809
8810    // ─── (#s) start / (#e) end anchors — Test/D02glob.ztst:168-179 ────
8811
8812    /// `Test/D02glob.ztst:168` — `*((#s)|/)test((#e)|/)*` matches
8813    /// "test" — start-anchor (#s) at position 0, end-anchor (#e)
8814    /// after "test".
8815    #[test]
8816    fn zsh_corpus_hash_s_e_anchors_match_bare_test() {
8817        let _g = crate::test_util::global_state_lock();
8818        assert!(
8819            ext_glob_match("*((#s)|/)test((#e)|/)*", "test"),
8820            "ztst:168 — start/end anchors match bare 'test'",
8821        );
8822    }
8823
8824    /// `Test/D02glob.ztst:172` — `*((#s)|/)test((#e)|/)*` rejects
8825    /// "atest" — `(#s)|/` requires position 0 OR a `/`, but `a`
8826    /// is neither.
8827    #[test]
8828    fn zsh_corpus_hash_s_anchor_rejects_a_prefix() {
8829        let _g = crate::test_util::global_state_lock();
8830        assert!(
8831            !ext_glob_match("*((#s)|/)test((#e)|/)*", "atest"),
8832            "ztst:172 — atest fails the (#s) start-or-slash anchor",
8833        );
8834    }
8835
8836    // ─── Misc/globtests pins — `~` exclusion and `^` negation ─────────
8837
8838    /// `Misc/globtests` — `[[ foo~ = foo~ ]]` — bare tilde is literal.
8839    #[test]
8840    fn zsh_corpus_literal_tilde_in_pattern() {
8841        let _g = crate::test_util::global_state_lock();
8842        assert!(
8843            ext_glob_match("foo~", "foo~"),
8844            "literal ~ in non-extglob position"
8845        );
8846    }
8847
8848    /// `Misc/globtests` — `[[ foo.c = *.c~boo* ]]` — extended-glob
8849    /// exclusion: `*.c` minus things matching `boo*`. "foo.c" doesn't
8850    /// match "boo*", so it stays.
8851    #[test]
8852    fn zsh_corpus_exclusion_keeps_non_excluded() {
8853        let _g = crate::test_util::global_state_lock();
8854        assert!(ext_glob_match("*.c~boo*", "foo.c"), "exclusion keeps foo.c");
8855    }
8856
8857    /// `Misc/globtests` — `[[ foo.c = *.c~boo*~foo* ]]` — chained
8858    /// exclusions: `*.c` minus `boo*` minus `foo*`. "foo.c" matches
8859    /// `foo*`, so excluded.
8860    #[test]
8861    fn zsh_corpus_chained_exclusion_removes_match() {
8862        let _g = crate::test_util::global_state_lock();
8863        assert!(
8864            !ext_glob_match("*.c~boo*~foo*", "foo.c"),
8865            "chained exclusion excludes foo.c",
8866        );
8867    }
8868
8869    /// `Misc/globtests` — `[[ fofo = (fo#)# ]]` — outer `(...)#` is
8870    /// zero-or-more closure over `fo#` (one f followed by zero+ o).
8871    #[test]
8872    fn zsh_corpus_closure_of_closure_matches_repeated_fo() {
8873        let _g = crate::test_util::global_state_lock();
8874        assert!(ext_glob_match("(fo#)#", "fofo"), "(fo#)# matches fofo");
8875    }
8876
8877    /// `Misc/globtests` — `[[ ffo = (fo#)# ]]` — "ffo" = "f"+"fo".
8878    #[test]
8879    fn zsh_corpus_closure_matches_ffo() {
8880        let _g = crate::test_util::global_state_lock();
8881        assert!(ext_glob_match("(fo#)#", "ffo"), "(fo#)# matches ffo");
8882    }
8883
8884    /// `Misc/globtests` — `[[ xfoooofof = (fo#)# ]]` — leading "x"
8885    /// breaks the pattern.
8886    #[test]
8887    fn zsh_corpus_closure_rejects_leading_x() {
8888        let _g = crate::test_util::global_state_lock();
8889        assert!(
8890            !ext_glob_match("(fo#)#", "xfoooofof"),
8891            "(fo#)# rejects leading x",
8892        );
8893    }
8894
8895    /// `Misc/globtests` — `[[ foo = ((^x)) ]]` — `(^x)` matches one
8896    /// char that isn't 'x'. "foo" has 'f' first, so matches.
8897    /// Note: zsh's `(^x)` is exactly-one-not-x with extendedglob.
8898    #[test]
8899    fn zsh_corpus_negation_caret_matches_non_x_start() {
8900        let _g = crate::test_util::global_state_lock();
8901        assert!(ext_glob_match("((^x)*)", "foo"), "(^x)* matches foo");
8902    }
8903
8904    /// `Misc/globtests` — `[[ foo = ((^foo)) ]]` — `(^foo)` is "not foo",
8905    /// "foo" matches `foo`, so excluded → false.
8906    #[test]
8907    fn zsh_corpus_negation_caret_rejects_full_match() {
8908        let _g = crate::test_util::global_state_lock();
8909        assert!(
8910            !ext_glob_match("((^foo))", "foo"),
8911            "(^foo) rejects 'foo' itself",
8912        );
8913    }
8914
8915    /// `Misc/globtests` — `[[ abcd = ?(a|b)c#d ]]` — `?(a|b)` is
8916    /// "zero-or-one of a|b", `c#d` is "zero+ c then d". "abcd" = a + b? no, ?
8917    /// is single char or alternation. Let me re-read: `?(a|b)` matches
8918    /// one char which is `a` OR `b`. So "abcd": ?=a, then needs `c#d`
8919    /// = (c)*d, matches "bcd" if first char then need cd. Hmm.
8920    /// Actually: `?(a|b)` = `?` then `(a|b)` as separate? More likely
8921    /// `?(a|b)` is ksh-style "0-or-1 of a|b" only with KSH_GLOB.
8922    /// In zsh native (extendedglob) `?` is single char, `(a|b)` is
8923    /// alternation. So `?(a|b)c#d` = anychar (a|b) c* d. "abcd" =
8924    /// a + b + (zero c) + ... fails. The ztst says it MATCHES.
8925    /// Conclusion: requires KSH_GLOB which we may not enable.
8926    #[test]
8927    fn zsh_corpus_ksh_question_alternation() {
8928        let _g = crate::test_util::global_state_lock();
8929        assert!(
8930            ext_glob_match("?(a|b)c#d", "abcd"),
8931            "?(a|b)c#d matches abcd",
8932        );
8933    }
8934
8935    // ── patmatchlen — c:2649 ─────────────────────────────────────────
8936
8937    /// `Src/pattern.c:2649` — `int patmatchlen(void)` returns the
8938    /// byte-length of the last successful pattry match. After a
8939    /// successful match against `"hello"` with pattern `"hel*"`,
8940    /// the recorded length is 5 (all of "hello" was consumed).
8941    #[test]
8942    fn patmatchlen_records_consumed_byte_length() {
8943        let _g = crate::test_util::global_state_lock();
8944        let prog = compile("hel*");
8945        assert!(pattry(&prog, "hello"), "hel* matches hello");
8946        assert_eq!(patmatchlen(), 5, "all 5 bytes of 'hello' consumed by hel*",);
8947    }
8948
8949    /// Anchored prefix match: `"foo"` against `"foobar"` with the
8950    /// `NOANCH` form is the user-visible case `[[ foobar = foo* ]]`,
8951    /// which consumes all 6 bytes when the pattern matches the whole
8952    /// string.
8953    #[test]
8954    fn patmatchlen_full_string_match_returns_full_length() {
8955        let _g = crate::test_util::global_state_lock();
8956        let prog = compile("foo*");
8957        assert!(pattry(&prog, "foobar"));
8958        assert_eq!(patmatchlen(), 6, "foo* against 'foobar' = 6 bytes");
8959    }
8960
8961    // ── patmatchindex (c:4004) ──────────────────────────────────────
8962
8963    /// `patmatchindex` on a literal-byte range returns the byte at
8964    /// the requested position.
8965    #[test]
8966    fn patmatchindex_literal_byte_at_index() {
8967        let _g = crate::test_util::global_state_lock();
8968        let range = b"abc";
8969        assert_eq!(patmatchindex(range, 0), Some((Some(b'a'), 0)));
8970        assert_eq!(patmatchindex(range, 1), Some((Some(b'b'), 0)));
8971        assert_eq!(patmatchindex(range, 2), Some((Some(b'c'), 0)));
8972        assert_eq!(patmatchindex(range, 3), None, "out-of-range index");
8973    }
8974
8975    /// `patmatchindex` on a PP_ALPHA class marker returns `(None,
8976    /// PP_ALPHA)` to signal the class without a literal char.
8977    #[test]
8978    fn patmatchindex_posix_class_marker_returns_mtp() {
8979        let _g = crate::test_util::global_state_lock();
8980        // Meta + PP_ALPHA = 0x83 + 1 = 0x84
8981        let range = &[Meta + PP_ALPHA as u8];
8982        let r = patmatchindex(range, 0);
8983        assert_eq!(r, Some((None, PP_ALPHA)));
8984    }
8985
8986    // ── mb_patmatchrange (c:3610) ───────────────────────────────────
8987
8988    /// `mb_patmatchrange` literal hit on `'a'`.
8989    #[test]
8990    fn mb_patmatchrange_literal_ascii_hits() {
8991        let _g = crate::test_util::global_state_lock();
8992        let range = b"a";
8993        let mut mtp = -1;
8994        assert!(
8995            mb_patmatchrange(range, 'a', 0, None, Some(&mut mtp)),
8996            "literal 'a' matches"
8997        );
8998        assert_eq!(mtp, 0, "literal hit sets mtp=0");
8999    }
9000
9001    /// `mb_patmatchrange` PP_DIGIT class hit on `'5'`.
9002    #[test]
9003    fn mb_patmatchrange_digit_class_hits() {
9004        let _g = crate::test_util::global_state_lock();
9005        let range = &[Meta + PP_DIGIT as u8];
9006        assert!(mb_patmatchrange(range, '5', 0, None, None));
9007        assert!(!mb_patmatchrange(range, 'x', 0, None, None));
9008    }
9009
9010    /// `mb_patmatchrange` PP_RANGE on `'b'` in `a..c`.
9011    #[test]
9012    fn mb_patmatchrange_range_hits_middle() {
9013        let _g = crate::test_util::global_state_lock();
9014        // Meta + PP_RANGE then two literal bytes for the endpoints.
9015        let range = &[Meta + PP_RANGE as u8, b'a', b'c'];
9016        assert!(mb_patmatchrange(range, 'b', 0, None, None));
9017        assert!(mb_patmatchrange(range, 'a', 0, None, None));
9018        assert!(mb_patmatchrange(range, 'c', 0, None, None));
9019        assert!(!mb_patmatchrange(range, 'd', 0, None, None));
9020    }
9021
9022    // ── mb_patmatchindex (c:3767) ───────────────────────────────────
9023
9024    /// `mb_patmatchindex` literal char at index 0.
9025    #[test]
9026    fn mb_patmatchindex_literal_first_index() {
9027        let _g = crate::test_util::global_state_lock();
9028        let range = b"xyz";
9029        let r = mb_patmatchindex(range, 0);
9030        assert_eq!(r, Some((Some('x'), 0)));
9031    }
9032
9033    /// `mb_patmatchindex` on PP_UPPER marker at index 0.
9034    #[test]
9035    fn mb_patmatchindex_class_marker_returns_mtp() {
9036        let _g = crate::test_util::global_state_lock();
9037        let range = &[Meta + PP_UPPER as u8];
9038        assert_eq!(mb_patmatchindex(range, 0), Some((None, PP_UPPER)));
9039    }
9040
9041    // ═══════════════════════════════════════════════════════════════════
9042    // Additional C-parity tests for Src/pattern.c bit-flag predicates.
9043    // ═══════════════════════════════════════════════════════════════════
9044
9045    /// c:200 — P_ISBRANCH checks bit 0x20.
9046    #[test]
9047    fn P_ISBRANCH_recognizes_bit_0x20() {
9048        assert!(P_ISBRANCH(0x20));
9049        assert!(P_ISBRANCH(0x20 | 0x01));
9050        assert!(P_ISBRANCH(0xFF));
9051        assert!(!P_ISBRANCH(0x00));
9052        assert!(!P_ISBRANCH(0x1F));
9053    }
9054
9055    /// c:201 — P_ISEXCLUDE checks bits 0x30 = 0x10 | 0x20 (BOTH must be set).
9056    #[test]
9057    fn P_ISEXCLUDE_requires_both_bits() {
9058        assert!(P_ISEXCLUDE(0x30));
9059        assert!(P_ISEXCLUDE(0x30 | 0x01));
9060        assert!(!P_ISEXCLUDE(0x10), "only 0x10 — must require both");
9061        assert!(!P_ISEXCLUDE(0x20), "only 0x20 — must require both");
9062        assert!(!P_ISEXCLUDE(0x00));
9063    }
9064
9065    /// c:202 — P_NOTDOT checks bit 0x40.
9066    #[test]
9067    fn P_NOTDOT_recognizes_bit_0x40() {
9068        assert!(P_NOTDOT(0x40));
9069        assert!(P_NOTDOT(0x40 | 0x01));
9070        assert!(P_NOTDOT(0xFF));
9071        assert!(!P_NOTDOT(0x00));
9072        assert!(!P_NOTDOT(0x3F));
9073    }
9074
9075    /// c:216-218 — P_SIMPLE/HSTART/PURESTR are distinct bits.
9076    #[test]
9077    fn p_flag_constants_are_distinct() {
9078        assert_eq!(P_SIMPLE & P_HSTART, 0);
9079        assert_eq!(P_SIMPLE & P_PURESTR, 0);
9080        assert_eq!(P_HSTART & P_PURESTR, 0);
9081    }
9082
9083    /// c:216 — P_SIMPLE is bit 0 (= 0x01).
9084    #[test]
9085    fn p_simple_is_bit_zero() {
9086        assert_eq!(P_SIMPLE, 0x01);
9087    }
9088
9089    /// c:217 — P_HSTART is bit 1 (= 0x02).
9090    #[test]
9091    fn p_hstart_is_bit_one() {
9092        assert_eq!(P_HSTART, 0x02);
9093    }
9094
9095    /// c:218 — P_PURESTR is bit 2 (= 0x04).
9096    #[test]
9097    fn p_purestr_is_bit_two() {
9098        assert_eq!(P_PURESTR, 0x04);
9099    }
9100
9101    /// c:406-407 — PA_NOALIGN=1, PA_UNMETA=2 (single-bit flags).
9102    #[test]
9103    fn pa_flag_constants_canonical() {
9104        assert_eq!(PA_NOALIGN, 1);
9105        assert_eq!(PA_UNMETA, 2);
9106    }
9107
9108    /// PA_NOALIGN | PA_UNMETA combine without overlap.
9109    #[test]
9110    fn pa_flags_pairwise_disjoint() {
9111        assert_eq!(PA_NOALIGN & PA_UNMETA, 0);
9112    }
9113
9114    /// c:336 — metacharinc on ASCII char advances by 1 byte.
9115    #[test]
9116    fn metacharinc_ascii_advances_one() {
9117        let _g = crate::test_util::global_state_lock();
9118        assert_eq!(metacharinc("hello", 0), 1);
9119        assert_eq!(metacharinc("hello", 4), 5);
9120    }
9121
9122    /// c:336 — metacharinc at end-of-string returns same pos.
9123    #[test]
9124    fn metacharinc_end_of_string_returns_same() {
9125        let _g = crate::test_util::global_state_lock();
9126        assert_eq!(metacharinc("abc", 3), 3, "at end → no advance");
9127    }
9128
9129    /// c:336 — metacharinc on multibyte char advances by codepoint width.
9130    #[test]
9131    fn metacharinc_multibyte_advances_by_codepoint_len() {
9132        let _g = crate::test_util::global_state_lock();
9133        // 'é' is 2 bytes in UTF-8.
9134        assert_eq!(metacharinc("é", 0), 2);
9135        // '日' is 3 bytes.
9136        assert_eq!(metacharinc("日", 0), 3);
9137    }
9138
9139    // ═══════════════════════════════════════════════════════════════════
9140    // Additional C-parity tests for Src/pattern.c
9141    // c:155 P_ISBRANCH / c:161 P_ISEXCLUDE / c:167 P_NOTDOT /
9142    // c:262 metacharinc / c:502 patcompstart / c:536 patcompile /
9143    // c:1008 patgetglobflags / c:1129 range_type / c:1152 pattern_range_to_string
9144    // c:2059 charref / c:2066 charnext / c:2153 pattry / c:2304 patmatchlen
9145    // ═══════════════════════════════════════════════════════════════════
9146
9147    /// c:262 — `metacharinc` is pure for ASCII input.
9148    #[test]
9149    fn metacharinc_is_pure_for_ascii() {
9150        for s in ["", "a", "abc", "hello"] {
9151            for pos in 0..s.len() {
9152                let first = metacharinc(s, pos);
9153                for _ in 0..3 {
9154                    assert_eq!(
9155                        metacharinc(s, pos),
9156                        first,
9157                        "metacharinc({:?}, {}) must be pure",
9158                        s,
9159                        pos
9160                    );
9161                }
9162            }
9163        }
9164    }
9165
9166    /// c:536 — `patcompile("", 0, None)` returns Option<Patprog>.
9167    #[test]
9168    fn patcompile_returns_option_patprog_type() {
9169        let _g = crate::test_util::global_state_lock();
9170        let _: Option<Patprog> = patcompile("", 0, None);
9171    }
9172
9173    /// c:1008 — `patgetglobflags("")` empty returns None.
9174    #[test]
9175    fn patgetglobflags_empty_returns_none() {
9176        let _g = crate::test_util::global_state_lock();
9177        assert!(patgetglobflags("").is_none(), "empty → None");
9178    }
9179
9180    /// c:1008 — `patgetglobflags` returns Option<(i32, i64, usize)> type.
9181    #[test]
9182    fn patgetglobflags_returns_option_tuple_type() {
9183        let _g = crate::test_util::global_state_lock();
9184        let _: Option<(i32, i64, usize)> = patgetglobflags("abc");
9185    }
9186
9187    /// c:1129 — `range_type("")` empty returns None.
9188    #[test]
9189    fn range_type_empty_returns_none() {
9190        assert!(range_type("").is_none(), "empty → None");
9191    }
9192
9193    /// c:1129 — `range_type` returns Option<usize>.
9194    #[test]
9195    fn range_type_returns_option_usize_type() {
9196        let _: Option<usize> = range_type("abc");
9197    }
9198
9199    /// c:1152 — `pattern_range_to_string("")` empty returns empty String.
9200    #[test]
9201    fn pattern_range_to_string_empty_returns_empty() {
9202        assert_eq!(pattern_range_to_string(""), "");
9203    }
9204
9205    /// c:1152 — `pattern_range_to_string` is pure.
9206    #[test]
9207    fn pattern_range_to_string_is_pure() {
9208        for s in ["", "abc", "a-z"] {
9209            let first = pattern_range_to_string(s);
9210            for _ in 0..3 {
9211                assert_eq!(
9212                    pattern_range_to_string(s),
9213                    first,
9214                    "pattern_range_to_string({:?}) must be pure",
9215                    s
9216                );
9217            }
9218        }
9219    }
9220
9221    /// c:2059 — `charref("", 0)` empty returns None.
9222    #[test]
9223    fn charref_empty_returns_none() {
9224        assert!(charref("", 0).is_none(), "empty → None");
9225    }
9226
9227    /// c:2066 — `charnext("", 0)` empty returns 0 (no advance).
9228    #[test]
9229    fn charnext_empty_returns_zero() {
9230        assert_eq!(charnext("", 0), 0);
9231    }
9232
9233    /// c:2304 — `patmatchlen` returns i32 (compile-time type pin).
9234    #[test]
9235    fn patmatchlen_returns_i32_type() {
9236        let _g = crate::test_util::global_state_lock();
9237        let _: i32 = patmatchlen();
9238    }
9239
9240    /// c:155 — P_ISBRANCH/P_ISEXCLUDE/P_NOTDOT all return bool (type pin).
9241    #[test]
9242    fn p_predicates_return_bool_type() {
9243        let _: bool = P_ISBRANCH(0);
9244        let _: bool = P_ISEXCLUDE(0);
9245        let _: bool = P_NOTDOT(0);
9246    }
9247}