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. `patmatchrange` — pattern.c:3865. The Cpattern-byte-stream
34//!      walker, relocated here from `zle/compmatch.rs` (the Rule C
35//!      move this section used to say was pending). The earlier
36//!      Rust-only entries `patmatchrange(&[char], char, igncase)` and
37//!      `patmatchindex(&[char], idx)` were deleted as Rule B /
38//!      semantic deviations; see the deletion sites.
39//!  16. String pre-processing (`patmungestring`, `patallocstr`,
40//!      `pattrystart`)
41//!  17. Module-loader / disable mgmt (`startpatternscope`,
42//!      `endpatternscope`, `savepatterndisables`,
43//!      `restorepatterndisables`, `clearpatterndisables`,
44//!      `freepatprog`, `pat_enables`)
45//!  18. (Section removed — formerly held Rust-only convenience entries
46//!      `patmatch(pat, text)`, `patmatchlen(prog, string)`, `patrepeat(
47//!      prog, s, max)`, `mb_patmatchrange`, `mb_patmatchindex`. All
48//!      deleted: `patmatch` got renamed to the C-faithful bytecode-
49//!      walker name; the others had zero Rust callers + Rule S1
50//!      signature deviations. `haswilds` is a real C name — port lives
51//!      in section 4 helpers.)
52//!
53//! See `docs/PORT.md` Rules A/B/C/D/E.
54
55#![allow(non_upper_case_globals)]
56#![allow(non_camel_case_types)]
57
58use crate::ported::params::{paramtab, paramtab_hashed_storage};
59use crate::ported::utils::ztrsub;
60use crate::ported::zle::zle_h::{COMP_LIST_COMPLETE, COMP_LIST_EXPAND};
61pub use crate::ported::zsh_h::{
62    patstralloc, Patstralloc, GF_BACKREF, GF_IGNCASE, GF_LCMATCHUC, GF_MATCHREF, GF_MULTIBYTE,
63    PAT_ANY, PAT_FILE, PAT_FILET, PAT_HAS_EXCLUDP, PAT_HEAPDUP, PAT_LCMATCHUC, PAT_NOANCH,
64    PAT_NOGLD, PAT_NOTEND, PAT_NOTSTART, PAT_PURES, PAT_SCAN, PAT_STATIC, PAT_ZDUP, PP_ALNUM,
65    PP_ALPHA, PP_ASCII, PP_BLANK, PP_CNTRL, PP_DIGIT, PP_GRAPH, PP_IDENT, PP_IFS, PP_IFSSPACE,
66    PP_INCOMPLETE, PP_INVALID, PP_LOWER, PP_PRINT, PP_PUNCT, PP_RANGE, PP_SPACE, PP_UPPER, PP_WORD,
67    PP_XDIGIT, ZMB_INCOMPLETE, ZMB_INVALID, ZPC_SEG_COUNT,
68};
69use crate::utils::zerrnam;
70use crate::zsh_h::{
71    isset, patprog, Bang, Bar, Hat, Inang, Inbrack, Inpar, Marker, Meta, Nularg, Outbrack, Pound,
72    Quest, Star, BASHAUTOLIST, CASEGLOB, CASEPATHS, EXTENDEDGLOB, KSHGLOB, MULTIBYTE,
73    NUMERICGLOBSORT, PM_HASHED, PM_TYPE, RCQUOTES, SHGLOB, SORTIT_IGNORING_BACKSLASHES,
74    SORTIT_NUMERICALLY, ZPC_BAR, ZPC_BNULLKEEP, ZPC_COUNT, ZPC_HASH, ZPC_HAT, ZPC_INANG,
75    ZPC_INBRACK, ZPC_INPAR, ZPC_KSH_AT, ZPC_KSH_BANG, ZPC_KSH_BANG2, ZPC_KSH_PLUS, ZPC_KSH_QUEST,
76    ZPC_KSH_STAR, ZPC_NULL, ZPC_OUTPAR, ZPC_QUEST, ZPC_SLASH, ZPC_STAR, ZPC_TILDE,
77};
78use std::sync::atomic::{AtomicI32, AtomicUsize, Ordering};
79use std::sync::Mutex;
80
81// =====================================================================
82// 6. ZPC_* enum from zsh.h:1644 — indexes into the active-pattern-
83// characters table that compile-time and runtime both consult.
84// =====================================================================
85
86/// Maximum captures, from `pattern.c:94 NSUBEXP`.
87pub const NSUBEXP: usize = 9;
88
89// =====================================================================
90// 1. P_* opcode constants — pattern.c:97-127
91//
92// Numbered identically to C so a buffer compiled by this port matches
93// the C source's bytecode-cache format byte-for-byte (modulo native
94// endianness; we pin LE — see file header).
95// =====================================================================
96/// `P_END` constant.
97pub const P_END: u8 = 0x00; // c:97  End of program.
98/// `P_EXCSYNC` constant.
99pub const P_EXCSYNC: u8 = 0x01; // c:98  Test if following exclude already failed
100/// `P_EXCEND` constant.
101pub const P_EXCEND: u8 = 0x02; // c:99  Test if exclude matched orig branch
102/// `P_BACK` constant.
103pub const P_BACK: u8 = 0x03; // c:100 Match "", "next" ptr points backward.
104/// `P_EXACTLY` constant.
105pub const P_EXACTLY: u8 = 0x04; // c:101 lstr — match this string.
106/// `P_NOTHING` constant.
107pub const P_NOTHING: u8 = 0x05; // c:102 Match empty string.
108/// `P_ONEHASH` constant.
109pub const P_ONEHASH: u8 = 0x06; // c:103 node — match 0 or more of preceding simple.
110/// `P_TWOHASH` constant.
111pub const P_TWOHASH: u8 = 0x07; // c:104 node — match 1 or more of preceding simple.
112/// `P_GFLAGS` constant.
113pub const P_GFLAGS: u8 = 0x08; // c:105 long — match nothing and set globbing flags.
114/// `P_ISSTART` constant.
115pub const P_ISSTART: u8 = 0x09; // c:106 Match start of string.
116/// `P_ISEND` constant.
117pub const P_ISEND: u8 = 0x0a; // c:107 Match end of string.
118/// `P_COUNTSTART` constant.
119pub const P_COUNTSTART: u8 = 0x0b; // c:108 Initialise P_COUNT.
120/// `P_COUNT` constant.
121pub const P_COUNT: u8 = 0x0c; // c:109 3*long uc* node — match a number of repetitions.
122/// `P_BRANCH` constant.
123pub const P_BRANCH: u8 = 0x20; // c:112 node — match this alternative, or the next.
124/// `P_WBRANCH` constant.
125pub const P_WBRANCH: u8 = 0x21; // c:113 uc* node — P_BRANCH, but match at least 1 char.
126/// `P_EXCLUDE` constant.
127pub const P_EXCLUDE: u8 = 0x30; // c:114 uc* node — exclude this from previous branch.
128/// `P_EXCLUDP` constant.
129pub const P_EXCLUDP: u8 = 0x31; // c:115 uc* node — exclude, using full file path so far.
130/// `P_ANY` constant.
131pub const P_ANY: u8 = 0x40; // c:117 Match any one character.
132/// `P_ANYOF` constant.
133pub const P_ANYOF: u8 = 0x41; // c:118 str — match any character in this string.
134/// `P_ANYBUT` constant.
135pub const P_ANYBUT: u8 = 0x42; // c:119 str — match any character not in this string.
136/// `P_STAR` constant.
137pub const P_STAR: u8 = 0x43; // c:120 Match any set of characters.
138/// `P_NUMRNG` constant.
139pub const P_NUMRNG: u8 = 0x44; // c:121 zr,zr — match a numeric range.
140/// `P_NUMFROM` constant.
141pub const P_NUMFROM: u8 = 0x45; // c:122 zr — match a number >= X.
142/// `P_NUMTO` constant.
143pub const P_NUMTO: u8 = 0x46; // c:123 zr — match a number <= X.
144/// `P_NUMANY` constant.
145pub const P_NUMANY: u8 = 0x47; // c:124 Match any set of decimal digits.
146/// `P_OPEN` constant.
147pub const P_OPEN: u8 = 0x80; // c:126 Mark this point in input as start of n.
148/// `P_CLOSE` constant.
149pub const P_CLOSE: u8 = 0x90; // c:127 Analogous to OPEN.
150
151/// Port of `P_ISBRANCH()` from `Src/pattern.c:200`.
152/// C macro `#define P_ISBRANCH(p)   ((p)->l & 0x20)`.
153#[inline]
154pub fn P_ISBRANCH(op: u8) -> bool {
155    (op & 0x20) != 0
156}
157
158/// Port of `P_ISEXCLUDE()` from `Src/pattern.c:201`.
159/// C macro `#define P_ISEXCLUDE(p)	(((p)->l & 0x30) == 0x30)`.
160#[inline]
161pub fn P_ISEXCLUDE(op: u8) -> bool {
162    (op & 0x30) == 0x30
163}
164
165/// Port of `P_NOTDOT()` from `Src/pattern.c:202`.
166/// C macro `#define P_NOTDOT(p)	((p)->l & 0x40)`.
167#[inline]
168pub fn P_NOTDOT(op: u8) -> bool {
169    (op & 0x40) != 0
170}
171
172// =====================================================================
173// 3. Flag-bit constants returned via flagp out-params during compile.
174// pattern.c:216-218
175// =====================================================================
176/// `P_SIMPLE` constant.
177pub const P_SIMPLE: i32 = 0x01; // c:216 Simple enough to be # / ## operand.
178/// `P_HSTART` constant.
179pub const P_HSTART: i32 = 0x02; // c:217 Starts with # or ##'d pattern.
180/// `P_PURESTR` constant.
181pub const P_PURESTR: i32 = 0x04; // c:218 Can be matched with a strcmp.
182
183// =====================================================================
184// 5. PAT_* flag constants — re-exports of zsh.h:1623-1640 already in
185// zsh_h.rs. Re-published here so callers in pattern's API don't need
186// the longer path. C source has these as `#define` in zsh.h, not
187// pattern.c, so the canonical home is zsh_h.rs; we just alias.
188// =====================================================================
189
190// C: `static int patnpar;` — number of active parens (1-indexed at
191// compile time; the *struct* patnpar is the actual count).
192pub static patnpar: AtomicI32 = AtomicI32::new(0); // c:271
193
194// GF_* glob-flag bits live in `zsh.h:1763-1773`, ported to
195// `src/ported/zsh_h.rs:2287-2291` per Rule C. Re-export so pattern's
196// matcher arms can read them without the longer path.
197
198// =====================================================================
199// 7. File-static globals — direct mirror of pattern.c file-scope
200// statics. Each C `static` ports to a Rust `static` of matching name
201// (Rule A) and `Mutex<>` / `Atomic*` for thread-safe access. None
202// are aggregated (Rule D).
203// =====================================================================
204
205// C: `static char *patout, *patcode;` + `static long patsize;` +
206// `static int patalloc;` — pattern.c:267-272 (in macro region).
207//
208// patout: the bytecode buffer.
209// patcode: write cursor (offset into patout).
210// patsize: current logical size.
211// patalloc: allocated capacity.
212//
213// In Rust, `Vec<u8>` already tracks both len and capacity, so we
214// hold just the buffer; patcode/patsize/patalloc are derived.
215pub static patout: Mutex<Vec<u8>> = Mutex::new(Vec::new()); // c:267
216
217// C: `static int patflags;` — current PAT_* flag set during compile.
218pub static patflags: AtomicI32 = AtomicI32::new(0); // c:272
219
220// C: `static int patglobflags;` — current globbing flags during compile.
221pub static patglobflags: AtomicI32 = AtomicI32::new(0); // c:273
222
223/// Port of file-static `int patinlen` from `pattrystate` struct
224/// at `Src/pattern.c:1877` (accessed via `#define patinlen
225/// (pattrystate.patinlen)` at line 1894). Length in metafied bytes
226/// of the last successful pattry match; computed at the end of
227/// `pattry`/`pattrylen`/`pattryrefs` (`patinput - patinstart`,
228/// pattern.c:2508). Read by `patmatchlen()` and by the
229/// `${var//pat/repl}` paramsubst pipeline that needs to know how
230/// much of the input was consumed.
231pub static patinlen: AtomicI32 = AtomicI32::new(0); // c:1877
232
233/// Port of file-static `int globdots` from the `pattrystate` struct at
234/// `Src/pattern.c:1890` ("Glob initial dots?"), reached through
235/// `#define globdots (pattrystate.globdots)` at `Src/pattern.c:1903`.
236/// Set once per top-level match from the program's flags —
237/// `Src/pattern.c:2494` `globdots = !(patflags & PAT_NOGLD);` — and
238/// forced on for the duration of an exclusion operand
239/// (`Src/pattern.c:3116` `globdots = 1; /* OK to match . first */`,
240/// restored at `Src/pattern.c:3192`).
241///
242/// The matcher reads it in exactly two places, both guarded by
243/// `P_NOTDOT` — the "opcode has bit 0x40" test (`Src/pattern.c:202`)
244/// that marks the wildcard opcodes `P_ANY`/`P_ANYOF`/`P_ANYBUT`/
245/// `P_STAR`/`P_NUM*` (`Src/pattern.c:116-124`): `Src/pattern.c:2731`
246/// (top of the node walk) and `Src/pattern.c:3322` (the closure
247/// operand). That is the whole of zsh's "a wildcard does not match a
248/// leading dot" rule; a LITERAL dot in the pattern is unaffected
249/// because `P_EXACTLY` does not carry bit 0x40.
250pub static globdots: AtomicI32 = AtomicI32::new(1); // c:1890
251
252// =====================================================================
253// 12. Char-decode helpers — pattern.c:327, :336, :1909-1997
254// =====================================================================
255
256/// Port of `clear_shiftstate()` from `Src/pattern.c:327`. C uses
257/// `mbstate_t`; Rust `char` is already a code point, so no shift
258/// state to clear.
259pub fn clear_shiftstate() {} // c:327
260
261/// Port of `metacharinc()` from `Src/pattern.c:336`.
262/// C: `static wchar_t metacharinc(char **x)`.
263/// Advances `*x` past one metafied / multibyte char and returns the
264/// decoded codepoint. The C body branches on:
265///   - `GF_MULTIBYTE` clear OR high-bit-clear: single-byte path
266///     with `itok(*x)` zsh-token translation and `Meta` xor-32
267///   - else: `mbrtowc` over the metafied bytes with state machine
268///
269/// **Rust port:** UTF-32-native delegation to `&str.chars()`.
270/// Delegates to Rust's native UTF-8 iterator — both C branches
271/// collapse because Rust's `char` is already the wide-char form,
272/// and the stored slice is the post-Meta-decode form (zshrs's
273/// source bytes are already UTF-8, not Meta-encoded). The `itok` →
274/// `ztokens[]` zsh-token table mapping doesn't apply because the
275/// pattern compiler stores raw chars; translation happens at
276/// compile time, not at scan time.
277///
278/// Rust signature differs from C `metacharinc(char **x)`: C mutates
279/// `*x` to advance the pointer; Rust returns the new byte position
280/// since `&str` length is immutable. Callers update their cursor
281/// from the return value.
282pub fn metacharinc(s: &str, pos: usize) -> usize {
283    // c:336
284    // c:343-360 single-byte short-circuit + c:363-380 mbrtowc loop:
285    // both collapse to Rust's `chars().next()` which decodes one
286    // valid UTF-8 codepoint from the slice, regardless of byte width.
287    s[pos..]
288        .chars()
289        .next()
290        .map(|c| pos + c.len_utf8())
291        .unwrap_or(pos)
292}
293
294// =====================================================================
295// 8. Bytecode write helpers — pattern.c:412-1856
296// =====================================================================
297
298/// Port of the anonymous `enum { PA_NOALIGN = 1, PA_UNMETA = 2 };`
299/// from `Src/pattern.c:405-408`. Flags passed as the `paflags` arg
300/// to `patadd`.
301pub const PA_NOALIGN: i32 = 1; // c:406
302/// `PA_UNMETA` constant.
303pub const PA_UNMETA: i32 = 2; // c:407
304
305/// Port of `patadd()` from `Src/pattern.c:412`.
306/// C: `static void patadd(char *add, int ch, long n,
307/// int paflags)` from `Src/pattern.c:410-450`.
308///
309/// Append `n` bytes from `add` (or a single `ch` byte when `add ==
310/// None`) to `patout`, growing the backing storage if needed and
311/// padding to the C `union upat` alignment unless `PA_NOALIGN` is
312/// set. With `PA_UNMETA`, walk the input as zsh-metafied bytes
313/// (Meta + (b^32)) and untokenize (`itok` → `ztokens[]`).
314///
315/// **The previous Rust impl had three concrete defects:**
316///   1. Declared `-> i64` returning the starting offset. C is
317///      `static void`; callers use side-effects on `patcode`,
318///      not a return value.
319///   2. When `add == None`, the C body writes `ch` **once**
320///      (`*patcode++ = ch;`). The Rust port looped `0..n` pushing
321///      `ch` n times, so a single-byte literal grew to n copies.
322///   3. Skipped the C alignment-round-up to `sizeof(union upat)`
323///      (8 bytes on every supported arch — c:417-418), so any
324///      caller reading `patout` as a `[upat]` would mis-align.
325///
326/// zshrs's `patcode` pointer collapses into `patout.len()`; the
327/// realloc dance in C (c:419-425) is implicit via `Vec::extend`.
328/// `patsize` updates are the post-write `patout.len()`.
329fn patadd(add: Option<&[u8]>, ch: u8, n: i64, paflags: i32) {
330    use crate::ported::lex::ztokens as ztokens_str;
331    use crate::ported::zsh_h::Pound;
332    use crate::ported::ztype_h::itok;
333    let ztokens = ztokens_str.as_bytes();
334    // c:412
335    // c:415 — `long newpatsize = patsize + n;`
336    let mut buf = patout.lock().unwrap();
337    let patsize = buf.len() as i64;
338    let mut newpatsize = patsize + n;
339    // c:416-418 — round up to upat-union alignment (sizeof(union upat) =
340    // 8 on every supported arch) unless PA_NOALIGN.
341    if (paflags & PA_NOALIGN) == 0 {
342        // c:416
343        let upat = 8i64; // c:417 sizeof(union upat)
344        newpatsize = (newpatsize + upat - 1) & !(upat - 1); // c:417-418
345    }
346    // c:419-425 — realloc; Rust Vec auto-grows so just resize_to.
347    if newpatsize > buf.len() as i64 {
348        buf.resize(newpatsize as usize, 0);
349    }
350    // c:426 — `patsize = newpatsize;` — patsize is the buffer's
351    // logical write head; zshrs tracks it via buf.len() AFTER the
352    // write below.
353    let mut patcode_pos = patsize as usize; // c:Src/pattern.c — `patcode` pointer.
354
355    if let Some(add_bytes) = add {
356        // c:427 — `if (add) {`
357        if (paflags & PA_UNMETA) != 0 {
358            // c:428-442 — PA_UNMETA: walk add_bytes, unmetafy + untokenize
359            // as we go. Meta chars aren't counted in n. itok bytes route
360            // through ztokens[*add - Pound].
361            let mut idx = 0usize;
362            let mut remaining = n;
363            while remaining > 0 && idx < add_bytes.len() {
364                let b = add_bytes[idx];
365                if itok(b) {
366                    // c:434-435 — `if (itok(*add)) *patcode++ =
367                    //               ztokens[*add++ - Pound];`
368                    if idx < add_bytes.len() {
369                        let tok_idx = b.wrapping_sub(Pound as u8) as usize;
370                        if tok_idx < ztokens.len() {
371                            // c:435
372                            if patcode_pos < buf.len() {
373                                buf[patcode_pos] = ztokens[tok_idx];
374                            }
375                            patcode_pos += 1;
376                        }
377                        idx += 1;
378                    }
379                } else if b == Meta {
380                    // c:436-438 — `else if (*add == Meta) { add++;
381                    //               *patcode++ = *add++ ^ 32; }`
382                    idx += 1;
383                    if idx < add_bytes.len() {
384                        if patcode_pos < buf.len() {
385                            buf[patcode_pos] = add_bytes[idx] ^ 32;
386                        }
387                        patcode_pos += 1;
388                        idx += 1;
389                    }
390                } else {
391                    // c:439-440 — `else { *patcode++ = *add++; }`
392                    if patcode_pos < buf.len() {
393                        buf[patcode_pos] = b;
394                    }
395                    patcode_pos += 1;
396                    idx += 1;
397                }
398                remaining -= 1;
399            }
400        } else {
401            // c:444-445 — `while (n--) *patcode++ = *add++;` — plain copy.
402            let n_actual = (n as usize).min(add_bytes.len());
403            for &b in &add_bytes[..n_actual] {
404                if patcode_pos < buf.len() {
405                    buf[patcode_pos] = b;
406                }
407                patcode_pos += 1;
408            }
409        }
410    } else {
411        // c:447-448 — `else *patcode++ = ch;` — single-byte write.
412        if patcode_pos < buf.len() {
413            buf[patcode_pos] = ch;
414        }
415        patcode_pos += 1;
416    }
417    // c:449 — `patcode = patout + patsize;` — restore the post-write
418    // head so subsequent patadd calls append at the next slot. zshrs
419    // tracks via buf.len(); trim back to newpatsize so the alignment
420    // padding stays visible to consumers as zeroes.
421    let _ = patcode_pos;
422    // (No explicit truncate — buf.len() == newpatsize already.)
423}
424
425/// Port of `patcompcharsset()` from `Src/pattern.c:464`.
426///
427/// Initializes the `zpc_special` table for the active globbing
428/// regime. The C source resets every ZPC_* slot to its literal
429/// character, then masks off characters via `Marker` (0xa2 — the
430/// canonical "invalid" sentinel) for three option-driven cases:
431///   1. `!isset(EXTENDEDGLOB)` → Tilde/Hat/Hash disabled.
432///   2. `!isset(KSHGLOB)`      → KSH_QUEST/STAR/PLUS/BANG/BANG2/AT disabled.
433///   3. `isset(SHGLOB)`        → Inpar/Inang disabled.
434///
435pub fn patcompcharsset() {
436    // c:464
437    let mut sp = zpc_special.lock().unwrap();
438    *sp = [0u8; ZPC_COUNT as usize];
439    // c:469 — `memcpy(zpc_special, zpc_chars, ZPC_COUNT)`. The default
440    // char for every ZPC_* slot. Direct positional assignment here
441    // since zshrs doesn't carry the `zpc_chars` const array yet.
442    //
443    // NOTE on token bytes: C's `zpc_chars[]` (pattern.c:248) uses
444    // the TOKENIZED high-bit bytes (`Bar`, `Tilde`, etc.); the C
445    // shell lexer tokenizes raw ASCII to these only inside grouped
446    // alternation. zshrs's parser doesn't yet run that pre-tokenize
447    // pass — every byte reaches patcompile as raw ASCII. Switching
448    // these slots to the high-bit form would break ~80 library
449    // tests that rely on raw `|` triggering alternation. The
450    // narrower fix for bug #12 lives in vm_helper::glob_match_static
451    // where we conservatively escape pattern bytes the param-strip
452    // path could not have intended as metacharacters.
453    sp[ZPC_SLASH as usize] = b'/';
454    sp[ZPC_NULL as usize] = 0;
455    sp[ZPC_BAR as usize] = b'|';
456    sp[ZPC_OUTPAR as usize] = b')';
457    sp[ZPC_TILDE as usize] = b'~';
458    sp[ZPC_INPAR as usize] = b'(';
459    sp[ZPC_QUEST as usize] = b'?';
460    sp[ZPC_STAR as usize] = b'*';
461    sp[ZPC_INBRACK as usize] = b'[';
462    sp[ZPC_INANG as usize] = b'<';
463    sp[ZPC_HAT as usize] = b'^';
464    sp[ZPC_HASH as usize] = b'#';
465    sp[ZPC_BNULLKEEP as usize] = 0;
466    // c:478-490 — KSH_GLOB slots (omitted from previous Rust port).
467    // Each defaults to the literal ksh-glob trigger char. The
468    // option-mask pass below disables them when KSHGLOB is off.
469    sp[ZPC_KSH_QUEST as usize] = b'?';
470    sp[ZPC_KSH_STAR as usize] = b'*';
471    sp[ZPC_KSH_PLUS as usize] = b'+';
472    sp[ZPC_KSH_BANG as usize] = b'!';
473    sp[ZPC_KSH_BANG2 as usize] = b'!';
474    sp[ZPC_KSH_AT as usize] = b'@';
475
476    let marker_byte = Marker as u32 as u8;
477
478    // c:471-478 — `for (...; i < ZPC_COUNT; ...) if (*disp) *spp = Marker;`
479    // Apply user disables from `disable -p` BEFORE the option-driven
480    // masks (so EXTENDEDGLOB / KSHGLOB / SHGLOB layer over the per-
481    // pattern disables).
482    {
483        let disp = zpc_disables.lock().unwrap();
484        for i in 0..(ZPC_COUNT as usize) {
485            if disp[i] != 0 {
486                sp[i] = marker_byte; // c:476
487            }
488        }
489    }
490
491    // c:480-483 — `if (!isset(EXTENDEDGLOB))` mask Tilde/Hat/Hash.
492    if !isset(EXTENDEDGLOB) {
493        sp[ZPC_TILDE as usize] = marker_byte;
494        sp[ZPC_HAT as usize] = marker_byte;
495        sp[ZPC_HASH as usize] = marker_byte;
496    }
497
498    // c:485-491 — `if (!isset(KSHGLOB))` mask the six KSH_* slots.
499    if !isset(KSHGLOB) {
500        sp[ZPC_KSH_QUEST as usize] = marker_byte;
501        sp[ZPC_KSH_STAR as usize] = marker_byte;
502        sp[ZPC_KSH_PLUS as usize] = marker_byte;
503        sp[ZPC_KSH_BANG as usize] = marker_byte;
504        sp[ZPC_KSH_BANG2 as usize] = marker_byte;
505        sp[ZPC_KSH_AT as usize] = marker_byte;
506    }
507
508    // c:499-505 — `if (isset(SHGLOB))` mask Inpar/Inang (case/numeric
509    // ranges not valid under sh-emulation).
510    if isset(SHGLOB) {
511        sp[ZPC_INPAR as usize] = marker_byte;
512        sp[ZPC_INANG as usize] = marker_byte;
513    }
514}
515
516/// Port of `patcompstart()` from `Src/pattern.c:517`.
517///
518/// Resets per-compile globals. Called at the start of `patcompile`.
519///
520/// **C body (c:517-526)** — strict order matters:
521///   1. `patcompcharsset()` — must run FIRST so the zpc_special
522///      table reflects the current option state before parsing.
523///   2. `patglobflags = isset(CASEGLOB) || isset(CASEPATHS) ? 0 :
524///      GF_IGNCASE;` — case-insensitivity is the default UNLESS
525///      one of the case-sensitive options is set.
526///   3. `if (isset(MULTIBYTE)) patglobflags |= GF_MULTIBYTE;` —
527///      multibyte handling is option-gated, NOT unconditional.
528///
529/// The previous Rust port had three divergences: (a) called
530/// patcompcharsset LAST instead of FIRST, (b) unconditionally set
531/// GF_MULTIBYTE even when `setopt nomultibyte`, (c) NEVER set
532/// GF_IGNCASE — `setopt nocaseglob` had zero effect on pattern
533/// case-folding.
534pub fn patcompstart() {
535    // c:517
536    // c:519 — `patcompcharsset()` FIRST.
537    patcompcharsset();
538    patout.lock().unwrap().clear();
539    patnpar.store(1, Ordering::Relaxed);
540    patflags.store(0, Ordering::Relaxed);
541    // c:520-523 — CASE option dispatch.
542    let mut flags: i32 = if isset(CASEGLOB) || isset(CASEPATHS) {
543        0 // c:521 case-sensitive
544    } else {
545        GF_IGNCASE // c:523 default = ignore case
546    };
547    // c:524-525 — MULTIBYTE option respect.
548    if isset(MULTIBYTE) {
549        flags |= GF_MULTIBYTE; // c:525
550    }
551    patglobflags.store(flags, Ordering::Relaxed);
552    errsfound.store(0, Ordering::Relaxed);
553    forceerrs.store(-1, Ordering::Relaxed);
554    patparse_off.store(0, Ordering::Relaxed);
555}
556
557// =====================================================================
558// 9. Compiler entry points — pattern.c:540
559// =====================================================================
560
561/// Port of `patcompile(char *exp, int inflags, char **endexp)` from `Src/pattern.c:540`.
562///
563/// C signature: `Patprog patcompile(char *exp, int inflags, char **endexp)`.
564/// Compiles pattern `exp` under flags `inflags`, returns a `Patprog`
565/// on success or `NULL` on failure. `endexp` (if non-NULL) is set to
566/// the input cursor at end of parse — used by `bin_zregexparse` to
567/// detect partial-parse cases.
568pub fn patcompile(exp: &str, inflags: i32, mut endexp: Option<&mut String>) -> Option<Patprog> {
569    // Global compiled-pattern cache (crate::pat_cache, a zshrs-only opt —
570    // see that module for the safety/key/threading rationale). Skipped when
571    // `endexp` is requested: that out-param is a compile side effect the
572    // cache can't reproduce. The check runs BEFORE the compile mutex so a
573    // hit never serialises on the compiler.
574    let cacheable = endexp.is_none();
575    if cacheable {
576        if let Some(hit) = crate::pat_cache::get(exp, inflags) {
577            return Some(hit);
578        }
579    }
580    // Capture the ORIGINAL pattern text for the cache-store key: `exp` is
581    // shadowed below by the decoded form, and the option state that feeds
582    // the fingerprint is unchanged between here and the store (patcompstart
583    // only reads options), so get/put keys match.
584    let exp_orig: String = if cacheable {
585        exp.to_string()
586    } else {
587        String::new()
588    };
589    // Hold the compile mutex for the entire body — `patcompstart`
590    // resets every file-scope static (`Src/pattern.c:267-281`) and the
591    // emit/parse helpers mutate them in sequence. C is single-threaded
592    // so the statics are race-free there; Rust must serialise.
593    let _compile_guard = PATCOMPILE_LOCK.lock().unwrap_or_else(|e| e.into_inner());
594    // c:1610 `patstartch` — the leading plain character of the pattern.
595    // C only needs the NOGLD leading-dot rule to know whether a pattern
596    // explicitly begins with `.` (so `.*` matches dot files while `*`
597    // does not). Capture it before `exp` is shadowed by the normalizer
598    // below. A leading glob token (Star/Quest/Inbrack/…) is not plain, so
599    // only a literal `.` is recorded; everything else stays 0.
600    let patstartch_lead: u8 = if exp.starts_with('.') { b'.' } else { 0 };
601    // c:796 — `patcompstart()` has exactly ONE caller in C: `parsepat`
602    // (Src/glob.c:796), which runs it ONCE for a whole file glob before
603    // `parsecomplist` compiles the individual path components. C's
604    // `patcompile` never re-enters it. zshrs calls it here instead, which is
605    // harmless for the char-set setup but NOT for the globflags seed: a
606    // pattern-level `(#i)` that `parsepat` folded into `patglobflags`
607    // (Src/glob.c:801-807) was wiped on entry to every component after the
608    // first, so `(#i)SUB/DEEP.TXT` matched case-insensitively in `SUB` and
609    // case-SENSITIVELY in `DEEP.TXT` — i.e. not at all. Snapshot the incoming
610    // value and restore it for a FILE compile, which is exactly what C's
611    // `patcompile` does by leaving `patglobflags` alone unless the pattern is
612    // non-FILE (c:568-576).
613    let incoming_globflags = patglobflags.load(Ordering::Relaxed);
614    patcompstart();
615    if (inflags & PAT_FILE as i32) != 0 {
616        patglobflags.store(incoming_globflags, Ordering::Relaxed); // c:568 — no reset for a file glob
617    } else {
618        // c:568-570 — `if (!(patflags & PAT_FILE)) { patcompcharsset();
619        // zpc_special[ZPC_SLASH] = Marker; … }`. A non-file pattern has no
620        // path components, so '/' is an ordinary character. C masks the slot
621        // rather than testing PAT_FILE at each use site, which is what lets
622        // `patcompbranch` (c:950) break on '/' unconditionally while
623        // `[[ x = ((#s)|/)a ]]` and `${arr:#*/*}` keep '/' literal.
624        // `patcompstart` → `patcompcharsset` re-seeds the slot to '/' on every
625        // compile (pattern.rs:434), so this mask does not leak between calls.
626        zpc_special.lock().unwrap()[ZPC_SLASH as usize] = Marker as u32 as u8; // c:570
627    }
628    // c:525 — `patcompstart` seeds `patglobflags` with the option-derived
629    // default (GF_IGNCASE when CASEGLOB/CASEPATHS are off, GF_MULTIBYTE when
630    // MULTIBYTE is on). The `patglobflags.store(0)` reset below (clean slate
631    // for the `(#…)` hoist loop) would otherwise drop that seed before the
632    // prog's globflags are built — capture the case bits now so `pattry`
633    // honors `setopt nocaseglob`.
634    let mut seeded_globflags = patglobflags.load(Ordering::Relaxed);
635    // c:568-576 — `patcompile` RESETS patglobflags to `GF_MULTIBYTE`/0 for a
636    // NON-FILE pattern, DROPPING the nocaseglob-derived GF_IGNCASE that
637    // patcompstart seeded. `setopt nocaseglob` makes only FILENAME GLOBBING
638    // (PAT_FILE) case-insensitive; `[[ ]]`, `${arr:#pat}`, `case`, and `${p//pat}`
639    // stay case-SENSITIVE. Without this drop, `setopt nocaseglob; [[ ABC == abc ]]`
640    // wrongly matched and `${(ABC):#abc}` wrongly filtered. GF_MULTIBYTE stays.
641    if (inflags & PAT_FILE as i32) == 0 {
642        seeded_globflags &= !GF_IGNCASE;
643    }
644    // === C-contract input decode (zpc_chars, Src/pattern.c:248) =====
645    // C's patcompile consumes the LEXER'S tokenized encoding: glob
646    // metachars arrive as token bytes (`Pound`..`Bang`, zsh.h:159-183
647    // — zpc_chars holds `Star`/`Quest`/`Inbrack`/... so a raw `*`
648    // NEVER dispatches as ZPC_STAR), quoted/escaped chars arrive as
649    // `Bnull`/`Bnullkeep` + payload (zsh.h:195-200), and `Nularg` is
650    // stripped (remnulargs). Callers pair `tokenize()` with
651    // `patcompile()` exactly like C (zutil.c:734, etc.).
652    //
653    // The Rust parser below uses a raw-ASCII internal encoding with
654    // `\X` as its literal form; transpose the C contract onto it:
655    //   token char (U+e184..=U+e19E)         -> ztokens[c - Pound]  (meta)
656    //   Bnull/Bnullkeep + X (U+e19F/U+e1A0)  -> \X                  (literal)
657    //   Nularg (U+e1A1)                      -> dropped
658    //   Meta (U+0083) + X                    -> \X                  (literal)
659    //   raw `\` + X                          -> \X  passthrough — raw
660    //       backslash is the parser's established Bnull-equivalent
661    //       (see the `\X` arm below); C's lexer encodes the same
662    //       quoting info as Bnull+X before zshtokenize ever runs.
663    //   raw ASCII glob metachar              -> \X                  (literal)
664    // `/`, `+`, `!`, `@` stay verbatim — C's zpc_chars keeps those
665    // slots RAW (path split + ksh-glob triggers fire on raw bytes).
666    // c:Src/pattern.c:751 — C sets `*endexp = patparse`, a POINTER INTO
667    // the caller's own tokenized buffer, so the remainder keeps its exact
668    // original bytes. zshrs normalizes the input below, so record for each
669    // NORMALIZED char the index of the ORIGINAL char it came from; the
670    // endexp sites then slice the ORIGINAL string instead of re-tokenizing
671    // the normalized suffix. That round-trip was lossy: parsecomplist
672    // strips the leading `(` of `(sub/)#end` before calling patcompile, so
673    // the normalizer saw the group's `Outpar` as UNBALANCED and demoted it
674    // to a literal `\)`, and re-tokenizing produced `Bnull )` where C still
675    // has `Outpar`. The `(dir/)#` branch's c:752 test
676    // `instr[1] == Outpar` then failed and the closure never formed.
677    let exp_tokenized: Vec<char> = exp.chars().collect();
678    let mut norm_to_orig: Vec<usize> = Vec::with_capacity(exp.len());
679    let exp: String = {
680        let ztokens: Vec<char> = crate::ported::glob::ZTOKENS.chars().collect();
681        let chars: Vec<char> = exp.chars().collect();
682        let mut out = String::with_capacity(exp.len());
683        // Track open Inpar/Outpar groups so an UNBALANCED Outpar token
684        // (a `)` with no group to close) transposes to a literal instead
685        // of the raw `)` metachar. C's tokenizer never emits an Outpar
686        // for an unbalanced `)`, and patcompile then treats it as an
687        // ordinary string char (Src/pattern.c:1294-1298: "allow ')' as an
688        // ordinary string character if there are no parentheses to
689        // close"). Without this, `${arr:#*)--*}` (e.g. _rm's option
690        // filter) broke at the stray `)` and collapsed to `*`. Note the
691        // asymmetry: an unbalanced `(` is a "bad pattern" in zsh, not a
692        // literal, so only Outpar is demoted here.
693        let mut open_paren: i32 = 0;
694        // Inside a `[...]` bracket class, `(`/`)` (Inpar/Outpar tokens) are
695        // LITERAL members, not group delimiters — so they must not move
696        // `open_paren`. The lexer tokenizes a `)` inside `[^)]` to Outpar
697        // (0x8a) all the same, so without this guard the bracket-interior
698        // Outpar wrongly decremented open_paren, and the real group-close
699        // then read `open_paren==0` and got demoted to a literal — collapsing
700        // `(|[^)]#…)` (every `_gnu_generic`/`_arguments --help` option-dedup
701        // pattern) into a "bad pattern". Track Inbrack(0x91)/Outbrack(0x92).
702        let mut in_bracket = false;
703        let mut i = 0;
704        // Emit one normalized char and record which ORIGINAL char it came
705        // from, so `endexp` can be sliced out of the untouched tokenized
706        // input the way C's `*endexp = patparse` pointer is. Indexed by
707        // BYTE offset into `out`, because both endexp sites slice the
708        // normalized string by byte offset. Defined after `i` so
709        // macro_rules definition-site hygiene resolves it to the cursor.
710        macro_rules! opush {
711            ($c:expr) => {{
712                let before = out.len();
713                out.push($c);
714                for _ in before..out.len() {
715                    norm_to_orig.push(i);
716                }
717            }};
718        }
719        while i < chars.len() {
720            let c = chars[i];
721            let cu = c as u32;
722            if cu == 0xe191 {
723                in_bracket = true; // Inbrack — fall through to the generic `[` emit.
724            } else if cu == 0xe192 {
725                in_bracket = false; // Outbrack — fall through to the generic `]` emit.
726            }
727            if cu == 0xe188 && !in_bracket {
728                // Inpar — opens a group.
729                open_paren += 1;
730                opush!('(');
731                i += 1;
732                continue;
733            }
734            if cu == 0xe18a && !in_bracket {
735                // Outpar — closes an open group, else literal `)`.
736                if open_paren > 0 {
737                    open_paren -= 1;
738                    opush!(')');
739                } else {
740                    opush!('\\');
741                    opush!(')');
742                }
743                i += 1;
744                continue;
745            }
746            if cu == 0x83 {
747                // Meta + payload — a metafied RAW byte
748                // (vm_helper::meta_encode_byte, c:Src/utils.c:7289-
749                // 7294). C's pattern matcher compares metafied bytes
750                // on BOTH sides, so the compiled pattern must match
751                // the pair AS STORED in the subject string: emit both
752                // chars as literals (`\Meta \payload`). The previous
753                // `\payload` form dropped the Meta char and never
754                // matched a metafied subject — `[[ $'\xff' ==
755                // $'\xff' ]]` failed. Bug #127.
756                opush!('\\');
757                opush!(c);
758                if i + 1 < chars.len() {
759                    opush!('\\');
760                    opush!(chars[i + 1]);
761                    i += 2;
762                } else {
763                    i += 1;
764                }
765                continue;
766            }
767            if cu == 0xe19f || cu == 0xe1a0 {
768                // Bnull / Bnullkeep — payload is a literal.
769                if i + 1 < chars.len() {
770                    opush!('\\');
771                    opush!(chars[i + 1]);
772                    i += 2;
773                } else {
774                    i += 1;
775                }
776                continue;
777            }
778            if cu == 0xe1a1 {
779                // Nularg — stripped like C's remnulargs.
780                i += 1;
781                continue;
782            }
783            if (0xe184..=0xe19e).contains(&cu) {
784                // Token -> the raw metachar the parser dispatches on.
785                opush!(ztokens[(cu - 0xe184) as usize]);
786                i += 1;
787                continue;
788            }
789            if c == '\\' {
790                // Raw `\X` — Bnull-equivalent quoting marker in the
791                // zshrs pipeline; pass both through to the parser's
792                // `\X` literal arm. Trailing lone `\` stays itself.
793                //
794                // C draws two provenances here that this arm cannot see
795                // apart (docs/BUGS.md #1090):
796                //   source quote — `[[ "man ls" = man\ * ]]`, `${v//\%/%%}`
797                //   pattern DATA — `p='a\ b'; ${arr[(I)$p]}`
798                // c:Src/glob.c:3633-3643 `zshtokenize` honors the escape only
799                // before a `ztokens` metacharacter and leaves BOTH bytes as
800                // literals before anything else — so in C a raw backslash in
801                // the tokenized input is DATA and a quote is `Bnull`. Every
802                // source path in zshrs hands this arm a raw backslash for the
803                // quote (the cond/case pattern builder in
804                // `extensions::compile_zsh`, `${…//…}`'s builder in
805                // `ported::subst`), so the raw form has to keep meaning
806                // "quote" here; enforcing the C rule at this site alone
807                // regressed 8 real-world corpus tests across zinit / p10k /
808                // zpwr / fsh.
809                //
810                // The provenance is therefore settled UPSTREAM instead: a
811                // pattern built from a VALUE spells a data backslash `\\`
812                // (this arm's literal-backslash form) before it gets here,
813                // via `crate::pattern_data_escape::escape_data_backslashes`.
814                // Two families of caller do that:
815                //   * `ported::subst`'s paramsubst, for the search-subscript
816                //     patterns (`${a[(I)…]}` / `(i)` / `(r)` / `(R)` / `(K)`)
817                //     — the whole set that reaches `patcompile` through
818                //     `zshtokenize` alone (c:Src/params.c:1727).
819                //   * the cond/case pattern builder
820                //     (`extensions::compile_zsh::emit_glob_subst_pattern`),
821                //     for a `${~spec}` / `$~spec` segment and for
822                //     `setopt globsubst` — the `strcatsub` `shtokenize` C
823                //     runs at c:Src/subst.c:822/830. The escape is emitted
824                //     ONLY on that pattern path, so a NON-pattern use of
825                //     `${~arr[i]}` (which nothing untokenizes back) still
826                //     prints its single backslash.
827                // Covered by `tests/parity/cond_parity.rs`'s
828                // `backslash_provenance_in_patterns`.
829                opush!('\\');
830                if i + 1 < chars.len() {
831                    opush!(chars[i + 1]);
832                    i += 2;
833                } else {
834                    i += 1;
835                }
836                continue;
837            }
838            if matches!(c, '*' | '?' | '[' | '(' | ')' | '|' | '~' | '^' | '#' | '<') {
839                // Untokenized ASCII metachar — literal per zpc_chars.
840                opush!('\\');
841                opush!(c);
842                i += 1;
843                continue;
844            }
845            opush!(c);
846            i += 1;
847        }
848        out
849    };
850    // c:751 — resolve a byte offset in the NORMALIZED string back to the
851    // corresponding suffix of the ORIGINAL tokenized input, which is what
852    // C's `*endexp = patparse` yields for free (it is a pointer into the
853    // caller's own buffer). An offset past the last recorded char means
854    // the whole pattern was consumed, so the remainder is empty.
855    let orig_remainder = |orig: &[char], map: &[usize], norm_byte_off: usize| -> String {
856        let start = map.get(norm_byte_off).copied().unwrap_or(orig.len());
857        orig[start.min(orig.len())..].iter().collect()
858    };
859    let exp = exp.as_str();
860    *patstart.lock().unwrap() = exp.to_string();
861    *patparse.lock().unwrap() = exp.to_string();
862    patflags.store(
863        inflags & !(PAT_PURES | PAT_HAS_EXCLUDP) as i32,
864        Ordering::Relaxed,
865    ); // c:566
866       // c:568-576 — `patcompile` re-seeds the RUNNING `patglobflags`
867       // (`if (isset(MULTIBYTE)) patglobflags = GF_MULTIBYTE; else
868       // patglobflags = 0;`) for a non-file pattern, and leaves
869       // `patcompstart`'s equally option-gated seed alone for a file glob.
870       // Every P_GFLAGS node emitted below records that running value
871       // verbatim (c:993 `up.l = patglobflags`), so storing a bare 0 here
872       // made a mid-pattern `(#i)` / `(#b)` node clear GF_MULTIBYTE for the
873       // remainder of the match (c:2942 assigns the payload ABSOLUTELY).
874       //
875       // GF_MULTIBYTE is forced ON rather than taken from `seeded_globflags`:
876       // the option gate is correct C (c:572-575) but zshrs's capture and
877       // substitution layers index the subject as UTF-8, and `unsetopt
878       // multibyte` makes zsh match in raw BYTES (c:1946), which those layers
879       // cannot yet represent — see the `charinc` note below.
880    patglobflags.store(seeded_globflags | GF_MULTIBYTE, Ordering::Relaxed);
881
882    // c:583-590 — leading `(#...)` flag specifiers are hoisted into
883    // patprog.globflags so the matcher applies them globally for the
884    // whole match; mid-pattern `(#...)` forms emit real P_GFLAGS nodes
885    // (see the patnode(P_GFLAGS) sites below).
886    // c:525 — `patglobflags = isset(MULTIBYTE) ? GF_MULTIBYTE : 0`.
887    // (#U) inside the spec clears the bit per c:1116.
888    //
889    // c:953-957 — C gates the `(#...)` recognition on
890    //   `*patparse == zpc_special[ZPC_INPAR] &&`
891    //   `patparse[1] == zpc_special[ZPC_HASH]`.
892    // When EXTENDEDGLOB is off, patcompcharsset (c:480-483) masks
893    // ZPC_HASH to the Marker byte (c:476). The second-byte equality
894    // therefore fails and the parser falls through to "treat `(` as
895    // a literal group" (c:1020+). Previously this Rust hoist loop
896    // compared the raw byte `b'#'`, allowing `(#s)` / `(#e)` /
897    // `(#i)` to fire even without EXTENDEDGLOB (parity bugs #18/#19
898    // vs real zsh).
899    // c:525 — the compiled prog's globflags accumulate from `patglobflags`,
900    // which `patcompstart` seeded with GF_IGNCASE when CASEGLOB/CASEPATHS are
901    // off (`setopt nocaseglob`). Carry those case bits through so `pattry`
902    // matches case-insensitively; `matchpat` masks the loss by pre-folding
903    // both sides, but the glob scanner now drives `pattry` directly.
904    //
905    // c:572-575 — `if (isset(MULTIBYTE)) patglobflags = GF_MULTIBYTE; else
906    // patglobflags = 0;`. The bit is OPTION-GATED in C, but it is kept
907    // unconditional here (see the `patglobflags.store` note above): zshrs's
908    // capture / substitution layers slice the subject as UTF-8, and the
909    // nomultibyte unit is a raw BYTE. An explicit in-pattern `(#U)` still
910    // clears it below (c:1116), which the matcher now honours.
911    //
912    // The `0xff` is the `(#aN)` APPROXIMATION BUDGET (c:1054-1066), which C
913    // carries in the low byte of the same word. Masking it off here dropped a
914    // budget that `parsepat` (c:801-807) had set from a PATTERN-LEVEL `(#aN)`
915    // in front of a multi-component path: c:568 leaves `patglobflags` alone for
916    // a PAT_FILE compile precisely so the budget reaches every component, and
917    // the restore at pattern.rs:615 put it back only for this mask to discard
918    // it. `(#i)` survived (its bit is in the mask) while `(#a1)` did not, so
919    // `(#a1)/abs/path/ofo` matched nothing where zsh 5.9.2 answers
920    // `/abs/path/{fo,foo,oof}`. A relative `(#a1)ofo` was unaffected — the flag
921    // sits inside the sole component and this very hoist loop reads it there.
922    let mut hoisted_globflags: i32 =
923        GF_MULTIBYTE | (seeded_globflags & (GF_IGNCASE | GF_LCMATCHUC | 0xff));
924    // c:953-954 gates BOTH bytes: `*patparse == zpc_special[ZPC_INPAR]`
925    // as well as `patparse[1] == zpc_special[ZPC_HASH]`. SHGLOB
926    // (c:500-510) and `disable -p '('` mask the INPAR slot to Marker,
927    // and then `(#i)abc` is ordinary text, not a flag spec.
928    let (inpar_char_pre, hash_char_pre) = {
929        let sp = zpc_special.lock().unwrap();
930        (sp[ZPC_INPAR as usize], sp[ZPC_HASH as usize]) // c:953, c:954
931    };
932    while hash_char_pre == b'#' && inpar_char_pre == b'(' {
933        let off = patparse_off.load(Ordering::Relaxed);
934        let p = patparse.lock().unwrap();
935        if off + 1 >= p.len() || &p.as_bytes()[off..off + 2] != b"(#" {
936            break;
937        }
938        let rest = p[off..].to_string();
939        drop(p);
940        match patgetglobflags(&rest) {
941            Some((_bits, assert, _consumed)) if assert != 0 => {
942                // c:Src/pattern.c:1103-1109 — `(#s)`/`(#e)` set `*assertp`
943                // (P_ISSTART/P_ISEND), a POSITIONAL zero-width anchor, not a
944                // whole-match glob flag. Do NOT hoist/consume it here: leave
945                // it (and everything after) for the main compile loop
946                // (pattern.rs:1245) to emit as a positional node. Hoisting
947                // dropped the anchor, so a leading `(#e)PAT` matched as if
948                // the `(#e)` were absent — `[[ o == (#e)o ]]` wrongly matched
949                // and `${s//(#e)r/END}` wrongly replaced. `(#s)` was masked
950                // in `[[ ]]` only because that context is start-anchored.
951                break;
952            }
953            Some((bits, _assert, consumed)) => {
954                // The fold / backref / matchref / multibyte flags are ABSOLUTE
955                // state, not a set-only delta. patgetglobflags's returned `bits`
956                // starts at 0 and can only carry flags turned ON, so `(#I)`
957                // clearing LCMATCHUC (c: `patglobflags &= ~(GF_LCMATCHUC |
958                // GF_IGNCASE)`) comes back as 0 — an `|=` accumulate then leaves
959                // an earlier `(#l)`/`(#i)`'s fold bit set, so `(#l)(#I)foo` kept
960                // folding where zsh, after `(#I)`, matches case-sensitively.
961                //
962                // patgetglobflags ALSO updates the global patglobflags with the
963                // sets AND the clears, so it holds the authoritative running
964                // value. REPLACE the toggle-able bits from it rather than
965                // OR-accumulating the lossy return.
966                // GF_MULTIBYTE is deliberately NOT in the mask: the init above
967                // force-sets it (this port always matches UTF-8) while the
968                // global patglobflags may not carry it, so replacing it from the
969                // global would wrongly clear it and break multibyte matching.
970                // The original `|=` could not clear it either, so leaving it out
971                // preserves that (rare `(#U)` single-byte requests are no more
972                // broken than before). The fold/backref/matchref bits, which DO
973                // need clear semantics for `(#I)`/`(#B)`/`(#M)`, come from the
974                // authoritative global.
975                let gmask = GF_IGNCASE | GF_LCMATCHUC | GF_BACKREF | GF_MATCHREF;
976                let cur = patglobflags.load(Ordering::Relaxed);
977                hoisted_globflags = (hoisted_globflags & !gmask) | (cur & gmask);
978                // `(#aN)` substitution budget — low 8 bits of
979                // `patglobflags` per c:1066. Only carry it when this
980                // flag-spec actually had `(#a)` digits (non-zero err
981                // count); other specs may set high bits and we don't
982                // want their bits-AND to clear our budget byte.
983                // Per-spec OR-leak is C-faithful (a later `(#a3)` after
984                // `(#a1)` raises the budget); the matcher reads the
985                // final cumulative byte.
986                let errs_byte = bits & 0xff;
987                if errs_byte != 0 {
988                    hoisted_globflags = (hoisted_globflags & !0xff) | errs_byte;
989                    // c:1066
990                }
991                if (bits & GF_MULTIBYTE) == 0 && rest.contains('U') {
992                    hoisted_globflags &= !GF_MULTIBYTE;
993                }
994                patparse_off.fetch_add(consumed, Ordering::Relaxed);
995            }
996            None => break,
997        }
998    }
999
1000    // c:584-610 — PAT_PURES fast path. A literal with no glob tokens
1001    // compiles to a "pure string" rather than bytecode. For FILE globs
1002    // (PAT_FILE) the scan STOPS at '/', so each path component becomes a
1003    // pure section and `parsecomplist` can split the path on '/' (it
1004    // reads endexp = the '/' remainder). Gated on PAT_FILE so non-file
1005    // callers (matchpat/[[ ]]/:#) keep normal compilation unchanged; the
1006    // matcher consumes PURES sections via literal extraction (the
1007    // scanner), never `pattry`, so no PAT_PURES match path is needed.
1008    let pf_pre = patflags.load(Ordering::Relaxed);
1009    // c:Src/pattern.c:644-704 — for a FILE glob a literal path component is
1010    // emitted as a PAT_PURES section even under GF_IGNCASE (`setopt
1011    // nocaseglob`): C compiles it, then converts the single P_EXACTLY back
1012    // to PURES (c:664) while flagging the prog GF_IGNCASE (c:645). The net
1013    // effect is that intermediate literal directory components descend by
1014    // EXACT name (the scanner's PURES path stats the real path, glob.rs:436
1015    // — case-insensitivity applies to the FINAL wildcard component, not to
1016    // directory descent), so path splitting on `/` keeps working. zshrs's
1017    // port only had the first PURES gate (c:586), which excludes GF_IGNCASE,
1018    // so under nocaseglob every literal component (e.g. `tmp` in
1019    // `/tmp/.../*.zsh`) fell to the general compiler and matched nothing —
1020    // the whole path collapsed and `${(N)...}(DN)` globs returned empty.
1021    // That is what made zinit's plugin-discovery globs empty on a shell with
1022    // `caseglob` off → the `.zinit-load-plugin:source:110: no such file or
1023    // directory: ./` startup flood. Take the PURES fast path here for file
1024    // globs when the only extra glob flag is GF_IGNCASE; GF_IGNCASE stays on
1025    // `hoisted_globflags` so the final wildcard component still matches
1026    // case-insensitively.
1027    //
1028    // CORRECTION (see the `(#i)` note below): the exemption above transcribed
1029    // C's `__CYGWIN__` arm. c:586's fast-path gate is
1030    // `!(patglobflags & ~GF_MULTIBYTE)` on every other platform — the
1031    // `|| (!(patglobflags & ~GF_IGNCASE) && (patflags & PAT_FILE))` alternative
1032    // is inside `#ifdef __CYGWIN__` (c:587-594) precisely because only there is
1033    // the FILESYSTEM itself case-insensitive, making a stat of the pattern's
1034    // own spelling equivalent to a scan. Everywhere else C reaches PAT_PURES
1035    // for a literal component only via the c:650-664 conversion, and c:1392-1417
1036    // withholds it whenever `patglobflags & (0xFF|GF_LCMATCHUC|GF_IGNCASE)`:
1037    // "It's much simpler to turn off pure string mode for any case-insensitive
1038    // or approximate matching" (c:1382-1385). The single exception is a `.` or
1039    // `..` file component, which stays pure (c:1414-1416).
1040    //
1041    // This matters beyond flag bookkeeping: the PURES path STATS the pattern's
1042    // spelling, so on a case-insensitive filesystem `(#i)ALPHA.TXT` "found" the
1043    // file and emitted `ALPHA.TXT` — the pattern echoed back — where zsh scans
1044    // the directory and emits the real on-disk `alpha.txt`.
1045    let case_or_approx = hoisted_globflags & (0xff | GF_LCMATCHUC | GF_IGNCASE); // c:1392, c:1409
1046    if (pf_pre & PAT_FILE as i32) != 0 && (pf_pre & PAT_ANY as i32) == 0 {
1047        let off = patparse_off.load(Ordering::Relaxed);
1048        let p = patparse.lock().unwrap();
1049        let s = &p[off..];
1050        // c:606 — scan for a pure literal segment. By this point patparse
1051        // has been NORMALIZED to raw metachars (Star -> '*') with literal
1052        // metachars backslash-escaped (`\*`), so scan that form: stop at
1053        // '/' (segment boundary) or any UNescaped glob metachar (=> not
1054        // pure). `\X` is an escaped literal — skip both chars.
1055        let mut cut = s.len();
1056        let mut at_token = false;
1057        // The component's literal spelling. c:Src/glob.c:3633-3643
1058        // `zshtokenize` turns `\` before a `ztokens` metacharacter into
1059        // `Bnull`, a token, so C compiles that component and c:650-664
1060        // hands the scanner the UNQUOTED string (`A\(B\)` -> `A(B)`). Before
1061        // any other character the backslash is data and stays. Copying the
1062        // escapes verbatim stat'ed `A\(B\)`, so a quoted directory component
1063        // never matched (Y01completion #16).
1064        let mut literal_s = String::with_capacity(s.len());
1065        let mut it = s.char_indices();
1066        while let Some((i, c)) = it.next() {
1067            if c == '/' {
1068                cut = i;
1069                break;
1070            }
1071            if c == '\\' {
1072                match it.next() {
1073                    Some((_, n)) if crate::ported::lex::ztokens.contains(n) => literal_s.push(n),
1074                    Some((_, n)) => {
1075                        literal_s.push('\\');
1076                        literal_s.push(n);
1077                    }
1078                    None => literal_s.push('\\'),
1079                }
1080                continue;
1081            }
1082            if matches!(c, '*' | '?' | '[' | '(' | '|' | '~' | '^' | '#' | '<') {
1083                cut = i;
1084                at_token = true;
1085                break;
1086            }
1087            literal_s.push(c);
1088        }
1089        // c:1414-1416 — `.` and `..` stay pure even under case-insensitive or
1090        // approximate matching, so a `../x` path component still descends by
1091        // name instead of being scanned for.
1092        let dot_or_dotdot = matches!(&s[..cut], "." | ".."); // c:1414-1415
1093        // c:947 `*flagp = P_PURESTR;` — only a compiled P_EXACTLY clears it
1094        // (c:1409-1417), so an EMPTY component (the tail of `*/`) stays pure
1095        // under nocaseglob too. Withholding it made the tail a scan for the
1096        // empty name, and `setopt nocaseglob; print */` matched nothing.
1097        let empty_component = cut == 0;
1098                                                             // c:610 — pure iff we stopped at end or '/', not at a glob meta.
1099        if !at_token && (case_or_approx == 0 || dot_or_dotdot || empty_component) {
1100            let literal = literal_s.into_bytes();
1101            drop(p);
1102            let mlen = literal.len() as i64;
1103            if let Some(end) = endexp.as_deref_mut() {
1104                // c:751 `*endexp = patparse` — a pointer INTO the caller's
1105                // tokenized buffer. Map the normalized cut back to the
1106                // original char index and slice the untouched tokenized
1107                // input, so the remainder keeps its exact original tokens.
1108                // `cut` indexes into `s`, which starts at `off` — the offset
1109                // patparse_off reached after the leading `(#...)` flag block
1110                // was hoisted above. `orig_remainder` maps an ABSOLUTE
1111                // normalized offset (the general path at the bottom of this
1112                // function passes `consumed_off`, already absolute), so a
1113                // bare `cut` under-reports the consumed length by exactly the
1114                // width of the hoisted flags: `(#i)alpha.txt` compiled the
1115                // literal correctly but reported `.txt` as unconsumed, and
1116                // `parsecomplist` (Src/glob.c:773) then saw a remainder that
1117                // was neither `/` nor empty and failed the whole pattern with
1118                // "bad pattern". Only wildcard-free components hit this path,
1119                // which is why `(#i)a*` always worked and `(#i)alpha.txt` did
1120                // not.
1121                *end = orig_remainder(&exp_tokenized, &norm_to_orig, off + cut);
1122            }
1123            let prog: Patprog = Box::new((
1124                patprog {
1125                    startoff: 0,
1126                    size: mlen,
1127                    mustoff: 0,
1128                    patmlen: mlen, // c:623 — pure-string length
1129                    globflags: hoisted_globflags,
1130                    globend: patglobflags.load(Ordering::Relaxed),
1131                    flags: pf_pre | PAT_PURES as i32, // c:625
1132                    patnpar: 0,
1133                    patstartch: patstartch_lead, // c:1610
1134                },
1135                literal,
1136            ));
1137            if cacheable {
1138                crate::pat_cache::put(&exp_orig, inflags, &prog);
1139            }
1140            return Some(prog);
1141        }
1142    }
1143
1144    let mut flagp: i32 = 0;
1145    let root = patcompswitch(0, &mut flagp);
1146    if root < 0 {
1147        return None; // c:646 compile error
1148    }
1149    // Emit the terminal P_END and chain every branch's operand to it.
1150    let end_off = patnode(P_END);
1151    chain_branches_to(root as usize, end_off);
1152
1153    let code = patout.lock().unwrap().clone();
1154    let consumed_off = patparse_off.load(Ordering::Relaxed);
1155    if let Some(end) = endexp.as_deref_mut() {
1156        // c:751 `*endexp = patparse` — a pointer INTO the caller's
1157        // tokenized buffer. Map the normalized consume-point back to the
1158        // original char index and slice the untouched tokenized input.
1159        *end = orig_remainder(&exp_tokenized, &norm_to_orig, consumed_off);
1160    }
1161
1162    let prog: Patprog = Box::new((
1163        patprog {
1164            startoff: 0,
1165            size: code.len() as i64,
1166            mustoff: 0,
1167            patmlen: 0,
1168            globflags: hoisted_globflags,
1169            globend: patglobflags.load(Ordering::Relaxed),
1170            // c:637 `p->flags = patflags;` — patflags ONLY. A prior
1171            // Rust port OR'd `hoisted_globflags` into here, contaminating
1172            // `prog.flags & 0xff` checks (which read PAT_STATIC / PAT_PURES
1173            // bits in the low byte) with whatever ended up in the
1174            // `(#aN)` budget byte. The two flag-sets stay strictly
1175            // separated per C source.
1176            flags: patflags.load(Ordering::Relaxed),
1177
1178            patnpar: patnpar.load(Ordering::Relaxed) - 1,
1179            patstartch: patstartch_lead, // c:1610
1180        },
1181        code,
1182    ));
1183    if cacheable {
1184        crate::pat_cache::put(&exp_orig, inflags, &prog);
1185    }
1186    Some(prog)
1187}
1188
1189/// Port of `patcompswitch(int paren, int *flagp)` from `Src/pattern.c:765`.
1190///
1191/// C: `static long patcompswitch(int paren, int *flagp)`. Parses an
1192/// alternation (`a|b|c`), emitting a chain of P_BRANCH nodes. Returns
1193/// offset of the first branch, or -1 on error.
1194pub fn patcompswitch(paren: i32, flagp: &mut i32) -> i64 {
1195    // c:765
1196    // Emit the first P_BRANCH header. Its operand is the content
1197    // emitted by the immediately-following patcompbranch call (lives
1198    // inline at starter+I_BODY). Does NOT emit a terminator — caller
1199    // (patcompile for top-level, patcomppiece for sub-pattern)
1200    // chains branches to the appropriate follow-on opcode.
1201    // c:773 — `long savglobflags = (long)patglobflags;`
1202    let savglobflags = patglobflags.load(Ordering::Relaxed);
1203    // c:772 — `int flags, gfchanged = 0;`
1204    let mut gfchanged: i32 = 0;
1205    let starter = patnode(P_BRANCH);
1206    let mut branch_flags: i32 = 0;
1207    let first_branch = patcompbranch(&mut branch_flags, paren);
1208    if first_branch < 0 {
1209        return -1;
1210    }
1211    // c:786-787 — `if (patglobflags != (int)savglobflags) gfchanged++;`
1212    if patglobflags.load(Ordering::Relaxed) != savglobflags {
1213        gfchanged += 1;
1214    }
1215    *flagp |= branch_flags & P_HSTART;
1216
1217    let mut last_branch = starter;
1218    // c:769 — `Upat excsync = NULL;` — set on first `~` exclusion;
1219    // reused so consecutive `~clause` chain to the same sync node.
1220    let mut excsync: usize = 0;
1221
1222    // Snapshot zpc_special for this compile pass — locked once to
1223    // avoid re-locking inside the per-iteration parse-byte check.
1224    let (sp_bar, sp_tilde, sp_special_set) = {
1225        let sp = zpc_special.lock().unwrap();
1226        let bar = sp[ZPC_BAR as usize];
1227        let tilde = sp[ZPC_TILDE as usize];
1228        // c:803 — `memchr(zpc_special, patparse[1], ZPC_SEG_COUNT)` —
1229        // is the lookahead byte one of the segment-special bytes? If
1230        // so, `~X` is NOT an exclusion (`~|`, `~)`, etc.).
1231        let mut set = [false; 256];
1232        for i in 0..(ZPC_SEG_COUNT as usize) {
1233            set[sp[i] as usize] = true;
1234        }
1235        (bar, tilde, set)
1236    };
1237
1238    // Alternation + exclusion loop:
1239    //   `|`  → next alternative (P_BRANCH)
1240    //   `~`  → exclusion (P_EXCLUDE / P_EXCLUDP) — when followed by
1241    //          `/` (top-level path component split) OR a non-special
1242    //          char (ordinary content). `~~`, `~|`, `~)` stay literal.
1243    loop {
1244        let off = patparse_off.load(Ordering::Relaxed);
1245        let parse = patparse.lock().unwrap();
1246        if off >= parse.len() {
1247            break;
1248        }
1249        let bytes = parse.as_bytes();
1250        let c = bytes[off];
1251        // c:799-803 — accept `|` always; accept `~` only when the
1252        // lookahead char is `/` or NOT a segment-special byte.
1253        let is_bar = c == sp_bar;
1254        let is_tilde_exclude = c == sp_tilde && off + 1 < bytes.len() && {
1255            let la = bytes[off + 1];
1256            la == b'/' || !sp_special_set[la as usize]
1257        };
1258        if !is_bar && !is_tilde_exclude {
1259            break;
1260        }
1261        drop(parse);
1262        patparse_off.fetch_add(1, Ordering::Relaxed); // c:803 `*patparse++`
1263        let br: usize;
1264        // c:805 — `long gfnode = 0, newbr;`
1265        let mut gfnode: usize = 0;
1266        // c:839-841 — C tracks this by bumping `tilde` to 2; a bool is the
1267        // same signal in Rust, where `tilde` is the `is_tilde_exclude` bool.
1268        let mut tilde_masked_slash = false;
1269        if is_tilde_exclude {
1270            // c:808-836 — `if (tilde)` arm. Emit the EXCSYNC sync node
1271            // (if first `~` in this switch) then the EXCLUDE / EXCLUDP
1272            // node with an 8-byte NULL syncptr payload. Note we DON'T
1273            // reset patglobflags's low byte (`(#aN)` budget) here —
1274            // the Rust port doesn't yet propagate per-pattern `(#a)`
1275            // through nested patmatch frames, so dropping the budget
1276            // mid-compile is a no-op.
1277            if excsync == 0 {
1278                excsync = patnode(P_EXCSYNC); // c:813
1279                patoptail(last_branch, excsync); // c:814
1280            }
1281            // c:820-824 — "By default, approximations are turned off in
1282            // exclusions: we need to do this here as otherwise the code
1283            // compiling the exclusion doesn't know if the flags have
1284            // really changed if the error count gets restored."
1285            //     `patglobflags &= ~0xff;`
1286            // That last clause is exactly why this matters now that
1287            // patcompbranch honours c:995-997's "No effect" skip: without
1288            // the clear, `(#ia1)README~(#a1)READ_ME` sees the exclusion's
1289            // own `(#a1)` as a no-op and emits no P_GFLAGS, so the
1290            // exclusion never re-arms its error budget.
1291            patglobflags.fetch_and(!0xff, Ordering::Relaxed);
1292            // c:816-825 — `if (!(patflags & PAT_FILET) || paren)` →
1293            // P_EXCLUDE; else P_EXCLUDP for top-level file globs.
1294            let pf = patflags.load(Ordering::Relaxed);
1295            let use_excludp = (pf & (PAT_FILET as i32)) != 0 && paren == 0;
1296            if use_excludp {
1297                br = patnode(P_EXCLUDP); // c:823
1298                patflags.fetch_or(PAT_HAS_EXCLUDP as i32, Ordering::Relaxed); // c:824
1299            } else {
1300                br = patnode(P_EXCLUDE); // c:818
1301            }
1302            // c:826-827 — `up.p = NULL; patadd((char *)&up, 0, sizeof(up), 0);`
1303            // 8-byte syncptr slot, NULL-initialised. Sized for a
1304            // 64-bit pointer to match C `union upat`.
1305            {
1306                let mut buf = patout.lock().unwrap();
1307                buf.extend_from_slice(&[0u8; 8]);
1308            }
1309            /* / is not treated as special if we are at top level */ // c:839
1310            // c:840-842 — `if (!paren && zpc_special[ZPC_SLASH] == '/') {
1311            // tilde++; zpc_special[ZPC_SLASH] = Marker; }`. This is what
1312            // keeps a top-level exclusion like `*~(bench/x)y` legal, and it
1313            // is the reason C can afford the unconditional '/' break in
1314            // patcompbranch (c:950).
1315            if paren == 0 {
1316                let mut sp = zpc_special.lock().unwrap();
1317                if sp[ZPC_SLASH as usize] == b'/' {
1318                    tilde_masked_slash = true; // c:841 — `tilde++` (tilde becomes 2)
1319                    sp[ZPC_SLASH as usize] = Marker as u32 as u8; // c:842
1320                }
1321            }
1322        } else {
1323            // c:843 — `excsync = 0; br = patnode(P_BRANCH);`
1324            excsync = 0;
1325            br = patnode(P_BRANCH);
1326            // c:845-847 — "The position of the following statements
1327            // means globflags set in the main branch carry over to the
1328            // exclusion."
1329            if paren == 0 {
1330                // c:849 — `patglobflags = 0;`
1331                //
1332                // c:850-870 — "If at top level, we need to reinitialize
1333                // flags to zero, since (#i)foo|bar only applies to foo
1334                // and we stuck the #i into the global flags."
1335                //
1336                // !!! RUST-ONLY DETAIL !!! C tests
1337                // `((Patprog)patout)->globflags`, the header field it
1338                // wrote at c:977 when the flag spec sat at patstart.
1339                // zshrs computes the header value in a separate
1340                // patcompile hoist scan (pattern.rs:844+), so the live
1341                // equivalent here is "were any flags in effect on the
1342                // way into this branch?".
1343                let before_reset = patglobflags.load(Ordering::Relaxed);
1344                patglobflags.store(0, Ordering::Relaxed); // c:849
1345                if before_reset != 0 {
1346                    // c:861-864 — `gfnode = patnode(P_GFLAGS); up.l =
1347                    // patglobflags;` (which c:849 just set to 0).
1348                    gfnode = patnode(P_GFLAGS);
1349                    let mut buf = patout.lock().unwrap();
1350                    buf.extend_from_slice(&0i32.to_le_bytes());
1351                }
1352            } else {
1353                // c:872 — `patglobflags = (int)savglobflags;`
1354                patglobflags.store(savglobflags, Ordering::Relaxed);
1355            }
1356        }
1357        // Chain previous branch's `next` directly to this new branch
1358        // (alternative chain, not operand chain).
1359        set_next(last_branch, br);
1360        let mut bf: i32 = 0;
1361        let inner = patcompbranch(&mut bf, paren);
1362        // c:880-883 — `if (tilde == 2) { /* restore special treatment of / */
1363        // zpc_special[ZPC_SLASH] = '/'; }`. C does this BEFORE the
1364        // `if (!newbr) return 0` bail at c:884-885, so the restore must not
1365        // sit behind the error return either.
1366        if tilde_masked_slash {
1367            zpc_special.lock().unwrap()[ZPC_SLASH as usize] = b'/';
1368        }
1369        if inner < 0 {
1370            return -1;
1371        }
1372        // c:884-885 — `if (gfnode) pattail(gfnode, newbr);`. In the Rust
1373        // encoding a branch's operand lives inline at `br + I_BODY`, so
1374        // the P_GFLAGS emitted above IS the operand; chain it to the
1375        // branch body that patcompbranch just emitted.
1376        if gfnode != 0 {
1377            set_next(gfnode, inner as usize);
1378        }
1379        // c:888-889 — `if (!tilde && patglobflags != (int)savglobflags)
1380        // gfchanged++;`
1381        if !is_tilde_exclude && patglobflags.load(Ordering::Relaxed) != savglobflags {
1382            gfchanged += 1;
1383        }
1384        // c:Src/pattern.c:891-892 — `if (excsync) patoptail(br,
1385        // patnode(P_EXCEND));`. Terminate an exclusion branch's operand
1386        // with P_EXCEND so the matcher knows where the excluded pattern
1387        // ends; without it `A~B` ran B past its own end into the
1388        // following pattern and never excluded (`STATES__*~*local*`
1389        // failed to drop STATES__local_bar).
1390        if excsync != 0 {
1391            let excend = patnode(P_EXCEND);
1392            patoptail(br, excend);
1393        }
1394        *flagp |= bf & P_HSTART;
1395        last_branch = br;
1396    }
1397
1398    // c:913-917 — "check for proper termination":
1399    //     if ((paren && *patparse++ != Outpar) ||
1400    //         (!paren && *patparse &&
1401    //          !((patflags & PAT_FILE) && *patparse == '/')))
1402    //         return 0;
1403    // The `paren` half lives at the two call sites that OPEN a group —
1404    // patcomppiece's `b'('` arm and patcompnot's paren arm both test for
1405    // the closing `)` and consume it — so only the `!paren` half belongs
1406    // here: input left over after the top-level switch is a BAD PATTERN.
1407    //
1408    // What reaches this point. `patcompbranch` (c:950) stops on any of the
1409    // first ZPC_SEG_COUNT slots — SLASH, NULL, BAR, OUTPAR. BAR is consumed
1410    // by the alternation loop above, NULL is end-of-input, and SLASH is
1411    // Marker'd for every non-PAT_FILE compile (patcompile, c:570), which is
1412    // why C spells the PAT_FILE escape hatch explicitly. That leaves `)` as
1413    // the byte this test actually rejects, and it can only survive to here
1414    // when `(` was NOT a grouping character — `isset(SHGLOB)` (c:500-510) or
1415    // `disable -p '('` — so the `(` opened no group for it to close:
1416    //     setopt shglob; [[ "-A" = -([AMO]*|[0CRSWnsw]) ]]
1417    //     zsh: bad pattern: -([AMO]*|[0CRSWnsw])
1418    // which is `_arguments`:14 under the option state compsys runs it in.
1419    //
1420    // A leftover `)` is NOT the same thing as the c:1294-1298 rule that lets
1421    // patcomppiece swallow `)` as an ordinary character: that one fires only
1422    // when there is no group to close AND the scan is already inside a
1423    // literal run, so `-(a|b)` compiles to `-(a` | `b)` while `-(a|b*)` —
1424    // the `*` ends the literal run and hands `)` back to patcompbranch —
1425    // is rejected. zshrs reproduces the same split because its input
1426    // normalizer (see patcompile) demotes an UNBALANCED `)` to a literal
1427    // before the compile starts, leaving exactly the balanced-but-ungrouped
1428    // case for this test.
1429    if paren == 0 {
1430        let off = patparse_off.load(Ordering::Relaxed);
1431        let parse = patparse.lock().unwrap();
1432        let bytes = parse.as_bytes();
1433        if off < bytes.len() {
1434            // c:915-916 — `!((patflags & PAT_FILE) && *patparse == '/')`:
1435            // a file glob legitimately stops at the component separator,
1436            // and its caller (parsecomplist) resumes from there.
1437            let is_file_slash =
1438                (patflags.load(Ordering::Relaxed) & PAT_FILE as i32) != 0 && bytes[off] == b'/';
1439            if !is_file_slash {
1440                return -1; // c:917 `return 0`
1441            }
1442        }
1443    }
1444    let _ = first_branch;
1445    // c:919-929 — C emits the closing P_GFLAGS restore right here, using
1446    // this local `gfchanged`. The Rust port emits it from patcomppiece's
1447    // `(` arm (which owns the P_CLOSE node), so hand the flag across the
1448    // call boundary. See PATSWITCH_GFCHANGED's warning block.
1449    PATSWITCH_GFCHANGED.store(gfchanged, Ordering::Relaxed);
1450    starter as i64
1451}
1452
1453// !!! WARNING: RUST-ONLY HELPER !!!
1454// C keeps `gfchanged` as a local of `patcompswitch` and emits the
1455// group-closing P_GFLAGS restore inside the same function
1456// (Src/pattern.c:919-929), immediately after the `ender` node it also
1457// creates there. The Rust port splits that work: patcomppiece's `b'('`
1458// arm owns P_OPEN/P_CLOSE and therefore owns the restore emission, so
1459// the counter has to cross the call boundary. patcompswitch stores it
1460// just before returning; the `(` arm reads it immediately after its own
1461// patcompswitch call returns, so nested groups each observe their own
1462// value.
1463static PATSWITCH_GFCHANGED: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
1464
1465/// Port of `patcompbranch(int *flagp, int paren)` from `Src/pattern.c:942`.
1466///
1467/// C: `static long patcompbranch(int *flagp, int paren)`. Parses a
1468/// single branch — a sequence of pieces. Returns offset of the first
1469/// node in the branch, or -1 on error.
1470pub fn patcompbranch(flagp: &mut i32, paren: i32) -> i64 {
1471    // c:942
1472    let mut chain_start: i64 = -1;
1473    let mut last_tail: usize = 0;
1474    // Track preceding piece for POSTFIX `(#cN,M)` wrap. C does
1475    // `patinsert(P_COUNTSTART, starter, ...)` to embed COUNTSTART
1476    // BEFORE the just-compiled piece (c:pattern.c:1686). We snapshot
1477    // the piece bytes, truncate, emit P_COUNT, then re-append.
1478    let mut last_piece_off: i64 = -1; // start offset of preceding piece
1479    let mut prev_chain_tail: i64 = -1; // tail of chain BEFORE preceding piece (-1 if piece was first)
1480                                       // Flags the preceding patcomppiece reported. P_HSTART marks a piece that
1481                                       // already had a closure applied (`a#`, `a##`, or an earlier `(#cN,M)`) —
1482                                       // patcomppiece sets `*flagp = P_HSTART` on every such arm, mirroring C
1483                                       // (c:1626/1629/1636/1640/1643). Used below to reject a stacked count.
1484    let mut last_piece_flags: i32 = 0;
1485    *flagp = P_PURESTR;
1486
1487    // c:951-952 — snapshot the segment-special set so we can do
1488    // `memchr(zpc_special, byte, ZPC_SEG_COUNT)`-equivalent lookups.
1489    // Only the first ZPC_SEG_COUNT slots (SLASH, NULL, BAR, OUTPAR,
1490    // TILDE) matter here.
1491    let (sp_tilde, sp_seg_set, sp_inpar, sp_hash, sp_slash) = {
1492        let sp = zpc_special.lock().unwrap();
1493        let tilde = sp[ZPC_TILDE as usize];
1494        let mut set = [false; 256];
1495        for i in 0..(ZPC_SEG_COUNT as usize) {
1496            set[sp[i] as usize] = true;
1497        }
1498        (
1499            tilde,
1500            set,
1501            sp[ZPC_INPAR as usize],
1502            sp[ZPC_HASH as usize],  // c:1609
1503            sp[ZPC_SLASH as usize], // c:949 — the slot holds '/' only while special
1504        )
1505    };
1506
1507    loop {
1508        let off = patparse_off.load(Ordering::Relaxed);
1509        // Snapshot the parse buffer into an owned slice for branch
1510        // decisions; release the lock so subsequent emit helpers
1511        // (which acquire patout's lock) can't contend.
1512        let snapshot: Vec<u8> = {
1513            let parse = patparse.lock().unwrap();
1514            parse.as_bytes().to_vec()
1515        };
1516        if off >= snapshot.len() {
1517            break;
1518        }
1519        let c = snapshot[off];
1520        // Branch terminators: |, ), end of pattern.
1521        if c == b'|' || c == b')' {
1522            break;
1523        }
1524        // c:949-952 — '/' is ZPC_SLASH (slot 0, within ZPC_SEG_COUNT) and
1525        // terminates the segment at EVERY paren depth: C's loop condition is
1526        // `!memchr(zpc_special, *patparse, ZPC_SEG_COUNT)` with no `paren`
1527        // test anywhere. C keeps '/' literal by MASKING THE SLOT instead —
1528        // for a non-FILE pattern (c:568-570) and around a top-level `~`
1529        // exclusion (c:840-842, restored c:880-883). A `/` inside a group
1530        // therefore leaves the group unterminated and c:913-914
1531        // (`if (paren && *patparse++ != Outpar) return 0`) rejects the whole
1532        // pattern, which is why `*(-/):t:directories` is a "bad pattern" in
1533        // zsh. Gating on `paren == 0 && PAT_FILE` instead compiled it happily
1534        // and answered "no matches found": `_files` then never took the
1535        // errflag unwind that stops it reaching its remaining `-g` patterns,
1536        // so `CC <TAB>` listed every directory where zsh inserts one match.
1537        if c == b'/' && sp_slash == b'/' {
1538            break;
1539        }
1540        // c:950-952 — `~` is an additional terminator when active as
1541        // the exclusion operator. The C condition includes all five
1542        // segment-specials (SLASH, NULL, BAR, OUTPAR, TILDE), but
1543        // `|` and `)` are already covered above and SLASH inside
1544        // `(...)` alternation is intentionally left literal here to
1545        // preserve the existing `zsh_corpus_hash_s_e_anchors_match_
1546        // bare_test` parity pin.
1547        if c == sp_tilde && c != 0 {
1548            let la = snapshot.get(off + 1).copied().unwrap_or(0);
1549            // Literal-tilde exception: `~` followed by a segment-
1550            // special OTHER than `/` keeps the `~` as literal.
1551            let literal_tilde_exception = la != b'/' && sp_seg_set[la as usize];
1552            if !literal_tilde_exception {
1553                break;
1554            }
1555        }
1556        let bytes = snapshot.as_slice();
1557        // Mid-pattern `(#cN,M)` counted-repetition specifier — emit
1558        // P_COUNT with bounds + inline operand following. Detected
1559        // BEFORE the generic patgetglobflags path because `c` is not
1560        // a flag char in that fn.
1561        //
1562        // c:1608-1610 — the `(` and `#` are compared against
1563        // `zpc_special[ZPC_INPAR]` / `zpc_special[ZPC_HASH]`, NOT against
1564        // literal bytes. That indirection is the whole EXTENDED_GLOB gate:
1565        // patcompcharsset() rewrites `zpc_special[ZPC_HASH]` to Marker
1566        // (c:482) when EXTENDED_GLOB is off, so a literal `#` can never
1567        // equal it and `(#c...)` stops being a counted closure — it falls
1568        // through and parses as an ordinary group. Testing `b'#'` directly
1569        // applied the closure with EXTENDED_GLOB unset, so
1570        // `[[ aaa = a(#c2,3) ]]` matched (zsh: no match).
1571        if off + 2 < bytes.len()
1572            && bytes[off] == sp_inpar
1573            && bytes[off + 1] == sp_hash
1574            && bytes[off + 2] == b'c'
1575        {
1576            let mut j = off + 3;
1577            let mut min: i64 = 0;
1578            let min_start = j;
1579            while j < bytes.len() && bytes[j].is_ascii_digit() {
1580                min = min * 10 + (bytes[j] - b'0') as i64;
1581                j += 1;
1582            }
1583            let mut max: i64 = i64::MAX;
1584            // Three valid shapes (per zsh extended-glob (#cN,M)):
1585            //   (#cN)      — exact count N  (min=N, max=N)
1586            //   (#cN,)     — min N, no max  (min=N, max=∞)
1587            //   (#cN,M)    — N..M range     (min=N, max=M)
1588            //   (#c,M)     — max M, no min  (min=0, max=M)  ← bug #489 missing case
1589            //   (#c,)      — equivalent to no count (min=0, max=∞)
1590            // The original `if j > min_start` gate skipped the `)`
1591            // check for the no-min shapes, leaving them unparsed.
1592            let has_min_digits = j > min_start;
1593            let has_comma = j < bytes.len() && bytes[j] == b',';
1594            if has_min_digits || has_comma {
1595                if has_comma {
1596                    j += 1; // skip ,
1597                    let max_start = j;
1598                    let mut mx: i64 = 0;
1599                    while j < bytes.len() && bytes[j].is_ascii_digit() {
1600                        mx = mx * 10 + (bytes[j] - b'0') as i64;
1601                        j += 1;
1602                    }
1603                    if j > max_start {
1604                        max = mx;
1605                    }
1606                } else {
1607                    // (#cN) — exact count N.
1608                    max = min;
1609                }
1610                if j < bytes.len() && bytes[j] == b')' {
1611                    j += 1;
1612                    patparse_off.store(j, Ordering::Relaxed);
1613                    // POSTFIX semantics — c:pattern.c:1686.
1614                    // C does `patinsert(P_COUNTSTART, starter, ...)`
1615                    // then `pattail(opnd, patnode(P_BACK))` to loop the
1616                    // operand back. zshrs uses a simpler encoding where
1617                    // P_COUNT carries [min, max] then operand bytes
1618                    // inline; matcher iterates outside. So we relocate
1619                    // the preceding piece into the operand slot.
1620                    // c:1431-1436 — `case Star:` sets `kshchar = -1`, whose
1621                    // only purpose is (per C's own comment) "a sign that we
1622                    // can't have #'s". c:1620-1622 then rejects the pattern:
1623                    //
1624                    //     /* too much at once doesn't currently work */
1625                    //     if (kshchar && (hash || count))
1626                    //         return 0;
1627                    //
1628                    // So a count may not follow `*`, nor stack on a piece
1629                    // that already carries a closure (`a#(#c2,3)`). zshrs
1630                    // attaches the count to the preceding piece rather than
1631                    // tracking kshchar, so the same rule is expressed by
1632                    // looking at that piece's opcode. Without this, zshrs
1633                    // silently accepted `*(#c2,3)` / `a#(#c2,3)`, which zsh
1634                    // rejects as a bad pattern.
1635                    if last_piece_off >= 0 {
1636                        let prev_op = {
1637                            let buf = patout.lock().unwrap();
1638                            buf.get(last_piece_off as usize + I_OP)
1639                                .copied()
1640                                .unwrap_or(0)
1641                        };
1642                        // `*` is C's kshchar = -1 (its head node is P_STAR);
1643                        // P_HSTART marks a piece that already closured, which
1644                        // is what makes `a#(#c2,3)` / `a##(#c2,3)` bad too.
1645                        if prev_op == P_STAR || (last_piece_flags & P_HSTART) != 0 {
1646                            return -1; // c:1622 `return 0;`
1647                        }
1648                    }
1649                    if last_piece_off >= 0 {
1650                        let piece_start = last_piece_off as usize;
1651                        // Snapshot preceding piece (everything emitted
1652                        // by the prior patcomppiece call).
1653                        let piece_bytes: Vec<u8> = {
1654                            let buf = patout.lock().unwrap();
1655                            buf[piece_start..].to_vec()
1656                        };
1657                        // Truncate to remove the piece.
1658                        {
1659                            let mut buf = patout.lock().unwrap();
1660                            buf.truncate(piece_start);
1661                        }
1662                        // Cut chain at prev_chain_tail (was pointing
1663                        // into the piece via set_next or as
1664                        // chain_start).
1665                        if prev_chain_tail >= 0 {
1666                            set_next(prev_chain_tail as usize, 0);
1667                        } else {
1668                            chain_start = -1;
1669                        }
1670                        // Emit P_COUNT at current end.
1671                        let count_off = patnode(P_COUNT);
1672                        let operand_new_start: usize;
1673                        {
1674                            let mut buf = patout.lock().unwrap();
1675                            buf.extend_from_slice(&min.to_le_bytes());
1676                            buf.extend_from_slice(&max.to_le_bytes());
1677                            operand_new_start = buf.len();
1678                        }
1679                        // Relocate piece bytes: rewrite internal next
1680                        // links (absolute offsets >= piece_start) by
1681                        // delta = operand_new_start - piece_start.
1682                        let delta: i64 = operand_new_start as i64 - piece_start as i64;
1683                        let mut relocated = piece_bytes.clone();
1684                        let mut i = 0;
1685                        while i + I_BODY <= relocated.len() {
1686                            let op = relocated[i + I_OP];
1687                            if op == 0 {
1688                                i += 1;
1689                                continue;
1690                            }
1691                            let nxt = u32::from_le_bytes(
1692                                relocated[i + I_NEXT..i + I_NEXT + 4].try_into().unwrap(),
1693                            );
1694                            if nxt != 0 {
1695                                let new_nxt = (nxt as i64 + delta) as u32;
1696                                relocated[i + I_NEXT..i + I_NEXT + 4]
1697                                    .copy_from_slice(&new_nxt.to_le_bytes());
1698                            }
1699                            let next_i = advance_past_instr(&relocated, i);
1700                            if next_i == 0 || next_i <= i {
1701                                break;
1702                            }
1703                            i = next_i;
1704                        }
1705                        {
1706                            let mut buf = patout.lock().unwrap();
1707                            buf.extend_from_slice(&relocated);
1708                        }
1709                        // Hook P_COUNT into chain.
1710                        if chain_start < 0 {
1711                            chain_start = count_off as i64;
1712                        } else {
1713                            set_next(prev_chain_tail as usize, count_off);
1714                        }
1715                        last_tail = count_off;
1716                        // Consumed — clear tracking. The resulting piece IS a
1717                        // closure (C: `*flagp = P_HSTART`, c:1636), so a second
1718                        // count stacked on it must be rejected.
1719                        last_piece_off = -1;
1720                        prev_chain_tail = -1;
1721                        last_piece_flags = P_HSTART;
1722                        continue;
1723                    }
1724                    // No preceding piece — `(#cN,M)` is a POSTFIX
1725                    // modifier in real zsh and requires a piece to
1726                    // attach to. Without one C `Src/pattern.c:1606+`
1727                    // rejects the pattern. Bug #521: zshrs previously
1728                    // took a legacy PREFIX path (compile next piece as
1729                    // operand) which silently matched empty for `0,0`
1730                    // and similar degenerate ranges.
1731                    //
1732                    // Report nothing here: C's patcomppiece signals a bad
1733                    // pattern by `return 0` alone (c:1600, c:1622) and the
1734                    // CALLER prints the diagnostic with the pattern it was
1735                    // given — `matchpat` zerrs "bad pattern: %s" with the
1736                    // full pattern (glob.c:2522), and `[[ ]]` zwarnnams it
1737                    // from cond.c:314. Emitting a message from inside the
1738                    // compiler printed only the `(#cN,M)` fragment, losing
1739                    // the rest of the pattern the user actually wrote.
1740                    return -1; // c:1622 `return 0;`
1741                }
1742            }
1743            // Malformed `(#c...)` — fall through to generic flag handler.
1744        }
1745        // c:953-984 — mid-pattern `(#...)` glob-flag specifier. Emits
1746        // P_GFLAGS in C to switch GF_IGNCASE / GF_LCMATCHUC /
1747        // GF_MULTIBYTE / etc. mid-match. C body:
1748        //   `if ((*patparse == zpc_special[ZPC_INPAR] &&`
1749        //   `     patparse[1] == zpc_special[ZPC_HASH]) || ...)`
1750        //   `    if (!patgetglobflags(&patparse, &assert, &ignore))`
1751        //   `        return 0;`
1752        // Both bytes must equal their zpc_special slots, which
1753        // patcompcharsset (c:480-483) masks to Marker when
1754        // EXTENDEDGLOB is off (c:476). Previously this Rust arm
1755        // compared the raw byte `b'#'`, allowing `(#s)` / `(#e)` /
1756        // `(#i)` to fire even without EXTENDEDGLOB. Parity bugs
1757        // #18/#19 vs real zsh.
1758        let hash_char = zpc_special.lock().unwrap()[ZPC_HASH as usize]; // c:957
1759                                                                        // c:953-954 compares the FIRST byte against
1760                                                                        // `zpc_special[ZPC_INPAR]` too, which patcompcharsset masks to
1761                                                                        // Marker under SHGLOB (c:500-510) or `disable -p '('`. With `(`
1762                                                                        // disabled, `(#i)abc` is the LITERAL text `(#i)abc`, so the
1763                                                                        // flag spec must not fire.
1764                                                                        // c:955-956 — the ksh form `@(#...)`:
1765                                                                        //   `(*patparse == zpc_special[ZPC_KSH_AT] &&
1766                                                                        //     patparse[1] == Inpar && patparse[2] == zpc_special[ZPC_HASH])`
1767                                                                        // is equally a glob-flag specifier. c:961 then skips 3 bytes
1768                                                                        // instead of 2. Misc/globtests.ksh:94 writes `(#i)FOO@(#I)X@(#i)X`
1769                                                                        // and zshrs reported "bad pattern" without this arm.
1770        let sp_ksh_at = zpc_special.lock().unwrap()[ZPC_KSH_AT as usize];
1771        let at_form = hash_char == b'#'
1772            && sp_ksh_at != 0
1773            && off + 2 < bytes.len()
1774            && bytes[off] == sp_ksh_at
1775            && bytes[off + 1] == sp_inpar
1776            && bytes[off + 2] == b'#';
1777        if at_form
1778            || (hash_char == b'#'
1779                && off + 1 < bytes.len()
1780                && bytes[off] == sp_inpar
1781                && bytes[off + 1] == b'#')
1782        {
1783            // c:961 — `patparse += (*patparse == '@') ? 3 : 2;` — the
1784            // Rust patgetglobflags() consumes the leading `(#` itself, so
1785            // hand it the string starting at the `(`.
1786            let skip = usize::from(at_form);
1787            let rest = std::str::from_utf8(&bytes[off + skip..])
1788                .unwrap_or("")
1789                .to_string();
1790            // c:959 — `int oldglobflags = patglobflags, ignore;`
1791            let oldglobflags = patglobflags.load(Ordering::Relaxed);
1792            if let Some((_bits, assertp, consumed)) = patgetglobflags(&rest) {
1793                patparse_off.fetch_add(consumed + skip, Ordering::Relaxed);
1794                // c:995-997 — `} else { /* No effect. */ continue; }`.
1795                // A specifier that leaves patglobflags unchanged emits
1796                // nothing. Matters after c:872's per-branch reset:
1797                // `((#I)foo|(#i)rod)` re-enters branch 2 already
1798                // case-insensitive, so its `(#i)` is a no-op.
1799                if assertp == 0 && patglobflags.load(Ordering::Relaxed) == oldglobflags {
1800                    continue;
1801                }
1802                // Emit P_GFLAGS for flag-bit changes if any. Include the
1803                // low byte (the `(#aN)` approximation budget) so a
1804                // mid-pattern `(#a0)` / `(#a2)` actually changes the
1805                // error allowance for the following segment — c:Src/
1806                // pattern.c:2941 `patglobflags = P_OPERAND(scan)->l`
1807                // sets the WHOLE value. Without the 0xff, `(#a1)cat(#a0)
1808                // dog` kept the outer budget and `dog` wrongly tolerated
1809                // an error.
1810                // c:989-994 — the payload is the RUNNING `patglobflags`
1811                // (`up.l = patglobflags`), not the delta this specifier
1812                // contributed. `patgetglobflags` only ever reports bits it
1813                // turned ON, so building the payload from it dropped
1814                // GF_MULTIBYTE — and since c:2942 assigns the payload
1815                // ABSOLUTELY (`patglobflags = P_OPERAND(scan)->l`), a
1816                // mid-pattern `(#b)` demoted the rest of the match to
1817                // single-byte stepping: `[[ $'αβγδε' = *(#b)(?) ]]` then
1818                // matched only the last BYTE of `ε`.
1819                let flag_bits = patglobflags.load(Ordering::Relaxed)
1820                    & (GF_IGNCASE | GF_LCMATCHUC | GF_MULTIBYTE | 0xff);
1821                if flag_bits != 0 || (flag_bits == 0 && assertp == 0) {
1822                    let gf_off = patnode(P_GFLAGS);
1823                    let mut buf = patout.lock().unwrap();
1824                    buf.extend_from_slice(&flag_bits.to_le_bytes());
1825                    drop(buf);
1826                    if chain_start < 0 {
1827                        chain_start = gf_off as i64;
1828                    } else {
1829                        set_next(last_tail, gf_off);
1830                    }
1831                    last_tail = gf_off;
1832                }
1833                // Emit P_ISSTART / P_ISEND when assertp set.
1834                if assertp != 0 {
1835                    let as_off = patnode(assertp as u8);
1836                    if chain_start < 0 {
1837                        chain_start = as_off as i64;
1838                    } else {
1839                        set_next(last_tail, as_off);
1840                    }
1841                    last_tail = as_off;
1842                }
1843                continue;
1844            }
1845            // patgetglobflags failed — treat the `(` as a literal group.
1846        }
1847
1848        // c:1011-1014 — `^pat` standalone negation. When `^` is the
1849        // active EXTENDEDGLOB special and appears at piece position
1850        // (not inside a `[...]` bracket class), consume it and call
1851        // patcompnot(0, ...) to emit the EXCLUDE structure.
1852        let sp_hat = zpc_special.lock().unwrap()[ZPC_HAT as usize];
1853        if c == sp_hat && sp_hat != 0 {
1854            patparse_off.fetch_add(1, Ordering::Relaxed); // c:1012 patparse++
1855            let mut not_flags: i32 = 0;
1856            let starter = patcompnot(0, &mut not_flags); // c:1013 patcompnot(0, ...)
1857            if starter < 0 {
1858                return -1;
1859            }
1860            *flagp |= not_flags & P_HSTART;
1861            if chain_start < 0 {
1862                chain_start = starter;
1863            } else {
1864                set_next(last_tail, starter as usize);
1865            }
1866            // patcompnot (c:1759-1784) ends by emitting the shared trailing
1867            // P_NOTHING (the `excend`/`excl` convergence point), so the
1868            // chain tail is the last node emitted: the current emit position
1869            // is the start of the next node.
1870            let cur_emit = patout.lock().unwrap().len();
1871            last_tail = cur_emit.saturating_sub(I_BODY);
1872            continue;
1873        }
1874        drop(snapshot); // hint: explicit release
1875        let mut piece_flags: i32 = 0;
1876        let mut piece_tail: usize = 0;
1877        // Snapshot the chain tail BEFORE patcomppiece — used by a
1878        // following POSTFIX (#cN,M) to detach this piece from the chain.
1879        let prev_tail_before_piece: i64 = if chain_start < 0 {
1880            -1
1881        } else {
1882            last_tail as i64
1883        };
1884        // !!! WARNING: RUST-ONLY HELPER !!!
1885        //
1886        // C has no such guard here because it cannot need one: c:1336's
1887        // `DPUTS(patparse == str0, "BUG: matched nothing in patcomppiece.")`
1888        // is a debug-only assertion resting on the invariant that every
1889        // `patcomppiece` arm advances `patparse`. When a Rust arm broke that
1890        // invariant (the multibyte `é#` backtrack, pattern.rs:2960) the loop
1891        // below became an unkillable CPU spin that ate the whole shell.
1892        // Turning the same invariant into a hard bail keeps a future
1893        // regression a "bad pattern" error instead of a hang.
1894        let off_before_piece = patparse_off.load(Ordering::Relaxed);
1895        let piece = patcomppiece(&mut piece_flags, paren, &mut piece_tail);
1896        if piece < 0 {
1897            return -1;
1898        }
1899        if patparse_off.load(Ordering::Relaxed) <= off_before_piece {
1900            // c:1336 — the DPUTS condition, made fatal.
1901            return -1;
1902        }
1903        if chain_start < 0 {
1904            chain_start = piece;
1905        } else {
1906            // Chain previous piece's tail → this piece's head directly.
1907            set_next(last_tail, piece as usize);
1908        }
1909        last_tail = piece_tail;
1910        last_piece_off = piece;
1911        prev_chain_tail = prev_tail_before_piece;
1912        last_piece_flags = piece_flags;
1913        *flagp &= piece_flags;
1914    }
1915
1916    if chain_start < 0 {
1917        chain_start = patnode(P_NOTHING) as i64;
1918    }
1919    chain_start
1920}
1921
1922// =====================================================================
1923// 10. Glob-flag parser — pattern.c:1037
1924// =====================================================================
1925
1926/// Port of `patgetglobflags()` from `Src/pattern.c:1037`.
1927/// C: `patgetglobflags(char **strp, long *assertp, int *ignore)`.
1928///
1929/// C signature: `int patgetglobflags(char **strp, long *assertp,
1930/// int *ignore)`. C reads/writes the file-static `patglobflags`
1931/// directly via `|=` / `&=` at each switch arm (c:1064, 1070, 1075,
1932/// 1080, 1085, 1090, 1095, 1100, 1112, 1116). The hoist loop in
1933/// patcompile at c:582 / c:636 snapshots `patglobflags` into the
1934/// compiled `Patprog`'s `globflags` and `globend` fields, so the
1935/// per-arm writes are the canonical store — the return value is
1936/// only the success/fail signal (1 / 0).
1937///
1938/// The Rust port stores `patglobflags` in an AtomicI32 and mirrors
1939/// every C write through `fetch_or` / `fetch_and` ops so the same
1940/// snapshot at c:636 works. The return tuple `(flag_bits, assertp,
1941/// consumed)` is kept as an adapter for the patcompile hoist loop
1942/// (pattern.rs:589) which used it to accumulate `hoisted_globflags`
1943/// — those callers see the same bits via the atomic now, but
1944/// changing the return shape is a wider refactor.
1945///
1946/// Returns `Some((flag_bits, assertp_val, consumed_bytes))` on
1947/// success (C returns 1), or `None` on parse failure (C returns 0).
1948pub fn patgetglobflags(s: &str) -> Option<(i32, i64, usize)> {
1949    // c:1037
1950    let bytes = s.as_bytes();
1951    if !s.starts_with("(#") {
1952        return None;
1953    }
1954    let mut i = 2;
1955    // `bits` tracks what this call set so the caller can OR into
1956    // `hoisted_globflags` (the patcompile-side accumulator). The
1957    // canonical store is `patglobflags` (updated below per C arm).
1958    let mut bits: i32 = 0;
1959    let mut assertp: i64 = 0;
1960
1961    while i < bytes.len() && bytes[i] != b')' {
1962        match bytes[i] {
1963            // c:1073-1076 `'i'`: `patglobflags = (patglobflags &
1964            // ~GF_LCMATCHUC) | GF_IGNCASE`.
1965            b'i' => {
1966                patglobflags.fetch_and(!GF_LCMATCHUC, Ordering::Relaxed);
1967                patglobflags.fetch_or(GF_IGNCASE, Ordering::Relaxed);
1968                bits |= GF_IGNCASE;
1969                bits &= !GF_LCMATCHUC;
1970                i += 1;
1971            } // c:1075
1972            // c:1078-1081 — `'I'`: `patglobflags &= ~(GF_LCMATCHUC|GF_IGNCASE)`.
1973            b'I' => {
1974                patglobflags.fetch_and(!(GF_LCMATCHUC | GF_IGNCASE), Ordering::Relaxed);
1975                bits &= !(GF_LCMATCHUC | GF_IGNCASE);
1976                i += 1;
1977            } // c:1080
1978            // c:1068-1071 — `'l'`: `patglobflags = (patglobflags &
1979            // ~GF_IGNCASE) | GF_LCMATCHUC`.
1980            b'l' => {
1981                patglobflags.fetch_and(!GF_IGNCASE, Ordering::Relaxed);
1982                patglobflags.fetch_or(GF_LCMATCHUC, Ordering::Relaxed);
1983                bits |= GF_LCMATCHUC;
1984                bits &= !GF_IGNCASE;
1985                i += 1;
1986            } // c:1070
1987            // c:1083-1086 — `'b'`: `patglobflags |= GF_BACKREF`.
1988            b'b' => {
1989                patglobflags.fetch_or(GF_BACKREF, Ordering::Relaxed);
1990                bits |= GF_BACKREF;
1991                i += 1;
1992            } // c:1085
1993            // c:1088-1091 — `'B'`: `patglobflags &= ~GF_BACKREF`.
1994            b'B' => {
1995                patglobflags.fetch_and(!GF_BACKREF, Ordering::Relaxed);
1996                bits &= !GF_BACKREF;
1997                i += 1;
1998            } // c:1090
1999            // c:1093-1096 — `'m'`: `patglobflags |= GF_MATCHREF`.
2000            b'm' => {
2001                patglobflags.fetch_or(GF_MATCHREF, Ordering::Relaxed);
2002                bits |= GF_MATCHREF;
2003                i += 1;
2004            } // c:1095
2005            // c:1098-1101 — `'M'`: `patglobflags &= ~GF_MATCHREF`.
2006            b'M' => {
2007                patglobflags.fetch_and(!GF_MATCHREF, Ordering::Relaxed);
2008                bits &= !GF_MATCHREF;
2009                i += 1;
2010            } // c:1100
2011            // c:1103-1105 — `'s'`: sets `*assertp = P_ISSTART`,
2012            // doesn't touch patglobflags.
2013            b's' => {
2014                assertp = P_ISSTART as i64;
2015                i += 1;
2016            } // c:1104
2017            // c:1107-1109 — `'e'`: sets `*assertp = P_ISEND`.
2018            b'e' => {
2019                assertp = P_ISEND as i64;
2020                i += 1;
2021            } // c:1108
2022            // c:1111-1113 — `'u'`: `patglobflags |= GF_MULTIBYTE`.
2023            b'u' => {
2024                patglobflags.fetch_or(GF_MULTIBYTE, Ordering::Relaxed);
2025                bits |= GF_MULTIBYTE;
2026                i += 1;
2027            } // c:1112
2028            // c:1115-1117 — `'U'`: `patglobflags &= ~GF_MULTIBYTE`.
2029            b'U' => {
2030                patglobflags.fetch_and(!GF_MULTIBYTE, Ordering::Relaxed);
2031                bits &= !GF_MULTIBYTE;
2032                i += 1;
2033            } // c:1116
2034            // c:1054-1066 — `'a'`: approximate-match error count.
2035            // `ret = zstrtol(++ptr, &nptr, 10);` then
2036            // `patglobflags = (patglobflags & ~0xff) | (ret & 0xff)`.
2037            b'a' => {
2038                i += 1;
2039                let digit_start = i;
2040                let mut errs: i32 = 0;
2041                while i < bytes.len() && bytes[i].is_ascii_digit() {
2042                    errs = errs * 10 + (bytes[i] - b'0') as i32;
2043                    i += 1;
2044                }
2045                if i == digit_start {
2046                    // c:1062 `ptr == nptr` — no digits consumed.
2047                    return None;
2048                }
2049                if errs < 0 || errs > 254 {
2050                    return None; // c:1062
2051                }
2052                // c:1064 — `patglobflags = (patglobflags & ~0xff) | (ret & 0xff)`.
2053                let mask: i32 = !0xff;
2054                let cur = patglobflags.load(Ordering::Relaxed);
2055                patglobflags.store((cur & mask) | (errs & 0xff), Ordering::Relaxed);
2056                bits = (bits & !0xff) | (errs & 0xff);
2057            }
2058            // c:1046-1050 — `'q'`: glob qualifier, ignored in pattern
2059            // code. Skip to the closing `)` without consuming bits.
2060            b'q' => {
2061                while i < bytes.len() && bytes[i] != b')' {
2062                    i += 1;
2063                }
2064            }
2065            // c:1119-1120 — `default: return 0`.
2066            _ => return None,
2067        }
2068    }
2069    // c:1124-1125 — `if (*ptr != Outpar) return 0;`.
2070    if i >= bytes.len() {
2071        return None;
2072    }
2073    i += 1; // c:1129 — `*strp = ptr + 1;` advance past `)`.
2074    Some((bits, assertp, i))
2075}
2076
2077// =====================================================================
2078// 11. Range helpers — pattern.c:1148, :1179
2079// =====================================================================
2080
2081/// Port of `range_type()` from `Src/pattern.c:1148`.
2082/// C: `range_type(char *start, int len)`. Looks up the
2083/// integer code for a POSIX character class name (e.g. "alpha" → 1).
2084/// Returns None for unknown names.
2085///
2086/// C signature takes a `(char *start, int len)` non-NUL-terminated
2087/// substring; Rust's `&str` carries the length implicitly, so the
2088/// two-arg C shape collapses to a single arg. Param name `start`
2089/// matches C verbatim (Rule E).
2090pub fn range_type(start: &str) -> Option<usize> {
2091    // c:1148
2092    POSIX_CLASS_NAMES
2093        .iter()
2094        .position(|n| *n == start)
2095        .map(|i| i + 1)
2096}
2097
2098/// Port of `pattern_range_to_string()` from `Src/pattern.c:1179`.
2099/// C: `int pattern_range_to_string(char *rangestr, char *outstr)`
2100/// from `Src/pattern.c:1179`. Walks a Meta-encoded range bytestring,
2101/// re-emitting the human-readable form: literal chars as-is,
2102/// PP_RANGE pairs as `c1-c2`, POSIX classes as `[:name:]`.
2103/// Returns the output length; `outstr` is the destination buffer
2104/// (NULL = measure-only). The Rust port operates on `&str` (UTF-8
2105/// native) — `Meta`-byte decode collapses since zshrs's pattern
2106/// compiler stores raw chars, so the function effectively walks
2107/// the chars and emits POSIX class names from the `[i]` table at
2108/// `range_type`'s reverse lookup.
2109///
2110/// Rust signature: drops C's `outstr` out-param. C measures-then-fills
2111/// via two passes when `outstr` is non-NULL; Rust returns the String
2112/// directly. Callers needing the C "measure-only" mode read `.len()`
2113/// on the result.
2114pub fn pattern_range_to_string(rangestr: &str) -> String {
2115    // c:1179
2116    let mut out = String::with_capacity(rangestr.len()); // c:1181 int len = 0
2117    let mut chars = rangestr.chars().peekable();
2118    while let Some(c) = chars.next() {
2119        // c:1184-1247 — swtype dispatch via Meta+PP_*. zshrs stores
2120        // POSIX-class markers as a sentinel byte followed by the
2121        // class name. The Meta encoding doesn't apply (UTF-8 native),
2122        // so we just pass chars through, recognizing PP_* class tags
2123        // when they appear as `[:name:]` literal syntax.
2124        if c == '[' && chars.peek() == Some(&':') {
2125            // c:1242 — `[:alpha:]` and similar.
2126            let mut name = String::new();
2127            chars.next(); // consume ':'
2128            while let Some(&cc) = chars.peek() {
2129                if cc == ':' {
2130                    chars.next();
2131                    if chars.peek() == Some(&']') {
2132                        chars.next();
2133                        break;
2134                    }
2135                    name.push(':');
2136                } else {
2137                    name.push(cc);
2138                    chars.next();
2139                }
2140            }
2141            out.push_str(&format!("[:{}:]", name)); // c:1244
2142        } else {
2143            // c:1185-1213 — single-char or range pair.
2144            // Check for `c1-c2` PP_RANGE form.
2145            out.push(c); // c:1210/1216
2146            if chars.peek() == Some(&'-') {
2147                chars.next();
2148                if let Some(c2) = chars.next() {
2149                    out.push('-'); // c:1219
2150                    out.push(c2); // c:1220
2151                }
2152            }
2153        }
2154    }
2155    out // c:1261 return len
2156}
2157
2158/// Port of `patcomppiece()` from `Src/pattern.c:1261`.
2159/// C: `patcomppiece(int *flagp, int paren)`.
2160///
2161/// C: `static long patcomppiece(int *flagp, int paren)`. Parses a
2162/// single atom + optional quantifier. Returns offset of compiled node.
2163/// Out-param `tail_out` receives the byte offset of the LAST opcode
2164/// in the compiled piece — the node whose `.next` should be chained
2165/// to whatever follows in the sequence. For simple atoms (P_EXACTLY,
2166/// P_ANY, etc.) the tail equals the head; for compound pieces
2167/// `(...)` / quantified atoms it points to the trailing P_CLOSE_N or
2168/// quantifier-injected node.
2169///
2170/// Rust signature adds `tail_out: &mut usize` for the LAST-opcode-
2171/// offset that C threads through pointer arithmetic on the
2172/// caller-side `Upat` cursor. Without the out-param, every Rust
2173/// caller would need to re-walk the just-emitted bytecode to find
2174/// the tail; the explicit out-param avoids the re-scan. Rule B
2175/// deviation sanctioned by zshrs's byte-offset bytecode (vs C's
2176/// pointer-arithmetic) substrate.
2177pub fn patcomppiece(flagp: &mut i32, paren: i32, tail_out: &mut usize) -> i64 {
2178    // c:1261
2179    // c:1509-1512 — `case Inpar:` carries two DPUTS asserting that a
2180    // BARE `(` (no kshchar prefix) never reaches the group compiler
2181    // when `zpc_special[ZPC_INPAR] == Marker`. patcompcharsset sets
2182    // that Marker for `isset(SHGLOB)` (c:500-510) and for a user
2183    // `disable -p '('`. So `(` opens a group only when its slot still
2184    // holds the literal byte, or when a ksh trigger (`@(`, `*(`, …)
2185    // put us here deliberately (c:1419 consumes the trigger and then
2186    // falls into `case Inpar`).
2187    let inpar_active = { zpc_special.lock().unwrap()[ZPC_INPAR as usize] == b'(' };
2188    // c:1312-1313 — the literal run ends on `memchr(zpc_special, *patparse,
2189    // ZPC_NO_KSH_GLOB)`, and c:1430-1444 DPUTS that `case Quest` / `case Star`
2190    // / `case Inbrack` are never reached with the slot at Marker. So after
2191    // `disable -p '?'` / `'*'` / `'['` (c:471-476 masks the slot) the byte is
2192    // an ordinary string character, like `(` and `<` above and below.
2193    let (quest_active, star_active, inbrack_active) = {
2194        let sp = zpc_special.lock().unwrap();
2195        (
2196            sp[ZPC_QUEST as usize] == b'?',
2197            sp[ZPC_STAR as usize] == b'*',
2198            sp[ZPC_INBRACK as usize] == b'[',
2199        )
2200    };
2201    let off = patparse_off.load(Ordering::Relaxed);
2202    let parse = patparse.lock().unwrap();
2203    if off >= parse.len() {
2204        return patnode(P_NOTHING) as i64;
2205    }
2206    let bytes = parse.as_bytes();
2207    let c = bytes[off];
2208
2209    // c:1278 — `kshchar = '\0';`. KSH-glob trigger detection: when the
2210    // current byte matches one of the six ZPC_KSH_* slots AND the next
2211    // byte is `(`, record which kshchar so the post-atom dispatch can
2212    // emit the right quantifier shape (c:1615-1746).
2213    //
2214    // c:1279 — `if (*patparse && patparse[1] == Inpar) { ... }`. Note
2215    // the C code uses literal `Inpar` (the `(` token) for the lookahead
2216    // — NOT `zpc_special[ZPC_INPAR]` — so SHGLOB disabling `(` as a
2217    // pattern char doesn't suppress ksh-glob detection here.
2218    let mut kshchar: u8 = 0; // c:1278
2219    if off + 1 < parse.len() && bytes[off + 1] == b'(' {
2220        // c:1279
2221        let sp = zpc_special.lock().unwrap();
2222        if c == sp[ZPC_KSH_PLUS as usize] {
2223            kshchar = b'+';
2224        }
2225        // c:1280-1281
2226        else if c == sp[ZPC_KSH_BANG as usize] {
2227            kshchar = b'!';
2228        }
2229        // c:1282-1283
2230        else if c == sp[ZPC_KSH_BANG2 as usize] {
2231            kshchar = b'!';
2232        }
2233        // c:1284-1285
2234        else if c == sp[ZPC_KSH_AT as usize] {
2235            kshchar = b'@';
2236        }
2237        // c:1286-1287
2238        else if c == sp[ZPC_KSH_STAR as usize] {
2239            kshchar = b'*';
2240        }
2241        // c:1288-1289
2242        else if c == sp[ZPC_KSH_QUEST as usize] {
2243            kshchar = b'?';
2244        } // c:1290-1291
2245    }
2246    drop(parse);
2247
2248    // c:1419-1420 — `if (kshchar) patparse++;`. Skip the trigger byte
2249    // so the atom dispatch consumes the leading `(` as a group.
2250    let dispatch_c = if kshchar != 0 {
2251        patparse_off.fetch_add(1, Ordering::Relaxed);
2252        b'(' // c:1419 — fall through to Inpar case
2253    } else {
2254        c
2255    };
2256
2257    // Atom dispatch. Each arm sets `*tail_out` to the offset of the
2258    // last opcode emitted by this piece (for simple atoms, tail = head).
2259    let atom = match dispatch_c {
2260        b'?' if quest_active => {
2261            // c:1430
2262            patparse_off.fetch_add(1, Ordering::Relaxed);
2263            *flagp |= P_SIMPLE;
2264            *flagp &= !P_PURESTR;
2265            let h = patnode(P_ANY);
2266            *tail_out = h;
2267            h as i64
2268        }
2269        b'*' if star_active => {
2270            // c:1436
2271            patparse_off.fetch_add(1, Ordering::Relaxed);
2272            *flagp &= !P_PURESTR;
2273            let h = patnode(P_STAR);
2274            *tail_out = h;
2275            h as i64
2276        }
2277        b'[' if inbrack_active => {
2278            // c:Src/pattern.c:1438 `case Inbrack` + c:1497-1498 —
2279            // `if (*patparse != Outbrack) return 0;`: an ACTIVE `[`
2280            // (token-derived; the patcompile entry normalization maps
2281            // the Inbrack token to raw `[` and every literal/escaped
2282            // bracket to `\[`, which the `\X` arm consumed before
2283            // reaching here) with no closing `]` is a BAD PATTERN.
2284            // Callers zerr "bad pattern: %s" exactly like zsh:
2285            // `print [a-` / `[[ x == [a- ]]` / `case x in [a-)`.
2286            //
2287            // History: a 2026-06-03 partial fix for Bug #564 made
2288            // this case fall back to a literal `[` because the cond
2289            // path then stripped `\` before patcompile. The escape
2290            // now survives into the `\X` literal arm (all three #564
2291            // probes pass), so the leniency only masked genuine bad
2292            // patterns.
2293            let probe_off = off + 1;
2294            let parse_check = patparse.lock().unwrap();
2295            let check_bytes = parse_check.as_bytes();
2296            let has_close = check_bytes[probe_off..].contains(&b']');
2297            drop(parse_check);
2298            if !has_close {
2299                return -1; // c:1498 `return 0;` — bad pattern
2300            }
2301            patparse_off.fetch_add(1, Ordering::Relaxed);
2302            *flagp |= P_SIMPLE;
2303            *flagp &= !P_PURESTR;
2304            // Inline bracket-expression parse (C patcomppiece bracket case).
2305            let mut chars: Vec<u8> = Vec::new();
2306            // Multibyte augmentation: `chars` is a flat ASCII byte set, so a
2307            // multibyte input char (Cyrillic/Greek/CJK/accented) can never
2308            // match it. C's `mb_patmatchrange` handles wide chars via
2309            // `iswalpha`/wide ranges. To match without disturbing the
2310            // (heavily-tested, byte-for-byte) ASCII path, collect the class
2311            // predicates and the non-ASCII literals/ranges SEPARATELY and
2312            // consult them only when the input char is multibyte (P_ANYOF).
2313            let mut mb_classmask: u32 = 0;
2314            let mut mb_chars: Vec<u32> = Vec::new();
2315            let mut mb_ranges: Vec<(u32, u32)> = Vec::new();
2316            let mut negate = false;
2317            let bracket_start = patparse_off.load(Ordering::Relaxed);
2318            let parse_b = patparse.lock().unwrap();
2319            let bb = parse_b.as_bytes();
2320            let mut i_b = bracket_start;
2321            if i_b < bb.len() && (bb[i_b] == b'^' || bb[i_b] == b'!') {
2322                negate = true;
2323                i_b += 1;
2324            }
2325            // c:Src/pattern.c:1456-1459 — `[]...]` exception: when `]` is
2326            // the FIRST char inside the class AND another `]` follows
2327            // later (so the close is not ambiguous), the first `]` is a
2328            // class member. Mirrors `[]x]` matching `]` or `x` in C.
2329            if i_b < bb.len() && bb[i_b] == b']' && bb[i_b + 1..].contains(&b']') {
2330                chars.push(b']');
2331                i_b += 1;
2332            }
2333            // A METAFIED raw byte (Meta U+0083 + byte^32, how zshrs holds a
2334            // byte that is not valid UTF-8) is ONE member, the character
2335            // C's range walk decodes from it: the byte itself without
2336            // GF_MULTIBYTE, else `mbrtowc` over the raw run, or
2337            // WCHAR_INVALID (c:384 metacharinc, compared at c:3634
2338            // `metacharinc(&range) == ch`). The input decode above spells
2339            // each pair `\<Meta>\<payload>`, which the `\X` arm below took
2340            // as two one-byte members — the first UTF-8 byte of each char —
2341            // so `[\xcc]` never matched `\xcc`. Rebuild the run of pairs
2342            // (a payload is always a 2-byte char, so a pair is 4 bytes
2343            // there and 6 here) and decode it with charrefinc.
2344            let esc_pair = |at: usize| -> bool {
2345                bb.get(at..at + 4) == Some(&[b'\\', 0xc2, 0x83, b'\\'][..])
2346                    && bb.get(at + 4).is_some_and(|&x| x >= 0xc2)
2347            };
2348            let member = |at: usize, z: &mut i32| -> Option<(u32, usize)> {
2349                let mut run = String::new();
2350                let mut q = at;
2351                while esc_pair(q) {
2352                    let payload = std::str::from_utf8(bb.get(q + 4..q + 6)?).ok()?;
2353                    run.push('\u{83}');
2354                    run.push_str(payload);
2355                    q += 6;
2356                }
2357                let mut used = 0;
2358                let wc = charrefinc(&run, &mut used, patglobflags.load(Ordering::Relaxed), z)?;
2359                Some((wc, at + used / 4 * 6))
2360            };
2361            while i_b < bb.len() && bb[i_b] != b']' {
2362                if esc_pair(i_b) {
2363                    let mut z = 0;
2364                    if let Some((lo, mut p)) = member(i_b, &mut z) {
2365                        let mut hi = lo;
2366                        if bb.get(p) == Some(&b'-') && esc_pair(p + 1) {
2367                            if let Some((h, q)) = member(p + 1, &mut z) {
2368                                hi = h;
2369                                p = q;
2370                            }
2371                        }
2372                        if (patglobflags.load(Ordering::Relaxed) & GF_MULTIBYTE) == 0 {
2373                            // c:1974-1975 — one raw byte per character.
2374                            for r in lo..=hi.min(0xff) {
2375                                chars.push(r as u8);
2376                            }
2377                        } else if lo == hi {
2378                            mb_chars.push(lo);
2379                        } else {
2380                            mb_ranges.push((lo, hi));
2381                        }
2382                        i_b = p;
2383                        continue;
2384                    }
2385                }
2386                // c:Src/pattern.c — `\X` inside a class is handled in C
2387                // by shtokenize converting it to `Bnullkeep X` upstream;
2388                // by the time C's bracket walker runs, the `]` after `\`
2389                // doesn't appear as raw `]` so the close-`]` scan
2390                // doesn't trip on it. The Rust port may receive raw
2391                // `\X` (callers that bypass shtokenize), so handle the
2392                // backslash-escape inline: consume `\X` as a literal
2393                // class-member byte. Bug surface:
2394                // `${q//(#m)[\][()|\\*?#<>~^]/...}` — escaped `]` /
2395                // `\\` / etc. inside the class are class members, not
2396                // metacharacters.
2397                if i_b + 1 < bb.len() && bb[i_b] == b'\\' {
2398                    chars.push(bb[i_b + 1]);
2399                    i_b += 2;
2400                    continue;
2401                }
2402                if i_b + 1 < bb.len() && bb[i_b] == b'[' && bb[i_b + 1] == b':' {
2403                    let class_start = i_b + 2;
2404                    let mut j_b = class_start;
2405                    while j_b + 1 < bb.len() && !(bb[j_b] == b':' && bb[j_b + 1] == b']') {
2406                        j_b += 1;
2407                    }
2408                    if j_b + 1 < bb.len() {
2409                        let class_name = std::str::from_utf8(&bb[class_start..j_b]).unwrap_or("");
2410                        // Inline POSIX class expansion.
2411                        match class_name {
2412                            "alpha" => {
2413                                for c in b'a'..=b'z' {
2414                                    chars.push(c);
2415                                }
2416                                for c in b'A'..=b'Z' {
2417                                    chars.push(c);
2418                                }
2419                            }
2420                            "upper" => {
2421                                for c in b'A'..=b'Z' {
2422                                    chars.push(c);
2423                                }
2424                            }
2425                            "lower" => {
2426                                for c in b'a'..=b'z' {
2427                                    chars.push(c);
2428                                }
2429                            }
2430                            "digit" => {
2431                                for c in b'0'..=b'9' {
2432                                    chars.push(c);
2433                                }
2434                            }
2435                            "xdigit" => {
2436                                for c in b'0'..=b'9' {
2437                                    chars.push(c);
2438                                }
2439                                for c in b'a'..=b'f' {
2440                                    chars.push(c);
2441                                }
2442                                for c in b'A'..=b'F' {
2443                                    chars.push(c);
2444                                }
2445                            }
2446                            "alnum" => {
2447                                for c in b'a'..=b'z' {
2448                                    chars.push(c);
2449                                }
2450                                for c in b'A'..=b'Z' {
2451                                    chars.push(c);
2452                                }
2453                                for c in b'0'..=b'9' {
2454                                    chars.push(c);
2455                                }
2456                            }
2457                            "space" => {
2458                                for b in b" \t\n\r\x0b\x0c".iter() {
2459                                    chars.push(*b);
2460                                }
2461                            }
2462                            "blank" => {
2463                                chars.push(b' ');
2464                                chars.push(b'\t');
2465                            }
2466                            "punct" => {
2467                                for b in b"!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~".iter() {
2468                                    chars.push(*b);
2469                                }
2470                            }
2471                            "cntrl" => {
2472                                for c in 0u8..=31 {
2473                                    chars.push(c);
2474                                }
2475                                chars.push(127);
2476                            }
2477                            "print" => {
2478                                for c in 32u8..=126 {
2479                                    chars.push(c);
2480                                }
2481                            }
2482                            "graph" => {
2483                                for c in 33u8..=126 {
2484                                    chars.push(c);
2485                                }
2486                            }
2487                            // c:Src/pattern.c:3894 — `PP_ASCII: return !(c & ~0x7f)`.
2488                            // A FIXED, locale-independent set, so unlike the
2489                            // PP_IDENT/PP_IFS/PP_WORD family below it belongs in
2490                            // the compile-time `chars` set rather than a
2491                            // match-time classmask bit.
2492                            //
2493                            // It had NO arm here and NO bit in the classmask
2494                            // match below, so it fell to both `_` catch-alls and
2495                            // contributed nothing: `[[ a == [[:ascii:]] ]]` was
2496                            // FALSE where zsh is true, and the negation inverted.
2497                            // `[[:ascii:]0-9]` still matched `0-9`, which is what
2498                            // made the class look present. Every other POSIX
2499                            // class was already correct.
2500                            //
2501                            // A byte >= 0x80 leads a UTF-8 sequence and is
2502                            // handled by the multibyte path below, which
2503                            // correctly reports no match for this class — so the
2504                            // ASCII-only member set is complete, not partial.
2505                            "ascii" => {
2506                                for c in 0u8..=127 {
2507                                    chars.push(c);
2508                                }
2509                            }
2510                            // c:Src/pattern.c:3693-3700 + Src/ztype.h
2511                            // IIDENT — `[[:IDENT:]]` is zsh's
2512                            // identifier-character class (alnum + `_`,
2513                            // the chars valid in a parameter name).
2514                            // Static for the ASCII range; the
2515                            // multibyte wcsitype(IIDENT) path isn't
2516                            // expanded here. p10k/zsh-z validate names
2517                            // with `[[:IDENT:]]##`.
2518                            // c:3702-3719 — PP_IDENT / PP_IFS /
2519                            // PP_IFSSPACE / PP_WORD are evaluated at MATCH
2520                            // time in C (`wcsitype(ch, IIDENT|ISEP|IWORD)`,
2521                            // `iwsep`), reading the live `$IFS` / `$WORDCHARS`
2522                            // through the ztype table. Expanding them into a
2523                            // static `chars` set at COMPILE time froze the
2524                            // first observed value into the cached Patprog, so
2525                            // `IFS=' ' [[ $'\n' = [[:IFS:]] ]]` reused the set
2526                            // built under the previous `$IFS`. Emit a
2527                            // classmask bit instead; anyof_membership resolves
2528                            // it per candidate character.
2529                            // c:3737-3744 — PP_INCOMPLETE / PP_INVALID test the
2530                            // multibyte DECODE STATE of the candidate
2531                            // character (`zmb_ind == ZMB_INCOMPLETE` /
2532                            // `ZMB_INVALID`), never a fixed character set, so
2533                            // like the four live classes above they carry no
2534                            // compile-time members.
2535                            "IDENT" | "IFS" | "IFSSPACE" | "WORD" | "INCOMPLETE" | "INVALID" => {}
2536                            _ => {}
2537                        }
2538                        // Record the class predicate for multibyte input.
2539                        // Bits mirror the Unicode-aware `iswXXX` checks C's
2540                        // mb_patmatchrange applies (pattern.rs:3364). digit/
2541                        // xdigit stay ASCII-only (already in `chars`) — zsh's
2542                        // iswdigit is ASCII in the common locale.
2543                        mb_classmask |= match class_name {
2544                            "alpha" => 1 << 0,
2545                            "alnum" => 1 << 1,
2546                            "upper" => 1 << 2,
2547                            "lower" => 1 << 3,
2548                            "space" => 1 << 4,
2549                            "IFS" => 1 << 11,
2550                            "IFSSPACE" => 1 << 12,
2551                            "blank" => 1 << 5,
2552                            "punct" => 1 << 6,
2553                            "cntrl" => 1 << 7,
2554                            "print" => 1 << 8,
2555                            "graph" => 1 << 9,
2556                            // c:3702-3719 — live PP_ classes, resolved at
2557                            // match time (see the arm above).
2558                            "IDENT" => 1 << 10,
2559                            "WORD" => 1 << 13,
2560                            // c:3737-3744 — decode-state classes.
2561                            "INCOMPLETE" => 1 << 14,
2562                            "INVALID" => 1 << 15,
2563                            _ => 0,
2564                        };
2565                        i_b = j_b + 2;
2566                        continue;
2567                    }
2568                }
2569                // Multibyte member: a byte >= 0x80 leads a UTF-8 sequence
2570                // that cannot live in the ASCII `chars` set. Decode it as a
2571                // wide literal, or a wide range `<ch>-<ch>`, for the P_ANYOF
2572                // multibyte path.
2573                if bb[i_b] >= 0x80 {
2574                    if let Some(lo) = std::str::from_utf8(&bb[i_b..])
2575                        .ok()
2576                        .and_then(|s| s.chars().next())
2577                    {
2578                        let lolen = lo.len_utf8();
2579                        if i_b + lolen < bb.len()
2580                            && bb[i_b + lolen] == b'-'
2581                            && i_b + lolen + 1 < bb.len()
2582                            && bb[i_b + lolen + 1] != b']'
2583                        {
2584                            if let Some(hi) = std::str::from_utf8(&bb[i_b + lolen + 1..])
2585                                .ok()
2586                                .and_then(|s| s.chars().next())
2587                            {
2588                                mb_ranges.push((lo as u32, hi as u32));
2589                                i_b += lolen + 1 + hi.len_utf8();
2590                                continue;
2591                            }
2592                        }
2593                        mb_chars.push(lo as u32);
2594                        i_b += lolen;
2595                        continue;
2596                    }
2597                }
2598                if i_b + 2 < bb.len() && bb[i_b + 1] == b'-' && bb[i_b + 2] != b']' {
2599                    let lo = bb[i_b];
2600                    let hi = bb[i_b + 2];
2601                    // ASCII-low, multibyte-high range (`[a-я]`): record as a
2602                    // wide range so the high end matches; the ASCII portion
2603                    // of the range still expands into `chars` below.
2604                    if hi >= 0x80 {
2605                        if let Some(hich) = std::str::from_utf8(&bb[i_b + 2..])
2606                            .ok()
2607                            .and_then(|s| s.chars().next())
2608                        {
2609                            mb_ranges.push((lo as u32, hich as u32));
2610                            for c in lo..=0x7f {
2611                                chars.push(c);
2612                            }
2613                            i_b += 2 + hich.len_utf8();
2614                            continue;
2615                        }
2616                    }
2617                    for c in lo..=hi {
2618                        chars.push(c);
2619                    }
2620                    i_b += 3;
2621                } else {
2622                    chars.push(bb[i_b]);
2623                    i_b += 1;
2624                }
2625            }
2626            drop(parse_b);
2627            if let Some(p_lock) = patparse.lock().ok() {
2628                if i_b < p_lock.len() && p_lock.as_bytes()[i_b] == b']' {
2629                    i_b += 1;
2630                }
2631            }
2632            patparse_off.store(i_b, Ordering::Relaxed);
2633            let opcode = if negate { P_ANYBUT } else { P_ANYOF };
2634            let off2 = patnode(opcode);
2635            let mut buf = patout.lock().unwrap();
2636            // Body layout: `[len:u32]` (total following bytes) then
2637            // `[chars_len:u32][chars][classmask:u32][n_mbchars:u32]
2638            // [mbchars:u32*n][n_mbranges:u32][(lo,hi):u32*2]`. `len` counts
2639            // the WHOLE body so `advance_past_instr` / node traversal (which
2640            // do `4 + len`) skip it unchanged — only the P_ANYOF/P_ANYBUT
2641            // matcher parses the sub-structure. Pure-ASCII brackets carry a
2642            // zeroed 12-byte tail (classmask 0, no mbchars/mbranges).
2643            let mut body_ext: Vec<u8> = Vec::new();
2644            body_ext.extend_from_slice(&(chars.len() as u32).to_le_bytes());
2645            body_ext.extend_from_slice(&chars);
2646            body_ext.extend_from_slice(&mb_classmask.to_le_bytes());
2647            body_ext.extend_from_slice(&(mb_chars.len() as u32).to_le_bytes());
2648            for c in &mb_chars {
2649                body_ext.extend_from_slice(&(*c as u32).to_le_bytes());
2650            }
2651            body_ext.extend_from_slice(&(mb_ranges.len() as u32).to_le_bytes());
2652            for (lo, hi) in &mb_ranges {
2653                body_ext.extend_from_slice(&(*lo as u32).to_le_bytes());
2654                body_ext.extend_from_slice(&(*hi as u32).to_le_bytes());
2655            }
2656            let len = body_ext.len() as u32;
2657            buf.extend_from_slice(&len.to_le_bytes());
2658            buf.extend_from_slice(&body_ext);
2659            *tail_out = off2;
2660            off2 as i64
2661        }
2662        // c:1509-1512 — guard mirrors the two DPUTS: a disabled `(`
2663        // (SHGLOB, or `disable -p '('`) is an ORDINARY character unless
2664        // a ksh trigger routed us here.
2665        b'(' if kshchar != 0 || inpar_active => {
2666            patparse_off.fetch_add(1, Ordering::Relaxed);
2667            *flagp &= !P_PURESTR;
2668            // c:1508-1525 — `if (kshchar == '!') patcompnot(1, ...) else
2669            // patcompswitch(1, ...)`. The `!(pat)` form needs a
2670            // negation-aware compile that builds an EXCLUDE subtree;
2671            // every other case (including `@(pat)`, the bare `(pat)`
2672            // group, and the `+/*/?` ksh forms) takes the alternation
2673            // switch path.
2674            if kshchar == b'!' {
2675                // c:1522-1523
2676                let mut flags2: i32 = 0;
2677                let starter_off = patcompnot(1, &mut flags2);
2678                if starter_off < 0 {
2679                    return -1;
2680                }
2681                *flagp |= flags2 & P_HSTART; // c:1526
2682                                             // c:1019 — the caller chains the FOLLOWING piece with
2683                                             // `pattail(chain, latest)`, which WALKS to the end of this
2684                                             // piece's next-chain. patcompnot's chain is
2685                                             // `P_BRANCH -> P_EXCLUDE -> P_NOTHING` (c:1773, c:1779-1781),
2686                                             // so the tail is the trailing P_NOTHING, NOT the P_BRANCH.
2687                                             // Reporting the P_BRANCH made patcompbranch's `set_next`
2688                                             // OVERWRITE the branch's link to the P_EXCLUDE, silently
2689                                             // deleting the negation whenever anything followed it:
2690                                             // `[[ foob = !(foo)b* ]]` matched, as did
2691                                             // `[[ mad.moo.cow = !(*.*).!(*.*) ]]`. (The `^pat` arm in
2692                                             // patcompbranch already compensates by hand.)
2693                let mut t = starter_off as usize;
2694                loop {
2695                    let n = {
2696                        let buf = patout.lock().unwrap();
2697                        if t + I_NEXT + 4 > buf.len() {
2698                            0
2699                        } else {
2700                            u32::from_le_bytes(buf[t + I_NEXT..t + I_NEXT + 4].try_into().unwrap())
2701                                as usize
2702                        }
2703                    };
2704                    if n == 0 || n == t {
2705                        break;
2706                    }
2707                    t = n;
2708                }
2709                *tail_out = t;
2710                // c:1419 already consumed `!`; the b'(' branch consumed
2711                // `(`; patcompnot drained through to `)`. Verify here.
2712                return starter_off;
2713            }
2714            // c:775-783 — `if (paren && (patglobflags & GF_BACKREF) &&
2715            // patnpar <= NSUBEXP) { parno = patnpar++; starter =
2716            // patnode(P_OPEN + parno); } else starter = 0;`. A group is
2717            // NUMBERED (capture slot allocated) only when GF_BACKREF is
2718            // live in patglobflags at the point the `(` is compiled —
2719            // wherever the `(#b)` appeared (leading, after a literal
2720            // prefix, mid-pattern). Beyond NSUBEXP, C "just use[s]
2721            // P_OPEN on its own" (c:777-780): parno stays 0 and the
2722            // match arms record nothing (c:2957 `if (no && ...)`).
2723            // The previous Rust arm numbered EVERY paren and returned
2724            // -1 past NSUBEXP — both divergent.
2725            let gf_now = patglobflags.load(Ordering::Relaxed);
2726            let n = if (gf_now & GF_BACKREF) != 0
2727                && patnpar.load(Ordering::Relaxed) <= NSUBEXP as i32
2728            {
2729                patnpar.fetch_add(1, Ordering::Relaxed)
2730            } else {
2731                0 // plain (uncaptured) group — P_OPEN+0 / P_CLOSE+0
2732            };
2733            let opcode = P_OPEN + n as u8;
2734            let open_off = patnode(opcode);
2735            // c:770 — `savglobflags = patglobflags`. Snapshot the glob
2736            // flags entering the group so a flag set INSIDE it (e.g.
2737            // `((#i)foo)`) can be restored on the way out.
2738            let savglobflags = patglobflags.load(Ordering::Relaxed);
2739            let mut inner_flags: i32 = 0;
2740            let inner = patcompswitch(1, &mut inner_flags);
2741            if inner < 0 {
2742                return -1;
2743            }
2744            // Expect closing ')'.
2745            let cur_off = patparse_off.load(Ordering::Relaxed);
2746            let p = patparse.lock().unwrap();
2747            if cur_off >= p.len() || p.as_bytes()[cur_off] != b')' {
2748                return -1;
2749            }
2750            drop(p);
2751            patparse_off.fetch_add(1, Ordering::Relaxed);
2752            let close_off = patnode(P_CLOSE + n as u8);
2753            // P_OPEN_N.next → first BRANCH of inner alternation.
2754            set_next(open_off, inner as usize);
2755            // Mirror C pattern.c:903 `pattail(starter, ender)`:
2756            // walk the BRANCH alt-chain from P_OPEN and patch the
2757            // last BRANCH's `.next` to P_CLOSE_N. Without this, the
2758            // outer `pattail` walk would descend into the inner
2759            // alt-chain (P_OPEN.next → inner BRANCH) and corrupt
2760            // its terminator.
2761            pattail(open_off, close_off);
2762            // Each branch's operand chain ends at P_CLOSE_N.
2763            chain_branches_to(inner as usize, close_off);
2764            *flagp &= !P_PURESTR;
2765            // c:920-929 — `if (paren && gfchanged) { pattail(ender,
2766            // patnode(P_GFLAGS)); patglobflags = savglobflags; }`. When
2767            // a glob flag that the matcher honors mid-pattern
2768            // (IGNCASE / LCMATCHUC / MULTIBYTE) changed inside the
2769            // group, append a P_GFLAGS node after P_CLOSE that restores
2770            // the entering value. Without it `[[ FOOXx = ((#i)foox)X ]]`
2771            // wrongly matched: the `(#i)` set GF_IGNCASE for the rest of
2772            // the match, so the trailing case-sensitive `X` folded too.
2773            //
2774            // c:921-925 — "gfchanged detects a change in any branch
2775            // (except exclusions which are separate), since we need to
2776            // emit this even if a later branch happened to put the flags
2777            // back." Comparing only the END state missed
2778            // `((#I)foo|(#i)rod)grud`: branch 1's P_GFLAGS(0) leaks into
2779            // the trailing `grud` because branch 2 restored the entering
2780            // value, so no restore node was emitted.
2781            let relevant = GF_IGNCASE | GF_LCMATCHUC | GF_MULTIBYTE;
2782            if PATSWITCH_GFCHANGED.load(Ordering::Relaxed) != 0 {
2783                let gf_off = patnode(P_GFLAGS);
2784                {
2785                    let mut buf = patout.lock().unwrap();
2786                    buf.extend_from_slice(&(savglobflags & relevant).to_le_bytes());
2787                }
2788                set_next(close_off, gf_off);
2789                patglobflags.store(savglobflags, Ordering::Relaxed); // c:927
2790                *tail_out = gf_off;
2791                return open_off as i64;
2792            }
2793            *tail_out = close_off;
2794            open_off as i64
2795        }
2796        b'\\' => {
2797            // c:Src/pattern.c — raw `\X` reaches this arm when the
2798            // input bypassed shtokenize (Src/glob.c:3565), which is
2799            // the path taken by paramsubst callers passing singsub-
2800            // computed pattern strings. C's faithful path: shtokenize
2801            // converts `\X` (special X) to `Bnullkeep X` (case at
2802            // pattern.c:1584), and a `\X` with non-special X stays
2803            // raw — both bytes survive to pattern.c's literal-run
2804            // default arm. Trailing lone `\` also survives shtokenize
2805            // (bslash flag stays 1 to end-of-string, no replacement
2806            // fires) and reaches the default-arm literal emission.
2807            //
2808            // Two arms mirror that:
2809            //   - `\X` (off2 in range): emit X as literal. Equivalent
2810            //     to Bnullkeep X arm in C's pattern.c — the next byte
2811            //     is the user-literal payload.
2812            //   - trailing lone `\`: emit `\` as literal. Equivalent
2813            //     to C's pattern.c default arm receiving a raw `\`
2814            //     byte that survived shtokenize.
2815            patparse_off.fetch_add(1, Ordering::Relaxed);
2816            let p = patparse.lock().unwrap();
2817            let off2 = patparse_off.load(Ordering::Relaxed);
2818            if off2 >= p.len() {
2819                drop(p);
2820                *flagp |= P_SIMPLE;
2821                let lit_off = patnode(P_EXACTLY);
2822                let mut buf_lit = patout.lock().unwrap();
2823                buf_lit.extend_from_slice(&1u32.to_le_bytes());
2824                buf_lit.push(b'\\');
2825                *tail_out = lit_off;
2826                return lit_off as i64;
2827            }
2828            let escaped = p.as_bytes()[off2];
2829            drop(p);
2830            patparse_off.fetch_add(1, Ordering::Relaxed);
2831            *flagp |= P_SIMPLE;
2832            let lit_off = patnode(P_EXACTLY);
2833            let mut buf_lit = patout.lock().unwrap();
2834            buf_lit.extend_from_slice(&1u32.to_le_bytes());
2835            buf_lit.push(escaped);
2836            *tail_out = lit_off;
2837            lit_off as i64
2838        }
2839        b'<' => {
2840            // Numeric range: <a-b> / <a-> / <-b> / <-> .
2841            // Port of pattern.c:1528-1570 (Inang case).
2842            //
2843            // c:Src/pattern.c:1528 — C's `case Inang:` only fires for
2844            // the Inang TOKEN (0x99) emitted by the lexer at
2845            // Src/lex.c:1202 when `isnumglob()` returns true (i.e.
2846            // the `<…>` body parses as a numeric range). Raw `<` (0x3C)
2847            // in source — quoted `"<"`, escaped `\<`, or any `<` in a
2848            // non-numglob position — never reaches the pattern compiler
2849            // as a metachar; it falls through to the literal-run arm.
2850            //
2851            // zshrs's pattern compiler is fed by callers that mix lexer
2852            // tokens with raw ASCII (singsub output, runtime-computed
2853            // pattern strings that never went through `Src/lex.c:1201`'s
2854            // isnumglob check). The faithful port: do the same
2855            // `[digit]*-[digit]*>` walk inline that `isnumglob` does at
2856            // lex.c:580-610, with full state-save + rewind on failure so
2857            // a non-numeric `<` (e.g. `<[:digit:]]#>` from zconvey's
2858            // color-marker pattern, or `<no-data>` from zinit.zsh:2507)
2859            // falls back to the literal-run arm.
2860            //
2861            // The numglob shape mirrors C's isnumglob exactly: digits
2862            // then `-` then digits then `>`. Empty leading / trailing
2863            // digit runs are OK (gives the `<-N>` / `<N->` / `<->` forms).
2864            // c:Src/pattern.c:499-506 — `if (isset(SHGLOB))
2865            // zpc_special[ZPC_INPAR] = zpc_special[ZPC_INANG] = Marker;`
2866            // ("Grouping and numeric ranges are not valid. We do allow
2867            // alternation, however; it's needed for case."). patcompcharsset
2868            // (pattern.rs:487) already applies that mask, but this arm never
2869            // consulted the table: it ran its inline isnumglob walk
2870            // unconditionally, so `<->` stayed an ACTIVE numeric range under
2871            // `emulate sh` / `emulate ksh`. C asserts the case is unreachable
2872            // there — `DPUTS(isset(SHGLOB), "Treating <..> as numeric range
2873            // with SHGLOB")` (c:1532). A disabled slot also covers user
2874            // `disable -p '<'`. Fall through to the same literal-`<` emit the
2875            // non-numeric rewind path uses. Bug #1053-B.
2876            let inang_disabled = { zpc_special.lock().unwrap()[ZPC_INANG as usize] != b'<' };
2877            if inang_disabled {
2878                let h = patnode(P_EXACTLY);
2879                let mut buf = patout.lock().unwrap();
2880                let len: u32 = 1;
2881                buf.extend_from_slice(&len.to_le_bytes());
2882                buf.push(b'<');
2883                drop(buf);
2884                patparse_off.fetch_add(1, Ordering::Relaxed);
2885                *flagp |= P_SIMPLE;
2886                *tail_out = h;
2887                return h as i64;
2888            }
2889            let entry_off = patparse_off.load(Ordering::Relaxed);
2890            patparse_off.fetch_add(1, Ordering::Relaxed); // consume `<`
2891            let parse_n = patparse.lock().unwrap();
2892            let nb = parse_n.as_bytes();
2893            let mut j = patparse_off.load(Ordering::Relaxed);
2894            let mut len_flag: u8 = 0; // bit 0 = lo present, bit 1 = hi present
2895            let mut from: i64 = 0;
2896            let lo_start = j;
2897            while j < nb.len() && nb[j].is_ascii_digit() {
2898                from = from * 10 + (nb[j] - b'0') as i64;
2899                j += 1;
2900            }
2901            if j > lo_start {
2902                len_flag |= 1;
2903            } // c:1538 — `len |= 1`
2904              // Mandatory dash. C's isnumglob bails (ret=0) here too —
2905              // walks until non-digit, then if non-digit isn't `-` (or `>`
2906              // after seeing `-`), returns "not numglob". Rewind the `<`
2907              // consumption and fall through to the literal-run arm.
2908            if j >= nb.len() || nb[j] != b'-' {
2909                drop(parse_n);
2910                patparse_off.store(entry_off, Ordering::Relaxed);
2911                let h = patnode(P_EXACTLY);
2912                let mut buf = patout.lock().unwrap();
2913                let len: u32 = 1;
2914                buf.extend_from_slice(&len.to_le_bytes());
2915                buf.push(b'<');
2916                patparse_off.fetch_add(1, Ordering::Relaxed);
2917                *flagp |= P_SIMPLE;
2918                *tail_out = h;
2919                return h as i64;
2920            }
2921            j += 1; // c:1543 patparse++
2922            let mut to: i64 = 0;
2923            let hi_start = j;
2924            while j < nb.len() && nb[j].is_ascii_digit() {
2925                to = to * 10 + (nb[j] - b'0') as i64;
2926                j += 1;
2927            }
2928            if j > hi_start {
2929                len_flag |= 2;
2930            } // c:1548 — `len |= 2`
2931              // Expect closing '>'. Mirror the dash-failure rewind here
2932              // too: a `<N-` without `>` is non-numglob per C's isnumglob.
2933            if j >= nb.len() || nb[j] != b'>' {
2934                drop(parse_n);
2935                patparse_off.store(entry_off, Ordering::Relaxed);
2936                let h = patnode(P_EXACTLY);
2937                let mut buf = patout.lock().unwrap();
2938                let len: u32 = 1;
2939                buf.extend_from_slice(&len.to_le_bytes());
2940                buf.push(b'<');
2941                patparse_off.fetch_add(1, Ordering::Relaxed);
2942                *flagp |= P_SIMPLE;
2943                *tail_out = h;
2944                return h as i64;
2945            }
2946            j += 1;
2947            drop(parse_n);
2948            patparse_off.store(j, Ordering::Relaxed);
2949            *flagp &= !P_PURESTR;
2950
2951            let off2 = match len_flag {
2952                // c:1552-1567
2953                3 => {
2954                    // c:1554 P_NUMRNG
2955                    let off2 = patnode(P_NUMRNG);
2956                    let mut buf = patout.lock().unwrap();
2957                    buf.extend_from_slice(&from.to_le_bytes());
2958                    buf.extend_from_slice(&to.to_le_bytes());
2959                    off2
2960                }
2961                2 => {
2962                    // c:1559 P_NUMTO
2963                    let off2 = patnode(P_NUMTO);
2964                    let mut buf = patout.lock().unwrap();
2965                    buf.extend_from_slice(&to.to_le_bytes());
2966                    off2
2967                }
2968                1 => {
2969                    // c:1563 P_NUMFROM
2970                    let off2 = patnode(P_NUMFROM);
2971                    let mut buf = patout.lock().unwrap();
2972                    buf.extend_from_slice(&from.to_le_bytes());
2973                    off2
2974                }
2975                _ => patnode(P_NUMANY), // c:1568
2976            };
2977            *tail_out = off2;
2978            off2 as i64
2979        }
2980        _ => {
2981            // Accumulate a literal run.
2982            let mut buf: Vec<u8> = Vec::new();
2983            let mut local_off = off;
2984            let p = patparse.lock().unwrap();
2985            // c:1312-1317 — break on kshchar trigger (any of the six
2986            // ZPC_KSH_* slots whose literal byte is followed by `(`).
2987            // Snapshot the zpc_special slots that participate; when
2988            // KSHGLOB is off they're Marker (0xa2) and won't match
2989            // ordinary bytes.
2990            let (ksh_plus, ksh_bang, ksh_bang2, ksh_at, ksh_star, ksh_quest) = {
2991                let sp = zpc_special.lock().unwrap();
2992                (
2993                    sp[ZPC_KSH_PLUS as usize],
2994                    sp[ZPC_KSH_BANG as usize],
2995                    sp[ZPC_KSH_BANG2 as usize],
2996                    sp[ZPC_KSH_AT as usize],
2997                    sp[ZPC_KSH_STAR as usize],
2998                    sp[ZPC_KSH_QUEST as usize],
2999                )
3000            };
3001            // c:1314-1317 — `~` is a literal-run terminator IFF it's
3002            // active as the exclusion operator (EXTENDEDGLOB on) AND
3003            // the lookahead is `/` or a non-segment-special byte.
3004            // Without EXTENDEDGLOB the byte is Marker (0xa2) so the
3005            // equality check fails naturally. Limited to TILDE only —
3006            // the other ZPC_SEG_COUNT slots (SLASH, NULL, BAR, OUTPAR)
3007            // are already covered by the explicit stop list above
3008            // (`|` `)` handled there; `/` was deliberately NOT a
3009            // literal-run break in this Rust port, see
3010            // `zsh_corpus_hash_s_e_anchors_match_bare_test` which
3011            // expects `/` to stay literal inside `(...)` alternation).
3012            let (sp_tilde_lit, sp_seg_lit_set, sp_hat_lit, sp_hash_lit, sp_slash_lit) = {
3013                let sp = zpc_special.lock().unwrap();
3014                let mut set = [false; 256];
3015                for i in 0..(ZPC_SEG_COUNT as usize) {
3016                    set[sp[i] as usize] = true;
3017                }
3018                (
3019                    sp[ZPC_TILDE as usize],
3020                    set,
3021                    sp[ZPC_HAT as usize],
3022                    sp[ZPC_HASH as usize],
3023                    sp[ZPC_SLASH as usize],
3024                )
3025            };
3026            while local_off < p.len() {
3027                let b = p.as_bytes()[local_off];
3028                // c:Src/pattern.c:480-483 — `if (!isset(EXTENDEDGLOB))`
3029                // masks ZPC_HAT and ZPC_HASH to Marker so the
3030                // patcompiece dispatch treats `^` / `#` as literals
3031                // (no negation / no zero-or-more closure).
3032                // The literal-run break list hardcoded `b'^'` /
3033                // `b'#'` though, which still terminated the literal
3034                // accumulator and forced patcompiece's empty-buf
3035                // -1 return → "bad pattern: ^.*" for the unset
3036                // EXTENDEDGLOB case. Gate the break on the actual
3037                // zpc_special slot so an EXTENDEDGLOB-off run treats
3038                // `^` and `#` as ordinary chars. Bug #421.
3039                // c:1294-1319 — the literal-run loop is WRAPPED in
3040                //     if (zpc_special[ZPC_INPAR] != Marker ||
3041                //         *patparse != Outpar || paren) { ...break... }
3042                // "If '(' is disabled as a pattern char, allow ')' as
3043                //  an ordinary string character if there are no
3044                //  parentheses to close.  Don't allow it otherwise, it
3045                //  changes the syntax."  So with `(` disabled (SHGLOB /
3046                // `disable -p '('`) at top level, a `)` stays in the
3047                // run; inside a group (`paren`) it still closes it.
3048                // `(` itself is likewise only a run terminator while
3049                // its slot is live (c:1312-1313 memchr over
3050                // zpc_special, which holds Marker when disabled).
3051                let stop_here = match b {
3052                    b'(' => inpar_active,
3053                    b')' => inpar_active || paren != 0,
3054                    // c:1312-1313 — a disabled `?` / `*` / `[` is not in
3055                    // zpc_special, so it stays in the literal run.
3056                    b'?' => quest_active,
3057                    b'*' => star_active,
3058                    b'[' => inbrack_active,
3059                    b'|' | b'\\' | b'<' => true,
3060                    b'^' => sp_hat_lit == b'^',
3061                    b'#' => sp_hash_lit == b'#',
3062                    // c:950 / c:1312-1317 — `/` is ZPC_SLASH, a segment
3063                    // terminator, and C keeps it one by MASKING the slot
3064                    // (`zpc_special[ZPC_SLASH] = Marker`) where it must stay
3065                    // literal — for a non-FILE pattern (c:570) and inside
3066                    // parens (c:840-842) — rather than by dropping the rule.
3067                    // Mirror that with the same gate `patcompbranch` already
3068                    // uses above, so `(...)` alternation and `[[ ]]`/`:#`
3069                    // patterns are untouched. Without this a literal file
3070                    // component swallowed the rest of the path: the general
3071                    // compiler is only reached for such a component when the
3072                    // PURES fast path is off, i.e. under `(#i)`/`(#a1)`, so
3073                    // `(#i)sub/deep.txt` compiled `sub/deep.txt` as ONE
3074                    // component and matched nothing — even with exact case.
3075                    // c:949-952 — see patcompbranch: liveness of the segment
3076                    // break comes from the MASKED SLOT, never from `paren`.
3077                    b'/' => sp_slash_lit == b'/',
3078                    _ => false,
3079                };
3080                if stop_here {
3081                    break;
3082                }
3083                if b == sp_tilde_lit && sp_tilde_lit != 0 {
3084                    let la = p.as_bytes().get(local_off + 1).copied().unwrap_or(0);
3085                    // Literal-tilde exception: `~` followed by a
3086                    // segment-special OTHER than `/` stays in the run.
3087                    let literal_tilde_exception = la != b'/' && sp_seg_lit_set[la as usize];
3088                    if !literal_tilde_exception {
3089                        break;
3090                    }
3091                }
3092                // c:1278-1292 — ksh-glob trigger lookahead. If the next
3093                // byte is `(` AND this byte matches one of the six
3094                // ZPC_KSH_* slots, stop the literal run BEFORE this byte
3095                // so the next patcomppiece call consumes it as a ksh
3096                // quantifier prefix (e.g. `pre+(x)post` accumulates
3097                // `pre` then breaks at `+` since `+(` follows).
3098                if local_off + 1 < p.len() && p.as_bytes()[local_off + 1] == b'(' {
3099                    if b == ksh_plus
3100                        || b == ksh_bang
3101                        || b == ksh_bang2
3102                        || b == ksh_at
3103                        || b == ksh_star
3104                        || b == ksh_quest
3105                    {
3106                        break;
3107                    }
3108                }
3109                buf.push(b);
3110                local_off += 1;
3111            }
3112            drop(p);
3113            if buf.is_empty() {
3114                return -1;
3115            }
3116            // c:Src/pattern.c:1340-1351 — multi-char run with TRAILING `#`
3117            // (or `(#cN,M)`) must backtrack ONE char so the trailing
3118            // quantifier applies to the LAST char only. Without this,
3119            // `fo#` compiles as `(fo)#` instead of `f` + `o#`, breaking
3120            // `(fo#)#` against "ffo" (the test pin we are fixing).
3121            //
3122            // C body:
3123            //   if ((*patparse == zpc_special[ZPC_HASH] || ...) && morelen)
3124            //       patparse = patprev;
3125            //
3126            // morelen = "more than one char in the literal run". The
3127            // backtrack pops the LAST byte from buf and rewinds patparse
3128            // by 1 so the next patcomppiece call sees that byte as its
3129            // own atom.
3130            // c:1341 — "If we have more than one character, a following hash
3131            // or (#c...) only applies to the last, so backtrack one character."
3132            // C tests THREE shapes, not just the bare `#` (c:1342-1350):
3133            //
3134            //   if ((*patparse == zpc_special[ZPC_HASH] ||
3135            //        (*patparse == zpc_special[ZPC_INPAR] &&
3136            //         patparse[1] == zpc_special[ZPC_HASH] &&
3137            //         patparse[2] == 'c') ||
3138            //        (*patparse == zpc_special[ZPC_KSH_AT] &&
3139            //         patparse[1] == Inpar &&
3140            //         patparse[2] == zpc_special[ZPC_HASH] &&
3141            //         patparse[3] == 'c')) && morelen)
3142            //       patparse = patprev;
3143            //
3144            // Only the first arm was ported, so `FPATH+(#c0,1)=` compiled the
3145            // count over the whole literal run `FPATH+` instead of over the
3146            // trailing `+` alone, and `FPATH=` never matched.
3147            let has_trailing_quantifier = {
3148                let sp = zpc_special.lock().unwrap();
3149                let sp_hash = sp[ZPC_HASH as usize];
3150                let sp_inpar = sp[ZPC_INPAR as usize];
3151                let sp_ksh_at = sp[ZPC_KSH_AT as usize];
3152                drop(sp);
3153                let p = patparse.lock().unwrap();
3154                let b = p.as_bytes();
3155                let at = |i: usize| -> u8 { b.get(local_off + i).copied().unwrap_or(0) };
3156                // c:1342 — bare `#` / `##` closure.
3157                let hash_at = sp_hash == b'#' && at(0) == b'#';
3158                // c:1343-1345 — `(#c...)` count spec.
3159                let count_at =
3160                    sp_hash == b'#' && at(0) == sp_inpar && at(1) == b'#' && at(2) == b'c';
3161                // c:1346-1349 — ksh `@(#c...)` form. `Inpar` is the tokenized
3162                // `(`; this port keeps pattern bytes raw (see patcompcharsset
3163                // at pattern.rs:443), so the literal `(` stands in for it.
3164                let ksh_count_at = sp_hash == b'#'
3165                    && at(0) == sp_ksh_at
3166                    && at(1) == b'('
3167                    && at(2) == b'#'
3168                    && at(3) == b'c';
3169                drop(p);
3170                hash_at || count_at || ksh_count_at
3171            };
3172            // c:1322 — `patprev = patparse; METACHARINC(patparse);`, so
3173            // `patprev` is the START of the LAST character of the run, which
3174            // may be multibyte. `last_char_start` is that boundary: walk back
3175            // over UTF-8 continuation bytes, exactly what METACHARINC's
3176            // stride implies.
3177            let last_char_start = {
3178                let mut cut = buf.len() - 1;
3179                while cut > 0 && (buf[cut] & 0xC0) == 0x80 {
3180                    cut -= 1;
3181                }
3182                cut
3183            };
3184            // c:1338 — `morelen = (patprev > str0);`. This is "the run holds
3185            // MORE THAN ONE CHARACTER", not more than one BYTE. The previous
3186            // port tested `buf.len() > 1`, so a run consisting of a SINGLE
3187            // multibyte character followed by `#` (`é#` under EXTENDEDGLOB)
3188            // took the backtrack branch, popped the whole 2-byte character,
3189            // rewound `local_off` to where the piece started, and returned a
3190            // zero-length P_EXACTLY having consumed NOTHING — so
3191            // `patcompbranch`'s piece loop (pattern.rs:1406) span forever
3192            // emitting empty nodes. C cannot reach that state: c:1336's
3193            // `DPUTS(patparse == str0, "BUG: matched nothing in
3194            // patcomppiece.")` records that a piece always consumes, and
3195            // `morelen` being character-based is what guarantees it — the
3196            // backtrack can only ever give back a character that was NOT the
3197            // first one in the run.
3198            let morelen = last_char_start > 0; // c:1338
3199            // c:1340-1351 — "If we have more than one character, a following
3200            // hash or (#c...) only applies to the last, so backtrack one
3201            // character."  `if (… ) && morelen) patparse = patprev;`
3202            if morelen && has_trailing_quantifier {
3203                let popped = buf.len() - last_char_start;
3204                buf.truncate(last_char_start);
3205                local_off -= popped;
3206            }
3207            patparse_off.store(local_off, Ordering::Relaxed);
3208            // c:1352-1357 — "If len is 1, we can't have an active # following,
3209            // so doesn't matter that we don't make X in `XX#' simple."
3210            //     if (!morelen) flags |= P_SIMPLE;
3211            // Only a ONE-character run can be the operand of a following
3212            // closure; a multi-character run stays non-simple (and pure
3213            // string). This port used to set P_SIMPLE unconditionally.
3214            if !morelen {
3215                *flagp |= P_SIMPLE; // c:1357
3216            }
3217            // c:1372-1376 —
3218            //     if ((patglobflags & GF_MULTIBYTE) && slen > 1)
3219            //         /* for multibyte single characters, treat x# as (x)# */
3220            //         flags &= ~P_SIMPLE;
3221            // `slen` is the run's UNMETAFIED byte length (c:1371), which for
3222            // this port is simply `buf.len()`. The P_SIMPLE closure path
3223            // (c:1718-1720 `patinsert(op, starter, NULL, 0)`, matched by
3224            // `patrepeat` at c:4121-4129, which asserts `P_LS_LEN(p) == 1`)
3225            // assumes a ONE-BYTE operand, so a single multibyte character
3226            // has to go through the general `(x)#` branch construction
3227            // instead.
3228            if (patglobflags.load(Ordering::Relaxed) & GF_MULTIBYTE) != 0 && buf.len() > 1 {
3229                *flagp &= !P_SIMPLE; // c:1375
3230            }
3231            let lit_off = patnode(P_EXACTLY);
3232            let mut buf_lit = patout.lock().unwrap();
3233            let len = buf.len() as u32;
3234            buf_lit.extend_from_slice(&len.to_le_bytes());
3235            buf_lit.extend_from_slice(&buf);
3236            *tail_out = lit_off;
3237            lit_off as i64
3238        }
3239    };
3240
3241    if atom < 0 {
3242        return atom;
3243    }
3244
3245    // c:1606-1644 — quantifier dispatch. Three sources of quantifier:
3246    //   (a) trailing `#` / `##` (zsh-extended hash repetition)
3247    //   (b) trailing `(#cN,M)` count-spec (handled in patcompbranch, not here)
3248    //   (c) kshchar form `+/*/?` already at the atom's leading byte.
3249    //
3250    // Rule c:1615 — if no hash AND no count AND kshchar is one of
3251    // (none, '@', '!'), no quantifier; return atom as-is. `@` is the
3252    // identity quantifier (match group exactly once); `!` already had
3253    // its negation compiled by patcompnot inside the atom.
3254    let q_off = patparse_off.load(Ordering::Relaxed);
3255    let parse2 = patparse.lock().unwrap();
3256    // c:1611-1620 — `if (*patparse == zpc_special[ZPC_HASH])` quantifier
3257    // dispatch. The C source reads `zpc_special[ZPC_HASH]`, which
3258    // patcompcharsset (c:480-483) sets to `'#'` only when
3259    // EXTENDEDGLOB is on, else to the Marker byte (c:476). A literal
3260    // '#' in the source pattern therefore does NOT trigger the
3261    // quantifier path when EXTENDEDGLOB is off — the comparison
3262    // `*patparse == Marker` fails. Previously this Rust check used
3263    // a bare `b'#'` comparison instead of consulting zpc_special,
3264    // making `(x)#` and `[a-z]##[0-9]##` match even with
3265    // extendedglob OFF (parity bugs #5/#6 vs real zsh).
3266    let hash_char = zpc_special.lock().unwrap()[ZPC_HASH as usize]; // c:1612
3267    let has_hash = hash_char == b'#' && q_off < parse2.len() && parse2.as_bytes()[q_off] == b'#'; // c:1611-1614
3268    drop(parse2);
3269    if !has_hash && (kshchar == 0 || kshchar == b'@' || kshchar == b'!') {
3270        return atom; // c:1616-1617
3271    }
3272
3273    // c:1626-1627 — `if (kshchar && (hash || count)) return 0;`
3274    //   /* too much at once doesn't currently work */
3275    //
3276    // `case Star:` (c:1440) sets `kshchar = -1` — per C's own comment there,
3277    // "kshchar is used as a sign that we can't have #'s" — so a closure may
3278    // not follow a `*`. This port's `kshchar` is a u8 trigger byte and cannot
3279    // hold C's sentinel, so the atom's head opcode expresses it instead: a
3280    // Star atom is a P_STAR node (c:1441).
3281    //
3282    // The `count` half of this same C condition WAS ported, in patcompbranch
3283    // (pattern.rs:1587-1600, which rejects `*(#c2,3)`); only the `hash` half
3284    // was missing, so the two arms of one C test disagreed. Measured, `zsh -f`
3285    // 5.9 vs this port: `*#`, `*##` and `a*#b` are `bad pattern` in zsh and
3286    // were all accepted here, while `*(#c2,3)` was already rejected by both.
3287    let atom_is_star = {
3288        let buf = patout.lock().unwrap();
3289        buf.get(atom as usize + I_OP).copied().unwrap_or(0) == P_STAR // c:1440-1441
3290    };
3291    if (kshchar != 0 || atom_is_star) && has_hash {
3292        return -1; // c:1627 `return 0;`
3293    }
3294
3295    // c:1710 / c:1723 — `flags`, the ATOM's own flag word. C keeps it in a
3296    // local for the whole of patcomppiece and only publishes it through
3297    // `*flagp` at c:1621 (the no-quantifier return); the dispatch below
3298    // OVERWRITES `*flagp` with P_HSTART (c:1631-1648) before either
3299    // optimisation reads its operand's flags. This port merged the two into
3300    // `*flagp`, so `*flagp & P_SIMPLE` was necessarily 0 by the time the
3301    // c:1710 and c:1723 tests ran and BOTH simple paths were dead: every
3302    // `x#` compiled to the c:1730 P_WBRANCH form instead.
3303    //
3304    // That is what kept the leading-dot rule off closures. C's `?#` becomes
3305    // a bare P_STAR (c:1721) and `[a]#` a P_ONEHASH whose operand is the
3306    // P_ANYOF (c:1725) — both P_NOTDOT-visible, at c:2731 and c:3322
3307    // respectively. A P_WBRANCH (0x21) is neither. Measured with `.hidden`
3308    // present and GLOB_DOTS off, `zsh -f` 5.9 lists nothing for `?#.hidden`
3309    // and `[a]#.hidden`; this port listed `.hidden` for both.
3310    let flags = *flagp; // c:1710 `flags`
3311
3312    // c:1624-1644 — pick the operator + post-atom flags.
3313    let op: u8;
3314    let mut consume_hashes = 0;
3315    if kshchar == b'*' {
3316        // c:1624-1626
3317        op = P_ONEHASH;
3318        *flagp = P_HSTART;
3319    } else if kshchar == b'+' {
3320        // c:1627-1629
3321        op = P_TWOHASH;
3322        *flagp = P_HSTART;
3323    } else if kshchar == b'?' {
3324        // c:1630-1632
3325        op = 0; // sentinel — `?` desugars to (x|) via the BRANCH path below
3326        *flagp = 0;
3327    } else {
3328        // c:1637-1644 — `#` / `##`.
3329        let parse_h = patparse.lock().unwrap();
3330        let two = q_off + 1 < parse_h.len() && parse_h.as_bytes()[q_off + 1] == b'#';
3331        drop(parse_h);
3332        if two {
3333            op = P_TWOHASH;
3334            consume_hashes = 2;
3335        } else {
3336            op = P_ONEHASH;
3337            consume_hashes = 1;
3338        }
3339        *flagp = P_HSTART;
3340    }
3341    if consume_hashes > 0 {
3342        patparse_off.fetch_add(consume_hashes, Ordering::Relaxed);
3343    }
3344
3345    // c:1705-1746 — quantifier emission.
3346    //
3347    // Read the atom's opcode so c:1705 (`?#` → `*`) optimization can
3348    // apply. The atom byte sits at `atom + I_OP` in patout.
3349    let atom_op = {
3350        let buf = patout.lock().unwrap();
3351        if (atom as usize) + I_OP < buf.len() {
3352            buf[atom as usize + I_OP]
3353        } else {
3354            0
3355        }
3356    };
3357
3358    if ((flags & P_SIMPLE) != 0) && (op == P_ONEHASH || op == P_TWOHASH) && atom_op == P_ANY {
3359        // c:1705-1717 — `?#` becomes `*`; `?##` becomes `?*`. The atom
3360        // is P_ANY at offset `atom`; rewrite or pad as needed.
3361        let mut buf = patout.lock().unwrap();
3362        if op == P_TWOHASH {
3363            // c:1717-1719 — `/* ?## becomes ?* */`; the C keeps the atom
3364            // as P_ANY (`uptr->l = (uptr->l & ~0xff) | P_ANY;` is a no-op
3365            // on an already-P_ANY node) and then runs
3366            // `pattail(starter, patnode(P_STAR));`. The `pattail` is the
3367            // load-bearing half: it writes the new P_STAR into the P_ANY's
3368            // next slot, so the two nodes form the `?*` chain. Emitting the
3369            // P_STAR without chaining it leaves it unreachable and makes
3370            // `?##` match exactly one character.
3371            drop(buf);
3372            let star = patnode(P_STAR);
3373            pattail(atom as usize, star); // c:1719
3374            // The piece's chain-tail is now the P_STAR, not the P_ANY:
3375            // whatever patcompbranch splices in next belongs after the
3376            // star (C reaches the same node because `pattail` walks the
3377            // chain from the returned `starter`).
3378            *tail_out = star;
3379        } else {
3380            // c:1715-1716 — `?#` → `*`: just rewrite atom's opcode.
3381            buf[atom as usize + I_OP] = P_STAR;
3382            drop(buf);
3383            *tail_out = atom as usize;
3384        }
3385        *flagp &= !P_PURESTR;
3386    } else if (flags & P_SIMPLE) != 0
3387        && op != 0
3388        && (patglobflags.load(Ordering::Relaxed) & 0xff) == 0
3389    {
3390        // c:1718-1720 — simple operand, no approximate-match counter:
3391        // emit `patinsert(op, starter, NULL, 0)`. The matcher walks
3392        // the inserted P_ONEHASH/P_TWOHASH operand at scan+I_BODY.
3393        patinsert(op, atom as usize, None, 0);
3394        *flagp &= !P_PURESTR;
3395        *tail_out = atom as usize;
3396    } else if op == P_ONEHASH {
3397        // c:1721-1729 — emit x# as (x&|): P_WBRANCH with NULL payload
3398        // ahead of atom; loop via P_BACK; null alternative branch.
3399        // patinsert with `sz=size_of::<union upat>()=8` (we use 8 to
3400        // mirror the C `union upat` slot, then zero-pad it).
3401        let payload = [0u8; 8];
3402        patinsert(P_WBRANCH, atom as usize, Some(&payload), 8);
3403        // Either x — atom is now at atom+I_BODY+8.
3404        let back = patnode(P_BACK);
3405        patoptail(atom as usize, back); // c:1726 — loop back
3406        patoptail(atom as usize, atom as usize); // c:1727
3407        let alt = patnode(P_BRANCH);
3408        pattail(atom as usize, alt); // c:1728 — or
3409        let null_node = patnode(P_NOTHING);
3410        pattail(atom as usize, null_node); // c:1729 — null
3411        *flagp &= !P_PURESTR;
3412        // The piece's chain-tail is the trailing P_NOTHING node — its
3413        // `.next` slot is empty and is the correct splice point for
3414        // whatever piece patcompbranch chains in next.
3415        *tail_out = null_node;
3416    } else if op == P_TWOHASH {
3417        // c:1730-1738 — emit x## as x(&|): P_WBRANCH after atom; loop
3418        // back; null branch.
3419        let wbranch = patnode(P_WBRANCH); // c:1732 — either
3420        let payload = [0u8; 8]; // c:1733-1734 patadd((char *)&up, ..., sizeof(up), 0)
3421        {
3422            let mut buf = patout.lock().unwrap();
3423            buf.extend_from_slice(&payload);
3424        }
3425        pattail(atom as usize, wbranch); // c:1735
3426        let back = patnode(P_BACK);
3427        pattail(back, atom as usize); // c:1736 — loop back
3428        let alt = patnode(P_BRANCH);
3429        pattail(wbranch, alt); // c:1737 — or
3430        let null_node = patnode(P_NOTHING);
3431        pattail(atom as usize, null_node); // c:1738 — null
3432        *flagp &= !P_PURESTR;
3433        // Same as ONEHASH path — tail at the trailing P_NOTHING.
3434        *tail_out = null_node;
3435    } else if kshchar == b'?' {
3436        // c:1739-1746 — emit ?(x) as (x|).
3437        patinsert(P_BRANCH, atom as usize, None, 0); // c:1741 — either x
3438        let alt = patnode(P_BRANCH); // c:1742 — or
3439        pattail(atom as usize, alt);
3440        let null_node = patnode(P_NOTHING); // c:1743 — null
3441        pattail(atom as usize, null_node); // c:1744
3442        patoptail(atom as usize, null_node); // c:1745
3443        *flagp &= !P_PURESTR;
3444        // Same as ONEHASH/TWOHASH — tail at the trailing P_NOTHING.
3445        *tail_out = null_node;
3446    }
3447
3448    // c:1747-1748 — a stray `#` immediately after = compile error.
3449    {
3450        let p = patparse.lock().unwrap();
3451        let q2 = patparse_off.load(Ordering::Relaxed);
3452        if q2 < p.len() && p.as_bytes()[q2] == b'#' {
3453            // Only flag as error when EXTENDEDGLOB-style # is enabled.
3454            let sp = zpc_special.lock().unwrap();
3455            if sp[ZPC_HASH as usize] == b'#' {
3456                return -1;
3457            }
3458        }
3459    }
3460
3461    atom
3462}
3463
3464/// Port of `patcompnot()` from `Src/pattern.c:1760`.
3465/// C: `patcompnot(int paren, int *flagsp)`.
3466///
3467/// C: `static long patcompnot(int paren, int *flagsp)`. Implements
3468/// the `^pat` (paren=0) or `!(pat)` (paren=1) extended/ksh-glob
3469/// negation by emitting `P_BRANCH → P_STAR → P_EXCSYNC` followed by
3470/// `P_EXCLUDE [payload]` and the asserted pattern terminated by
3471/// `P_EXCEND`, all joined at a trailing `P_NOTHING`.
3472///
3473/// NOTE: matcher support for `P_EXCSYNC`/`P_EXCEND`/`P_EXCLUDE` is
3474/// only partial in the Rust port — compile is faithful per
3475/// pattern.c:1760-1784 but match-time negation semantics may diverge
3476/// from C for complex backtracking edge cases. Tests covering
3477/// `!(foo)` / `!(foo|bar)` exercise the basic working subset.
3478pub fn patcompnot(paren: i32, flagsp: &mut i32) -> i64 {
3479    // c:1760
3480    // c:1767 — `*flagsp = P_HSTART;`. Negation always starts with `*`
3481    // semantically so the caller knows the piece can match at the
3482    // start of input.
3483    *flagsp = P_HSTART;
3484
3485    // c:1769 — `starter = patnode(P_BRANCH);`
3486    let starter = patnode(P_BRANCH);
3487    // c:1770 — `br = patnode(P_STAR);`
3488    let br = patnode(P_STAR);
3489    // c:1771 — `excsync = patnode(P_EXCSYNC);`
3490    let excsync = patnode(P_EXCSYNC);
3491    // c:1772 — `pattail(br, excsync);`
3492    pattail(br, excsync);
3493    // c:1773 — `pattail(starter, excl = patnode(P_EXCLUDE));`
3494    let excl = patnode(P_EXCLUDE);
3495    pattail(starter, excl);
3496    // c:1774-1775 — `up.p = NULL; patadd((char *)&up, 0, sizeof(up), 0);`
3497    // The EXCLUDE node carries an 8-byte syncptr payload, NULL-init.
3498    {
3499        let mut buf = patout.lock().unwrap();
3500        buf.extend_from_slice(&[0u8; 8]);
3501    }
3502    // c:1776 — `br = (paren ? patcompswitch(1, &dummy) : patcompbranch(&dummy, 0));`
3503    let mut dummy: i32 = 0;
3504    let inner = if paren != 0 {
3505        // c:1776 `patcompswitch(1, ...)` — in C the switch itself opens
3506        // the group: a P_OPEN when `(#b)` is live (c:775-783) and the
3507        // closing `ender` every branch is hooked to (c:897-909). The Rust
3508        // patcompswitch leaves both to its caller (see the `b'('` arm of
3509        // patcomppiece), so emit them here the same way. Without the
3510        // ender each branch's operand chain ended in next == 0, which
3511        // patmatch reads as "restart the program": a negated group whose
3512        // last piece was `*` re-matched the WHOLE pattern from the split
3513        // point, so `[[ xmc = x!(m*) ]]` and globtests.ksh's
3514        // `[[ mad.moo.cow = !(*.*).!(*.*) ]]` matched.
3515        let savglobflags = patglobflags.load(Ordering::Relaxed); // c:770
3516        let parno = if (savglobflags & GF_BACKREF) != 0
3517            && patnpar.load(Ordering::Relaxed) <= NSUBEXP as i32
3518        {
3519            patnpar.fetch_add(1, Ordering::Relaxed) // c:782 `parno = patnpar++;`
3520        } else {
3521            0
3522        };
3523        let open_off = if parno != 0 {
3524            Some(patnode(P_OPEN + parno as u8)) // c:783
3525        } else {
3526            None
3527        };
3528        let r = patcompswitch(1, &mut dummy);
3529        if r < 0 {
3530            return -1;
3531        }
3532        // c:913 — `if ((paren && *patparse++ != Outpar) ...) return 0;`
3533        {
3534            let cur = patparse_off.load(Ordering::Relaxed);
3535            let p = patparse.lock().unwrap();
3536            if cur >= p.len() || p.as_bytes()[cur] != b')' {
3537                return -1;
3538            }
3539            drop(p);
3540            patparse_off.fetch_add(1, Ordering::Relaxed);
3541        }
3542        // c:789-792 — `if (starter) pattail(starter, br); else starter = br;`
3543        let starter_sw = match open_off {
3544            Some(o) => {
3545                set_next(o, r as usize);
3546                o
3547            }
3548            None => r as usize,
3549        };
3550        // c:902 — `ender = patnode(paren ? parno ? P_CLOSE+parno : P_NOTHING : P_END);`
3551        let ender = patnode(if parno != 0 {
3552            P_CLOSE + parno as u8
3553        } else {
3554            P_NOTHING
3555        });
3556        // c:903 — `pattail(starter, ender);`
3557        pattail(starter_sw, ender);
3558        // c:909-911 — hook the tail of every branch to the closing node.
3559        chain_branches_to(r as usize, ender);
3560        // c:919-929 — `if (paren && gfchanged) { pattail(ender,
3561        // patnode(P_GFLAGS)); patglobflags = savglobflags; ... }`, with the
3562        // same honored-bits mask the `b'('` arm stores.
3563        if PATSWITCH_GFCHANGED.load(Ordering::Relaxed) != 0 {
3564            let relevant = GF_IGNCASE | GF_LCMATCHUC | GF_MULTIBYTE;
3565            let gf_off = patnode(P_GFLAGS);
3566            {
3567                let mut buf = patout.lock().unwrap();
3568                buf.extend_from_slice(&(savglobflags & relevant).to_le_bytes());
3569            }
3570            pattail(ender, gf_off);
3571            patglobflags.store(savglobflags, Ordering::Relaxed); // c:927
3572        }
3573        starter_sw as i64
3574    } else {
3575        patcompbranch(&mut dummy, 0)
3576    };
3577    if inner < 0 {
3578        return -1; // c:1777 `return 0;` (Rust uses -1 for failure)
3579    }
3580    // c:1778 — `pattail(br, patnode(P_EXCEND));`
3581    let excend = patnode(P_EXCEND);
3582    pattail(inner as usize, excend);
3583    // c:1779 — `n = patnode(P_NOTHING);`
3584    let n = patnode(P_NOTHING);
3585    // c:1780 — `pattail(excsync, n);`
3586    pattail(excsync, n);
3587    // c:1781 — `pattail(excl, n);`
3588    pattail(excl, n);
3589    // c:1783
3590    let _ = br; // suppress unused-var (kept for c-name parity)
3591    starter as i64
3592}
3593
3594/// Port of `patnode()` from `Src/pattern.c:1790`.
3595/// C: `patnode(long op)`.
3596///
3597/// C: `static long patnode(long op)` — writes a 1-byte opcode plus a
3598/// 4-byte zeroed next-offset. Returns the offset of the opcode byte.
3599fn patnode(op: u8) -> usize {
3600    // c:1790
3601    let mut buf = patout.lock().unwrap();
3602    let off = buf.len();
3603    buf.push(op); // I_OP
3604    buf.extend_from_slice(&[0, 0, 0, 0]); // I_NEXT zeroed
3605    off
3606}
3607
3608/// Port of `patinsert()` from `Src/pattern.c:1807`.
3609/// C: `patinsert(long op, int opnd, char *xtra, int sz)`.
3610///
3611/// C: `static void patinsert(long op, int opnd, char *xtra, int sz)`.
3612/// Inserts an opcode (+ next slot) at position `opnd`, shifting bytes
3613/// after it down by `5 + sz`, then writes `xtra` payload of `sz` bytes.
3614fn patinsert(op: u8, opnd: usize, xtra: Option<&[u8]>, sz: usize) {
3615    // c:1807
3616    let mut buf = patout.lock().unwrap();
3617    let header_sz = 1 + 4; // op + next
3618    let total = header_sz + sz;
3619    // Insert `total` zeroed bytes at opnd, then overwrite.
3620    let mut inserted = vec![0u8; total];
3621    inserted[0] = op;
3622    if let Some(x) = xtra {
3623        let copy_n = x.len().min(sz);
3624        inserted[header_sz..header_sz + copy_n].copy_from_slice(&x[..copy_n]);
3625    }
3626    buf.splice(opnd..opnd, inserted);
3627    // Patch up next_off chains pointing past opnd by adding `total`.
3628    fixup_offsets_after_insert(&mut buf, opnd, total as u32);
3629}
3630
3631/// Port of `pattail()` from `Src/pattern.c:1834`.
3632/// C: `pattail(long p, long val)`.
3633///
3634/// C: `static void pattail(long p, long val)` — patches the next-offset
3635/// field of the opcode at offset `p` to point to `val`. Walks any
3636/// existing chain to the end before patching.
3637fn pattail(p: usize, val: usize) {
3638    // c:1834
3639    let mut buf = patout.lock().unwrap();
3640    let mut cur = p;
3641    loop {
3642        if cur + I_BODY > buf.len() {
3643            return;
3644        }
3645        let next_bytes: [u8; 4] = buf[cur + I_NEXT..cur + I_NEXT + 4].try_into().unwrap();
3646        let next = u32::from_le_bytes(next_bytes) as usize;
3647        if next == 0 {
3648            break;
3649        }
3650        cur = next;
3651    }
3652    let val_bytes = (val as u32).to_le_bytes();
3653    if cur + I_NEXT + 4 <= buf.len() {
3654        buf[cur + I_NEXT..cur + I_NEXT + 4].copy_from_slice(&val_bytes);
3655    }
3656}
3657
3658/// Port of `patoptail()` from `Src/pattern.c:1856`.
3659/// C: `patoptail(long p, long val)`.
3660///
3661/// C: `static void patoptail(long p, long val)` — like pattail but
3662/// only patches branches (P_BRANCH/P_WBRANCH).
3663///
3664/// C: c:1862-1865 — for P_BRANCH the operand sits at `P_OPERAND(p)` =
3665/// `p+1` (Upat slot, i.e. byte offset `p + I_BODY` in Rust); for
3666/// P_WBRANCH the operand sits at `P_OPERAND(p) + 1` (skipping the
3667/// 8-byte syncptr payload Upat slot), i.e. byte offset
3668/// `p + I_BODY + 8` in Rust.
3669fn patoptail(p: usize, val: usize) {
3670    // c:1856
3671    let buf = patout.lock().unwrap();
3672    if p + I_OP >= buf.len() {
3673        return;
3674    }
3675    let op = buf[p + I_OP];
3676    drop(buf);
3677    if P_ISBRANCH(op) {
3678        // c:1862-1865 — operand offset depends on op kind.
3679        if op == P_BRANCH {
3680            pattail(p + I_BODY, val);
3681        } else {
3682            // P_WBRANCH / P_EXCLUDE / P_EXCLUDP — operand sits after
3683            // the 8-byte syncptr payload.
3684            pattail(p + I_BODY + 8, val);
3685        }
3686    }
3687}
3688
3689// =====================================================================
3690// 13/14. Matcher — pattern.c:2223-3579
3691// =====================================================================
3692
3693/// State accumulated during a single `patmatch` walk. C uses
3694/// per-thread globals (`patbeginp[]` / `patendp[]` for captures);
3695/// the Rust port encapsulates them in this struct passed by `&mut`.
3696/// Rule D: this struct represents matcher-internal scratch state
3697/// (analogous to `struct rpat pattrystate` at pattern.c:248), not a
3698/// bag-of-globals from unrelated subsystems.
3699///
3700/// **C counterpart**: `struct rpat` at `pattern.c:248`.
3701#[derive(Clone)]
3702#[allow(non_camel_case_types)]
3703pub struct rpat {
3704    /// Per-P_WBRANCH visit bitmap, keyed by WBRANCH-opcode offset.
3705    /// Mirrors C's `Upat ptrp` (`pattern.c:3217`): every WBRANCH carries
3706    /// an 8-byte payload sized as `union upat` — initialised to NULL,
3707    /// then lazily filled by `patmatch` with a buffer the size of the
3708    /// input string. Each byte tracks "have we already tried this
3709    /// WBRANCH at this input position with at most this many errors?".
3710    /// On revisit at the same position with the same-or-fewer errors,
3711    /// C returns 0 to bound the recursion (`pattern.c:3245-3248`). The
3712    /// Rust port keeps the bitmap on rpat (per-pattry call) instead of
3713    /// inside the bytecode payload so the bytecode stays read-only —
3714    /// the key is the WBRANCH offset; the value is a Vec<u8> the size
3715    /// of the input. Without it, `(fo#)#` against ANY input that
3716    /// requires the closure to consume at least one char per iteration
3717    /// (like "ffo") burns through PATMATCH_MAX_DEPTH and aborts.
3718    ///
3719    /// SHARED, not copied, across `rpat` clones. C hangs the buffer off
3720    /// the bytecode operand (`ptrp->p`, c:3226-3233) and frees it once on
3721    /// unwind (c:3253-3257), so every backtracking branch of one `pattry`
3722    /// sees the SAME marks — that persistence is the whole point of the
3723    /// guard: a branch already tried at this input position with no fewer
3724    /// errors must not be re-explored.
3725    ///
3726    /// Owning it by value made `#[derive(Clone)]` deep-copy a
3727    /// `|input|+1`-byte Vec per WBRANCH at all 12 `state.clone()` sites,
3728    /// one of which (c:6379 here) runs per branch BEFORE the skip check.
3729    /// Profiling `i<TAB>` put 58% of non-idle CPU in the resulting
3730    /// `_platform_memmove` + `madvise`. It also silently WEAKENED the
3731    /// guard: marks made inside a failed branch were thrown away with the
3732    /// clone, so the same branch could be re-explored anyway.
3733    pub wbranch_visits: std::rc::Rc<std::cell::RefCell<std::collections::HashMap<usize, Vec<u8>>>>,
3734    pub patbeginp: [usize; NSUBEXP], // c:241 capture starts (byte offsets)
3735    pub patendp: [usize; NSUBEXP],   // c:242 capture ends
3736    /// `parsfound` from `Src/pattern.c` (per c:2957/c:2989 references).
3737    /// Two-stripe bitmap: bits `0..NSUBEXP` track per-group P_OPEN
3738    /// first-write (`patbeginp[i]` committed); bits `NSUBEXP..2*NSUBEXP`
3739    /// track per-group P_CLOSE first-write (`patendp[i]` committed).
3740    /// Bit `n-1` (open) = `1 << (n-1)`; bit `n-1+NSUBEXP` (close) =
3741    /// `1 << (n-1+NSUBEXP)`. Width u32 to fit `2*NSUBEXP = 18` bits.
3742    /// The prior u16 width with a single-stripe (close-only) bit
3743    /// allowed P_OPEN to re-overwrite `patbeginp[i]` on every
3744    /// backtrack iteration — turning the SECOND capture's start
3745    /// offset into the FIRST iteration's, e.g. `(*)-(*)` against
3746    /// `hello-world` returned `match[2] = "hello-world"` instead of
3747    /// the right `"world"`.
3748    pub captures_set: u32, // c:Src/pattern.c parsfound
3749    /// Port of file-static `int errsfound` from `Src/pattern.c:2046`.
3750    /// Cumulative edit-count for approximate-match `(#aN)`. Reset to 0
3751    /// at the top of each pattry, incremented on P_EXACTLY mismatches
3752    /// when `glob_flags & 0xff > 0` (the substitution-budget byte).
3753    pub errsfound: i32, // c:2046
3754}
3755
3756/// Port of `charref()` from `Src/pattern.c:1909`.
3757/// C: `wchar_t charref(char *x, char *y, int *zmb_ind)` from
3758/// `Src/pattern.c:1909`. Decode the char at `pos` without
3759/// advancing. UTF-32-native delegation: `s.chars().next()` returns
3760/// the decoded codepoint; delegates to Rust's native UTF-8 string
3761/// iterator instead of the C Meta-decode + zshtoken-translate +
3762/// mbrtowc state machine, which all collapse because Rust's `&str`
3763/// is already UTF-8.
3764///
3765/// Rust signature differs from C `charref(char *x, char *y, int *zmb_ind)`:
3766/// the C `y` end-of-string pointer is captured by `&str` length; the
3767/// `zmb_ind` out-param (multibyte-completion index) is dropped — the
3768/// Rust UTF-8 decode is one-shot, never partial.
3769pub fn charref(s: &str, pos: usize) -> Option<char> {
3770    // c:1909
3771    s[pos..].chars().next()
3772}
3773
3774/// Port of `charnext()` from `Src/pattern.c:1936`.
3775/// C: `char *charnext(char *x, char *y)`.
3776/// C returns the pointer past the current character; the Rust port
3777/// returns that position as a byte offset. Delegates to `metacharinc`
3778/// — same advance-by-one-codepoint logic.
3779pub fn charnext(x: &str, y: usize) -> usize {
3780    // c:1936
3781    metacharinc(x, y)
3782}
3783
3784/// Port of `charrefinc()` from `Src/pattern.c:1969`.
3785/// C: `wchar_t charrefinc(char **x, char *y, int *z)` — decode the
3786/// character at `*x` and advance past it. A natively stored `char` is
3787/// already one decoded character (the byte-at-a-time view of a native
3788/// multibyte character without GF_MULTIBYTE is stepped by the callers).
3789///
3790/// Rust signature differs from C: C mutates `*x` via the
3791/// `char **` to advance; Rust mutates `pos` directly. The `y`
3792/// end-of-string sentinel is captured by `&str` length. C reads the
3793/// file-static `patglobflags`; the Rust matcher threads its flags as a
3794/// value, so they are the `gflags` argument. Returns the C `wchar_t`
3795/// as a `u32` because `WCHAR_INVALID` (c:241-242, `0xDC00 + byte`) is a
3796/// surrogate no Rust `char` can hold.
3797///
3798/// zshrs keeps a byte that is not valid UTF-8 METAFIED inside the `&str`
3799/// (Meta `U+0083` + `byte ^ 32`, the encoding `script_bytes` and the
3800/// `$'\xNN'` decoders produce), where C's matcher sees the raw byte. One
3801/// such pair is one raw byte, so it is decoded here the way C decodes
3802/// that byte: on its own without GF_MULTIBYTE (c:1974-1975), otherwise
3803/// through `mbrtowc` together with any pairs that follow (c:1977), and
3804/// as `WCHAR_INVALID` with `*z = 1` when that fails (c:1979-1984).
3805pub fn charrefinc(s: &str, pos: &mut usize, gflags: i32, z: &mut i32) -> Option<u32> {
3806    // c:1969
3807    let rest = s.get(*pos..)?;
3808    let mut it = rest.chars();
3809    let c = it.next()?;
3810    let meta_byte = |n: Option<char>| n.filter(|&n| (n as u32) >= 0x80).map(|n| (n as u32 as u8) ^ 32);
3811    let Some(first) = (c == '\u{83}').then(|| meta_byte(it.clone().next())).flatten() else {
3812        // A natively stored character is valid UTF-8 already.
3813        *pos += c.len_utf8();
3814        return Some(c as u32);
3815    };
3816    let pair_len = c.len_utf8() + it.clone().next().map_or(0, |n| n.len_utf8());
3817    // c:1974-1975 — `if (!(patglobflags & GF_MULTIBYTE) || …) return
3818    // (wchar_t) (unsigned char) *(*x)++;`
3819    if (gflags & GF_MULTIBYTE) == 0 {
3820        *pos += pair_len;
3821        return Some(first as u32);
3822    }
3823    // c:1977 — `ret = mbrtowc(&wc, *x, y-*x, &shiftstate);`, over the raw
3824    // bytes the following Meta pairs stand for.
3825    let mut raw: Vec<u8> = Vec::new();
3826    let mut lens: Vec<usize> = Vec::new();
3827    let mut scan = it.clone();
3828    scan.next();
3829    raw.push(first);
3830    lens.push(pair_len);
3831    loop {
3832        let mut wc: libc::wchar_t = 0;
3833        let mut mbs = crate::ported::utils::MBSTATE_ZERO;
3834        let ret = unsafe {
3835            crate::ported::utils::mbrtowc(
3836                &mut wc,
3837                raw.as_ptr() as *const libc::c_char,
3838                raw.len(),
3839                &mut mbs as *mut crate::ported::utils::MbStateBuf as *mut libc::c_void,
3840            )
3841        };
3842        if ret == crate::ported::utils::MB_INCOMPLETE && raw.len() < crate::ported::zsh_h::MB_CUR_MAX {
3843            let mut look = scan.clone();
3844            if look.next() == Some('\u{83}') {
3845                if let Some(b) = meta_byte(look.clone().next()) {
3846                    let n = look.next().unwrap();
3847                    raw.push(b);
3848                    lens.push('\u{83}'.len_utf8() + n.len_utf8());
3849                    scan = look;
3850                    continue;
3851                }
3852            }
3853        }
3854        if ret == crate::ported::utils::MB_INVALID
3855            || ret == crate::ported::utils::MB_INCOMPLETE
3856        {
3857            // c:1979-1984 — "Error.  Treat as single byte, but flag."
3858            *z = 1;
3859            *pos += lens[0];
3860            return Some(0xDC00 + first as u32); // c:1984 WCHAR_INVALID (c:241-242)
3861        }
3862        // c:1988-1990 — `*x += ret ? ret : 1; return wc;`
3863        *pos += lens[..ret.max(1).min(lens.len())].iter().sum::<usize>();
3864        return Some(wc as u32);
3865    }
3866}
3867
3868/// Port of `charsub()` from `Src/pattern.c:1997`.
3869/// C: `ptrdiff_t charsub(char *x, char *y)`.
3870///
3871/// Returns the number of characters between the start `x` and the
3872/// position `y` — i.e. the character-distance of byte offset `y` from
3873/// the string start. Non-multibyte: the byte distance `y` (each byte
3874/// is one character). Multibyte: the codepoint count of `x[0..y]`.
3875/// `patmatch` uses this (via the C `CHARSUB` macro) to report match
3876/// lengths and backreference positions in characters.
3877///
3878/// Replaces the prior fake that stepped back one char and returned a
3879/// byte offset — unrelated to the C character-count semantics.
3880pub fn charsub(x: &str, y: usize) -> usize {
3881    // c:1997
3882    let y = y.min(x.len());
3883    if !isset(MULTIBYTE) {
3884        // c:2003-2004 — `if (!isset(MULTIBYTE)) return y - x;`
3885        return y;
3886    }
3887    // c:2011-2026 — count characters in `x[0..y]`. C bounds each
3888    // `mbrtowc` by `y-x` (c:2012), so an END OFFSET THAT CUTS a
3889    // multibyte character in half yields MB_INCOMPLETE and c:2014-2018
3890    // `return res + (y - x)` counts every leftover byte as one
3891    // character. That is reachable: with GF_MULTIBYTE clear (`(#U)`,
3892    // c:1116) the matcher steps raw BYTES (c:1946), so `patendp[i]` can
3893    // land mid-character. A plain `x[..y].chars().count()` PANICS there.
3894    let mut res = 0usize;
3895    let mut i = 0usize;
3896    while i < y {
3897        // `i` is always on a character boundary — it only ever advances
3898        // by a whole character below.
3899        match x[i..].chars().next() {
3900            Some(c) if i + c.len_utf8() <= y => {
3901                res += 1; // c:2024
3902                i += c.len_utf8(); // c:2025
3903            }
3904            _ => return res + (y - i), // c:2018
3905        }
3906    }
3907    res // c:2028
3908}
3909
3910// =====================================================================
3911// 16. String pre-processing — pattern.c:2063, :2080, :2132
3912// =====================================================================
3913
3914/// Port of `pattrystart()` from `Src/pattern.c:2063`. C resets per-
3915/// match state globals; Rust state is per-call so no-op.
3916pub fn pattrystart() {} // c:2063
3917
3918/// Port of `patmungestring()` from `Src/pattern.c:2080`.
3919/// C: `void patmungestring(char **string, int *stringlen, int *unmetalenin)`
3920/// from `Src/pattern.c:2080`.
3921///
3922/// Skips a leading `Nularg` (the empty-tokenised-string sentinel)
3923/// and computes `*stringlen` from `strlen` when caller passed `-1`.
3924/// Mutates all three in/out args; mirrors C's `char **` /
3925/// `int *` semantics via Rust mutable references.
3926pub fn patmungestring(string: &mut &str, stringlen: &mut i32, unmetalenin: &mut i32) {
3927    // c:2080
3928    // c:2082-2091 — `if (*stringlen > 0 && **string == Nularg)` — skip
3929    // the leading Nularg sentinel and adjust lengths.
3930    let bytes = string.as_bytes();
3931    if *stringlen > 0 && !bytes.is_empty() && bytes[0] as char == Nularg {
3932        // c:2085 — `(*string)++;` — advance past Nularg.
3933        *string = &string[1..]; // c:2085
3934                                // c:2090 — `if (*unmetalenin > 0) (*unmetalenin)--;`
3935        if *unmetalenin > 0 {
3936            // c:2089
3937            *unmetalenin -= 1; // c:2090
3938        }
3939        // c:2092 — `if (*stringlen > 0) (*stringlen)--;`
3940        if *stringlen > 0 {
3941            // c:2091
3942            *stringlen -= 1; // c:2092
3943        }
3944    }
3945
3946    // c:2096-2097 — `if (*stringlen < 0) *stringlen = strlen(*string);`
3947    if *stringlen < 0 {
3948        // c:2096
3949        *stringlen = string.len() as i32; // c:2097
3950    }
3951}
3952
3953/// Port of `pattry()` from `Src/pattern.c:2223`.
3954/// C: `pattry(Patprog prog, char *string)`.
3955///
3956/// C signature: `int pattry(Patprog prog, char *string)`. Returns
3957/// non-zero on match, 0 on no-match.
3958pub fn pattry(prog: &Patprog, string: &str) -> bool {
3959    // c:2223
3960    // c:2225 — `return pattrylen(prog, string, len, -1, NULL, 0);`
3961    pattrylen(prog, string, string.len() as i32, -1, None, 0) // c:2225
3962}
3963
3964/// Port of `pattrylen()` from `Src/pattern.c:2236`.
3965/// C: `int pattrylen(Patprog prog, char *string, int len,
3966/// int unmetalen, Patstralloc patstralloc, int offset)` from
3967/// `Src/pattern.c:2236`.
3968///
3969/// ```c
3970/// int
3971/// pattrylen(Patprog prog, char *string, int len, int unmetalen,
3972///           Patstralloc patstralloc, int offset)
3973/// {
3974///     return pattryrefs(prog, string, len, unmetalen, patstralloc, offset,
3975///                       NULL, NULL, NULL);
3976/// }
3977/// ```
3978pub fn pattrylen(
3979    prog: &Patprog,
3980    string: &str,
3981    len: i32, // c:2236
3982    unmetalen: i32,
3983    patstralloc: Option<&Patstralloc>,
3984    offset: i32,
3985) -> bool {
3986    // c:2238
3987    pattryrefs(
3988        prog,
3989        string,
3990        len,
3991        unmetalen,
3992        patstralloc,
3993        offset,
3994        None,
3995        None,
3996        None, // c:2239
3997    )
3998}
3999
4000/// Port of `pattryrefs()` from `Src/pattern.c:2294`.
4001/// C: `int pattryrefs(Patprog prog, char *string, int stringlen,
4002/// int unmetalenin, Patstralloc patstralloc, int patoffset,
4003/// int *nump, int *begp, int *endp)` from `Src/pattern.c:2294`.
4004/// Runs `prog` against `string[0..stringlen]` (or whole string when
4005/// stringlen=-1) at `patoffset`, returning capture-group ranges.
4006///
4007/// C signature preserved per Rule S1. `Patstralloc` (the metafied-
4008/// string-allocator carrier from zsh.h:1613) is threaded through as
4009/// `Option<&Patstralloc>` matching C's `NULL`-passable pointer; the
4010/// matcher body currently ignores its contents (the substrate that
4011/// uses pre-allocated metafied buffers isn't wired into the Rust
4012/// matcher yet), but the param shape matches C so call sites trace
4013/// 1:1 to upstream.
4014#[allow(clippy::too_many_arguments)]
4015pub fn pattryrefs(
4016    // c:2294
4017    prog: &Patprog,
4018    string: &str,
4019    stringlen: i32,
4020    _unmetalenin: i32,
4021    _patstralloc: Option<&Patstralloc>,
4022    patoffset: i32,
4023    nump: Option<&mut i32>,
4024    begp: Option<&mut Vec<i32>>,
4025    endp: Option<&mut Vec<i32>>,
4026) -> bool {
4027    let trial: &str = if stringlen < 0 || (stringlen as usize) >= string.len() {
4028        string
4029    } else {
4030        &string[..stringlen as usize]
4031    };
4032    // Substring fast path for the ubiquitous `*literal*` shape
4033    // (optionally `(#i)`): program is [P_GFLAGS] P_STAR P_EXACTLY
4034    // P_STAR P_END with no captures requested. history-search-multi-
4035    // word's `${history[(R)(#i)*pat*]}` runs this shape over EVERY
4036    // history entry — 566k backtracking patmatch walks took CPU-
4037    // minutes per ^R (the reported freeze); a memmem-style scan is
4038    // what the shape actually needs. C reads this spirit via its
4039    // `mustoff` must-match prefilter (Src/pattern.c:2460-2483) but
4040    // declines under globflags; here the exact-bytes variant covers
4041    // any content and the `(#i)` variant is taken only when pattern
4042    // AND subject are pure ASCII (byte-fold == the matcher's char
4043    // fold there); anything else falls through to the full matcher.
4044    if nump.is_none()
4045        && begp.is_none()
4046        && endp.is_none()
4047        && patoffset == 0
4048        && (prog.0.flags & (PAT_NOTSTART | PAT_NOTEND) as i32) == 0
4049        // c:2399-2406 — a successful match is REJECTED for a file pattern
4050        // (PAT_NOGLD) whose subject starts with '.', unless the pattern itself
4051        // starts with a literal dot. That rejection lives below, AFTER
4052        // patmatch; returning from the fast path skipped it entirely, so
4053        // `*.*` matched `.hidden` (the literal `.` was found at offset 0 by
4054        // the substring scan) where zsh lists no dot files at all. Hand any
4055        // dot-leading subject under a file pattern to the full matcher, which
4056        // applies the rule. Costs nothing where the fast path was aimed:
4057        // history search (`${history[(R)(#i)*pat*]}`) is not a file glob and
4058        // never sets PAT_NOGLD, and non-dot files still take the fast path.
4059        && !((prog.0.flags & PAT_NOGLD as i32) != 0 && trial.starts_with('.'))
4060    {
4061        // Shape probe, inline (build-gate: no new fn in ported/ without
4062        // a C counterpart): Some((literal, igncase)) iff the program is
4063        // exactly `[P_GFLAGS] P_STAR P_EXACTLY(lit) P_STAR P_END` with
4064        // glob flags ⊆ IGNCASE|MULTIBYTE (no approx budget, no backrefs).
4065        let shape: Option<(String, bool)> = (|| {
4066            let buf: &[u8] = &prog.1;
4067            let mut off = prog.0.startoff as usize;
4068            let mut gflags = prog.0.globflags;
4069            let op_at = |o: usize| -> Option<u8> {
4070                if o + I_BODY <= buf.len() {
4071                    Some(buf[o + I_OP])
4072                } else {
4073                    None
4074                }
4075            };
4076            // A sole leading BRANCH (next == 0: no alternative) wraps
4077            // the whole program — step inside it.
4078            if op_at(off)? == P_BRANCH {
4079                let next = u32::from_le_bytes(buf[off + I_NEXT..off + I_NEXT + 4].try_into().ok()?);
4080                if next != 0 {
4081                    return None; // real alternation — full matcher
4082                }
4083                off = advance_past_instr(buf, off);
4084            }
4085            if op_at(off)? == P_GFLAGS {
4086                let body = off + I_BODY;
4087                if body + 4 > buf.len() {
4088                    return None;
4089                }
4090                gflags |= i32::from_le_bytes(buf[body..body + 4].try_into().ok()?);
4091                off = advance_past_instr(buf, off);
4092            }
4093            if (gflags & !(GF_IGNCASE | GF_MULTIBYTE)) != 0 {
4094                return None;
4095            }
4096            if op_at(off)? != P_STAR {
4097                return None;
4098            }
4099            off = advance_past_instr(buf, off);
4100            // One or more contiguous EXACTLY chunks form the literal
4101            // (the compiler splits long/space-bearing literals).
4102            if op_at(off)? != P_EXACTLY {
4103                return None;
4104            }
4105            let mut lit = String::new();
4106            while op_at(off)? == P_EXACTLY {
4107                let body = off + I_BODY;
4108                if body + 4 > buf.len() {
4109                    return None;
4110                }
4111                let len = u32::from_le_bytes(buf[body..body + 4].try_into().ok()?) as usize;
4112                if body + 4 + len > buf.len() {
4113                    return None;
4114                }
4115                lit.push_str(std::str::from_utf8(&buf[body + 4..body + 4 + len]).ok()?);
4116                off = advance_past_instr(buf, off);
4117            }
4118            if op_at(off)? != P_STAR {
4119                return None;
4120            }
4121            off = advance_past_instr(buf, off);
4122            if off + I_OP >= buf.len() || buf[off + I_OP] != P_END {
4123                return None;
4124            }
4125            Some((lit, (gflags & GF_IGNCASE) != 0))
4126        })();
4127        // c:3316-3317 — the trailing `*` walks `patinput` to `patinend`
4128        // and `if (P_OP(next) == P_END) return 1;`, so a hit consumed the
4129        // whole trial: c:2508 `patinlen = patinput - patinstart` is its
4130        // length. The fast path returned without recording it, leaving
4131        // `patmatchlen()` stale for the PAT_NOANCH callers (igetmatch
4132        // `##`, the `(S)` scans): `x=aXb; ${(S)x##*X*}` stripped nothing.
4133        let hit = |ok: bool| -> bool {
4134            if ok {
4135                patinlen.store(trial.len() as i32, Ordering::Relaxed); // c:2508
4136            }
4137            ok
4138        };
4139        if let Some((lit, igncase)) = shape.as_ref().map(|(l, i)| (l.as_str(), *i)) {
4140            if !igncase {
4141                return hit(trial.contains(lit));
4142            }
4143            if lit.is_ascii() && trial.is_ascii() {
4144                let lb = lit.as_bytes();
4145                let tb = trial.as_bytes();
4146                if lb.is_empty() {
4147                    return hit(true);
4148                }
4149                if lb.len() <= tb.len() {
4150                    let l0 = lb[0].to_ascii_lowercase();
4151                    'scan: for s in 0..=(tb.len() - lb.len()) {
4152                        if tb[s].to_ascii_lowercase() != l0 {
4153                            continue;
4154                        }
4155                        for k in 1..lb.len() {
4156                            if tb[s + k].to_ascii_lowercase() != lb[k].to_ascii_lowercase() {
4157                                continue 'scan;
4158                            }
4159                        }
4160                        return hit(true);
4161                    }
4162                }
4163                return false;
4164            }
4165            // Non-ASCII under (#i): general matcher below.
4166        }
4167    }
4168    let mut state = rpat::new();
4169    // c:Src/pattern.c:2334 — `patflags = prog->flags;` C copies the
4170    // prog's flags into the file-static `patflags` so subsequent
4171    // patmatch calls can read PAT_NOTSTART / PAT_NOTEND inside the
4172    // P_ISSTART / P_ISEND arms (c:3393, 3397). Rust mirrors this via
4173    // the `patflags` AtomicI32 at pattern.rs:217.
4174    //
4175    // Without this propagation, `${str:#(#s)PAT}` / `${str:#PAT(#e)}`
4176    // anchor checks fired at slice-local offset 0 / slice-local end
4177    // regardless of whether the caller meant the slice was an
4178    // interior chunk of a larger string (PAT_NOTSTART/PAT_NOTEND
4179    // bits exist on the prog precisely so callers can signal this).
4180    patflags.store(prog.0.flags, Ordering::Relaxed);
4181    // c:2494 — `globdots = !(patflags & PAT_NOGLD);`. The leading-dot rule
4182    // is enforced INSIDE the walker (c:2731 / c:3322), on wildcard opcodes
4183    // only, so it has to be published before patmatch runs.
4184    globdots.store(
4185        i32::from((prog.0.flags & PAT_NOGLD as i32) == 0),
4186        Ordering::Relaxed,
4187    );
4188    // Pass the prog's GLOB flags (GF_* + `(#aN)` budget byte) to the
4189    // matcher. C threads `patglobflags` as a per-thread file-static;
4190    // the Rust port carries it through a fn param. The PAT_* flags
4191    // (PAT_STATIC etc.) stay on `prog.0.flags` for the outer
4192    // anchor/PURES checks at lines below.
4193    // c:Src/pattern.c — `if (prog->flags & PAT_ANY) { ret = 1; } else { ... }`.
4194    // Optimisation for a single "*" (the `**` complist node compiles its
4195    // section as `patcompile(NULL, …|PAT_ANY, …)`): it always matches the
4196    // whole string without running the bytecode. The PAT_NOGLD leading-dot
4197    // rejection below still applies (the "except for no_glob_dots" caveat).
4198    // Without this the empty `PAT_ANY` program was matched literally and
4199    // only matched the empty string, so `**` descended into nothing.
4200    let (mut ok, matched_end) = if (prog.0.flags & PAT_ANY as i32) != 0 {
4201        (true, trial.len())
4202    } else {
4203        match patmatch(&prog.1, 0, trial, 0, &mut state, prog.0.globflags) {
4204            Some(end_pos) => {
4205                // c:2438 — `if (matched && !(prog->flags & (PAT_NOANCH|PAT_NOTEND))) ...`
4206                let no_anchor = (prog.0.flags & (PAT_NOANCH | PAT_NOTEND) as i32) != 0;
4207                (no_anchor || end_pos == trial.len(), end_pos)
4208            }
4209            None => (false, 0),
4210        }
4211    };
4212    // c:2404-2410 — `if (ret) { if ((prog->flags & PAT_NOGLD) &&
4213    //                *patinstart == '.') ret = 0; else ... }`.
4214    //
4215    // This rejection sits INSIDE C's `if (prog->flags & (PAT_PURES|PAT_ANY))`
4216    // fast path (c:2347), where no bytecode runs and the walker's own
4217    // P_NOTDOT gates (c:2731 / c:3322) never get a chance to fire. It is NOT
4218    // a blanket rule over every match: for a compiled program C rejects a
4219    // leading dot only when a WILDCARD opcode is what would consume it.
4220    //
4221    // The port used to apply it to every match and compensate with
4222    // `prog.patstartch != b'.'`. `patstartch` is recorded only for a program
4223    // whose FIRST node is a bare `P_EXACTLY` with no glob flags (c:709-711),
4224    // so every other way of writing a literal leading dot lost its dot files:
4225    // measured against `zsh -f` 5.9 in a directory holding `.hidden`,
4226    // `(.)hidden`, `(.|x)hidden` and `(#i).HIDDEN` each listed `.hidden` in
4227    // zsh and NOTHING in this port.
4228    if ok && (prog.0.flags & (PAT_PURES | PAT_ANY) as i32) != 0 && trial.starts_with('.') && (prog.0.flags & PAT_NOGLD as i32) != 0
4229    {
4230        ok = false; // c:2410
4231    }
4232    if ok {
4233        // c:2508 — `patinlen = patinput - patinstart;` — record the
4234        // byte-length of the successful match so `patmatchlen()` can
4235        // return it after the strings/state are torn down. `end_pos`
4236        // is the in-trial byte offset where patmatch stopped, which
4237        // is `patinput - patinstart` in C terms.
4238        patinlen.store(matched_end as i32, Ordering::Relaxed);
4239    }
4240    if ok {
4241        let n = (prog.0.patnpar as usize).min(NSUBEXP);
4242        let have_nump = nump.is_some();
4243        // c:2536 / c:2587 — `metafy(patinstart, *sp - patinstart, META_DUP)`:
4244        // C hands back the RAW matched bytes, re-metafied. With GF_MULTIBYTE
4245        // clear (`(#U)`, c:1116) the match unit is a single byte (c:1946), so
4246        // `lo`/`hi` can cut a multibyte character in half and a plain
4247        // `trial[lo..hi]` slice PANICS. Rebuild such a span the way zshrs
4248        // stores `$'\xNN'` elsewhere — Meta (0x83) followed by `byte ^ 32` —
4249        // which is exactly what C's `metafy` produces.
4250        let metafy_span = |lo: usize, hi: usize| -> String {
4251            if let Some(s) = trial.get(lo..hi) {
4252                return s.to_string();
4253            }
4254            let mut out = String::new();
4255            for &b in &trial.as_bytes()[lo..hi] {
4256                if b >= crate::ported::zsh_h::Meta {
4257                    out.push('\u{83}'); // c:4862 Meta
4258                    out.push(char::from(b ^ 32)); // c:4863 `*t++ = *p ^ 32`
4259                } else {
4260                    out.push(char::from(b));
4261                }
4262            }
4263            out
4264        };
4265        if let Some(np) = nump {
4266            *np = n as i32;
4267        }
4268        // c:2526-2542 — GF_MATCHREF (`(#m)`): on success, write the
4269        // matched substring to $MATCH and its 1-based (KSHARRAYS-
4270        // aware) char span to $MBEGIN/$MEND. C does this INSIDE
4271        // pattryrefs; the previous Rust port left it to a bridge
4272        // wrapper that sniffed the pattern TEXT for "(#m)" — wrong
4273        // mechanism (missed hoisted/nested flag positions) and
4274        // wrong layer.
4275        // c:2526 — `if ((patglobflags & GF_MATCHREF) && !(patflags & PAT_FILE))`.
4276        // The source is the GLOBAL patglobflags (c:273), not `prog->globflags`.
4277        // They are not the same thing: c:Src/zsh.h:1606 documents the struct
4278        // field as "globbing flags to set at START", so it keeps whatever the
4279        // pattern opened with, while the global is what patgetglobflags leaves
4280        // after walking every flag — including a later `(#M)` turning
4281        // GF_MATCHREF back off (c:1099-1100).
4282        //
4283        // Reading the struct field made `(#M)` inert: for all three of
4284        // `(#m)a*`, `(#m)(#M)a*` and `(#M)(#m)a*` it is 0x1800, whereas the
4285        // global correctly ends at 0x800 / 0x0 / 0x800. So `(#m)(#M)a*` still
4286        // wrote $MATCH where zsh leaves it unset.
4287        let cur_globflags = patglobflags.load(Ordering::Relaxed);
4288        if (cur_globflags & GF_MATCHREF) != 0 && (prog.0.flags & PAT_FILE as i32) == 0 {
4289            let hi = matched_end.min(trial.len());
4290            let mstr: String = metafy_span(0, hi); // c:2536 metafy(patinstart..patinput)
4291            let mlen = mstr.chars().count() as i64; // c:2534 CHARSUB
4292            let base: i64 = if crate::ported::zsh_h::isset(crate::ported::zsh_h::KSHARRAYS) {
4293                0
4294            } else {
4295                1
4296            };
4297            crate::ported::params::setsparam("MATCH", &mstr); // c:2537
4298            crate::ported::params::setiparam("MBEGIN", patoffset as i64 + base); // c:2538
4299            crate::ported::params::setiparam("MEND", mlen + patoffset as i64 + base - 1);
4300            // c:2539-2541
4301        }
4302        // c:2425+ — emit captured offsets to begp/endp out-arrays.
4303        // Check the CLOSE bit (high stripe at NSUBEXP+i) since the
4304        // group is only fully captured after its P_CLOSE fired; the
4305        // matching open-bit `1 << i` would be set after P_OPEN even
4306        // when the rest of the match later fails to reach P_CLOSE.
4307        //
4308        // c:Src/pattern.c:2556-2558 — `*begp++ = CHARSUB(patinstart, *sp) + patoffset;`.
4309        // `patoffset` is the start position of `string` within the
4310        // ORIGINAL string the caller was matching against (used for
4311        // paramsubst's sliding-window scan via `${var/PAT/REPL}`). Add
4312        // it to every reported beg/end so MBEGIN/MEND / `$match` see
4313        // positions relative to the original string, not the local
4314        // trial slice.
4315        // c:2556-2566 — `*begp++ = …` writes into the caller's FIXED-SIZE array
4316        // (C: `int begpos[MAX_POS]`), filling the first `n` slots and leaving
4317        // the array's length unchanged. Write IN PLACE (extend only if the
4318        // caller's Vec is shorter than `n`) rather than clear()+push — clearing
4319        // shrank a MAX_POS-length array down to `n`, so the caller's
4320        // [n..MAX_POS] reset loop (complist.rs getcol, c:631) indexed past the
4321        // end and panicked (`index 4 on len 4`) on patterns with < MAX_POS
4322        // backrefs.
4323        if let Some(bv) = begp {
4324            for i in 0..n {
4325                let close_bit = 1u32 << (i + NSUBEXP);
4326                let val = if (state.captures_set & close_bit) != 0 {
4327                    // c:2556-2558 — `CHARSUB(patinstart, *sp)` counts
4328                    // CHARACTERS, not bytes (c:1997 charsub honours the
4329                    // MULTIBYTE option). `patbeginp[i]` is a byte offset into
4330                    // `trial`, so convert before adding the (already
4331                    // character-based, c:2285-2286) `patoffset`.
4332                    charsub(trial, state.patbeginp[i]) as i32 + patoffset
4333                } else {
4334                    -1 // c:2563-2566 — unset group (unmatched alternation branch)
4335                };
4336                if i < bv.len() {
4337                    bv[i] = val;
4338                } else {
4339                    bv.push(val);
4340                }
4341            }
4342        }
4343        if let Some(ev) = endp {
4344            for i in 0..n {
4345                let close_bit = 1u32 << (i + NSUBEXP);
4346                let val = if (state.captures_set & close_bit) != 0 {
4347                    // c:2562-2564 — `*endp++ = CHARSUB(patinstart, *ep) +
4348                    // patoffset - 1;`. The reported end is the index of the LAST
4349                    // matched character, not one past it; `patendp[i]` is the
4350                    // exclusive end, hence the `- 1`.
4351                    //
4352                    // Load-bearing for the only consumer of these arrays,
4353                    // complist's putmatchcol (c:Src/Zle/complist.c:889).
4354                    // `doiscol`'s finished-empty test at complist.c:646 is
4355                    // `endpos[i] < begpos[i]`, which an empty capture can only
4356                    // satisfy when the end is INCLUSIVE (b, b-1). With the
4357                    // exclusive value (b, b) an empty `(#b)` group consumed a
4358                    // colour AND painted a character, and every non-empty group
4359                    // stayed coloured one character too long.
4360                    //
4361                    // `CHARSUB` counts characters (c:1997), so the byte offset
4362                    // in `patendp[i]` converts through `charsub` first.
4363                    charsub(trial, state.patendp[i]) as i32 + patoffset - 1
4364                } else {
4365                    -1 // c:2565
4366                };
4367                if i < ev.len() {
4368                    ev[i] = val;
4369                } else {
4370                    ev.push(val);
4371                }
4372            }
4373        }
4374        // c:2570-2621 — `else if (prog->patnpar && !(patflags &
4375        // PAT_FILE))`: the caller passed NO capture arrays, so
4376        // pattryrefs itself publishes the `(#b)` groups as the
4377        // $match / $mbegin / $mend arrays. This arm was missing
4378        // from the port — every plain pattry of a (#b) pattern
4379        // silently dropped its captures (the bridge compensated
4380        // with a wrapper; now C-faithful at the source).
4381        if !have_nump && n > 0 && (prog.0.flags & PAT_FILE as i32) == 0 {
4382            let base: i32 = if crate::ported::zsh_h::isset(crate::ported::zsh_h::KSHARRAYS) {
4383                0
4384            } else {
4385                1
4386            };
4387            let mut match_arr: Vec<String> = Vec::with_capacity(n);
4388            let mut begin_arr: Vec<String> = Vec::with_capacity(n);
4389            let mut end_arr: Vec<String> = Vec::with_capacity(n);
4390            for i in 0..n {
4391                let close_bit = 1u32 << (i + NSUBEXP);
4392                if (state.captures_set & close_bit) != 0 {
4393                    let b = state.patbeginp[i];
4394                    let e = state.patendp[i];
4395                    let lo = b.min(trial.len());
4396                    let hi = e.min(trial.len()).max(lo);
4397                    match_arr.push(metafy_span(lo, hi)); // c:2587 metafy(*sp..*ep)
4398                                                         // c:2596-2599 — `CHARSUB(patinstart, *sp) + patoffset +
4399                                                         // !isset(KSHARRAYS)`. CHARSUB (c:1997) counts CHARACTERS;
4400                                                         // `patbeginp`/`patendp` hold BYTE offsets into `trial`, so
4401                                                         // convert through charsub before adding the (already
4402                                                         // character-based, c:2285-2286) patoffset.
4403                    begin_arr.push((charsub(trial, b) as i32 + patoffset + base).to_string()); // c:2596-2599
4404                                                                                               // c:2601-2604 — mend = last matched char index
4405                                                                                               // (inclusive): end + offset + base - 1.
4406                    end_arr.push((charsub(trial, e) as i32 + patoffset + base - 1).to_string());
4407                } else {
4408                    // c:2607-2613 — unmatched branch / hashed paren.
4409                    match_arr.push(String::new());
4410                    begin_arr.push("-1".to_string());
4411                    end_arr.push("-1".to_string());
4412                }
4413            }
4414            crate::ported::params::setaparam("match", match_arr); // c:2619
4415            crate::ported::params::setaparam("mbegin", begin_arr); // c:2620
4416            crate::ported::params::setaparam("mend", end_arr); // c:2621
4417        }
4418    }
4419    ok
4420}
4421
4422// =====================================================================
4423// 15. Range matching — pattern.c:3856, :4004, :3610, :3767
4424// =====================================================================
4425
4426/// Port of `patmatchlen()` from `Src/pattern.c:2649`.
4427/// C: `int patmatchlen(void)`.
4428///
4429/// ```c
4430/// /**/
4431/// int
4432/// patmatchlen(void)
4433/// {
4434///     return patinlen;
4435/// }
4436/// ```
4437///
4438/// Returns the length in metafied bytes of the last successful
4439/// `pattry` / `pattryrefs` match. `patinlen` is set at
4440/// `pattern.c:2508` (`patinlen = patinput - patinstart`); the
4441/// Rust port sets the equivalent `patinlen` AtomicI32 at the end
4442/// of `pattryrefs` (see `pattern.rs::pattryrefs`).
4443pub fn patmatchlen() -> i32 {
4444    // c:2649-2652
4445    patinlen.load(Ordering::Relaxed) // c:2651 — `return patinlen;`
4446}
4447/// Port of `patmatchindex()` from `Src/pattern.c:4004`.
4448/// C: `int patmatchindex(char *range, int ind, int *chr, int *mtp)`
4449/// from `Src/pattern.c:4004` (MULTIBYTE_SUPPORT-disabled single-byte
4450/// variant). Walks a NULL-terminated, METAFIED, PP_*-encoded byte
4451/// stream `range` and returns the character (or POSIX-class id)
4452/// at index `ind`. Output:
4453///   - `Some((Some(ch), mtp))` — literal character or PP_RANGE hit;
4454///     `chr = ch`, `mtp = 0`.
4455///   - `Some((None,    mtp))` — POSIX class match (PP_ALPHA etc);
4456///     `chr = -1` in C, `None` in Rust; `mtp = class id`.
4457///   - `None` — `ind` exceeds the descriptor's length.
4458///
4459/// Encoding the byte stream uses:
4460///   * literal byte `< 0x83` = match char as itself
4461///   * `Meta(0x83)` + (byte ^ 0x20) = ordinary metafied character
4462///   * `Meta + PP_*` (0x84..) = POSIX class marker
4463///   * `Meta + PP_RANGE` = next two metafied chars are `lo, hi`
4464pub fn patmatchindex(range: &[u8], mut ind: i32) -> Option<(Option<u8>, i32)> {
4465    // c:4004-4081
4466    let mut chr: Option<u8> = None; // c:4014 — `*chr = -1`
4467    let mut mtp: i32 = 0; // c:4015 — `*mtp = 0`
4468
4469    // C `UNMETA(range)` + `METACHARINC(range)` macro pair from
4470    // Src/zsh.h:1796-1797 — decode-and-advance one metafied
4471    // character. Returned as `(decoded_byte, byte_advance)`.
4472    let unmeta = |bytes: &[u8], i: usize| -> (u8, usize) {
4473        if i < bytes.len() && bytes[i] == Meta && i + 1 < bytes.len() {
4474            (bytes[i + 1] ^ 0x20, 2)
4475        } else if i < bytes.len() {
4476            (bytes[i], 1)
4477        } else {
4478            (0, 0)
4479        }
4480    };
4481
4482    let mut i = 0usize;
4483    while i < range.len() {
4484        // c:4017 — `for (; *range; range++)`
4485        let b = range[i];
4486        if crate::ported::utils::imeta_byte(b) {
4487            // c:4018 — `if (imeta((unsigned char) *range))`
4488            // c:4019 — `int swtype = (unsigned char) *range - (unsigned char) Meta;`
4489            let swtype = (b as i32) - (Meta as i32);
4490            match swtype {
4491                0 => {
4492                    // c:4021-4028 — `case 0: ordinary metafied character`
4493                    // c:4023 — `rchr = (unsigned char) *++range ^ 32;`
4494                    i += 1;
4495                    if i >= range.len() {
4496                        break;
4497                    }
4498                    let rchr = range[i] ^ 0x20;
4499                    if ind == 0 {
4500                        // c:4024-4026 — `if (!ind) { *chr = rchr; return 1; }`
4501                        chr = Some(rchr);
4502                        return Some((chr, mtp));
4503                    }
4504                    // c:4027 — falls through to `if (!ind--) break;`
4505                }
4506                t if (PP_ALPHA..=PP_INVALID).contains(&t) => {
4507                    // c:4030-4051 — POSIX class markers PP_ALPHA..PP_INVALID
4508                    if ind == 0 {
4509                        // c:4052-4054 — `if (!ind) { *mtp = swtype; return 1; }`
4510                        mtp = swtype;
4511                        return Some((None, mtp));
4512                    }
4513                }
4514                t if t == PP_RANGE => {
4515                    // c:4057-4069 — PP_RANGE: next two metafied chars
4516                    // `range++; r1 = UNMETA(range); METACHARINC(range);
4517                    //  r2 = UNMETA(range); if (*range == Meta) range++;`
4518                    i += 1;
4519                    if i >= range.len() {
4520                        break;
4521                    }
4522                    let (r1, adv1) = unmeta(range, i);
4523                    i += adv1;
4524                    if i >= range.len() {
4525                        break;
4526                    }
4527                    let (r2, adv2) = unmeta(range, i);
4528                    i += adv2;
4529                    let rdiff = r2 as i32 - r1 as i32;
4530                    if rdiff >= ind {
4531                        // c:4063-4067 — `if (rdiff >= ind) { *chr = r1 + ind; return 1; }`
4532                        chr = Some((r1 as i32 + ind) as u8);
4533                        return Some((chr, mtp));
4534                    }
4535                    // c:4068 — `ind -= rdiff;` (extra decrement happens via
4536                    // the `ind--` below, accounting for the C comment
4537                    // "note the extra decrement to ind below").
4538                    ind -= rdiff;
4539                    // Skip the trailing `if (!ind--) break;` decrement
4540                    // for this branch since C's loop already does it.
4541                }
4542                _ => {
4543                    // c:4070-4076 — PP_UNKWN / default → DPUTS bug warn
4544                    // (unreachable in well-formed compiled output; bail).
4545                }
4546            }
4547        } else {
4548            // c:4079-4082 — literal char path
4549            if ind == 0 {
4550                // c:4080-4082 — `if (!ind) { *chr = (unsigned char) *range; return 1; }`
4551                chr = Some(b);
4552                return Some((chr, mtp));
4553            }
4554        }
4555        // c:4087 — `if (!ind--) break;` — pre-decrement-check.
4556        if ind == 0 {
4557            break;
4558        }
4559        ind -= 1;
4560        i += 1; // c:4017 — `range++`
4561    }
4562    // c:4091 — `/* No corresponding index. */ return 0;`
4563    None
4564}
4565
4566/// Port of `patmatchrange(char *range, int ch, int *indptr, int *mtp)`
4567/// from `Src/pattern.c:3865-3995` (reached through the
4568/// `PATMATCHRANGE` macro). Walks an encoded character-range
4569/// descriptor in `str` (Cpattern.str byte sequence) and tests
4570/// whether `c` falls inside. Encoding written by
4571/// `complete.rs::parse_class` (c:Src/Zle/complete.c:523/539):
4572///   0x80 + PP_RANGE (=0x95): next 2 bytes are lo,hi range
4573///   0x80 + PP_* (POSIX class id): single-byte class marker
4574///   plain byte: literal char (0x00-0x7F)
4575/// The port's marker base is 0x80 where C uses `Meta` (0x83); every
4576/// decoder in the port agrees on 0x80, so the offset is internal.
4577///
4578/// `indptr` is the position of `c` among the class MEMBERS seen so far:
4579/// c:3970 adds `ch - r1` on a hit, c:3974 adds `r2 - r1` when stepping
4580/// over a non-matching range, and c:3992-3993 adds a further 1 at the
4581/// end of EVERY non-returning iteration — so a skipped range advances
4582/// by its full `r2 - r1 + 1` member count and a skipped literal or
4583/// POSIX-class marker advances by exactly 1. That is the same counting
4584/// `pattern_match_equivalence`'s PATMATCHINDEX walk (c:1320,
4585/// pattern.c:4013-4088) performs in reverse, so producer and consumer
4586/// must agree element-for-element. `pattern_match1` turns that
4587/// into the equivalence-class index (`ind + 1`, c:1283) that
4588/// `pattern_match` compares between the line and word side
4589/// (c:1573 `if (ind != wind) return 0;`), and that
4590/// `pattern_match_equivalence` feeds back through PATMATCHINDEX.
4591/// The port previously incremented by 1 per element, so every char of
4592/// a `{a-z}`-style class collapsed to index 0: `m:{a-z\-}={A-Z\_}`
4593/// then matched `a` against EVERY uppercase name (`cd /a<TAB>`
4594/// offered /Applications /Library /System /Users /Volumes, and the
4595/// bogus ambiguity suppressed the insertion entirely).
4596pub fn patmatchrange(
4597    s: Option<&[u8]>,
4598    c: u32,
4599    mut indp: Option<&mut u32>,
4600    mtp: Option<&mut i32>,
4601) -> bool {
4602    let Some(bytes) = s else {
4603        return false;
4604    };
4605    // c:3869 — `if (indptr) *indptr = 0;`
4606    if let Some(out) = indp.as_deref_mut() {
4607        *out = 0;
4608    }
4609
4610    let mut i = 0usize;
4611    let mut mtp_dest: Option<&mut i32> = mtp;
4612    // c:3876 — `for (; *range; range++)`
4613    while i < bytes.len() {
4614        let b = bytes[i];
4615        if b >= 0x80 {
4616            // c:3877-3878 — `imeta(*range)`; swtype = marker - base.
4617            let swtype = (b as i32) - 0x80;
4618            // c:3879-3880 — `if (mtp) *mtp = swtype;` runs for EVERY
4619            // meta element, whether or not it goes on to match.
4620            if let Some(out) = mtp_dest.as_deref_mut() {
4621                *out = swtype;
4622            }
4623            if swtype == 0 {
4624                // c:3882-3885 — metafied literal: next byte ^ 32.
4625                if i + 1 >= bytes.len() {
4626                    break;
4627                }
4628                if (bytes[i + 1] ^ 32) as u32 == c {
4629                    return true;
4630                }
4631                i += 2;
4632                // c:3992-3993 — fall through to the shared `(*indptr)++`.
4633                if let Some(out) = indp.as_deref_mut() {
4634                    *out += 1;
4635                }
4636                continue;
4637            }
4638            if swtype == PP_RANGE {
4639                // c:3961-3975
4640                if i + 2 >= bytes.len() {
4641                    break;
4642                }
4643                let r1 = bytes[i + 1] as u32;
4644                let r2 = bytes[i + 2] as u32;
4645                if r1 <= c && c <= r2 {
4646                    // c:3968-3971
4647                    if let Some(out) = indp.as_deref_mut() {
4648                        *out += c - r1;
4649                    }
4650                    return true;
4651                }
4652                // c:3973-3974 — `if (indptr && r1 < r2) *indptr += r2 - r1;`
4653                if r1 < r2 {
4654                    if let Some(out) = indp.as_deref_mut() {
4655                        *out += r2 - r1;
4656                    }
4657                }
4658                i += 3;
4659                // c:3992-3993 — plus the per-iteration increment, so a skipped
4660                // range advances the index by its full member count
4661                // (r2 - r1 + 1). Omitting it made the producer disagree with
4662                // pattern_match_equivalence's PATMATCHINDEX consumer, which
4663                // does count every element.
4664                if let Some(out) = indp.as_deref_mut() {
4665                    *out += 1;
4666                }
4667                continue;
4668            }
4669            // c:3886-3960 — POSIX classes. Single-byte locale
4670            // (the C build only reaches this file without
4671            // MULTIBYTE_SUPPORT), so `ch` outside 0-255 never matches.
4672            let hit = c < 256 && {
4673                let cb = c as u8;
4674                match swtype {
4675                    PP_ALPHA => cb.is_ascii_alphabetic(),   // c:3886
4676                    PP_ALNUM => cb.is_ascii_alphanumeric(), // c:3890
4677                    PP_ASCII => (c & !0x7f) == 0,           // c:3894
4678                    PP_BLANK => cb == b' ' || cb == b'\t',  // c:3898
4679                    PP_CNTRL => cb.is_ascii_control(),      // c:3907
4680                    PP_DIGIT => cb.is_ascii_digit(),        // c:3911
4681                    PP_GRAPH => cb.is_ascii_graphic(),      // c:3915
4682                    PP_LOWER => cb.is_ascii_lowercase(),    // c:3919
4683                    // c:3923 ZISPRINT — C isprint(): graphic or space.
4684                    PP_PRINT => cb.is_ascii_graphic() || cb == b' ',
4685                    PP_PUNCT => cb.is_ascii_punctuation(), // c:3927
4686                    // c:3931 isspace(): " \t\n\v\f\r"
4687                    PP_SPACE => matches!(cb, b' ' | 0x09..=0x0d),
4688                    PP_UPPER => cb.is_ascii_uppercase(), // c:3935
4689                    PP_XDIGIT => cb.is_ascii_hexdigit(), // c:3939
4690                    PP_IDENT => crate::ported::ztype_h::iident(cb), // c:3943
4691                    PP_IFS => crate::ported::ztype_h::isep(cb), // c:3947
4692                    PP_IFSSPACE => crate::ported::ztype_h::iwsep(cb), // c:3951
4693                    PP_WORD => crate::ported::ztype_h::iword(cb), // c:3955
4694                    // c:3959-3961 PP_INCOMPLETE / PP_INVALID are never
4695                    // true without MULTIBYTE_SUPPORT; PP_UNKWN / unknown
4696                    // markers fall through as no-match.
4697                    _ => false,
4698                }
4699            };
4700            if hit {
4701                return true;
4702            }
4703            i += 1;
4704            // c:3992-3993 — a skipped POSIX-class marker is one class member.
4705            if let Some(out) = indp.as_deref_mut() {
4706                *out += 1;
4707            }
4708        } else if b as u32 == c {
4709            // c:3987-3990 — plain literal match sets `*mtp = 0`.
4710            if let Some(out) = mtp_dest.as_deref_mut() {
4711                *out = 0;
4712            }
4713            return true;
4714        } else {
4715            i += 1;
4716            // c:3992-3993 — and so is a skipped literal.
4717            if let Some(out) = indp.as_deref_mut() {
4718                *out += 1;
4719            }
4720        }
4721    }
4722    false
4723}
4724
4725/// Port of `mb_patmatchrange()` from `Src/pattern.c:3610`.
4726/// C: `int mb_patmatchrange(char *range, wchar_t ch,
4727/// int zmb_ind, wint_t *indptr, int *mtp)` from `Src/pattern.c:3610`.
4728/// Multibyte variant of `patmatchrange`: walks the metafied,
4729/// PP_*-encoded byte stream `range` and tests whether wide char `ch`
4730/// is in it. `indptr` (when Some) accumulates the running index for
4731/// `mb_patmatchindex`-side lookup; `mtp` (when Some) records which
4732/// PP_* class fired.
4733///
4734/// `zmb_ind` is the `ZMB_*` multibyte-completion state from the
4735/// caller (typically the pattry input pos): ZMB_INCOMPLETE /
4736/// ZMB_INVALID feed the PP_INCOMPLETE / PP_INVALID branches.
4737///
4738/// The Rust port delegates `iswalpha` / `iswdigit` / `wcsitype` to
4739/// `is_alphabetic()` / `is_numeric()` / etc. on the decoded `char`,
4740/// matching the C `iswXXX(wchar_t)` semantics.
4741pub fn mb_patmatchrange(
4742    range: &[u8],
4743    ch: char,
4744    zmb_ind: i32,
4745    mut indptr: Option<&mut u32>,
4746    mut mtp: Option<&mut i32>,
4747) -> bool {
4748    // c:3610-3766
4749    if let Some(p) = indptr.as_deref_mut() {
4750        *p = 0; // c:3615-3616 — `if (indptr) *indptr = 0;`
4751    }
4752    // C `UNMETA(s)` + `METACHARINC(s)` macro pair (Src/zsh.h).
4753    let unmeta = |bytes: &[u8], i: usize| -> (u8, usize) {
4754        if i < bytes.len() && bytes[i] == Meta && i + 1 < bytes.len() {
4755            (bytes[i + 1] ^ 0x20, 2)
4756        } else if i < bytes.len() {
4757            (bytes[i], 1)
4758        } else {
4759            (0, 0)
4760        }
4761    };
4762    let mut i = 0usize;
4763    while i < range.len() {
4764        // c:3623 — `while (*range)`
4765        let b = range[i];
4766        if crate::ported::utils::imeta_byte(b) {
4767            // c:3624 — `if (imeta((unsigned char) *range))`
4768            // c:3625 — `swtype = (unsigned char) *range++ - (unsigned char) Meta;`
4769            let swtype = (b as i32) - (Meta as i32);
4770            i += 1;
4771            if let Some(p) = mtp.as_deref_mut() {
4772                *p = swtype; // c:3626-3627 — `if (mtp) *mtp = swtype;`
4773            }
4774            let class_hit = match swtype {
4775                0 => {
4776                    // c:3629-3634 — `case 0: ordinary metafied character`
4777                    i -= 1; // c:3631 — `range--;`
4778                    let (decoded, adv) = unmeta(range, i);
4779                    i += adv;
4780                    decoded == (ch as u32) as u8 && (ch as u32) < 256
4781                }
4782                t if t == PP_ALPHA => ch.is_alphabetic(),
4783                t if t == PP_ALNUM => ch.is_alphanumeric(),
4784                t if t == PP_ASCII => (ch as u32) < 128,
4785                t if t == PP_BLANK => ch == ' ' || ch == '\t',
4786                t if t == PP_CNTRL => ch.is_control(),
4787                t if t == PP_DIGIT => ch.is_ascii_digit(),
4788                t if t == PP_GRAPH => !ch.is_whitespace() && !ch.is_control(),
4789                t if t == PP_LOWER => ch.is_lowercase(),
4790                t if t == PP_PRINT => !ch.is_control(),
4791                t if t == PP_PUNCT => ch.is_ascii_punctuation(),
4792                t if t == PP_SPACE => ch.is_whitespace(),
4793                t if t == PP_UPPER => ch.is_uppercase(),
4794                t if t == PP_XDIGIT => ch.is_ascii_hexdigit(),
4795                t if t == PP_IDENT => {
4796                    // c:3704 — `PP_IDENT: if (wcsitype(ch, IIDENT)) return 1;`
4797                    // IIDENT = alphanumeric, underscore, or "$" depending on
4798                    // option state. Conservative: ASCII identifier chars.
4799                    ch.is_alphanumeric() || ch == '_'
4800                }
4801                t if t == PP_IFS => {
4802                    // c:3708 — `wcsitype(ch, ISEP)`. ISEP = space, tab,
4803                    // newline + user IFS additions. Default IFS in zsh
4804                    // is ` \t\n`.
4805                    ch == ' ' || ch == '\t' || ch == '\n' || ch == '\0'
4806                }
4807                t if t == PP_IFSSPACE => {
4808                    // c:3712-3713 — ASCII-only IFS-space.
4809                    (ch as u32) < 128 && (ch == ' ' || ch == '\t' || ch == '\n')
4810                }
4811                t if t == PP_WORD => {
4812                    // c:3716 — `wcsitype(ch, IWORD)`. IWORD = WORDCHARS-derived;
4813                    // conservative ASCII alphanumeric + `_`.
4814                    ch.is_alphanumeric() || ch == '_'
4815                }
4816                t if t == PP_RANGE => {
4817                    // c:3719-3735 — PP_RANGE: two metafied wide chars are
4818                    // `r1` and `r2`; matches if `r1 <= ch <= r2`.
4819                    let (r1_byte, adv1) = unmeta(range, i);
4820                    i += adv1;
4821                    let (r2_byte, adv2) = unmeta(range, i);
4822                    i += adv2;
4823                    let r1 = r1_byte as u32;
4824                    let r2 = r2_byte as u32;
4825                    let chu = ch as u32;
4826                    if r1 <= chu && chu <= r2 {
4827                        if let Some(p) = indptr.as_deref_mut() {
4828                            *p += chu - r1; // c:3725-3726
4829                        }
4830                        return true; // c:3727
4831                    }
4832                    if let Some(p) = indptr.as_deref_mut() {
4833                        if r1 < r2 {
4834                            *p += r2 - r1; // c:3733-3734
4835                        }
4836                    }
4837                    false
4838                }
4839                t if t == PP_INCOMPLETE => {
4840                    // c:3740 — `if (zmb_ind == ZMB_INCOMPLETE) return 1;`
4841                    zmb_ind == ZMB_INCOMPLETE
4842                }
4843                t if t == PP_INVALID => {
4844                    // c:3744 — `if (zmb_ind == ZMB_INVALID) return 1;`
4845                    zmb_ind == ZMB_INVALID
4846                }
4847                _ => {
4848                    // c:3747-3753 — PP_UNKWN / default → DPUTS bug warn
4849                    false
4850                }
4851            };
4852            if class_hit {
4853                return true;
4854            }
4855        } else {
4856            // c:3757-3762 — literal-char path
4857            let (decoded, adv) = unmeta(range, i);
4858            i += adv;
4859            if decoded == (ch as u32) as u8 && (ch as u32) < 256 {
4860                if let Some(p) = mtp.as_deref_mut() {
4861                    *p = 0; // c:3760
4862                }
4863                return true; // c:3761
4864            }
4865        }
4866        if let Some(p) = indptr.as_deref_mut() {
4867            *p += 1; // c:3764-3765 — `if (indptr) (*indptr)++;`
4868        }
4869    }
4870    false // c:3766 — `return 0;`
4871}
4872
4873/// Port of `mb_patmatchindex()` from `Src/pattern.c:3767`.
4874/// C: `int mb_patmatchindex(char *range, wint_t ind,
4875/// wint_t *chr, int *mtp)` from `Src/pattern.c:3767`. The reverse
4876/// of `mb_patmatchrange`: given a metafied byte range and an index
4877/// `ind` into it, return the character (or PP_* class) at that
4878/// position.
4879///
4880/// Returns:
4881///   - `Some((Some(ch),  0))` — literal or PP_RANGE hit (chr = ch).
4882///   - `Some((None,     mtp))` — POSIX class match; chr = WEOF / None.
4883///   - `None` — index out of range.
4884pub fn mb_patmatchindex(range: &[u8], mut ind: u32) -> Option<(Option<char>, i32)> {
4885    // c:3767-3849
4886    let mut chr: Option<char> = None; // c:3776 — `*chr = WEOF`
4887    let mut mtp: i32 = 0; // c:3777 — `*mtp = 0`
4888
4889    // C `UNMETA(s)` + `METACHARINC(s)` macro pair (Src/zsh.h).
4890    let unmeta = |bytes: &[u8], i: usize| -> (u8, usize) {
4891        if i < bytes.len() && bytes[i] == Meta && i + 1 < bytes.len() {
4892            (bytes[i + 1] ^ 0x20, 2)
4893        } else if i < bytes.len() {
4894            (bytes[i], 1)
4895        } else {
4896            (0, 0)
4897        }
4898    };
4899
4900    let mut i = 0usize;
4901    while i < range.len() {
4902        // c:3779 — `while (*range)`
4903        let b = range[i];
4904        if crate::ported::utils::imeta_byte(b) {
4905            // c:3780 — `if (imeta((unsigned char) *range))`
4906            let swtype = (b as i32) - (Meta as i32);
4907            i += 1;
4908            match swtype {
4909                0 => {
4910                    // c:3782-3789 — `case 0: ordinary metafied char`
4911                    i -= 1;
4912                    let (decoded, adv) = unmeta(range, i);
4913                    i += adv;
4914                    let rchr = decoded as char;
4915                    if ind == 0 {
4916                        chr = Some(rchr);
4917                        return Some((chr, mtp));
4918                    }
4919                }
4920                t if (PP_ALPHA..=PP_INVALID).contains(&t) => {
4921                    // c:3791-3812 — POSIX class markers
4922                    if ind == 0 {
4923                        mtp = swtype;
4924                        return Some((None, mtp));
4925                    }
4926                }
4927                t if t == PP_RANGE => {
4928                    // c:3814-3829 — PP_RANGE: two metafied wide chars
4929                    let (r1_byte, adv1) = unmeta(range, i);
4930                    i += adv1;
4931                    let (r2_byte, adv2) = unmeta(range, i);
4932                    i += adv2;
4933                    let r1 = r1_byte as u32;
4934                    let r2 = r2_byte as u32;
4935                    let rdiff = r2.saturating_sub(r1);
4936                    if rdiff >= ind {
4937                        chr = char::from_u32(r1 + ind);
4938                        return Some((chr, mtp));
4939                    }
4940                    ind = ind.saturating_sub(rdiff);
4941                }
4942                _ => {
4943                    // c:3831-3837 — PP_UNKWN / default → DPUTS
4944                }
4945            }
4946        } else {
4947            // c:3840-3843 — literal byte path
4948            if ind == 0 {
4949                chr = Some(b as char);
4950                return Some((chr, mtp));
4951            }
4952        }
4953        // c:3846 — `if (!ind--) break;`
4954        if ind == 0 {
4955            break;
4956        }
4957        ind -= 1;
4958        i += 1;
4959    }
4960    None // c:3849 — `return 0`
4961}
4962
4963/// Port of `freepatprog()` from `Src/pattern.c:4161`.
4964/// C: `freepatprog(Patprog prog)`. Frees a Patprog.
4965/// Rust's `Drop` on `Box<patprog>` handles this; the explicit fn
4966/// exists for C parity (Rule A).
4967#[allow(unused_variables)]
4968pub fn freepatprog(prog: Patprog) {} // c:4161
4969
4970/// Port of `pat_enables()` from `Src/pattern.c:4171`.
4971/// C: `int pat_enables(const char *cmd, char **patp, int enable)`
4972/// from `Src/pattern.c:4171`. Implements `enable -p`/`disable -p`: with
4973/// an empty `patp`, prints the currently enabled (or disabled, if
4974/// `!enable`) tokens by walking `zpc_strings[]`/`zpc_disables[]` in
4975/// lockstep. Otherwise toggles each named token's `zpc_disables[i]`
4976/// slot, emitting `invalid pattern: NAME` for misses.
4977pub fn pat_enables(cmd: &str, patp: &[&str], enable: bool) -> i32 {
4978    // c:4171
4979    let mut ret: i32 = 0; // c:4173
4980    if patp.is_empty() {
4981        // c:4177 !*patp
4982        let strings = ZPC_STRINGS; // c:4179 zpc_strings
4983        let disp = zpc_disables.lock().unwrap(); // c:4179 zpc_disables
4984        let mut done = false; // c:4178
4985        let mut out: String = String::new();
4986        for i in 0..(ZPC_COUNT as usize) {
4987            // c:4180
4988            let sp = match strings[i] {
4989                // c:4182 !*stringp
4990                Some(s) => s,
4991                None => continue,
4992            };
4993            let is_disabled = disp[i] != 0;
4994            if enable == is_disabled {
4995                // c:4184 enable?*disp:!*disp
4996                continue;
4997            }
4998            if done {
4999                // c:4186
5000                out.push(' '); // c:4187
5001            }
5002            out.push_str(&format!("'{}'", sp)); // c:4188
5003            done = true; // c:4189
5004        }
5005        if done {
5006            // c:4191
5007            println!("{}", out); // c:4187-4192
5008        }
5009        return 0; // c:4193
5010    }
5011    for p in patp {
5012        // c:4196
5013        let strings = ZPC_STRINGS;
5014        let mut disp = zpc_disables.lock().unwrap();
5015        let mut matched = false;
5016        for i in 0..(ZPC_COUNT as usize) {
5017            // c:4197
5018            if let Some(s) = strings[i] {
5019                if s == *p {
5020                    // c:4200 !strcmp
5021                    disp[i] = if enable { 0u8 } else { 1u8 }; // c:4201 *disp = !enable
5022                    matched = true;
5023                    break; // c:4202
5024                }
5025            }
5026        }
5027        if !matched {
5028            // c:4205
5029            zerrnam(cmd, &format!("invalid pattern: {}", p)); // c:4206
5030            ret = 1; // c:4207
5031        }
5032    }
5033    ret // c:4211
5034}
5035
5036/// Port of `mod_export const char *zpc_strings[ZPC_COUNT]` from
5037/// `Src/pattern.c:258`. Static token-name table indexed by ZPC_*;
5038/// NULL entries (ZPC_NULL, ZPC_BNULLKEEP, ZPC_INPAR_PIPE,
5039/// ZPC_KSHCHAR) have no user-visible name.
5040pub const ZPC_STRINGS: [Option<&'static str>; ZPC_COUNT as usize] = [
5041    // c:258
5042    None,
5043    None,
5044    Some("|"),
5045    None,
5046    Some("~"),
5047    Some("("),
5048    Some("?"),
5049    Some("*"),
5050    Some("["),
5051    Some("<"),
5052    Some("^"),
5053    Some("#"),
5054    None,
5055    Some("?("),
5056    Some("*("),
5057    Some("+("),
5058    Some("!("),
5059    Some("\\!("),
5060    Some("@("),
5061];
5062
5063/// Port of `savepatterndisables()` from `Src/pattern.c:4220`.
5064/// C: `unsigned int savepatterndisables(void)` from
5065/// `Src/pattern.c:4220`.
5066///
5067/// C body (c:4220-4233):
5068/// ```c
5069/// unsigned int disables, bit;
5070/// char *disp;
5071/// disables = 0;
5072/// for (bit = 1, disp = zpc_disables;
5073///      disp < zpc_disables + ZPC_COUNT;
5074///      bit <<= 1, disp++) {
5075///     if (*disp) disables |= bit;
5076/// }
5077/// return disables;
5078/// ```
5079///
5080/// Encodes the current `zpc_disables\[ZPC_COUNT\]` byte-array as a u32
5081/// bitmask (one bit per slot, low bit = `zpc_disables\[0\]`).
5082/// The previous Rust port returned a `Vec<String>` clone of
5083/// `patterndisables` (a completely different data structure — names
5084/// list, not the per-token byte array). `restorepatterndisables(u32)`
5085/// at c:4258 reads this bitmask back into `zpc_disables`, so the
5086/// returned shape MUST be the u32 bitmask.
5087pub fn savepatterndisables() -> u32 {
5088    // c:4220
5089    let disp = zpc_disables.lock().unwrap(); // c:4225 disp = zpc_disables
5090    let mut disables: u32 = 0; // c:4224
5091    let mut bit: u32 = 1; // c:4226 bit = 1
5092    for i in 0..(ZPC_COUNT as usize) {
5093        // c:4226-4228
5094        if disp[i] != 0 {
5095            // c:4230
5096            disables |= bit; // c:4231
5097        }
5098        bit <<= 1; // c:4226 bit <<= 1
5099    }
5100    disables // c:4232
5101}
5102
5103/// Port of `startpatternscope()` from `Src/pattern.c:4241`.
5104/// C: `void startpatternscope(void)`.
5105/// Pushes a frame onto `PATSCOPE_STACK` (`zpc_disables_stack` in C)
5106/// carrying the current `zpc_disables[]` state as a `savepatterndisables()`
5107/// u32 bitmap. Called at function entry; `endpatternscope` pops it.
5108///
5109/// ```c
5110/// void startpatternscope(void) {
5111///     Zpc_disables_save newdis = zalloc(sizeof(*newdis));
5112///     newdis->next = zpc_disables_stack;
5113///     newdis->disables = savepatterndisables();  // c:4247
5114///     zpc_disables_stack = newdis;
5115/// }
5116/// ```
5117pub fn startpatternscope() {
5118    // c:4241
5119    let saved = savepatterndisables(); // c:4247
5120    PATSCOPE_STACK.with(|s| s.borrow_mut().push(saved));
5121}
5122
5123/// Port of `restorepatterndisables()` from `Src/pattern.c:4258`.
5124/// C: `void restorepatterndisables(unsigned int disables)` from
5125/// `Src/pattern.c:4258`. Walks the 12-slot `zpc_disables[]` array,
5126/// setting each slot's byte from the bitmask: `disables & (1<<i)`
5127/// → slot `i` gets 1, else 0.
5128/// ```c
5129/// void
5130/// restorepatterndisables(unsigned int disables)
5131/// {
5132///     char *disp;
5133///     unsigned int bit;
5134///     for (bit = 1, disp = zpc_disables;
5135///          disp < zpc_disables + ZPC_COUNT;
5136///          bit <<= 1, disp++) {
5137///         if (disables & bit) *disp = 1;
5138///         else *disp = 0;
5139///     }
5140/// }
5141/// ```
5142pub fn restorepatterndisables(disables: u32) {
5143    // c:4258
5144    let mut disp = zpc_disables.lock().unwrap(); // c:4263
5145    let mut bit: u32 = 1;
5146    for i in 0..(ZPC_COUNT as usize) {
5147        // c:4263-4265
5148        if (disables & bit) != 0 {
5149            // c:4266
5150            disp[i] = 1; // c:4267
5151        } else {
5152            disp[i] = 0; // c:4269
5153        }
5154        bit <<= 1;
5155    }
5156}
5157
5158/// Port of `endpatternscope()` from `Src/pattern.c:4279`.
5159/// C: `void endpatternscope(void)`.
5160/// Pops the saved bitmap from `PATSCOPE_STACK` (`zpc_disables_stack`
5161/// in C); restores `zpc_disables[]` from the bitmap ONLY when
5162/// `isset(LOCALPATTERNS)` per C c:4286. Called at function exit.
5163///
5164/// ```c
5165/// void endpatternscope(void) {
5166///     Zpc_disables_save olddis = zpc_disables_stack;
5167///     zpc_disables_stack = olddis->next;
5168///     if (isset(LOCALPATTERNS))
5169///         restorepatterndisables(olddis->disables);     // c:4287
5170///     zfree(olddis, sizeof(*olddis));
5171/// }
5172/// ```
5173pub fn endpatternscope() {
5174    // c:4279
5175    if let Some(prev) = PATSCOPE_STACK.with(|s| s.borrow_mut().pop()) {
5176        if isset(crate::ported::zsh_h::LOCALPATTERNS) {
5177            // c:4286-4287
5178            restorepatterndisables(prev);
5179        }
5180    }
5181}
5182
5183/// Port of `clearpatterndisables()` from `Src/pattern.c:4296`.
5184/// C: `void clearpatterndisables(void)`.
5185/// C body: `memset(zpc_disables, 0, ZPC_COUNT)` — zero every slot.
5186pub fn clearpatterndisables() {
5187    // c:4296
5188    *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize]; // c:4298
5189}
5190
5191/// Port of `haswilds()` from `Src/pattern.c:4306`.
5192/// C: `haswilds(char *str)`.
5193///
5194/// Check whether `str` is eligible for filename generation.
5195///
5196/// C scans a TOKENIZED string: every input reaching it has been
5197/// through the lexer or `tokenize()`/`shtokenize()` (glob.c:3548/
5198/// 3563), so glob metachars are token codes and escaped/quoted
5199/// chars are Bnull'd literals. Callers holding un-tokenized strings
5200/// must `tokenize()` a copy first — exactly what C does for
5201/// runtime-built strings (compcore.c:2231 tokenizes fignore entries
5202/// immediately before its c:2235 haswilds call). The switch matches
5203/// ONLY token codes, never literal ASCII metachars.
5204///
5205/// The walk is over chars: zshrs strings hold token chars as
5206/// codepoints (Star = U+e187) and the input layer is char-domain
5207/// (input.rs `shingetline`/`ingetc`), so the char is the unit that
5208/// the metafied byte is in C. Scanning bytes here false-positived
5209/// on UTF-8 continuation bytes of plain text (`↔` = E2 86 94
5210/// carries 0x86/0x94 = Hat/Inang as u8) — bug #627.
5211///
5212/// The C source's `%?foo` job-ref special case (c:4317-4318), which
5213/// mutates `str[1]` in place to demote the `?`, becomes a "skip
5214/// position 1" scan adjustment here since `&str` is immutable.
5215pub fn haswilds(str: &str) -> bool {
5216    // c:4306
5217    // c:4325-4372 — every `return 1` needs one of Inpar / Bar / Star /
5218    // Inbrack / Inang / Quest / Pound / Hat, and all eight are TOKEN
5219    // chars (crate::token_char), i.e. non-ASCII. An all-ASCII string
5220    // therefore always falls through to c:4374 `return 0`. Answer it
5221    // with libcore's precompiled word-at-a-time `<[u8]>::is_ascii`
5222    // instead of building a `Vec<char>` to walk: `haswilds` runs once
5223    // per candidate word, and a `man <TAB>` offers 60605 of them.
5224    if str.is_ascii() {
5225        return false; // c:4374
5226    }
5227    let chars: Vec<char> = str.chars().collect();
5228    let len = chars.len();
5229    if len == 0 {
5230        return false;
5231    }
5232
5233    // c:4309 — `[' and `]' are legal even if bad patterns are usually not.
5234    if len == 1 && (chars[0] == Inbrack || chars[0] == Outbrack) {
5235        return false; // c:4311
5236    }
5237
5238    // c:4313-4315 — If % is immediately followed by ?, then that ? is
5239    // not treated as a wildcard.  This is so you don't have
5240    // to escape job references such as %?foo.
5241    let skip_pos_1 = len >= 2 && chars[0] == '%' && chars[1] == Quest; // c:4316-4317
5242
5243    // c:4319-4321 — Note that at this point zpc_special has not been set up.
5244    let disp = zpc_disables.lock().unwrap(); // c:read zpc_disables[]
5245
5246    for i in 0..len {
5247        // c:4323 for (; *str; str++)
5248        if skip_pos_1 && i == 1 {
5249            continue; // c:4317 `str[1] = '?'` demote
5250        }
5251        let c = chars[i]; // c:4324 switch (*str)
5252        let prev: char = if i > 0 { chars[i - 1] } else { '\0' }; // c: str[-1]
5253
5254        if c == Inpar {
5255            // c:4325-4335
5256            if (!isset(SHGLOB) && disp[ZPC_INPAR as usize] == 0)
5257                || (i > 0
5258                    && isset(KSHGLOB)
5259                    && ((prev == Quest && disp[ZPC_KSH_QUEST as usize] == 0)
5260                        || (prev == Star && disp[ZPC_KSH_STAR as usize] == 0)
5261                        || (prev == '+' && disp[ZPC_KSH_PLUS as usize] == 0)
5262                        || (prev == Bang && disp[ZPC_KSH_BANG as usize] == 0)
5263                        || (prev == '!' && disp[ZPC_KSH_BANG2 as usize] == 0)
5264                        || (prev == '@' && disp[ZPC_KSH_AT as usize] == 0)))
5265            {
5266                return true; // c:4335
5267            }
5268        } else if c == Bar {
5269            if disp[ZPC_BAR as usize] == 0 {
5270                return true; // c:4340
5271            }
5272        } else if c == Star {
5273            if disp[ZPC_STAR as usize] == 0 {
5274                return true; // c:4345
5275            }
5276        } else if c == Inbrack {
5277            if disp[ZPC_INBRACK as usize] == 0 {
5278                return true; // c:4350
5279            }
5280        } else if c == Inang {
5281            if disp[ZPC_INANG as usize] == 0 {
5282                return true; // c:4355
5283            }
5284        } else if c == Quest {
5285            if disp[ZPC_QUEST as usize] == 0 {
5286                return true; // c:4360
5287            }
5288        } else if c == Pound {
5289            if isset(EXTENDEDGLOB) && disp[ZPC_HASH as usize] == 0 {
5290                return true; // c:4365
5291            }
5292        } else if c == Hat {
5293            if isset(EXTENDEDGLOB) && disp[ZPC_HAT as usize] == 0 {
5294                return true; // c:4370
5295            }
5296        }
5297    }
5298    false // c:4374
5299}
5300
5301// =====================================================================
5302// 4. struct patprog — zsh.h:1601
5303// =====================================================================
5304
5305/// `typedef struct patprog *Patprog;` from `zsh.h:542`.
5306#[allow(non_camel_case_types)]
5307/// `typedef struct patprog *Patprog;` from `zsh.h:542`.
5308///
5309/// C zsh allocates the `struct patprog` header + bytecode as one
5310/// contiguous `malloc` block, accessing bytecode via
5311/// `(char *)prog + prog->startoff`. Rust has no flexible array
5312/// members, so this typedef pairs the C-exact `patprog` header
5313/// (zsh_h.rs:768) with an owned bytecode `Vec<u8>` — header at
5314/// `.0`, bytecode at `.1`. `startoff`/`size` index into `.1`.
5315pub type Patprog = Box<(patprog, Vec<u8>)>;
5316// =====================================================================
5317// Bytecode field offsets within each instruction.
5318//
5319// Instruction layout in `patout`:
5320//     +0       u8     opcode
5321//     +1..+5   u32 LE next_off (offset of next instr in chain, 0 = end)
5322//     +5..     u8...  opcode-specific payload
5323//
5324// C uses a different layout (Upat union = 4-byte or 8-byte slots);
5325// the Rust port pins the layout to byte offsets for portability.
5326// =====================================================================
5327
5328const I_OP: usize = 0; // opcode byte
5329
5330impl rpat {
5331    pub fn new() -> Self {
5332        Self {
5333            wbranch_visits: std::rc::Rc::new(std::cell::RefCell::new(
5334                std::collections::HashMap::new(),
5335            )),
5336            patbeginp: [usize::MAX; NSUBEXP],
5337            patendp: [0; NSUBEXP],
5338            captures_set: 0,
5339            errsfound: 0, // c:2066 — errsfound = 0 at pattry init
5340        }
5341    }
5342}
5343const I_NEXT: usize = 1; // u32 next-offset starts here
5344const I_BODY: usize = 5; // payload starts here
5345
5346/// Serialises every entry into `patcompile`. The C source at
5347/// `Src/pattern.c:267-281` declares `patout`, `patparse`, `patstart`,
5348/// `patnpar`, `patflags`, `patglobflags`, `errsfound`, `forceerrs`,
5349/// `zpc_special`, `patstrcache` as file-scope statics that the compile
5350/// mutates in sequence; zsh-the-program is single-threaded so the C
5351/// source is safe under that invariant. zshrs callers (zutil's
5352/// `style_table::get` via `crate::ported::pattern::patmatch`, params.rs,
5353/// subst.rs, options.rs) can invoke `patcompile` from concurrent test
5354/// threads, so the lock restores the single-writer invariant. Held
5355/// only for the compile phase; the matcher (`pattry`/`patmatch`)
5356/// operates on the returned `Patprog.code` and touches no globals.
5357static PATCOMPILE_LOCK: Mutex<()> = Mutex::new(());
5358
5359// C: `static char *patparse, *patstart;` — pattern parsing cursors
5360// into the *input* pattern string. patstart points to start of
5361// pattern; patparse moves forward as we consume tokens.
5362pub static patparse: Mutex<String> = Mutex::new(String::new()); // c:269
5363pub static patstart: Mutex<String> = Mutex::new(String::new()); // c:269
5364
5365/// Position within `patparse` we're currently looking at. C source
5366/// uses a `char *` cursor; Rust uses byte offset into the String.
5367pub static patparse_off: AtomicUsize = AtomicUsize::new(0);
5368
5369// C: `static int errsfound;` — approximate-match error count.
5370pub static errsfound: AtomicI32 = AtomicI32::new(0); // c:274
5371
5372// C: `static int forceerrs;` — required error count for approximate match.
5373pub static forceerrs: AtomicI32 = AtomicI32::new(-1); // c:275
5374
5375// C: `static long patglobflags_orig;` — saved at branch entry.
5376pub static patglobflags_orig: AtomicI32 = AtomicI32::new(0); // c:276
5377
5378// C: `static const char *zpc_special;` — table of currently-special
5379// characters during compile (indexed by ZPC_*).
5380//
5381// pattern.c uses `static char zpc_special[ZPC_COUNT];` and resets it
5382// in patcompcharsset(). Rust mirrors as a Mutex-wrapped byte array.
5383pub static zpc_special: Mutex<[u8; ZPC_COUNT as usize]> = Mutex::new([0u8; ZPC_COUNT as usize]); // c:278
5384
5385// C: `static char *patstrcache;` — caches the unmetafied trial string.
5386// Rust port has no Meta encoding so the cache is unnecessary; we leave
5387// the static declared for parity (Rule A — name exists in C).
5388pub static patstrcache: Mutex<String> = Mutex::new(String::new()); // c:281
5389
5390/// `Marker` constant — alias for `crate::ported::zsh_h::Marker`
5391/// (`Src/zsh.h:224` `#define Marker ((char) 0xa2)`).
5392///
5393/// The previous Rust port had this as `pub const Marker: u8 = 0x80`
5394/// which is WRONG — `\200` (0x80) is NOT the canonical Marker byte.
5395/// C's Marker is 0xa2 per `Src/zsh.h:224`. No in-tree callers used
5396
5397/// Port of `static const char *colon_stuffs[]` from `Src/pattern.c:1134-1138`.
5398/// 19 entries matching C's table in declaration order, so `range_type(name)`
5399/// returns the same `(index + PP_FIRST)` value C does:
5400/// alpha=1, alnum=2, ascii=3, blank=4, cntrl=5, digit=6, graph=7, lower=8,
5401/// print=9, punct=10, space=11, upper=12, xdigit=13, IDENT=14, IFS=15,
5402/// IFSSPACE=16, WORD=17, INCOMPLETE=18, INVALID=19. Prior Rust port had
5403/// only 12 lowercase entries and was MISSING `ascii` between `alnum` and
5404/// `blank`, so `range_type("digit")` returned 5 instead of C's PP_DIGIT=6
5405/// and every `[:class:]` byte marker emitted by `complete.rs:733`
5406/// (`0x80 + ch`) was off-by-one for classes after `alnum`. Real port bug.
5407const POSIX_CLASS_NAMES: &[&str] = &[
5408    "alpha",
5409    "alnum",
5410    "ascii",
5411    "blank",
5412    "cntrl",
5413    "digit",
5414    "graph",
5415    "lower",
5416    "print",
5417    "punct",
5418    "space",
5419    "upper",
5420    "xdigit",
5421    "IDENT",
5422    "IFS",
5423    "IFSSPACE",
5424    "WORD",
5425    "INCOMPLETE",
5426    "INVALID",
5427];
5428
5429/// Port of file-static `zpc_disables_stack` from `Src/pattern.c:4244`.
5430/// Per-evaluator function-scope disable save-stack (bucket-1: each
5431/// worker thread parses/executes its own function calls, so each must
5432/// have its own scope stack). Reason for `thread_local!` over `Mutex`:
5433/// in zsh C this is a per-process file-static; in zshrs each worker
5434/// thread is its own evaluator — TLS preserves the per-evaluator
5435/// semantic without serializing across workers.
5436///
5437/// Element type: u32 bitmap matching `savepatterndisables` return
5438/// shape (c:4220 `unsigned int disables`). Prior Rust port used
5439/// `Vec<Vec<String>>` and `startpatternscope` cloned a `Vec<String>`
5440/// of names instead of the bitmap — a real port bug that made
5441/// `setopt LOCALPATTERNS` function entry/exit silently fail to save/
5442/// restore the disable state.
5443thread_local! {
5444    static PATSCOPE_STACK: std::cell::RefCell<Vec<u32>> =
5445        const { std::cell::RefCell::new(Vec::new()) };
5446}
5447
5448// =====================================================================
5449// 17. Module-loader / disable mgmt — pattern.c:4161-4296
5450// =====================================================================
5451
5452// `pub static patterndisables: Mutex<Vec<String>>` deleted — was a
5453// dead tombstone with no callers outside the buggy
5454// `startpatternscope` / `endpatternscope` / `clearpatterndisables`
5455// fns (which cloned/cleared it instead of operating on the real
5456// `zpc_disables` byte array). The canonical zsh `zpc_disables[ZPC_COUNT]`
5457// (Src/pattern.c:268) is the disable state; the names list does not
5458// exist as a separate data structure in C.
5459
5460/// Port of `char zpc_disables[ZPC_COUNT]` from `Src/pattern.c:268`.
5461/// Per-token disable byte — when `zpc_disables[i]` is non-zero, the
5462/// pattern token at index `i` (ZPC_*) is treated as a literal,
5463/// not its meta-meaning.
5464pub static zpc_disables: Mutex<[u8; ZPC_COUNT as usize]> = Mutex::new([0u8; ZPC_COUNT as usize]); // c:268
5465
5466/// !!! WARNING: RUST-ONLY HELPER !!!
5467///
5468/// There is NO `fixup_offsets_after_insert()` in `Src/pattern.c`.
5469/// It stands in for C's ABSENCE of a fixup step: C's `patinsert()`
5470/// (`Src/pattern.c:1807`) links nodes by RELATIVE `P_NEXT` deltas
5471/// (`Src/pattern.c:198`) held in `union upat` slots, so shifting the
5472/// tail of `patout` down leaves every link valid and C only has to
5473/// `memmove` (`Src/pattern.c:1816-1820`). zshrs's bytecode stores
5474/// ABSOLUTE byte offsets in the 4-byte `I_NEXT` slot, which the
5475/// memmove invalidates — so every next-offset past `opnd` has to be
5476/// re-based by `delta`. Called only from `patinsert()`; both writer
5477/// and reader live in pattern.rs.
5478///
5479/// Walks the buffer linearly opcode-by-opcode reading I_NEXT slots.
5480/// Conservatively adjusts every nonzero next that lands past opnd.
5481fn fixup_offsets_after_insert(buf: &mut [u8], opnd: usize, delta: u32) {
5482    let mut i = 0;
5483    while i + I_BODY <= buf.len() {
5484        let op = buf[i + I_OP];
5485        if op == 0 {
5486            i += 1;
5487            continue;
5488        } // sentinel byte, skip
5489        let next_bytes = &buf[i + I_NEXT..i + I_NEXT + 4];
5490        let cur = u32::from_le_bytes(next_bytes.try_into().unwrap());
5491        if cur != 0 {
5492            let abs = cur as usize;
5493            if abs >= opnd && abs <= buf.len() {
5494                let new = cur + delta;
5495                buf[i + I_NEXT..i + I_NEXT + 4].copy_from_slice(&new.to_le_bytes());
5496            }
5497        }
5498        i = advance_past_instr(buf, i);
5499        if i == 0 {
5500            break;
5501        }
5502    }
5503}
5504
5505/// !!! WARNING: RUST-ONLY HELPER !!!
5506///
5507/// There is NO `advance_past_instr()` in `Src/pattern.c`. C never
5508/// needs one: its bytecode is an array of fixed-size `union upat`
5509/// slots (`union upat`, `Src/pattern.c:84-89`), so "the next slot"
5510/// is just
5511/// `p + 1` pointer arithmetic and the payload width is implicit in
5512/// the slot count each emitter wrote. zshrs emits a packed BYTE
5513/// stream instead, so stepping over an instruction requires decoding
5514/// its payload width from the opcode. This function is that decode
5515/// table. Called only from `fixup_offsets_after_insert()`; both
5516/// writer and reader live in pattern.rs.
5517///
5518/// Encodes the per-opcode payload size table — must stay in sync
5519/// with patnode/patinsert calls in the compiler.
5520fn advance_past_instr(buf: &[u8], pos: usize) -> usize {
5521    if pos + I_BODY > buf.len() {
5522        return 0;
5523    }
5524    let op = buf[pos + I_OP];
5525    let body_start = pos + I_BODY;
5526    match op {
5527        P_END | P_NOTHING | P_BACK | P_EXCSYNC | P_EXCEND | P_ISSTART | P_ISEND | P_COUNTSTART
5528        | P_ANY | P_STAR | P_NUMANY => body_start,
5529        P_GFLAGS => body_start + 4, // i32 flag-bits payload
5530        P_EXACTLY => {
5531            // payload: u32 len + len bytes
5532            if body_start + 4 > buf.len() {
5533                return 0;
5534            }
5535            let len =
5536                u32::from_le_bytes(buf[body_start..body_start + 4].try_into().unwrap()) as usize;
5537            body_start + 4 + len
5538        }
5539        P_ANYOF | P_ANYBUT => {
5540            if body_start + 4 > buf.len() {
5541                return 0;
5542            }
5543            let len =
5544                u32::from_le_bytes(buf[body_start..body_start + 4].try_into().unwrap()) as usize;
5545            body_start + 4 + len
5546        }
5547        P_ONEHASH | P_TWOHASH | P_BRANCH => body_start,
5548        // c:113 P_WBRANCH and c:114-115 P_EXCLUDE/P_EXCLUDP carry an
5549        // 8-byte syncptr payload (one Upat slot in C) right after
5550        // their header, before the chained operand.
5551        P_WBRANCH | P_EXCLUDE | P_EXCLUDP => body_start + 8,
5552        P_OPEN..=0x88 | P_CLOSE..=0x98 => body_start,
5553        P_NUMRNG => body_start + 16, // two i64
5554        P_NUMFROM | P_NUMTO => body_start + 8,
5555        P_COUNT => body_start + 16, // min i64 + max i64; operand inline follows
5556        _ => body_start,
5557    }
5558}
5559
5560/// !!! WARNING: RUST-ONLY HELPER !!!
5561///
5562/// There is NO `set_next()` in `Src/pattern.c`. C writes a node's
5563/// next-link inline as one assignment against the `union upat`
5564/// pointer — `scan->l |= offset << 8;` (`Src/pattern.c:1849`), the
5565/// write half of the `P_NEXT` macro at `Src/pattern.c:198`, as used
5566/// in `pattail()` (`Src/pattern.c:1846-1849`). zshrs stores that link
5567/// as a 4-byte little-endian absolute offset inside a `Vec<u8>`
5568/// behind a mutex, so the same one-liner needs a lock, a bounds
5569/// check and a `to_le_bytes` — factored here rather than repeated at
5570/// every emit site. Both writer and reader live in pattern.rs.
5571fn set_next(pos: usize, val: usize) {
5572    let mut buf = patout.lock().unwrap();
5573    if pos + I_NEXT + 4 <= buf.len() {
5574        buf[pos + I_NEXT..pos + I_NEXT + 4].copy_from_slice(&(val as u32).to_le_bytes());
5575    }
5576}
5577
5578/// !!! WARNING: RUST-ONLY HELPER !!!
5579///
5580/// There is NO `chain_branches_to()` in `Src/pattern.c`. C performs
5581/// this walk inline at the end of `patcompswitch()`
5582/// (`Src/pattern.c:909-911`): `for (ptr = (Upat)patout + starter; ptr;
5583/// ptr = PATNEXT(ptr)) if (!P_ISEXCLUDE(ptr)) patoptail(ptr -
5584/// (Upat)patout, ender);` — `union upat` pointer arithmetic reaches
5585/// each alternative and its operand. In zshrs the operand is at
5586/// `br + I_BODY` and the branch link is a
5587/// 4-byte absolute offset read under the `patout` mutex, so the loop
5588/// is factored out to keep `patcompswitch()` readable. Both writer
5589/// and reader live in pattern.rs.
5590///
5591/// Walks every branch's operand chain and patches each branch's
5592/// last-operand-node `.next` to `target`. Used to chain a fully-
5593/// compiled alternation switch to whatever opcode follows (P_END for
5594/// the outermost compile, P_CLOSE_N for a sub-group).
5595fn chain_branches_to(starter: usize, target: usize) {
5596    let mut cur = starter;
5597    loop {
5598        // Operand starts at cur + I_BODY (the byte right after this
5599        // branch's header). Walk operand's next-chain to its end
5600        // and set its .next = target.
5601        pattail(cur + I_BODY, target);
5602        // Move to next alternative.
5603        let buf = patout.lock().unwrap();
5604        if cur + I_NEXT + 4 > buf.len() {
5605            break;
5606        }
5607        let nb: [u8; 4] = buf[cur + I_NEXT..cur + I_NEXT + 4].try_into().unwrap();
5608        let n = u32::from_le_bytes(nb) as usize;
5609        drop(buf);
5610        if n == 0 {
5611            break;
5612        }
5613        cur = n;
5614    }
5615}
5616
5617/// !!! WARNING: RUST-ONLY HELPER !!!
5618///
5619/// There is NO `approx_match_exactly()` in `Src/pattern.c`. It is the
5620/// `case P_EXACTLY:` arm of `patmatch()` (c:2742-2784) together with the
5621/// shared approximation block it falls into on a mismatch (c:3472-3576),
5622/// for a program carrying an `(#aN)` budget. C keeps both inline and
5623/// communicates the half-matched literal through the file-statics
5624/// `exactpos`/`exactend`, retrying the SAME node with `patmatch(scan)`;
5625/// the Rust matcher threads its state through `&mut rpat`, so the retry
5626/// is a recursive call with the literal offset `p` standing in for
5627/// `exactpos`. Nothing outside the P_EXACTLY arm calls it.
5628///
5629/// Order is C's, first success wins: characters that match are consumed
5630/// with no alternative tried (c:2755-2778); at the first mismatch, one
5631/// error buys, in turn, (1) omitting an input character, (2) swapping
5632/// two characters, (3) omitting a character from both, (4) omitting a
5633/// pattern character (c:3477-3575). A fully matched literal hands off to
5634/// `next` without backtracking into the literal. The unit is a
5635/// CHARACTER under GF_MULTIBYTE (CHARREFINC / CHARINC, c:2770-2771), a
5636/// byte otherwise. Returns the end offset of the whole match.
5637fn approx_match_exactly(
5638    code: &[u8],
5639    next: usize,
5640    string: &str,
5641    s_off: usize,
5642    str_bytes: &[u8],
5643    state: &mut rpat,
5644    glob_flags: i32,
5645    max_errs: i32,
5646) -> Option<usize> {
5647    // CHARREFINC (c:1969) — the character at `off` and its byte length.
5648    let char_at = |b: &[u8], off: usize| -> (u32, usize) {
5649        if (glob_flags & GF_MULTIBYTE) != 0 {
5650            let want = match b[off] {
5651                0x00..=0x7f => 1,
5652                0xc0..=0xdf => 2,
5653                0xe0..=0xef => 3,
5654                0xf0..=0xf7 => 4,
5655                _ => 1,
5656            };
5657            if let Some(c) = b
5658                .get(off..off + want)
5659                .and_then(|s| std::str::from_utf8(s).ok())
5660                .and_then(|s| s.chars().next())
5661            {
5662                return (c as u32, want);
5663            }
5664        }
5665        (b[off] as u32, 1)
5666    };
5667    // c:Src/pattern.c CHARMATCH — case folding under (#i) / (#l).
5668    let charmatch = |chin: u32, chpa: u32| -> bool {
5669        let upper = |c: u32| -> u32 {
5670            char::from_u32(c)
5671                .filter(|ch| ch.is_lowercase())
5672                .and_then(|ch| ch.to_uppercase().next())
5673                .map_or(c, |u| u as u32)
5674        };
5675        let is_lower = |c: u32| char::from_u32(c).is_some_and(|ch| ch.is_lowercase());
5676        chin == chpa
5677            || if (glob_flags & GF_IGNCASE) != 0 {
5678                upper(chin) == upper(chpa)
5679            } else if (glob_flags & GF_LCMATCHUC) != 0 {
5680                is_lower(chpa) && upper(chpa) == chin
5681            } else {
5682                false
5683            }
5684    };
5685    fn walk(
5686        code: &[u8],
5687        next: usize,
5688        string: &str,
5689        str_bytes: &[u8],
5690        mut s: usize,
5691        mut p: usize,
5692        state: &mut rpat,
5693        glob_flags: i32,
5694        max_errs: i32,
5695        char_at: &dyn Fn(&[u8], usize) -> (u32, usize),
5696        charmatch: &dyn Fn(u32, u32) -> bool,
5697    ) -> Option<usize> {
5698        let input = string.as_bytes();
5699        loop {
5700            if p >= str_bytes.len() {
5701                // c:2779-2784 — the literal is used up: on to `next`.
5702                return if next == 0 {
5703                    Some(s)
5704                } else {
5705                    patmatch(code, next, string, s, state, glob_flags)
5706                };
5707            }
5708            let (chpa, plen) = char_at(str_bytes, p);
5709            if s < input.len() {
5710                let (chin, ilen) = char_at(input, s);
5711                if charmatch(chin, chpa) {
5712                    // c:2770-2777 — matched: consume, no alternative.
5713                    s += ilen;
5714                    p += plen;
5715                    continue;
5716                }
5717            }
5718            // c:3473-3474 — a mismatch spends an error, if any are left.
5719            let fe = forceerrs.load(Ordering::Relaxed);
5720            if !(state.errsfound < max_errs && (fe == -1 || state.errsfound < fe)) {
5721                return None; // c:3578-3579
5722            }
5723            state.errsfound += 1; // c:3497 `saverrsfound = ++errsfound`
5724            let saved = state.clone();
5725            if s < input.len() {
5726                let (chin0, ilen0) = char_at(input, s);
5727                // c:3503-3514 — (1) omit a character from the input.
5728                if let Some(r) = walk(
5729                    code, next, string, str_bytes, s + ilen0, p, state, glob_flags, max_errs,
5730                    char_at, charmatch,
5731                ) {
5732                    return Some(r);
5733                }
5734                *state = saved.clone(); // c:3524-3526
5735                let nextin = s + ilen0;
5736                let nextexact = p + plen;
5737                // c:3532-3551 — (2) transpose two characters.
5738                if nextin < input.len() && nextexact < str_bytes.len() {
5739                    let (chin1, ilen1) = char_at(input, nextin);
5740                    let (chpa1, plen1) = char_at(str_bytes, nextexact);
5741                    if charmatch(chin0, chpa1) && charmatch(chin1, chpa) {
5742                        if let Some(r) = walk(
5743                            code, next, string, str_bytes, nextin + ilen1, nextexact + plen1,
5744                            state, glob_flags, max_errs, char_at, charmatch,
5745                        ) {
5746                            return Some(r);
5747                        }
5748                        *state = saved.clone(); // c:3549-3550
5749                    }
5750                }
5751                // c:3553-3565 — (3) move up both strings.
5752                if let Some(r) = walk(
5753                    code, next, string, str_bytes, nextin, nextexact, state, glob_flags,
5754                    max_errs, char_at, charmatch,
5755                ) {
5756                    return Some(r);
5757                }
5758                *state = saved; // c:3562-3565
5759            }
5760            // c:3567-3575 — (4) move up the pattern only; the last
5761            // attempt, so C loops instead of recursing.
5762            p += plen;
5763        }
5764    }
5765    walk(
5766        code, next, string, str_bytes, s_off, 0, state, glob_flags, max_errs, &char_at,
5767        &charmatch,
5768    )
5769}
5770
5771thread_local! {
5772    /// Rust-only backstop counter (no C analogue) — gates `patmatch`
5773    /// recursion at PATMATCH_MAX_DEPTH so misbehaving closures convert
5774    /// would-be stack overflows into clean None returns. Documented in
5775    /// `fake_fn_allowlist.txt` under the patmatch arc.
5776    static PATMATCH_DEPTH: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
5777}
5778
5779/// Maximum patmatch recursion depth — Rust-only backstop, no C
5780/// analogue. The C source's primary protection against
5781/// closure-of-closure infinite recursion is the P_WBRANCH "must
5782/// match at least 1 char" semantics described at pattern.c:108-144:
5783///   `P_WBRANCH:  This works like a branch and is used in complex`
5784///   `closures, but the match must be at least 1 char in length to`
5785///   `avoid infinite loops.`
5786/// (Implemented in C's patmatch() switch at c:3044, `case P_WBRANCH`,
5787/// via the `errsfound`/`forceerrs` accounting at c:1059+.) The Rust
5788/// port's P_WBRANCH arm has gaps in that propagation, so we keep
5789/// this depth guard as a safety net.
5790///
5791/// **Tuned to test-thread stack size**, NOT the main-thread 8 MB.
5792/// Rust's `cargo test` spawns each test on a thread whose default
5793/// stack is ~2 MB (per `std::thread::Builder::stack_size` default at
5794/// `library/std/src/thread/mod.rs`), and `(fo#)#` patterns blow that
5795/// at ~250-300 frames per measured SIGABRT. 128 leaves headroom
5796/// (~256 KB worst case at ~2 KB/frame) while still admitting any
5797/// legitimate zsh pattern (Misc/globtests tops out around 30).
5798const PATMATCH_MAX_DEPTH: u32 = 128;
5799
5800thread_local! {
5801    // c:Src/pattern.c — the P_EXCLUDE `syncptr->p` heap buffer. Keyed by
5802    // the EXCLUDE node offset; value is a per-input-position byte array
5803    // recording where the asserted branch's excludable part matched (so
5804    // EXCSYNC can fail a revisit and force backtracking to a different
5805    // split). C stores this in the bytecode payload + a raw heap pointer
5806    // that survives backtracking; the Rust matcher clones `rpat` per
5807    // branch, so the buffer lives here (outside the cloned state) to
5808    // stay shared across recursion, exactly like C's global syncstrp->p.
5809    static EXCSYNC_BUF: std::cell::RefCell<std::collections::HashMap<usize, Vec<u8>>> =
5810        std::cell::RefCell::new(std::collections::HashMap::new());
5811}
5812
5813/// Port of `patmatch()` from `Src/pattern.c:2694`. The bytecode
5814/// interpreter — `static int patmatch(Upat prog)`.
5815///
5816/// Returns `Some(end_pos)` on a successful match (`end_pos` = byte
5817/// offset in `string` where the match ended), `None` on no-match; C
5818/// returns 1/0 and leaves the end position in the file-static
5819/// `patinput`.
5820///
5821/// Rust signature differs from C's `int patmatch(Upat prog)`: the
5822/// bytecode, the current input position, the captures and the glob
5823/// flags are threaded through arguments rather than C's per-thread
5824/// file-statics (`patinput`, `patinstart`, `patbeginp`/`patendp`,
5825/// `patglobflags`). Rule S1 deviation justified by zshrs's threading
5826/// model — see PORT_PLAN.md Bucket 1.
5827pub fn patmatch(
5828    code: &[u8],
5829    prog_off: usize,
5830    string: &str,
5831    string_off: usize,
5832    state: &mut rpat,
5833    glob_flags: i32,
5834) -> Option<usize> {
5835    // c:2694
5836    // Depth-guard: convert what would be a stack overflow into a
5837    // clean None return (= no match). See PATMATCH_MAX_DEPTH doc.
5838    let d = PATMATCH_DEPTH.with(|c| {
5839        let cur = c.get();
5840        c.set(cur + 1);
5841        cur + 1
5842    });
5843    if d > PATMATCH_MAX_DEPTH {
5844        PATMATCH_DEPTH.with(|c| c.set(c.get() - 1));
5845        return None;
5846    }
5847    struct DepthGuard;
5848    impl Drop for DepthGuard {
5849        fn drop(&mut self) {
5850            PATMATCH_DEPTH.with(|c| c.set(c.get().saturating_sub(1)));
5851        }
5852    }
5853    let _depth_guard = DepthGuard;
5854
5855    let mut scan = prog_off;
5856    let mut s_off = string_off;
5857    // Locally-mutable copy of glob_flags so mid-pattern P_GFLAGS can
5858    // toggle bits without affecting the caller's branch view.
5859    let mut glob_flags = glob_flags;
5860
5861    // Inlined port of CHARMATCH macro from `Src/pattern.c:2671-2677`:
5862    //   #define CHARMATCH(chin, chpa) (chin == chpa || \
5863    //       ((patglobflags & GF_IGNCASE) ?
5864    //          ((ISUPPER(chin) ? TOLOWER(chin) : chin) ==
5865    //           (ISUPPER(chpa) ? TOLOWER(chpa) : chpa)) :
5866    //        (patglobflags & GF_LCMATCHUC) ?
5867    //          (ISLOWER(chpa) && TOUPPER(chpa) == chin) : 0))
5868    //
5869    // - exact byte match always wins
5870    // - GF_IGNCASE: SYMMETRIC fold (both lowercase before compare)
5871    // - GF_LCMATCHUC: ASYMMETRIC — lowercase pattern char ALSO matches
5872    //   uppercase text char; an UPPERCASE pattern char only matches
5873    //   that exact uppercase text char. This is the `(#l)` flag's
5874    //   "lowercase-in-pattern matches uppercase-in-text" semantic;
5875    //   previously zshrs treated LCMATCHUC the same as IGNCASE (full
5876    //   case-fold both sides) so `(#l)FOO` wrongly matched "foo".
5877    let charmatch = |chin: u8, chpa: u8, flags: i32| -> bool {
5878        if chin == chpa {
5879            return true; // c:2671
5880        }
5881        if (flags & GF_IGNCASE) != 0 {
5882            // c:2672-2674
5883            let a = if chin.is_ascii_uppercase() {
5884                chin.to_ascii_lowercase()
5885            } else {
5886                chin
5887            };
5888            let b = if chpa.is_ascii_uppercase() {
5889                chpa.to_ascii_lowercase()
5890            } else {
5891                chpa
5892            };
5893            return a == b;
5894        }
5895        if (flags & GF_LCMATCHUC) != 0 {
5896            // c:2675-2676
5897            return chpa.is_ascii_lowercase() && chpa.to_ascii_uppercase() == chin;
5898        }
5899        false // c:2677
5900    };
5901
5902    // Byte advance past ONE *input* character at `off` — C's `CHARINC`
5903    // macro (c:1963 `#define CHARINC(x, y) ((x) = charnext((x), (y)))`),
5904    // whose `charnext` body is c:1940-1960.
5905    //
5906    // The unit is option-dependent, and that is the whole point:
5907    // c:1946 `if (!(patglobflags & GF_MULTIBYTE) || !((unsigned char) *x
5908    // & 0x80)) return x + 1;` — with GF_MULTIBYTE clear (`unsetopt
5909    // multibyte`) a character is ONE RAW BYTE, so `?` matches a byte and
5910    // `*` backtracks over bytes. Only with GF_MULTIBYTE set does C decode
5911    // a whole multibyte character (c:1949 `mbrtowc`), falling back to a
5912    // single byte on MB_INVALID/MB_INCOMPLETE (c:1951-1955).
5913    //
5914    // C matches over an UNMETAFIED buffer (`patallocstr`), whereas zshrs
5915    // keeps non-UTF-8 input metafied inside the `&str` — Meta `\u{83}`
5916    // followed by `byte ^ 32`. One metafied pair stands for ONE raw byte,
5917    // so it is consumed as a unit in both modes; under GF_MULTIBYTE
5918    // consecutive pairs are accumulated until they form one valid UTF-8
5919    // character, which is what C's `mbrtowc` over the raw bytes does.
5920    //
5921    // Returns 0 only at end of input (C's `patinput == patinend`, c:2737).
5922    let charinc = |off: usize, gflags: i32| -> usize {
5923        if off >= string.len() {
5924            return 0; // c:2737 — no character left.
5925        }
5926        let multibyte = (gflags & GF_MULTIBYTE) != 0; // c:1946
5927                                                      // A non-boundary offset is only reachable while stepping bytes
5928                                                      // with GF_MULTIBYTE clear; C's raw-byte view advances one byte.
5929        let Some(rest) = string.get(off..) else {
5930            return 1; // c:1947
5931        };
5932        let mut raw: Vec<u8> = Vec::new();
5933        let mut steps: Vec<usize> = Vec::new();
5934        let mut advance = 0usize;
5935        let mut chs = rest.chars();
5936        while let Some(c) = chs.next() {
5937            if c == '\u{83}' {
5938                if let Some(n) = chs.clone().next() {
5939                    if (n as u32) >= 0x80 {
5940                        raw.push((n as u32 as u8) ^ 32);
5941                        steps.push(c.len_utf8() + n.len_utf8());
5942                        chs.next();
5943                        // c:1946-1947 — one raw byte IS one character
5944                        // without GF_MULTIBYTE.
5945                        if !multibyte {
5946                            advance += steps[0];
5947                            break;
5948                        }
5949                        // c:1949 — `ret = mbrtowc(&wc, x, y-x, &shiftstate)`.
5950                        // LOCALE-driven, which is the point: `zh_CN.GB2312`
5951                        // and `ja_JP.eucJP` are multibyte codesets that are
5952                        // not UTF-8. Collecting bytes "until they form valid
5953                        // UTF-8" also never terminated for input that is not
5954                        // UTF-8 at all, so `$'\xc4\xe3'` was consumed as ONE
5955                        // character and matched a single `?` where zsh — which
5956                        // treats each undecodable byte as its own character —
5957                        // matches `??`.
5958                        let mut wc: libc::wchar_t = 0;
5959                        let mut mbs = crate::ported::utils::MBSTATE_ZERO;
5960                        let ret = unsafe {
5961                            crate::ported::utils::mbrtowc(
5962                                &mut wc,
5963                                raw.as_ptr() as *const libc::c_char,
5964                                raw.len(),
5965                                &mut mbs as *mut crate::ported::utils::MbStateBuf
5966                                    as *mut libc::c_void,
5967                            )
5968                        };
5969                        if ret == crate::ported::utils::MB_INCOMPLETE
5970                            && raw.len() < crate::ported::zsh_h::MB_CUR_MAX
5971                        {
5972                            // C hands `mbrtowc` the whole remaining buffer at
5973                            // once; the port feeds it the metafied pairs as it
5974                            // decodes them, so a still-incomplete character
5975                            // means "collect the next pair".
5976                            continue;
5977                        }
5978                        advance += if ret == crate::ported::utils::MB_INVALID
5979                            || ret == crate::ported::utils::MB_INCOMPLETE
5980                            || ret == 0
5981                        {
5982                            // c:1951-1955 — "Error.  Treat as single byte."
5983                            // c:1959 — `x + (ret ? ret : 1)` for the NUL case.
5984                            steps[0]
5985                        } else {
5986                            steps[..ret.min(steps.len())].iter().sum()
5987                        };
5988                        break;
5989                    }
5990                }
5991                // Lone Meta — count it as one character.
5992                advance += c.len_utf8();
5993                break;
5994            }
5995            if !steps.is_empty() {
5996                // A metafied run was still assembling a character when a
5997                // natively stored one turned up, so the run cannot complete.
5998                // C reaches the same answer without noticing the boundary:
5999                // `mbrtowc` over the remaining buffer fails and it returns
6000                // `x + 1` (c:1951-1955). Leaving `advance` at 0 here dropped
6001                // the pending pair entirely, so `$'a\xc4\xe3b'` matched no
6002                // `????` even though `${#}` counted its 4 characters.
6003                advance = steps[0];
6004                break;
6005            }
6006            // Natively stored character: its `&str` bytes ARE the raw
6007            // bytes C would see, so without GF_MULTIBYTE only the first
6008            // one is consumed (c:1947).
6009            advance += if !multibyte && (c as u32) >= 0x80 {
6010                1
6011            } else {
6012                c.len_utf8()
6013            };
6014            break;
6015        }
6016        if advance == 0 && !steps.is_empty() {
6017            // Input ran out while the collected bytes were still an
6018            // incomplete character. C never sees this as a separate case —
6019            // it hands `mbrtowc` the whole remaining buffer, gets
6020            // MB_INCOMPLETE and returns `x + 1` (c:1951-1955) — but the port
6021            // discovers it by exhausting the metafied pairs, and leaving
6022            // `advance` at 0 made `P_ANY` read it as "no character left"
6023            // (c:2737). A trailing `$'\xe3'` then matched no `?` at all.
6024            advance = steps[0];
6025        }
6026        advance
6027    };
6028
6029    // Membership test for a P_ANYOF/P_ANYBUT body at `body_off` against the
6030    // input char at byte offset `off`. Returns (in_set, byte_advance). ASCII
6031    // input takes the unchanged byte `charmatch` path over the `chars` set; a
6032    // multibyte char is decoded (raw UTF-8, or a metafied `$'\xNN'` Meta-pair)
6033    // and tested against the appended class mask / wide ranges / wide literals
6034    // — C's `mb_patmatchrange` equivalent (pattern.c:3610).
6035    let anyof_membership = |body_off: usize, off: usize, gflags: i32| -> (bool, usize) {
6036        let range_flags = gflags & !(GF_IGNCASE | GF_LCMATCHUC);
6037        let chars_len =
6038            u32::from_le_bytes(code[body_off + 4..body_off + 8].try_into().unwrap()) as usize;
6039        let cs = body_off + 8;
6040        let set = &code[cs..cs + chars_len];
6041        let mut p = cs + chars_len;
6042        let classmask = u32::from_le_bytes(code[p..p + 4].try_into().unwrap());
6043        p += 4;
6044        let n_mbc = u32::from_le_bytes(code[p..p + 4].try_into().unwrap()) as usize;
6045        p += 4;
6046        let mbc_start = p;
6047        p += n_mbc * 4;
6048        let n_mbr = u32::from_le_bytes(code[p..p + 4].try_into().unwrap()) as usize;
6049        p += 4;
6050        let mbr_start = p;
6051
6052        let bytes = string.as_bytes();
6053        // c:1974-1975 — without GF_MULTIBYTE the candidate is one RAW byte.
6054        // A metafied pair (Meta U+0083 + byte^32) is that byte; take it and
6055        // its whole span through charrefinc rather than the pair's first
6056        // UTF-8 byte.
6057        let (b, badv) = {
6058            let mut p = off;
6059            let mut z = 0;
6060            match (gflags & GF_MULTIBYTE == 0 && string.is_char_boundary(off))
6061                .then(|| charrefinc(string, &mut p, gflags, &mut z))
6062                .flatten()
6063            {
6064                Some(r) if bytes[off] == 0xc2 && p - off > '\u{83}'.len_utf8() => (r as u8, p - off),
6065                _ => (bytes[off], 1),
6066            }
6067        };
6068        // ASCII input, OR `unsetopt multibyte`: byte-level match (the prior
6069        // behaviour). C only takes the wide `mb_patmatchrange` path under
6070        // GF_MULTIBYTE; with it clear a high byte is matched as a raw byte.
6071        //
6072        // `off` can also land inside a multibyte char: the approx omit-input
6073        // paths step `s_off + 1`, so a continuation P_ANYOF/P_ANYBUT can be
6074        // tested at a continuation byte. Decoding `string[off..]` there would
6075        // panic, so treat a non-boundary position as a raw byte (advance 1) —
6076        // the same fallback the byte-level machinery already relies on.
6077        // c:3702-3719 — PP_IDENT / PP_IFS / PP_IFSSPACE / PP_WORD are
6078        // resolved against the LIVE ztype table (`$IFS`, `$WORDCHARS`,
6079        // POSIX_IDENTIFIERS) every time a character is tested, never
6080        // frozen into the compiled program.
6081        let live_class_hit = |ch: char| -> bool {
6082            use crate::ported::ztype_h::{iwsep, IIDENT, ISEP, IWORD};
6083            if (classmask & (1 << 10)) != 0 && crate::ported::utils::wcsitype(ch, IIDENT as u32) {
6084                return true; // c:3702-3706
6085            }
6086            if (classmask & (1 << 11)) != 0 && crate::ported::utils::wcsitype(ch, ISEP as u32) {
6087                return true; // c:3707-3710
6088            }
6089            // c:3711-3715 — "must be ASCII space character":
6090            // `if (ch < 128 && iwsep((int)ch))`
6091            if (classmask & (1 << 12)) != 0 && (ch as u32) < 128 && iwsep(ch as u8) {
6092                return true;
6093            }
6094            if (classmask & (1 << 13)) != 0 && crate::ported::utils::wcsitype(ch, IWORD as u32) {
6095                return true; // c:3716-3719
6096            }
6097            false
6098        };
6099        if b < 0x80 || (gflags & GF_MULTIBYTE) == 0 || !string.is_char_boundary(off) {
6100            if set.iter().any(|&c| charmatch(b, c, range_flags)) {
6101                return (true, badv);
6102            }
6103            if b < 0x80 && live_class_hit(b as char) {
6104                return (true, badv);
6105            }
6106            // c:2800 — without GF_MULTIBYTE the input char is a single BYTE
6107            // (`charref`, c:1919-1920) and `patmatchrange` walks the operand's
6108            // RAW bytes, so a bracket member written as a multibyte character
6109            // matches each of its UTF-8 bytes individually. C keeps those
6110            // bytes in the operand; zshrs splits them out of `chars` into the
6111            // wide literal / wide range tables at compile time, so re-encode
6112            // them here. Without this, `unsetopt multibyte; [[ $'αβ' = [α]* ]]`
6113            // stopped matching once GF_MULTIBYTE became option-gated.
6114            if (gflags & GF_MULTIBYTE) == 0 {
6115                let mut utf8 = [0u8; 4];
6116                let mut member_byte = |cp: u32| -> bool {
6117                    char::from_u32(cp).is_some_and(|c| {
6118                        c.encode_utf8(&mut utf8).as_bytes().iter().any(|&x| x == b)
6119                    })
6120                };
6121                for k in 0..n_mbc {
6122                    let o = mbc_start + k * 4;
6123                    if member_byte(u32::from_le_bytes(code[o..o + 4].try_into().unwrap())) {
6124                        return (true, badv);
6125                    }
6126                }
6127                for k in 0..n_mbr {
6128                    let o = mbr_start + k * 8;
6129                    if member_byte(u32::from_le_bytes(code[o..o + 4].try_into().unwrap()))
6130                        || member_byte(u32::from_le_bytes(code[o + 4..o + 8].try_into().unwrap()))
6131                    {
6132                        return (true, badv);
6133                    }
6134                }
6135            }
6136            return (false, badv);
6137        }
6138        // Decode one logical input char + its source byte span.
6139        // c:3612 `mb_patmatchrange(range, ch, …)` receives the character C's
6140        // CHARREFINC decoded (c:1969-1990): a metafied run is `mbrtowc`'d as
6141        // the raw bytes it stands for, and an undecodable byte is
6142        // `WCHAR_INVALID` (0xDC00 + byte, c:241-242) — which no range member
6143        // equals except another invalid byte (c:3634 `metacharinc(&range) ==
6144        // ch`, metacharinc answering WCHAR_INVALID the same way, c:384).
6145        // Decoding the pair to the Latin-1 char `byte` instead made `\xcc`
6146        // match `[\ucc]` and no `[\xcc]` at all.
6147        let mut adv_pos = off;
6148        let mut zmb = 0;
6149        let Some(cp) = charrefinc(string, &mut adv_pos, gflags, &mut zmb) else {
6150            return (false, 1);
6151        };
6152        let adv = adv_pos - off;
6153        // Class tests take the decoded character (unused when `zmb` is set).
6154        let ch = char::from_u32(cp).unwrap_or('\u{FFFD}');
6155        // WCHAR_INVALID is in no `iswXXX` class.
6156        let class_hit = (zmb == 0
6157            && ((classmask & (1 << 0) != 0 && ch.is_alphabetic())
6158            || (classmask & (1 << 1) != 0 && ch.is_alphanumeric())
6159            || (classmask & (1 << 2) != 0 && ch.is_uppercase())
6160            || (classmask & (1 << 3) != 0 && ch.is_lowercase())
6161            || (classmask & (1 << 4) != 0 && ch.is_whitespace())
6162            || (classmask & (1 << 5) != 0 && (ch == ' ' || ch == '\t'))
6163            || (classmask & (1 << 6) != 0
6164                && !ch.is_alphanumeric()
6165                && !ch.is_whitespace()
6166                && !ch.is_control())
6167            || (classmask & (1 << 7) != 0 && ch.is_control())
6168            || (classmask & (1 << 8) != 0 && !ch.is_control())
6169            || (classmask & (1 << 9) != 0 && !ch.is_control() && !ch.is_whitespace())
6170            || live_class_hit(ch)))
6171            // c:3737-3744 — `case PP_INCOMPLETE: if (zmb_ind ==
6172            // ZMB_INCOMPLETE) return 1;` / `case PP_INVALID: if (zmb_ind ==
6173            // ZMB_INVALID) return 1;`. C's caller hands mb_patmatchrange the
6174            // decode verdict for the candidate character; zshrs keeps an
6175            // undecodable input byte METAFIED inside a Rust `String`
6176            // (Meta + byte^32), so recover the raw byte run here and ask
6177            // std's UTF-8 decoder for the same three-way answer:
6178            //   Ok / error past offset 0        -> ZMB_VALID
6179            //   Err, error_len() == None        -> ZMB_INCOMPLETE (truncated)
6180            //   Err, error_len() == Some(_)     -> ZMB_INVALID
6181            || ((classmask & ((1 << 14) | (1 << 15))) != 0 && {
6182                // Rebuild up to MB_CUR_MAX raw bytes starting at `off`.
6183                let mut raw: Vec<u8> = Vec::new();
6184                let mut rit = string[off..].chars();
6185                while raw.len() < 4 {
6186                    match rit.next() {
6187                        Some('\u{83}') => match rit.next() {
6188                            Some(n) => raw.push(((n as u32) as u8) ^ 0x20),
6189                            None => break,
6190                        },
6191                        Some(c) => {
6192                            let mut b = [0u8; 4];
6193                            raw.extend_from_slice(c.encode_utf8(&mut b).as_bytes());
6194                        }
6195                        None => break,
6196                    }
6197                }
6198                match std::str::from_utf8(&raw) {
6199                    Err(e) if e.valid_up_to() == 0 => {
6200                        if e.error_len().is_none() {
6201                            (classmask & (1 << 14)) != 0 // c:3737-3740
6202                        } else {
6203                            (classmask & (1 << 15)) != 0 // c:3741-3744
6204                        }
6205                    }
6206                    _ => false,
6207                }
6208            });
6209        if class_hit {
6210            return (true, adv);
6211        }
6212        for k in 0..n_mbc {
6213            let o = mbc_start + k * 4;
6214            if u32::from_le_bytes(code[o..o + 4].try_into().unwrap()) == cp {
6215                return (true, adv);
6216            }
6217        }
6218        for k in 0..n_mbr {
6219            let o = mbr_start + k * 8;
6220            let lo = u32::from_le_bytes(code[o..o + 4].try_into().unwrap());
6221            let hi = u32::from_le_bytes(code[o + 4..o + 8].try_into().unwrap());
6222            if cp >= lo && cp <= hi {
6223                return (true, adv);
6224            }
6225        }
6226        (false, adv)
6227    };
6228
6229    while scan < code.len() {
6230        let op = code[scan + I_OP];
6231        let next_bytes: [u8; 4] = code[scan + I_NEXT..scan + I_NEXT + 4].try_into().unwrap();
6232        let next = u32::from_le_bytes(next_bytes) as usize;
6233
6234        // c:2731-2733 — `if (!globdots && P_NOTDOT(scan) &&
6235        //                    patinput == patinstart && patinput < patinend &&
6236        //                    *patinput == '.') return 0;`
6237        //
6238        // `P_NOTDOT(p)` is `p->l & 0x40` (c:202), i.e. the opcode is one of
6239        // P_ANY/P_ANYOF/P_ANYBUT/P_STAR/P_NUMRNG/P_NUMFROM/P_NUMTO/P_NUMANY
6240        // (c:117-124, "numbered so we can test bit 6 so as not to match
6241        // initial '.'"). P_EXACTLY (0x03), P_BRANCH (0x20) and P_GFLAGS
6242        // (0x08) do not carry the bit, which is why `.hidden`, `(.)hidden`
6243        // and `(#i).HIDDEN` DO match a dot file while `*`, `?` and `[.]`
6244        // do not. P_OPEN/P_CLOSE are 0x80/0x90 and are likewise unaffected.
6245        //
6246        // `patinput == patinstart` is `s_off == 0`: this port passes the
6247        // whole subject to every recursive patmatch and moves the offset.
6248        //
6249        // C's second gate at c:3322 tests P_OPERAND of a P_ONEHASH /
6250        // P_TWOHASH and is a SEPARATE site, ported in that arm below. It is
6251        // not redundant with this one: reaching it through the arm's
6252        // recursion only fails the operand, and a P_ONEHASH with min = 0
6253        // (c:3328) treats that as zero repetitions and carries on. C returns
6254        // 0 for the whole match.
6255        // Conjunct order differs from C's only to keep the atomic load off
6256        // the hot path: `s_off == 0` is false for every node after the first
6257        // character is consumed, and C's `globdots` is a plain int. No
6258        // conjunct has a side effect, so the predicate is the same one.
6259        if s_off == 0
6260            && (op & 0x40) != 0
6261            && s_off < string.len()
6262            && string.as_bytes()[s_off] == b'.'
6263            && globdots.load(Ordering::Relaxed) == 0
6264        {
6265            return None; // c:2733
6266        }
6267
6268        match op {
6269            P_END => {
6270                // c:Src/pattern.c:3451-3454 — `case P_END: if (!(fail =
6271                // (patinput < patinend && !(patflags & PAT_NOANCH))))
6272                // return 1; break;`. When the WHOLE pattern is consumed
6273                // but the string ISN'T (anchored full match), this
6274                // branch FAILS so an enclosing alternation backtracks
6275                // to the next alternative — `[[ yes == (y|yes) ]]`
6276                // tries `y` (consumes 1 char, reaches P_END at offset
6277                // 1 ≠ 3), fails here, then tries `yes`. The previous
6278                // unconditional `Some(s_off)` committed to the first
6279                // (prefix) alternative and the external anchor check
6280                // rejected it without ever trying `yes`. PAT_NOANCH /
6281                // PAT_NOTEND callers (prefix/suffix strip, `(#e)`
6282                // interior matches) keep the partial-success return.
6283                let pf = patflags.load(Ordering::Relaxed);
6284                let anchored = (pf & (PAT_NOANCH | PAT_NOTEND) as i32) == 0;
6285                if s_off < string.len() && anchored {
6286                    // c:Src/pattern.c:3451-3454 sets `fail = 1` here, then the
6287                    // shared approximate-match block at c:3463-3475 deletes the
6288                    // trailing input one CHARACTER at a time — `++errsfound;
6289                    // CHARINC(patinput); continue;` — retrying P_END until the
6290                    // input is gone or the `(#aN)` budget is spent. So
6291                    // `[[ ab = (#a1)? ]]` matches: `?` consumes `a`, then the
6292                    // leftover `b` is deleted for one error.
6293                    //
6294                    // Only literal (P_EXACTLY) runs did this before, inside
6295                    // approx_match_exactly's own trailing-delete; a pattern
6296                    // ending in a class / `?` / `*` with input left reached
6297                    // here and simply failed, so `(#a2)[^0-9]` on `abc`
6298                    // (match `a`, delete `bc`) was rejected where zsh accepts
6299                    // it. The class/`?` arms already delete on a FAILED match
6300                    // (e.g. c:5046); this is the same edit at the pattern end.
6301                    let max_errs = (glob_flags & 0xff) as i32;
6302                    if (state.errsfound < max_errs && { let fe = forceerrs.load(Ordering::Relaxed); fe == -1 || state.errsfound < fe })/* c:3473-3474 */ {
6303                        // c:3475 `CHARINC(patinput, patinend)` + continue
6304                        // (retry P_END). CHARINC is option-gated, and
6305                        // `s_off` can sit mid-character when GF_MULTIBYTE
6306                        // is clear, so go through `charinc` rather
6307                        // than slicing `string` (which would panic).
6308                        let advance = charinc(s_off, glob_flags);
6309                        if advance != 0 {
6310                            state.errsfound += 1; // c:3466 ++errsfound
6311                            return patmatch(
6312                                code,
6313                                scan,
6314                                string,
6315                                s_off + advance,
6316                                state,
6317                                glob_flags,
6318                            );
6319                        }
6320                    }
6321                    return None; // c:3452 fail, no budget → caller tries next alt
6322                }
6323                return Some(s_off); // c:3453
6324            }
6325            P_NOTHING => { /* empty match, just continue */ }
6326            P_BACK => { /* zero-width, walk back via next */ }
6327            P_EXCEND => {
6328                // c:Src/pattern.c:3037-3047 — terminal node ending an
6329                // exclusion operand: the exclusion matches iff the
6330                // (truncated) excludable span was fully consumed
6331                // (`patinput >= patinend`). Returns success here without
6332                // following the chain (which would wrongly continue into
6333                // the post-group pattern). The caller truncates the span
6334                // so `string.len()` IS the excludable end.
6335                if s_off >= string.len() {
6336                    return Some(s_off);
6337                }
6338                return None;
6339            }
6340            P_EXCSYNC => {
6341                // c:Src/pattern.c:2992-3035 — record where the asserted
6342                // branch reached the EXCLUDE sync point (the end of the
6343                // excludable part) in the following EXCLUDE node's sync
6344                // buffer. If this position was already recorded (with
6345                // <= the current error count), fail so the asserted
6346                // branch backtracks to a different split. The EXCLUDE
6347                // node sits PHYSICALLY right after EXCSYNC (both
6348                // patcompnot and the `~` arm emit them adjacent).
6349                let exclude_node = scan + I_BODY;
6350                let already = EXCSYNC_BUF.with(|b| {
6351                    let mut m = b.borrow_mut();
6352                    if let Some(buf) = m.get_mut(&exclude_node) {
6353                        if s_off < buf.len() {
6354                            let cur = (state.errsfound + 1) as u8;
6355                            if buf[s_off] != 0 && (state.errsfound + 1) >= buf[s_off] as i32 {
6356                                return true; // c:3008 already matched here → fail
6357                            }
6358                            buf[s_off] = cur; // c:3017
6359                                              // c:3033 — earlier marks are now invalid.
6360                            for x in buf[..s_off].iter_mut() {
6361                                *x = 0;
6362                            }
6363                        }
6364                    }
6365                    false
6366                });
6367                if already {
6368                    return None;
6369                }
6370                // else fall through to next node
6371            }
6372            P_EXACTLY => {
6373                // c:P_EXACTLY arm
6374                let body = scan + I_BODY;
6375                let len = u32::from_le_bytes(code[body..body + 4].try_into().unwrap()) as usize;
6376                let str_bytes = &code[body + 4..body + 4 + len];
6377                let input_bytes = string.as_bytes();
6378                // `(#aN)` budget — low byte of patglobflags. C uses the
6379                // file-static `patglobflags`; the Rust port carries it
6380                // through `glob_flags` since rpat went bucket-1.
6381                let max_errs = (glob_flags & 0xff) as i32;
6382                if max_errs > 0 {
6383                    // c:3473-3579 — approximate match: omit input char,
6384                    // transpose, omit both, omit pattern char (see
6385                    // approx_match_exactly); `errsfound` is tracked on
6386                    // `state` across the recursive patmatch calls.
6387                    if let Some(new_off) = approx_match_exactly(
6388                        code, next, string, s_off, str_bytes, state, glob_flags, max_errs,
6389                    ) {
6390                        return Some(new_off);
6391                    }
6392                    return None;
6393                }
6394                if s_off + len > input_bytes.len() {
6395                    return None;
6396                }
6397                let case_flags = glob_flags & (GF_IGNCASE | GF_LCMATCHUC);
6398                let multibyte = (glob_flags & GF_MULTIBYTE) != 0; // c:349 GF_MULTIBYTE
6399                if case_flags != 0 {
6400                    let inp_slice = &input_bytes[s_off..s_off + len];
6401                    if multibyte && (glob_flags & GF_IGNCASE) != 0 {
6402                        // Char-level Unicode case fold for IGNCASE only —
6403                        // LCMATCHUC's asymmetric ASCII semantic doesn't
6404                        // map cleanly to non-ASCII chars; per C the
6405                        // CHARMATCH macro is byte-level anyway (TOLOWER/
6406                        // TOUPPER from utils.c work on bytes).
6407                        let pat_str = std::str::from_utf8(str_bytes).ok();
6408                        let inp_str = std::str::from_utf8(inp_slice).ok();
6409                        if let (Some(p), Some(i)) = (pat_str, inp_str) {
6410                            let mut pc = p.chars();
6411                            let mut ic = i.chars();
6412                            loop {
6413                                match (pc.next(), ic.next()) {
6414                                    (None, None) => break,
6415                                    (Some(_), None) | (None, Some(_)) => return None,
6416                                    (Some(a), Some(b)) => {
6417                                        // Equal chars (the common case) and ASCII
6418                                        // pairs fold without touching the heap.
6419                                        // The old per-char `to_lowercase()
6420                                        // .collect::<String>()` pair allocated
6421                                        // TWICE PER CHARACTER — `(#i)` scans over
6422                                        // a 566k-entry history (hsmw ^R) burned
6423                                        // ~4 CPU-minutes in RawVec::reserve.
6424                                        // Iterator::eq covers the multi-char
6425                                        // Unicode folds allocation-free.
6426                                        if a != b
6427                                            && (if a.is_ascii() && b.is_ascii() {
6428                                                a.to_ascii_lowercase() != b.to_ascii_lowercase()
6429                                            } else {
6430                                                !a.to_lowercase().eq(b.to_lowercase())
6431                                            })
6432                                        {
6433                                            return None;
6434                                        }
6435                                    }
6436                                }
6437                            }
6438                        } else {
6439                            // Non-UTF-8 input — byte fallback through CHARMATCH.
6440                            for k in 0..len {
6441                                if !charmatch(inp_slice[k], str_bytes[k], glob_flags) {
6442                                    return None;
6443                                }
6444                            }
6445                        }
6446                    } else {
6447                        // c:2694 — per-byte CHARMATCH walk (covers
6448                        // GF_IGNCASE byte-mode AND GF_LCMATCHUC asymmetric).
6449                        for k in 0..len {
6450                            if !charmatch(inp_slice[k], str_bytes[k], glob_flags) {
6451                                return None;
6452                            }
6453                        }
6454                    }
6455                } else if &input_bytes[s_off..s_off + len] != str_bytes {
6456                    return None;
6457                }
6458                s_off += len;
6459            }
6460            P_ANY => {
6461                // c:2736-2741 — `case P_ANY: if (patinput == patinend)
6462                // fail = 1; else CHARINC(patinput, patinend); break;`.
6463                // `CHARINC` (c:1963) is `charnext`, which advances a full
6464                // multibyte character ONLY under GF_MULTIBYTE and a single
6465                // raw byte otherwise (c:1946). `charinc` carries
6466                // that option gate plus zshrs's metafied-input handling;
6467                // it previously consumed a whole UTF-8 character
6468                // unconditionally, so `[[ $'α' = ?? ]]` failed under
6469                // `unsetopt multibyte` where zsh matches the two bytes.
6470                let advance = charinc(s_off, glob_flags);
6471                if advance == 0 {
6472                    return None;
6473                }
6474                s_off += advance;
6475            }
6476            P_ANYOF => {
6477                // c:2780-2800 — a bracket expression is matched by
6478                // `patmatchrange` / `mb_patmatchrange` on the RAW input char.
6479                // Neither consults `patglobflags`, so a bracket NEVER
6480                // case-folds: only C's CHARMATCH macro (c:2671, used by
6481                // P_EXACTLY) honours GF_IGNCASE / GF_LCMATCHUC.
6482                //
6483                // Passing glob_flags through here made `(#i)` fold ranges as
6484                // well as literals, so `[[ FooBar = (#i)[a-z]## ]]` matched
6485                // (zsh: no match) and `[[ F = (#i)[[:lower:]] ]]` matched
6486                // (zsh: no match). Mask the case bits off for the set test.
6487                let body = scan + I_BODY;
6488                let input_bytes = string.as_bytes();
6489                let max_errs = (glob_flags & 0xff) as i32;
6490                let (has_match, adv) = if s_off < input_bytes.len() {
6491                    anyof_membership(body, s_off, glob_flags)
6492                } else {
6493                    (false, 0)
6494                };
6495                if !has_match {
6496                    // c:Src/pattern.c:3463-3505 — approximate-match fail
6497                    // handler. For non-P_EXACTLY opcodes, the ONLY
6498                    // approximation path is "omit one input char" (skip
6499                    // the input byte that didn't match, costing 1 edit,
6500                    // then retry the same scan). For P_ANYOF that means
6501                    // `[b][b]` against "bob" can match by omitting the
6502                    // middle "o" with 1 edit — the `(#a1)[b][b]` pin.
6503                    if (state.errsfound < max_errs && { let fe = forceerrs.load(Ordering::Relaxed); fe == -1 || state.errsfound < fe })/* c:3473-3474 */ && s_off < input_bytes.len() {
6504                        state.errsfound += 1;
6505                        // Retry same scan with one input byte consumed.
6506                        return patmatch(code, scan, string, s_off + 1, state, glob_flags);
6507                    }
6508                    return None;
6509                }
6510                s_off += adv;
6511            }
6512            P_ANYBUT => {
6513                let body = scan + I_BODY;
6514                let input_bytes = string.as_bytes();
6515                let max_errs = (glob_flags & 0xff) as i32;
6516                // c:2781-2800 — the negated bracket shares P_ANYOF's matcher
6517                // (`… ^ (P_OP(scan) == P_ANYOF)`), so it is equally
6518                // case-BLIND (anyof_membership masks the case bits off).
6519                let (in_set, adv) = if s_off < input_bytes.len() {
6520                    anyof_membership(body, s_off, glob_flags)
6521                } else {
6522                    (true, 0)
6523                };
6524                let has_match = s_off < input_bytes.len() && !in_set;
6525                if !has_match {
6526                    if (state.errsfound < max_errs && { let fe = forceerrs.load(Ordering::Relaxed); fe == -1 || state.errsfound < fe })/* c:3473-3474 */ && s_off < input_bytes.len() {
6527                        // c:3463 — omit-input approx path (same as P_ANYOF).
6528                        state.errsfound += 1;
6529                        return patmatch(code, scan, string, s_off + 1, state, glob_flags);
6530                    }
6531                    return None;
6532                }
6533                s_off += adv;
6534            }
6535            P_STAR => {
6536                // c:3277-3400 — `*` is handled specially "although really
6537                // P_ONEHASH+P_ANY". C walks the input with CHARINC and
6538                // records every CHARACTER start in `charstart[]`
6539                // (c:3310-3315 `for (no = 0; patinput < patinend;
6540                // CHARINC(patinput, patinend)) { charstart[patinput-start]
6541                // = 1; no++; }`), then backtracks by rewinding to the
6542                // previous recorded start (c:3385-3389 `/* find start of
6543                // previous full character */ while (!*--lastcharstart) ...
6544                // patinput = start + (lastcharstart-charstart);`).
6545                //
6546                // Rewinding one BYTE instead left the continuation opcode
6547                // positioned inside a multibyte character: `[[ $'αβγδε' =
6548                // *(#b)(?) ]]` panicked slicing at a UTF-8 continuation
6549                // byte, and any `*`-then-atom pattern could match at a
6550                // non-character position. `charinc` reproduces
6551                // CHARINC exactly, so with GF_MULTIBYTE clear every byte
6552                // is still a valid stop (c:1946) — the byte-stepping
6553                // behaviour zsh has under `unsetopt multibyte`.
6554                let end = string.len();
6555                // c:3310-3315 — record the character starts.
6556                let mut starts: Vec<usize> = Vec::new();
6557                let mut p = s_off;
6558                while p < end {
6559                    starts.push(p);
6560                    let adv = charinc(p, glob_flags);
6561                    if adv == 0 {
6562                        break;
6563                    }
6564                    p += adv;
6565                }
6566                // c:3299-3315 — the greedy end position (`patinput` after
6567                // the walk) is tried first; `min` is 0 for P_STAR
6568                // (c:3328), so the rewind reaches `start` inclusive.
6569                starts.push(end);
6570                for &pos in starts.iter().rev() {
6571                    // c:3374-3382 — try the continuation at this position.
6572                    let mut sub_state = state.clone();
6573                    if let Some(matched) =
6574                        patmatch(code, next, string, pos, &mut sub_state, glob_flags)
6575                    {
6576                        *state = sub_state;
6577                        return Some(matched);
6578                    }
6579                }
6580                return None; // c:3400
6581            }
6582            P_ONEHASH | P_TWOHASH => {
6583                // c:P_ONEHASH / P_TWOHASH
6584                // The operand (the simple atom being repeated) starts
6585                // at `scan + I_BODY` — that's the byte immediately
6586                // after the quantifier opcode (which has its own
6587                // 5-byte header). The repeated atom occupies the
6588                // bytes from there until `next`.
6589                let operand = scan + I_BODY;
6590                // c:3322-3325 — `if (!globdots && P_NOTDOT(P_OPERAND(scan)) &&
6591                //                    patinput == patinstart && patinput < patinend &&
6592                //                    CHARREF(patinput, patinend) == ZWC('.')) return 0;`
6593                //
6594                // This is NOT covered by the c:2731 gate at the loop head, even
6595                // though the repeat below recurses into `operand` and trips it
6596                // there. C ABORTS THE WHOLE MATCH; the loop head only fails the
6597                // operand, which for P_ONEHASH (min = 0, c:3328) is an ordinary
6598                // "zero repetitions" outcome that then hands a still-dot-leading
6599                // subject to the continuation. Measured with `.hidden` present
6600                // and GLOB_DOTS off: `[a]#.hidden` and `?#.hidden` list nothing
6601                // in `zsh -f` 5.9 and listed `.hidden` here. `(x)#.hidden` DOES
6602                // match in both — P_BRANCH (0x20) is not P_NOTDOT — which is
6603                // what pins this to the operand's opcode rather than to closures.
6604                if s_off == 0
6605                    && code
6606                        .get(operand + I_OP)
6607                        .is_some_and(|o| (o & 0x40) != 0)
6608                    && s_off < string.len()
6609                    && string.as_bytes()[s_off] == b'.'
6610                    && globdots.load(Ordering::Relaxed) == 0
6611                {
6612                    return None; // c:3325
6613                }
6614                let min = if op == P_TWOHASH { 1 } else { 0 };
6615                // Greedy: match operand repeatedly until it fails,
6616                // then walk back trying continuations.
6617                let mut positions = vec![s_off];
6618                loop {
6619                    let cur = *positions.last().unwrap();
6620                    let mut sub_state = state.clone();
6621                    if let Some(new_pos) =
6622                        patmatch(code, operand, string, cur, &mut sub_state, glob_flags)
6623                    {
6624                        if new_pos == cur {
6625                            break;
6626                        } // zero-width fixed point
6627                        *state = sub_state;
6628                        positions.push(new_pos);
6629                    } else {
6630                        break;
6631                    }
6632                }
6633                if positions.len() - 1 < min {
6634                    return None;
6635                }
6636                // Walk back from longest match trying continuations.
6637                while positions.len() > min {
6638                    let cur = *positions.last().unwrap();
6639                    let mut sub_state = state.clone();
6640                    if let Some(end) = patmatch(code, next, string, cur, &mut sub_state, glob_flags)
6641                    {
6642                        *state = sub_state;
6643                        return Some(end);
6644                    }
6645                    if positions.len() <= min + 1 {
6646                        return None;
6647                    }
6648                    positions.pop();
6649                }
6650                return None;
6651            }
6652            P_BRANCH | P_WBRANCH => {
6653                // c:P_BRANCH / P_WBRANCH arm — c:3043-3044 in zsh.
6654                // c:3046-3050 — if next is NOT another BRANCH, this is
6655                // the only alternative; avoid the alt-loop and just
6656                // continue with the operand inline (no recursion, no
6657                // fallthrough on failure).
6658                //
6659                // For P_WBRANCH, the operand sits AFTER the 8-byte
6660                // syncptr payload (`P_OPERAND(p) + 1` per pattern.c:1865).
6661                let operand_off_extra = if op == P_WBRANCH { 8 } else { 0 };
6662                // c:3056 — if `next` is P_EXCLUDE/P_EXCLUDP, this BRANCH
6663                // is the asserted half of a `^pat` / `!(pat)` exclusion.
6664                // Port of c:3056-3201: try the asserted operand
6665                // (the `*`-based branch body), then for each EXCLUDE in
6666                // the next-chain run the exclude operand against the
6667                // same input range; if any exclude matches with the
6668                // SAME consumed length, fail; else succeed.
6669                if next != 0 && next < code.len() && P_ISEXCLUDE(code[next + I_OP]) {
6670                    // c:Src/pattern.c:3056-3201 — `^pat` / `(^pat)` /
6671                    // `!(pat)` / `A~B` exclusion. The asserted branch
6672                    // (`STAR EXCSYNC rest` for `^`/`!`, or `A EXCSYNC
6673                    // rest` for `A~B`) is matched normally; its EXCSYNC
6674                    // node records where the EXCLUDABLE part ended into
6675                    // the EXCLUDE node's sync buffer. We then truncate to
6676                    // that synclen and test the exclusion operand(s); if
6677                    // any matches the excludable span, this candidate is
6678                    // excluded and we re-run the asserted branch — EXCSYNC
6679                    // now fails at the recorded position, forcing a
6680                    // different split — until an un-excluded split is
6681                    // found or the asserted branch can no longer match.
6682                    let asserted_operand = scan + I_BODY + operand_off_extra;
6683                    // c:2941 — `patglobflags = P_OPERAND(scan)->l;` — in C
6684                    // P_GFLAGS writes the GLOBAL flag word, so a flag node
6685                    // sitting at the head of the asserted branch is already
6686                    // in effect by the time the exclusion operand runs at
6687                    // c:3134+ (which only clears the low `(#aN)` byte). The
6688                    // Rust port threads the flags as a parameter instead, so
6689                    // apply a leading P_GFLAGS here explicitly. Without it
6690                    // `readme` vs `(#i)readme~README|readme~README` ran the
6691                    // SECOND branch's exclusion case-insensitively and
6692                    // wrongly excluded the match.
6693                    let branch_flags_eff =
6694                        if code.get(asserted_operand + I_OP).copied().unwrap_or(0) == P_GFLAGS {
6695                            let b = asserted_operand + I_BODY;
6696                            let bits = i32::from_le_bytes(code[b..b + 4].try_into().unwrap());
6697                            (glob_flags & !(GF_IGNCASE | GF_LCMATCHUC | GF_MULTIBYTE | 0xff)) | bits
6698                        } else {
6699                            glob_flags
6700                        };
6701                    let exclude_node = next;
6702                    // Allocate (reset) the sync buffer for this EXCLUDE.
6703                    let prev_buf = EXCSYNC_BUF.with(|b| {
6704                        b.borrow_mut()
6705                            .insert(exclude_node, vec![0u8; string.len() + 1])
6706                    });
6707                    let mut found: Option<(usize, rpat)> = None;
6708                    loop {
6709                        let mut a_state = state.clone();
6710                        let matchpt = match patmatch(
6711                            code,
6712                            asserted_operand,
6713                            string,
6714                            s_off,
6715                            &mut a_state,
6716                            glob_flags,
6717                        ) {
6718                            Some(e) => e,
6719                            None => break,
6720                        };
6721                        // c:3128-3130 — synclen = first marked position in
6722                        // the sync buffer (the end of the excludable part).
6723                        let synclen = EXCSYNC_BUF.with(|b| {
6724                            b.borrow()
6725                                .get(&exclude_node)
6726                                .and_then(|buf| buf.iter().position(|&x| x != 0))
6727                                .unwrap_or(s_off)
6728                        });
6729                        // c:3134-3138 — test each EXCLUDE/EXCLUDP operand
6730                        // against the excludable span string[s_off..synclen].
6731                        // Truncating to `synclen` makes the operand's
6732                        // trailing P_EXCEND succeed iff the span matched in
6733                        // full.
6734                        let span_end = synclen.min(string.len());
6735                        let span = &string[..span_end];
6736                        let mut excluded = false;
6737                        let mut excl = exclude_node;
6738                        // c:3113/3116 — `savglobdots = globdots; globdots = 1;
6739                        // /* OK to match . first */`, restored at c:3192. The
6740                        // asserted branch has already matched at this point;
6741                        // the exclusion operand is matched against what it
6742                        // produced, so the leading-dot rule must not apply to
6743                        // it a second time.
6744                        let savglobdots = globdots.swap(1, Ordering::Relaxed);
6745                        while excl != 0 && excl < code.len() && P_ISEXCLUDE(code[excl + I_OP]) {
6746                            let excl_operand = excl + I_BODY + 8; // after 8-byte syncptr
6747                            let mut e_state = state.clone();
6748                            // c:3134-3142 — `patglobflags &= ~0xff;
6749                            // errsfound = 0;` — exclusions match EXACTLY,
6750                            // with approximation turned off. Clear both the
6751                            // running error count AND the budget (low byte
6752                            // of glob_flags); otherwise `(#a1)README~READ_ME`
6753                            // matched the exclusion READ_ME against READ.ME
6754                            // approximately and wrongly excluded it.
6755                            e_state.errsfound = 0;
6756                            let excl_flags = branch_flags_eff & !0xff;
6757                            if let Some(em) =
6758                                patmatch(code, excl_operand, span, s_off, &mut e_state, excl_flags)
6759                            {
6760                                if em == span_end {
6761                                    excluded = true;
6762                                    break;
6763                                }
6764                            }
6765                            let nb: [u8; 4] =
6766                                code[excl + I_NEXT..excl + I_NEXT + 4].try_into().unwrap();
6767                            let n = u32::from_le_bytes(nb) as usize;
6768                            if n == 0 || n == excl {
6769                                break;
6770                            }
6771                            excl = n;
6772                        }
6773                        globdots.store(savglobdots, Ordering::Relaxed); // c:3192
6774                        if !excluded {
6775                            found = Some((matchpt, a_state));
6776                            break;
6777                        }
6778                        // Excluded: loop. The sync buffer now records this
6779                        // split, so the next asserted patmatch's EXCSYNC
6780                        // fails here and backtracks to a different split.
6781                    }
6782                    // Restore/clear the sync buffer slot.
6783                    EXCSYNC_BUF.with(|b| {
6784                        let mut m = b.borrow_mut();
6785                        match prev_buf {
6786                            Some(p) => {
6787                                m.insert(exclude_node, p);
6788                            }
6789                            None => {
6790                                m.remove(&exclude_node);
6791                            }
6792                        }
6793                    });
6794                    match found {
6795                        Some((end, a_state)) => {
6796                            *state = a_state;
6797                            return Some(end);
6798                        }
6799                        None => {
6800                            // c:3209-3211 — `while ((scan = PATNEXT(scan)) &&
6801                            // P_ISEXCLUDE(scan)) ;` — when the asserted branch
6802                            // plus its exclusions cannot produce an un-excluded
6803                            // split, C skips PAST the whole exclusion chain and
6804                            // carries on with the next ALTERNATIVE of the
6805                            // enclosing switch. Returning None here instead
6806                            // dropped every branch written after an exclusion:
6807                            // `[[ xyz = readme~README|xyz ]]` never tried
6808                            // `xyz`.
6809                            let mut nxt = next;
6810                            while nxt != 0 && nxt < code.len() && P_ISEXCLUDE(code[nxt + I_OP]) {
6811                                let nb: [u8; 4] =
6812                                    code[nxt + I_NEXT..nxt + I_NEXT + 4].try_into().unwrap();
6813                                nxt = u32::from_le_bytes(nb) as usize;
6814                            }
6815                            if nxt == 0 || nxt >= code.len() {
6816                                return None;
6817                            }
6818                            let nop = code[nxt + I_OP];
6819                            if nop != P_BRANCH && nop != P_WBRANCH {
6820                                return None;
6821                            }
6822                            scan = nxt;
6823                            continue;
6824                        }
6825                    }
6826                }
6827                let next_is_branch = next != 0
6828                    && next < code.len()
6829                    && (code[next + I_OP] == P_BRANCH || code[next + I_OP] == P_WBRANCH);
6830                if !next_is_branch {
6831                    scan = scan + I_BODY + operand_off_extra;
6832                    continue;
6833                }
6834                // Alt-loop: try each branch's operand; on success
6835                // return; on failure walk to the next BRANCH via .next.
6836                let mut br = scan;
6837                loop {
6838                    let br_op = code[br + I_OP];
6839                    let br_extra = if br_op == P_WBRANCH { 8 } else { 0 };
6840                    let br_next_bytes: [u8; 4] =
6841                        code[br + I_NEXT..br + I_NEXT + 4].try_into().unwrap();
6842                    let br_next = u32::from_le_bytes(br_next_bytes) as usize;
6843                    let operand = br + I_BODY + br_extra;
6844                    // !!! RUST-ONLY DETAIL !!! C recurses into every
6845                    // alternative (c:3258 `ret = patmatch(opnd)`). When the
6846                    // alternative's operand is a P_EXACTLY that the input
6847                    // cannot match, that recursion fails at its first node
6848                    // with no side effect, so it is decided here instead —
6849                    // with the P_EXACTLY arm's own failure test, under the
6850                    // same conditions in which that arm compares raw bytes
6851                    // (no `(#aN)` budget, no case folding). Restricted to
6852                    // P_BRANCH: a P_WBRANCH records a visit mark before it
6853                    // recurses (c:3245-3248), which must still happen.
6854                    // compdump's `$^fpath/(${(o~j.|.)…})(N:t)` is a
6855                    // ~2000-way alternation tried against every file in
6856                    // every fpath directory; paying a full patmatch frame
6857                    // plus an `rpat` clone per alternative made it seconds
6858                    // of CPU in the debug build where zsh takes milliseconds.
6859                    if br_op == P_BRANCH
6860                        && (glob_flags & (0xff | GF_IGNCASE | GF_LCMATCHUC)) == 0
6861                        && code.get(operand + I_OP) == Some(&P_EXACTLY)
6862                    {
6863                        let body = operand + I_BODY;
6864                        let len = u32::from_le_bytes(code[body..body + 4].try_into().unwrap())
6865                            as usize;
6866                        let lit = &code[body + 4..body + 4 + len];
6867                        if string.as_bytes().get(s_off..s_off + len) != Some(lit) {
6868                            if br_next == 0 {
6869                                return None;
6870                            }
6871                            let op_next = code[br_next + I_OP];
6872                            if op_next != P_BRANCH && op_next != P_WBRANCH {
6873                                return None;
6874                            }
6875                            br = br_next;
6876                            continue;
6877                        }
6878                    }
6879                    let mut sub_state = state.clone();
6880                    // c:Src/pattern.c:3210-3248 — P_WBRANCH per-position
6881                    // visit guard. Allocate a bitmap sized to the input
6882                    // (`zshcalloc((patinend - patinstart) + 1)`), then
6883                    // check/set `*ptr = errsfound + 1` at the current
6884                    // input offset. On revisit with the same-or-fewer
6885                    // errors, return 0 to bound recursion. Without this,
6886                    // `(fo#)#` against any non-trivial input recurses
6887                    // until PATMATCH_MAX_DEPTH.
6888                    let mut wbranch_skip = false;
6889                    if br_op == P_WBRANCH {
6890                        // The map is shared with every clone of `state`, the
6891                        // way C's operand buffer is shared across the whole
6892                        // pattry — so a mark set here is still visible after
6893                        // this branch fails and the walk backtracks.
6894                        let mut visits = state.wbranch_visits.borrow_mut();
6895                        let bm = visits
6896                            .entry(br)
6897                            .or_insert_with(|| vec![0u8; string.len() + 1]);
6898                        let slot = bm.get(s_off).copied().unwrap_or(0);
6899                        let cur = (state.errsfound as i32 + 1) as u8;
6900                        if slot != 0 && (state.errsfound + 1) >= slot as i32 {
6901                            wbranch_skip = true; // c:3245-3247
6902                        } else if s_off < bm.len() {
6903                            bm[s_off] = cur; // c:3248
6904                        }
6905                    }
6906                    let sub_result = if wbranch_skip {
6907                        None
6908                    } else {
6909                        patmatch(code, operand, string, s_off, &mut sub_state, glob_flags)
6910                    };
6911                    if let Some(end) = sub_result {
6912                        // c:108-144 (pattern.c header doc on P_WBRANCH):
6913                        //   "P_WBRANCH:  This works like a branch and is
6914                        //    used in complex closures, but the match must
6915                        //    be at least 1 char in length to avoid
6916                        //    infinite loops.  The test for length is
6917                        //    done via the next pointer in the WBRANCH
6918                        //    test in patmatch()."
6919                        // C enforces this via the `errsfound`/`forceerrs`
6920                        // accounting at c:1059+ inside patcompbranch.
6921                        // The Rust walker checks end > s_off directly:
6922                        // if the body consumed nothing, reject this
6923                        // alternative and fall through to the next.
6924                        // Without this guard, `(fo#)#` against any input
6925                        // stack-overflows because the inner body (`fo#`)
6926                        // can match the empty string repeatedly.
6927                        if br_op == P_WBRANCH && end == s_off {
6928                            // c:108 "at least 1 char" — fall through to
6929                            // try the next alternative.
6930                        } else {
6931                            *state = sub_state;
6932                            return Some(end);
6933                        }
6934                    }
6935                    if br_next == 0 {
6936                        return None;
6937                    }
6938                    let op_next = code[br_next + I_OP];
6939                    if op_next != P_BRANCH && op_next != P_WBRANCH {
6940                        return None;
6941                    }
6942                    br = br_next;
6943                }
6944            }
6945            P_NUMRNG | P_NUMFROM | P_NUMTO => {
6946                // c:2815-2909 — `case P_NUMRNG: case P_NUMFROM: case P_NUMTO:`
6947                //
6948                // c:2818-2825 — "To do this properly, we really have to
6949                // treat numbers as closures: that's so things like
6950                // <1-1000>33 will match 633 (they didn't up to 3.1.6).
6951                // To avoid making this too inefficient, we see if there's
6952                // an exact match next: if there is, and it's not a digit,
6953                // we return 1 after the first attempt."
6954                let mut start_b = scan + I_BODY; // c:2827 `start = P_OPERAND(scan);`
6955                let mut from: i64 = 0; // c:2828 `from = to = 0;`
6956                let mut to: i64 = 0;
6957                if op != P_NUMTO {
6958                    // c:2829-2837
6959                    from = i64::from_le_bytes(code[start_b..start_b + 8].try_into().unwrap());
6960                    start_b += 8; // c:2836 `start += sizeof(zrange_t);`
6961                }
6962                if op != P_NUMFROM {
6963                    // c:2838-2845
6964                    to = i64::from_le_bytes(code[start_b..start_b + 8].try_into().unwrap());
6965                }
6966                let input_bytes = string.as_bytes();
6967                let patinend = input_bytes.len();
6968                // c:2846 — `start = compend = patinput;`
6969                let start = s_off;
6970                let mut compend = s_off;
6971                let mut comp: i64 = 0; // c:2847
6972                let mut patinput = s_off;
6973                // c:2848 — `while (patinput < patinend && idigit(*patinput))`
6974                while patinput < patinend && input_bytes[patinput].is_ascii_digit() {
6975                    let mut out_of_range = false; // c:2849
6976                    let digit = (input_bytes[patinput] - b'0') as i64; // c:2850
6977                    if comp > i64::MAX / 10 {
6978                        // c:2851-2852
6979                        out_of_range = true;
6980                    } else {
6981                        let c10 = if comp != 0 { comp * 10 } else { 0 }; // c:2854
6982                        if i64::MAX - c10 < digit {
6983                            out_of_range = true; // c:2856
6984                        } else {
6985                            comp = c10; // c:2858
6986                            comp += digit; // c:2859
6987                        }
6988                    }
6989                    patinput += 1; // c:2862
6990                    compend += 1; // c:2863
6991                                  // c:2865-2873 — out of range "allowing for signedness,
6992                                  // which we need if we are using zlongs".
6993                    if out_of_range || (comp & (1i64 << 62)) != 0 {
6994                        // c:2875-2881 — "This is as far as we can go. If
6995                        // we're doing a range \"from\", skip all the
6996                        // remaining numbers. Otherwise, we can't match
6997                        // beyond the previous point anyway. Leave the
6998                        // pointer to the last calculated position
6999                        // (compend) where it was before."
7000                        if op == P_NUMFROM {
7001                            // c:2882-2885
7002                            while patinput < patinend && input_bytes[patinput].is_ascii_digit() {
7003                                patinput += 1;
7004                            }
7005                        }
7006                    }
7007                }
7008                let mut save = patinput; // c:2888
7009                let mut no = 0; // c:2889
7010                while patinput > start {
7011                    // c:2890
7012                    // c:2891-2893 — "if already too small, no power on
7013                    // earth can save it"
7014                    if comp < from && patinput <= compend {
7015                        break;
7016                    }
7017                    if op == P_NUMFROM || comp <= to {
7018                        // c:2894
7019                        let mut sub_state = state.clone();
7020                        if let Some(end) =
7021                            patmatch(code, next, string, patinput, &mut sub_state, glob_flags)
7022                        {
7023                            *state = sub_state;
7024                            return Some(end); // c:2895
7025                        }
7026                    }
7027                    // c:2896-2900 — `if (!no && P_OP(next) == P_EXACTLY &&
7028                    // (!P_LS_LEN(next) || !idigit(*P_LS_STR(next))) &&
7029                    // !(patglobflags & 0xff)) return 0;`
7030                    if no == 0 && next != 0 && (glob_flags & 0xff) == 0 {
7031                        let nop = code.get(next + I_OP).copied().unwrap_or(0);
7032                        if nop == P_EXACTLY {
7033                            let nb = next + I_BODY;
7034                            let nlen =
7035                                u32::from_le_bytes(code[nb..nb + 4].try_into().unwrap()) as usize;
7036                            if nlen == 0 || !code[nb + 4].is_ascii_digit() {
7037                                return None;
7038                            }
7039                        }
7040                    }
7041                    // c:2901 — `patinput = --save;`
7042                    save -= 1;
7043                    patinput = save;
7044                    no += 1; // c:2902
7045                             // c:2903-2908 — "With a range start and an
7046                             // unrepresentable test number, we just back down the
7047                             // test string without changing the number until we get
7048                             // to a representable one."
7049                    if patinput < compend {
7050                        comp /= 10;
7051                    }
7052                }
7053                // c:2910-2911 — `patinput = start; fail = 1;`
7054                return None;
7055            }
7056            P_NUMANY => {
7057                // c:P_NUMANY — `<->` any non-empty digit run. Pins
7058                // `Test/D02glob.ztst:136` (`<->33` matches "633" by
7059                // consuming just "6" then literal "33").
7060                let input_bytes = string.as_bytes();
7061                let start = s_off;
7062                let mut k = start;
7063                while k < input_bytes.len() && input_bytes[k].is_ascii_digit() {
7064                    k += 1;
7065                }
7066                if k == start {
7067                    return None;
7068                }
7069                while k > start {
7070                    let mut sub_state = state.clone();
7071                    if let Some(end) = patmatch(code, next, string, k, &mut sub_state, glob_flags) {
7072                        *state = sub_state;
7073                        return Some(end);
7074                    }
7075                    k -= 1;
7076                }
7077                return None;
7078            }
7079            P_ISSTART => {
7080                // c:Src/pattern.c:3392-3394 — `if (patinput != patinstart
7081                // || (patflags & PAT_NOTSTART)) fail = 1;`. C bails on
7082                // BOTH conditions: local-position-not-zero OR the
7083                // caller-set PAT_NOTSTART flag (set by glob.c's
7084                // set_pat_start when the test string is an interior
7085                // slice of a larger buffer). Read patflags from the
7086                // file-static (set at pattryrefs entry from prog flags).
7087                if s_off != 0 || (patflags.load(Ordering::Relaxed) & PAT_NOTSTART as i32) != 0 {
7088                    return None;
7089                }
7090            }
7091            P_ISEND => {
7092                // c:Src/pattern.c:3396-3398 — `if (patinput < patinend
7093                // || (patflags & PAT_NOTEND)) fail = 1;`. Same shape as
7094                // P_ISSTART: bail on local-not-at-end OR caller-set
7095                // PAT_NOTEND (set by set_pat_end when the suffix was
7096                // truncated).
7097                if s_off < string.len()
7098                    || (patflags.load(Ordering::Relaxed) & PAT_NOTEND as i32) != 0
7099                {
7100                    return None;
7101                }
7102            }
7103            P_GFLAGS => {
7104                // c:P_GFLAGS arm
7105                let body = scan + I_BODY;
7106                let bits = i32::from_le_bytes(code[body..body + 4].try_into().unwrap());
7107                // C uses absolute set; for the on/off toggle pairs
7108                // we currently encode only the "on" bits (i.e. (#I)
7109                // emits 0 to clear). Set the running flags directly,
7110                // INCLUDING the low byte (`(#aN)` approximation budget)
7111                // so mid-pattern `(#a0)`/`(#a2)` re-arm the error
7112                // allowance — c:Src/pattern.c:2941.
7113                glob_flags =
7114                    (glob_flags & !(GF_IGNCASE | GF_LCMATCHUC | GF_MULTIBYTE | 0xff)) | bits;
7115            }
7116            P_COUNT => {
7117                // c:3428-3455 — `case P_COUNT: /* (#cN,M): execution is
7118                // relatively straightforward */`
7119                //
7120                //   long cur = scan[P_CT_CURRENT].l;
7121                //   long min = scan[P_CT_MIN].l;
7122                //   long max = scan[P_CT_MAX].l;
7123                //   if (cur && cur >= min &&
7124                //       (unsigned char *)patinput == scan[P_CT_PTR].p)
7125                //       return patmatch(next);
7126                //   scan[P_CT_PTR].p = (unsigned char *)patinput;
7127                //   if (max < 0 || cur < max) {
7128                //       char *patinput_thistime = patinput;
7129                //       scan[P_CT_CURRENT].l = cur + 1;
7130                //       if (patmatch(scan + P_CT_OPERAND)) return 1;
7131                //       scan[P_CT_CURRENT].l = cur;
7132                //       patinput = patinput_thistime;
7133                //   }
7134                //   if (cur < min) return 0;
7135                //   return patmatch(next);
7136                //
7137                // !!! RUST-ONLY STRUCTURE (same search, different plumbing) !!!
7138                // C makes the repetition loop by pointing the OPERAND's tail
7139                // back at this very node (c:1693-1696 `pattail(opnd,
7140                // patnode(P_BACK))`) and by keeping `cur` / `ptr` as mutable
7141                // cells INSIDE the compiled program (c:208-213 P_CT_CURRENT /
7142                // P_CT_PTR). That back-edge is what lets one iteration's
7143                // continuation re-enter P_COUNT, so the operand's own
7144                // backtracking picks a SHORTER match when the longer one
7145                // leaves the rest of the repetition unsatisfiable. zshrs
7146                // stores the operand inline with no back-edge and no
7147                // in-program cells, so the same search runs here over an
7148                // explicit DFS stack: at each step every end position the
7149                // operand can reach is tried, longest first (C's greedy
7150                // order), and the count rides the stack instead of the
7151                // program. The previous greedy-only walk let `*_` eat the
7152                // whole string on iteration 1 and never reconsidered, so
7153                // `[[ 1_2_ = (*_)(#c2) ]]` failed.
7154                let body = scan + I_BODY;
7155                let min = i64::from_le_bytes(code[body..body + 8].try_into().unwrap());
7156                let max = i64::from_le_bytes(code[body + 8..body + 16].try_into().unwrap());
7157                let operand = body + 16;
7158                // Stack frame: (input position, repetitions so far, next
7159                // candidate operand end to try — descending, saved state).
7160                let mut stack: Vec<(usize, i64, i64, rpat)> =
7161                    vec![(s_off, 0, string.len() as i64, state.clone())];
7162                while let Some((pos, cur, e, st)) = stack.pop() {
7163                    // c:3448 — `if (max < 0 || cur < max)`
7164                    if (max < 0 || cur < max) && e >= pos as i64 {
7165                        // Re-queue this level with the next-shorter operand
7166                        // end so an exhausted subtree backtracks here.
7167                        stack.push((pos, cur, e - 1, st.clone()));
7168                        let eu = e as usize;
7169                        if string.is_char_boundary(eu) {
7170                            let mut sub = st.clone();
7171                            // Truncating the subject to `eu` anchors the
7172                            // operand's end, which is how one candidate
7173                            // iteration length is proposed.
7174                            if let Some(got) =
7175                                patmatch(code, operand, &string[..eu], pos, &mut sub, glob_flags)
7176                            {
7177                                // c:3434-3444 — "the previous attempt managed
7178                                // zero length. We can do this indefinitely so
7179                                // there's no point in going on." A zero-length
7180                                // iteration still counts, but only up to `min`.
7181                                if got == eu && (eu != pos || cur + 1 <= min) {
7182                                    // c:3450 — `scan[P_CT_CURRENT].l = cur + 1;`
7183                                    stack.push((eu, cur + 1, string.len() as i64, sub));
7184                                }
7185                            }
7186                        }
7187                        continue;
7188                    }
7189                    // c:3453 — `if (cur < min) return 0;`
7190                    if cur < min {
7191                        continue;
7192                    }
7193                    // c:3454 — `return patmatch(next);`
7194                    if next == 0 {
7195                        *state = st;
7196                        return Some(pos);
7197                    }
7198                    let mut sub = st.clone();
7199                    if let Some(end) = patmatch(code, next, string, pos, &mut sub, glob_flags) {
7200                        *state = sub;
7201                        return Some(end);
7202                    }
7203                }
7204                return None;
7205            }
7206            op if op >= P_OPEN && op < P_CLOSE => {
7207                // c:P_OPEN_N arm (pattern.c:2939-2960).
7208                //
7209                // C `case P_OPEN+0..P_OPEN+9:
7210                //     no = P_OP(scan) - P_OPEN;
7211                //     save = patinput;
7212                //     if (patmatch(next)) {
7213                //         if (no && !(parsfound & (1 << (no - 1)))) {
7214                //             patbeginp[no-1] = save;
7215                //             parsfound |= 1 << (no - 1);
7216                //         }
7217                //         return 1;
7218                //     }
7219                //     return 0;`
7220                //
7221                // Recurse on `next`; only commit patbeginp[N-1] on
7222                // success AND only on the FIRST occurrence (c:2957
7223                // `!(parsfound & (1<<(no-1)))`). The first-write
7224                // semantic matters under `(*)*` and alternation
7225                // backtrack — a later iteration shouldn't overwrite
7226                // the saved start of the FIRST match.
7227                let n = (op - P_OPEN) as usize;
7228                let save = s_off;
7229                let saved_state = state.clone();
7230                // c:2957 — `if (no && !(parsfound & (1 << (no - 1))))`.
7231                // Open-bit is the LOW stripe: `1 << (n-1)`. n==0 is the
7232                // plain (uncaptured) P_OPEN — c:2957's `no &&` guard
7233                // means it records nothing; bit value is moot (0).
7234                let open_bit = if n > 0 { 1u32 << (n - 1) } else { 0 };
7235                if next == 0 {
7236                    // No continuation — leaf P_OPEN; just commit and continue.
7237                    if n > 0 && n <= NSUBEXP && (state.captures_set & open_bit) == 0 {
7238                        state.patbeginp[n - 1] = save;
7239                        state.captures_set |= open_bit; // c:2959
7240                    }
7241                    return Some(s_off);
7242                }
7243                match patmatch(code, next, string, s_off, state, glob_flags) {
7244                    Some(end) => {
7245                        // c:2957-2959 — first-write commit.
7246                        if n > 0 && n <= NSUBEXP && (state.captures_set & open_bit) == 0 {
7247                            state.patbeginp[n - 1] = save;
7248                            state.captures_set |= open_bit; // c:2959
7249                        }
7250                        return Some(end);
7251                    }
7252                    None => {
7253                        *state = saved_state;
7254                        return None;
7255                    }
7256                }
7257            }
7258            op if op >= P_CLOSE && op < 0xa0 => {
7259                // c:P_CLOSE_N arm (pattern.c:2980-3010).
7260                //
7261                // C `case P_CLOSE+0..P_CLOSE+9:
7262                //     no = P_OP(scan) - P_CLOSE;
7263                //     save = patinput;
7264                //     if (patmatch(next)) {
7265                //         if (no && !(parsfound & (1 << (no+NSUBEXP-1)))) {
7266                //             patendp[no-1] = save;
7267                //             parsfound |= 1 << (no+NSUBEXP-1);
7268                //         }
7269                //         return 1;
7270                //     }
7271                //     return 0;`
7272                //
7273                // Same save/recurse/first-write-on-success pattern as
7274                // P_OPEN. Rust uses `captures_set` bit (1<<(n-1)) for
7275                // BOTH open and close in the existing impl; semantically
7276                // the bit is set when the group's CLOSE has been seen,
7277                // i.e. the capture is complete. First-write on close
7278                // matters under `(...)*` so later iterations don't
7279                // overwrite the FIRST capture's end (matching C's
7280                // parsfound `no+NSUBEXP-1` bit-stripe).
7281                let n = (op - P_CLOSE) as usize;
7282                let save = s_off;
7283                let saved_state = state.clone();
7284                // c:2989 — `if (no && !(parsfound & (1 << (no+NSUBEXP-1))))`.
7285                // Close-bit is the HIGH stripe: `1 << (n-1+NSUBEXP)`.
7286                // n==0 = plain P_CLOSE, records nothing (c:2989 `no &&`).
7287                let close_bit = if n > 0 { 1u32 << (n - 1 + NSUBEXP) } else { 0 };
7288                if next == 0 {
7289                    if n > 0 && n <= NSUBEXP && (state.captures_set & close_bit) == 0 {
7290                        state.patendp[n - 1] = save;
7291                        state.captures_set |= close_bit;
7292                    }
7293                    return Some(s_off);
7294                }
7295                match patmatch(code, next, string, s_off, state, glob_flags) {
7296                    Some(end) => {
7297                        if n > 0 && n <= NSUBEXP && (state.captures_set & close_bit) == 0 {
7298                            state.patendp[n - 1] = save;
7299                            state.captures_set |= close_bit;
7300                        }
7301                        return Some(end);
7302                    }
7303                    None => {
7304                        *state = saved_state;
7305                        return None;
7306                    }
7307                }
7308            }
7309            _ => {
7310                // c:3465-3467 — `default: dputs("BUG: bad operand in
7311                // patmatch."); return 0;`
7312                crate::ported::utils::dputs("BUG: bad operand in patmatch.");
7313                return None;
7314            }
7315        }
7316
7317        if next == 0 {
7318            break;
7319        }
7320        scan = next;
7321    }
7322    Some(s_off)
7323}
7324
7325/// Port of `patallocstr()` from `Src/pattern.c:2132`.
7326/// C: `char *patallocstr(Patprog prog, char *string, int stringlen,
7327/// int unmetalen, int force, Patstralloc patstralloc)` from
7328/// `Src/pattern.c:2132`.
7329///
7330/// Sets up `patstralloc` for a match attempt: when `force` is set or
7331/// the input contains Meta bytes (or PAT_HAS_EXCLUDP demands a
7332/// full-path copy), allocates an un-metafied scratch buffer and
7333/// stashes pointer/length info on `patstralloc`. Returns the
7334/// allocated buffer or `None` if no allocation was needed.
7335pub fn patallocstr(
7336    prog: &Patprog,
7337    string: &str,
7338    stringlen: i32,
7339    unmetalen: i32,
7340    force: i32,
7341    patstralloc: &mut patstralloc,
7342) -> Option<String> {
7343    // c:2132
7344    // c:2137 — `int needfullpath;`
7345    let mut needfullpath: bool;
7346    // Working values (mutated when force triggers patmungestring).
7347    let mut string: &str = string; // c:2133 char *string param
7348    let mut stringlen: i32 = stringlen;
7349    let mut unmetalen: i32 = unmetalen;
7350
7351    if force != 0 {
7352        // c:2139
7353        // c:2140 — `patmungestring(&string, &stringlen, &unmetalen);`
7354        patmungestring(&mut string, &mut stringlen, &mut unmetalen);
7355    }
7356
7357    /*
7358     * For a top-level ~-exclusion, we will need the full
7359     * path to exclude, so copy the path so far and append the
7360     * current test string.
7361     */
7362    // c:2142-2146
7363    // c:2147 — `needfullpath = (prog->flags & PAT_HAS_EXCLUDP) && pathpos;`
7364    // `pathpos` is the `gd_pathpos` field of `curglobdata` (c:Src/glob.c:166-170
7365    // struct globdata; c:197 static curglobdata; c:199-201 macros expand
7366    // `pathpos`→`curglobdata.gd_pathpos`). Read directly from the
7367    // shared CURGLOBDATA mutex — the canonical port surface for glob
7368    // state in zshrs.
7369    let pathpos: i32 = crate::ported::glob::CURGLOBDATA
7370        .lock()
7371        .map(|gd| gd.pathpos as i32)
7372        .unwrap_or(0); // c:Src/glob.c:169 gd_pathpos
7373    needfullpath = (prog.0.flags & PAT_HAS_EXCLUDP as i32) != 0 && pathpos != 0; // c:2147
7374
7375    /* Get the length of the full string when unmetafied. */
7376    // c:2149
7377    if unmetalen < 0 {
7378        // c:2150
7379        // c:2151 — `patstralloc->unmetalen = ztrsub(string + stringlen, string);`
7380        // ztrsub returns the unmetafied char count between two pointers
7381        // in the same string. Rust analog: ztrsub(buf, start, end).
7382        patstralloc.unmetalen = ztrsub(string, 0, (stringlen as usize).min(string.len())) as i32;
7383    } else {
7384        // c:2152
7385        patstralloc.unmetalen = unmetalen; // c:2153
7386    }
7387    if needfullpath {
7388        // c:2154
7389        // c:2155 — `patstralloc->unmetalenp = ztrsub(pathbuf + pathpos, pathbuf);`
7390        // `pathbuf` is `curglobdata.gd_pathbuf` (c:Src/glob.c:170, macro
7391        // at c:200). ztrsub(end, start) returns unmetafied char count
7392        // between pointers. With pathpos in [0, pathbuf.len()] the
7393        // Rust analog is ztrsub(pathbuf, 0, pathpos as usize).
7394        let pathbuf = crate::ported::glob::CURGLOBDATA
7395            .lock()
7396            .map(|gd| gd.pathbuf.clone())
7397            .unwrap_or_default(); // c:Src/glob.c:170 gd_pathbuf
7398        let p_end = (pathpos as usize).min(pathbuf.len());
7399        patstralloc.unmetalenp = ztrsub(&pathbuf, 0, p_end) as i32; // c:2155
7400        if patstralloc.unmetalenp == 0 {
7401            // c:2156
7402            needfullpath = false; // c:2157 (`needfullpath = 0;`)
7403        }
7404    } else {
7405        // c:2158
7406        patstralloc.unmetalenp = 0; // c:2159
7407    }
7408    /* Initialise cache area */
7409    // c:2161
7410    patstralloc.progstrunmeta = None; // c:2162
7411    patstralloc.progstrunmetalen = 0; // c:2163
7412
7413    // c:2165-2166 — `DPUTS(needfullpath && (prog->flags & (PAT_PURES|PAT_ANY)),
7414    //                       "rum sort of file exclusion");`
7415    // Rust drops the debug assertion.
7416
7417    /*
7418     * Partly for efficiency, and partly for the convenience of
7419     * globbing, we don't unmetafy pure string patterns, and
7420     * there's no reason to if the pattern is just a *.
7421     */
7422    // c:2167-2171
7423    let pures_or_any = (prog.0.flags & (PAT_PURES | PAT_ANY) as i32) != 0;
7424    if force != 0 || (!pures_or_any && (needfullpath || patstralloc.unmetalen != stringlen))
7425    // c:2172
7426    {
7427        /*
7428         * We need to copy if we need to prepend the path so far
7429         * (in which case we copy both chunks), or if we have
7430         * Meta characters.
7431         */
7432        // c:2174-2178
7433        // c:2179 — `char *dst, *ptr; int i, icopy, ncopy;`
7434        let total = (patstralloc.unmetalen + patstralloc.unmetalenp) as usize;
7435        let mut dst = String::with_capacity(total); // c:2182 zhalloc
7436
7437        // c:2184-2192 — choose source chunk(s).
7438        let pathbuf: String = crate::ported::glob::CURGLOBDATA
7439            .lock()
7440            .map(|gd| gd.pathbuf.clone())
7441            .unwrap_or_default(); // c:Src/glob.c:170 gd_pathbuf
7442        let mut ptr: &str;
7443        let mut ncopy: i32;
7444        if needfullpath {
7445            // c:2185
7446            ptr = &pathbuf; // c:2186
7447            ncopy = patstralloc.unmetalenp; // c:2188
7448        } else {
7449            // c:2189
7450            ptr = string; // c:2190
7451            ncopy = patstralloc.unmetalen; // c:2191
7452        }
7453        // c:2193-2210 — for (icopy = 0; icopy < 2; icopy++) outer loop:
7454        //   copy ncopy bytes from ptr to dst, unmetafy Meta+X pairs.
7455        for icopy in 0..2 {
7456            // c:2193
7457            let ptr_bytes = ptr.as_bytes();
7458            let mut i = 0i32;
7459            let mut byte_idx = 0usize;
7460            while i < ncopy && byte_idx < ptr_bytes.len() {
7461                // c:2194
7462                if ptr_bytes[byte_idx] == Meta as u8 && byte_idx + 1 < ptr_bytes.len() {
7463                    // c:2195-2197 — `if (*ptr == Meta) { ptr++; *dst++ = *ptr++ ^ 32; }`
7464                    byte_idx += 1; // c:2196 ptr++
7465                    dst.push((ptr_bytes[byte_idx] ^ 32) as char); // c:2197 *dst++ = *ptr++ ^ 32
7466                    byte_idx += 1;
7467                } else {
7468                    // c:2198
7469                    // c:2199 — `else *dst++ = *ptr++;`
7470                    dst.push(ptr_bytes[byte_idx] as char);
7471                    byte_idx += 1;
7472                }
7473                i += 1;
7474            }
7475            if !needfullpath {
7476                // c:2203
7477                break; // c:2204
7478            }
7479            /* next time append test string to path so far */
7480            // c:2205
7481            ptr = string; // c:2207
7482            ncopy = patstralloc.unmetalen; // c:2208
7483            let _ = icopy;
7484        }
7485        patstralloc.alloced = Some(dst.clone()); // c:2182 dst = patstralloc->alloced
7486        return Some(dst); // c:2213 return patstralloc->alloced
7487    } else {
7488        // c:2214
7489        patstralloc.alloced = None; // c:2215
7490    }
7491
7492    None // c:2218 return patstralloc->alloced (NULL)
7493}
7494
7495// `patrepeat(Upat p, char *charstart)` (C: pattern.c:4096 — `static int`
7496// helper called from the bytecode walker at pattern.c:3321 for greedy
7497// `*` matches) had a Rust-only wrapper `pub fn patrepeat(prog: &Patprog,
7498// s: &str, max: Option<usize>)`. Zero Rust callers (the bytecode walker
7499// inlines its own greedy loop instead of calling out to patrepeat),
7500// Rule S1 deviation (extra `max` param, takes whole prog instead of a
7501// Upat into the bytecode). Deleted; reintroduce as a faithful port when
7502// the bytecode walker is refactored to use it.
7503
7504// =====================================================================
7505// Transitional aliases — older callers still use `PatProg` (camel-case
7506// from the previous AST-based port). Alias them to `Patprog` so the
7507// build doesn't break; future cleanup commit renames callers.
7508// =====================================================================
7509/// `PatProg` type alias.
7510#[deprecated(note = "use Patprog instead")]
7511pub type PatProg = Patprog;
7512
7513// =====================================================================
7514// Transitional Rust-only types — kept for external callers that bind
7515// to the previous AST-based port's surface area (vm_helper, exec_shims.rs,
7516// fusevm_bridge.rs, glob.rs). These are NOT C-faithful ports — they're
7517// helper aggregates the previous AST port introduced for one-shot
7518// pattern processing in the executor/VM bridge. Callers bind to the
7519// bytecode API (patcompile + pattry + patgetglobflags) where they can.
7520// =====================================================================
7521
7522// `NumericRange` struct + impl DELETED. C zsh's `patcomppiece`
7523// (Src/pattern.c:1450+) inlines `<N-M>` parsing and emits `P_NUMRNG`
7524// opcodes — no aggregator type. Rust port uses these bare helpers
7525// returning tuples `(start, end, lo, hi)` for the pre-pattern pass
7526// that exec_shims/fusevm need because the `glob` crate has no
7527// native `P_NUMRNG`. Dissolve fully when fusevm + exec_shims
7528// migrate to pure `patcompile` + `pattry`.
7529
7530/// !!! WARNING: RUST-ONLY HELPER !!!
7531///
7532/// There is NO `extract_numeric_ranges()` in `Src/pattern.c`. C has
7533/// no such pre-pass at all: `<N-M>` is parsed inside `patcomppiece()`
7534/// (`Src/pattern.c:1528-1573`, the `case Inang:` arm) and emitted as
7535/// `P_NUMRNG` / `P_NUMFROM` / `P_NUMTO` / `P_NUMANY` opcodes, then
7536/// matched by `patmatch()` (`Src/pattern.c:2694`). This scanner
7537/// exists only for the transitional callers named in the section
7538/// comment above (`vm_helper`, `exec_shims.rs`), which still route
7539/// some globs through the external `glob` crate — which has no
7540/// `P_NUMRNG` equivalent — and so must strip and re-check the
7541/// numeric ranges themselves. It disappears when those call sites
7542/// move to `patcompile()` + `pattry()`.
7543///
7544/// Extracts all `<N-M>` / `<N->` / `<-M>` / `<->` ranges from a glob
7545/// pattern. Returns `(start, end, lo, hi)` tuples — `start`/`end`
7546/// are byte offsets of `<` / past `>`, `lo`/`hi` are bounds (`None`
7547/// = unbounded on that side).
7548pub fn extract_numeric_ranges(s: &str) -> Vec<(usize, usize, Option<i64>, Option<i64>)> {
7549    let mut out = Vec::new();
7550    let bytes = s.as_bytes();
7551    let mut i = 0;
7552    while i < bytes.len() {
7553        if bytes[i] == b'<' {
7554            let start = i;
7555            let mut j = i + 1;
7556            let lo_start = j;
7557            while j < bytes.len() && bytes[j].is_ascii_digit() {
7558                j += 1;
7559            }
7560            let lo: Option<i64> = if j > lo_start {
7561                std::str::from_utf8(&bytes[lo_start..j])
7562                    .ok()
7563                    .and_then(|s| s.parse::<i64>().ok())
7564            } else {
7565                None
7566            };
7567            if j < bytes.len() && bytes[j] == b'-' {
7568                j += 1;
7569                let hi_start = j;
7570                while j < bytes.len() && bytes[j].is_ascii_digit() {
7571                    j += 1;
7572                }
7573                let hi: Option<i64> = if j > hi_start {
7574                    std::str::from_utf8(&bytes[hi_start..j])
7575                        .ok()
7576                        .and_then(|s| s.parse::<i64>().ok())
7577                } else {
7578                    None
7579                };
7580                if j < bytes.len() && bytes[j] == b'>' {
7581                    out.push((start, j + 1, lo, hi));
7582                    i = j + 1;
7583                    continue;
7584                }
7585            }
7586        }
7587        i += 1;
7588    }
7589    out
7590}
7591
7592/// !!! WARNING: RUST-ONLY HELPER !!!
7593///
7594/// There is NO `numeric_ranges_to_star()` in `Src/pattern.c`. C never
7595/// rewrites a pattern before matching it — `<N-M>` compiles straight
7596/// to `P_NUMRNG` in `patcomppiece()` (`Src/pattern.c:1528-1573`).
7597/// This is the second half of the transitional `extract_numeric_ranges()`
7598/// shim above: the external `glob` crate cannot express a numeric
7599/// range, so the range is widened to `*` for the crate's pass and
7600/// re-checked afterwards by `numeric_range_contains()`. Dies with the
7601/// same call sites.
7602///
7603/// Replaces every `<N-M>` in `s` with `*` for fallback glob
7604/// expansion.
7605pub fn numeric_ranges_to_star(s: &str) -> String {
7606    let mut out = String::with_capacity(s.len());
7607    let mut last = 0;
7608    for (start, end, _, _) in extract_numeric_ranges(s) {
7609        out.push_str(&s[last..start]);
7610        out.push('*');
7611        last = end;
7612    }
7613    out.push_str(&s[last..]);
7614    out
7615}
7616
7617/// !!! WARNING: RUST-ONLY HELPER !!!
7618///
7619/// There is NO `numeric_range_contains()` in `Src/pattern.c`. The C
7620/// equivalent is the inline bounds test in the `P_NUMRNG` arm of
7621/// `patmatch()` (`Src/pattern.c:2810-2910`, the `case P_NUMRNG:` arm), which reads
7622/// its two `zrange_t` operands straight out of the bytecode. This standalone
7623/// form exists only for the transitional `extract_numeric_ranges()`
7624/// callers above, which have the bounds as `Option<i64>` rather than
7625/// as compiled operands. Dies with the same call sites.
7626///
7627/// Tests whether `n` falls within numeric range `(lo, hi)`. Unbounded
7628/// sides always pass.
7629pub fn numeric_range_contains(lo: Option<i64>, hi: Option<i64>, n: i64) -> bool {
7630    lo.map_or(true, |l| n >= l) && hi.map_or(true, |h| n <= h)
7631}
7632
7633// =====================================================================
7634// Tests
7635// =====================================================================
7636
7637#[cfg(test)]
7638mod tests {
7639    use super::*;
7640    use crate::options::{opt_state_get, opt_state_set};
7641    use std::thread;
7642
7643    // Pattern compile shares file-static globals (patout, patparse,
7644    // patnpar, ...) with the same single-thread semantics as zsh's
7645    // C source. `patcompile` clones the globals into prog.1
7646    // before returning, so we only need the mutex held during
7647    // compile — pattry() reads from prog.1 with no global state.
7648    static TEST_MUTEX: Mutex<()> = Mutex::new(());
7649
7650    fn compile(p: &str) -> Patprog {
7651        let _g = TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
7652        patcompile(
7653            &{
7654                let mut __pat_tok = (p).to_string();
7655                crate::ported::glob::tokenize(&mut __pat_tok);
7656                __pat_tok
7657            },
7658            PAT_HEAPDUP as i32,
7659            None,
7660        )
7661        .expect("compile failed")
7662    }
7663
7664    /// Test-only `patcompile + pattry` pair (Rule 3 exempt — `#[cfg(test)]`).
7665    /// Mirrors the pattern most tests want: "does this pattern match
7666    /// this string?" without dragging compile boilerplate into every
7667    /// assertion. Does NOT acquire `TEST_MUTEX` — callers already hold
7668    /// it (or hold the broader `global_state_lock()` from `test_util`)
7669    /// when serialisation against `patcompile`'s file-statics matters.
7670    /// Acquiring it here too would deadlock on the non-reentrant
7671    /// `Mutex<()>` (e.g. `convenience_patmatch` holds TEST_MUTEX before
7672    /// calling, `patcompile_concurrent_safe` exercises 8 threads that
7673    /// would serialise via this fn instead of through the real engine).
7674    fn patmatch(pat: &str, text: &str) -> bool {
7675        patcompile(
7676            &{
7677                let mut __pat_tok = (pat).to_string();
7678                crate::ported::glob::tokenize(&mut __pat_tok);
7679                __pat_tok
7680            },
7681            PAT_HEAPDUP as i32,
7682            None,
7683        )
7684        .map_or(false, |prog| pattry(&prog, text))
7685    }
7686
7687    /// The P_BRANCH alt-loop decides a literal-led alternative without
7688    /// recursing when the input cannot match it. Every case here sits on
7689    /// one side of that decision: a literal longer than the input, a
7690    /// literal that fails while a later one matches, branches that must
7691    /// still run in the full walker (`(#i)`, `(#a1)`, empty branch,
7692    /// wildcard-led), and a compdump-sized alternation hit at its last
7693    /// branch.
7694    #[test]
7695    fn alternation_literal_branches_match_like_zsh() {
7696        let _g = crate::test_util::global_state_lock();
7697        // `(#i)` / `(#a1)` are EXTENDED_GLOB syntax.
7698        let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
7699        crate::ported::options::opt_state_set("extendedglob", true);
7700        assert!(patmatch("(ab|a)c", "ac"));
7701        assert!(patmatch("(abc|ab)", "ab"));
7702        assert!(!patmatch("(abc|ab)", "a"));
7703        assert!(!patmatch("(x|y|z)", "w"));
7704        assert!(patmatch("(foo|bar)*", "barbaz"));
7705        assert!(patmatch("(foo|*az)", "barbaz"));
7706        assert!(patmatch("(ab|)c", "c"));
7707        assert!(patmatch("(#i)(ab|cd)", "CD"));
7708        assert!(!patmatch("(ab|cd)", "CD"));
7709        assert!(patmatch("(#a1)(abc|xyz)", "abd"));
7710        assert!(!patmatch("(#a1)(abc|xyz)", "add"));
7711        let names: Vec<String> = (0..2000).map(|i| format!("_n{i}")).collect();
7712        let pat = format!("({})", names.join("|"));
7713        assert!(patmatch(&pat, "_n1999"));
7714        assert!(patmatch(&pat, "_n0"));
7715        assert!(!patmatch(&pat, "_n2000"));
7716        assert!(!patmatch(&pat, "_n"));
7717        crate::ported::options::opt_state_set("extendedglob", saved);
7718    }
7719
7720    #[test]
7721    fn literal_match() {
7722        let _g = crate::test_util::global_state_lock();
7723        let prog = compile("hello");
7724        assert!(pattry(&prog, "hello"));
7725        assert!(!pattry(&prog, "world"));
7726    }
7727
7728    #[test]
7729    fn star_matches_anything() {
7730        let _g = crate::test_util::global_state_lock();
7731        let prog = compile("*");
7732        assert!(pattry(&prog, ""));
7733        assert!(pattry(&prog, "abc"));
7734    }
7735
7736    #[test]
7737    fn star_in_middle() {
7738        let _g = crate::test_util::global_state_lock();
7739        let prog = compile("a*z");
7740        assert!(pattry(&prog, "az"));
7741        assert!(pattry(&prog, "abz"));
7742        assert!(pattry(&prog, "aXYZz"));
7743        assert!(!pattry(&prog, "ab"));
7744    }
7745
7746    #[test]
7747    fn question_matches_one() {
7748        let _g = crate::test_util::global_state_lock();
7749        let prog = compile("a?c");
7750        assert!(pattry(&prog, "abc"));
7751        assert!(pattry(&prog, "axc"));
7752        assert!(!pattry(&prog, "ac"));
7753    }
7754
7755    #[test]
7756    fn bracket_anyof() {
7757        let _g = crate::test_util::global_state_lock();
7758        let prog = compile("[abc]");
7759        assert!(pattry(&prog, "a"));
7760        assert!(pattry(&prog, "b"));
7761        assert!(pattry(&prog, "c"));
7762        assert!(!pattry(&prog, "d"));
7763    }
7764
7765    #[test]
7766    fn bracket_range() {
7767        let _g = crate::test_util::global_state_lock();
7768        let prog = compile("[a-z]");
7769        assert!(pattry(&prog, "m"));
7770        assert!(!pattry(&prog, "M"));
7771    }
7772
7773    #[test]
7774    fn bracket_negated() {
7775        let _g = crate::test_util::global_state_lock();
7776        let prog = compile("[^0-9]");
7777        assert!(pattry(&prog, "a"));
7778        assert!(!pattry(&prog, "5"));
7779    }
7780
7781    #[test]
7782    fn alternation() {
7783        let _g = crate::test_util::global_state_lock();
7784        let prog = compile("foo|bar");
7785        assert!(pattry(&prog, "foo"));
7786        assert!(pattry(&prog, "bar"));
7787        assert!(!pattry(&prog, "baz"));
7788    }
7789
7790    #[test]
7791    fn captures() {
7792        let _g = crate::test_util::global_state_lock();
7793        let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
7794        crate::ported::options::opt_state_set("extendedglob", true);
7795        let prog = compile("(#b)(foo)(bar)");
7796        crate::ported::options::opt_state_set("extendedglob", saved);
7797        let mut nump = 0i32;
7798        let mut begp: Vec<i32> = Vec::new();
7799        let mut endp: Vec<i32> = Vec::new();
7800        let ok = pattryrefs(
7801            &prog,
7802            "foobar",
7803            -1,
7804            -1,
7805            None,
7806            0,
7807            Some(&mut nump),
7808            Some(&mut begp),
7809            Some(&mut endp),
7810        );
7811        assert!(ok);
7812        // c:2556-2566 — begp holds the first character of each group, endp
7813        // the LAST character (inclusive).
7814        assert_eq!(nump, 2);
7815        assert_eq!(&begp[..2], &[0, 3]);
7816        assert_eq!(&endp[..2], &[2, 5]);
7817    }
7818
7819    /// c:1626-1627 — `if (kshchar && (hash || count)) return 0;`, where
7820    /// `case Star:` supplies the `kshchar = -1` sentinel (c:1440, "kshchar is
7821    /// used as a sign that we can't have #'s"). Both halves of that one C
7822    /// test have to reject: this port had ported only the `count` half (in
7823    /// patcompbranch), so `*(#c2,3)` was already a bad pattern here while
7824    /// `*#` / `*##` / `a*#b` compiled and matched. `zsh -f` 5.9 answers
7825    /// `bad pattern: *#` for all three.
7826    #[test]
7827    fn a_closure_may_not_follow_a_star() {
7828        let _g = crate::test_util::global_state_lock();
7829        let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
7830        crate::ported::options::opt_state_set("extendedglob", true);
7831        let try_compile = |p: &str| {
7832            let _t = TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
7833            patcompile(
7834                &{
7835                    let mut tok = p.to_string();
7836                    crate::ported::glob::tokenize(&mut tok);
7837                    tok
7838                },
7839                PAT_HEAPDUP as i32,
7840                None,
7841            )
7842        };
7843        for bad in ["*#", "*##", "a*#b"] {
7844            assert!(
7845                try_compile(bad).is_none(),
7846                "c:1627 — `{bad}` is a bad pattern: a closure cannot follow `*`"
7847            );
7848        }
7849        // The neighbouring forms still compile: a closure on a non-Star atom,
7850        // and a `*` with no closure after it.
7851        for good in ["a#", "[a]#", "?#", "a*b", "(a*)#"] {
7852            assert!(
7853                try_compile(good).is_some(),
7854                "c:1620 — `{good}` has no kshchar/Star + hash pair and must compile"
7855            );
7856        }
7857        crate::ported::options::opt_state_set("extendedglob", saved);
7858    }
7859
7860    #[test]
7861    fn hash_zero_or_more() {
7862        let _g = crate::test_util::global_state_lock();
7863        // `#`/`##` quantifiers require EXTENDEDGLOB per zsh.
7864        let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
7865        crate::ported::options::opt_state_set("extendedglob", true);
7866        let prog = compile("a#");
7867        assert!(pattry(&prog, ""));
7868        assert!(pattry(&prog, "a"));
7869        assert!(pattry(&prog, "aaa"));
7870        crate::ported::options::opt_state_set("extendedglob", saved);
7871    }
7872
7873    /// c:1338 / c:1372-1376 — a closure over a SINGLE MULTIBYTE character.
7874    ///
7875    /// `morelen` (c:1338 `patprev > str0`) counts CHARACTERS, not bytes, and
7876    /// this port used to test bytes. A one-character run whose character is
7877    /// multibyte therefore took the "backtrack one character so the `#`
7878    /// applies to the last one" branch (c:1343-1351), gave back the ONLY
7879    /// character in the run, and returned a piece that had consumed nothing —
7880    /// so `patcompbranch`'s piece loop never terminated. `[[ éé = é# ]]`
7881    /// burned 100% CPU forever (D07multibyte, "Multibyte handling of
7882    /// functions parameter" neighbourhood).
7883    ///
7884    /// The bound below is what makes this a hang test rather than a
7885    /// correctness test: compiling and matching six short patterns is
7886    /// microseconds of work, and the pre-fix code never returns at all, so
7887    /// any generous wall-clock ceiling separates the two. It is deliberately
7888    /// loose (this box runs many concurrent build/test sessions).
7889    #[test]
7890    fn multibyte_closure_terminates() {
7891        let _g = crate::test_util::global_state_lock();
7892        let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
7893        crate::ported::options::opt_state_set("extendedglob", true);
7894        let started = std::time::Instant::now();
7895        // c:1375 — `é#` compiles as `(é)#`, so it matches zero or more `é`.
7896        let prog = compile("é#");
7897        assert!(pattry(&prog, ""), "é# matches the empty string");
7898        assert!(pattry(&prog, "é"), "é# matches one é");
7899        assert!(pattry(&prog, "éé"), "é# matches two é");
7900        assert!(!pattry(&prog, "x"), "é# does not match x");
7901        // `##` (one or more) over the same single multibyte character.
7902        let prog2 = compile("é##");
7903        assert!(!pattry(&prog2, ""), "é## needs at least one é");
7904        assert!(pattry(&prog2, "ééé"), "é## matches three é");
7905        // A MULTI-character run ending in a multibyte character still
7906        // backtracks (c:1343-1351): `aé#` is `a` followed by `é#`.
7907        let prog3 = compile("aé#");
7908        assert!(pattry(&prog3, "a"), "aé# matches a");
7909        assert!(pattry(&prog3, "aéé"), "aé# matches a then two é");
7910        assert!(!pattry(&prog3, "aa"), "aé# does not match aa");
7911        crate::ported::options::opt_state_set("extendedglob", saved);
7912        assert!(
7913            started.elapsed() < std::time::Duration::from_secs(20),
7914            "multibyte closure compile/match must terminate promptly, took {:?}",
7915            started.elapsed()
7916        );
7917    }
7918
7919    #[test]
7920    fn double_hash_one_or_more() {
7921        let _g = crate::test_util::global_state_lock();
7922        let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
7923        crate::ported::options::opt_state_set("extendedglob", true);
7924        let prog = compile("a##");
7925        assert!(!pattry(&prog, ""));
7926        assert!(pattry(&prog, "a"));
7927        assert!(pattry(&prog, "aaa"));
7928        crate::ported::options::opt_state_set("extendedglob", saved);
7929    }
7930
7931    #[test]
7932    fn escape_literal() {
7933        let _g = crate::test_util::global_state_lock();
7934        let prog = compile("a\\*b");
7935        assert!(pattry(&prog, "a*b"));
7936        assert!(!pattry(&prog, "azb"));
7937    }
7938
7939    #[test]
7940    fn convenience_patmatch() {
7941        let _g = crate::test_util::global_state_lock();
7942        let _g = TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
7943        assert!(patmatch("hello*", "hello world"));
7944        assert!(!patmatch("x?z", "abc"));
7945    }
7946
7947    /// Concurrent compile must not corrupt the file-scope statics
7948    /// (`Src/pattern.c:267-281`). Verifies `PATCOMPILE_LOCK` serialises
7949    /// the entry so colon-bearing patterns from zutil-style consumers
7950    /// don't race against simpler call sites.
7951    #[test]
7952    fn patcompile_concurrent_safe() {
7953        let _g = crate::test_util::global_state_lock();
7954        let handles: Vec<_> = (0..8)
7955            .map(|i| {
7956                thread::spawn(move || {
7957                    for _ in 0..200 {
7958                        assert!(patmatch(":completion:*", ":completion:zsh"));
7959                        assert!(patmatch("hello*", "hello world"));
7960                        let _ = i;
7961                    }
7962                })
7963            })
7964            .collect();
7965        for h in handles {
7966            h.join().unwrap();
7967        }
7968    }
7969
7970    #[test]
7971    fn haswilds_detects_meta() {
7972        let _g = crate::test_util::global_state_lock();
7973        // c:Src/glob.c:3548 — tokenize input as every C caller does.
7974        let tok = |s: &str| {
7975            let mut t = s.to_string();
7976            crate::ported::glob::tokenize(&mut t);
7977            t
7978        };
7979        assert!(haswilds(&tok("*")));
7980        assert!(haswilds(&tok("foo?")));
7981        assert!(haswilds(&tok("[abc]")));
7982        assert!(!haswilds(&tok("plain")));
7983    }
7984
7985    #[test]
7986    /// `Src/pattern.c:1148` — walks `colon_stuffs[]` (c:1134-1138), returns
7987    /// `(index + PP_FIRST)`. PP_FIRST=1, so `alpha`→1, `alnum`→2, `ascii`→3,
7988    /// …, `digit`→6, …, `INVALID`→19. Unknown returns None (C returns
7989    /// PP_UNKWN=20).
7990    fn range_type_lookup() {
7991        let _g = crate::test_util::global_state_lock();
7992        assert_eq!(range_type("alpha"), Some(1), "PP_ALPHA (c:colon_stuffs[0])");
7993        assert_eq!(range_type("alnum"), Some(2), "PP_ALNUM (c:colon_stuffs[1])");
7994        assert_eq!(
7995            range_type("ascii"),
7996            Some(3),
7997            "PP_ASCII (c:colon_stuffs[2]) — was missing pre-fix"
7998        );
7999        assert_eq!(range_type("digit"), Some(6), "PP_DIGIT (c:colon_stuffs[5])");
8000        assert_eq!(
8001            range_type("xdigit"),
8002            Some(13),
8003            "PP_XDIGIT (c:colon_stuffs[12])"
8004        );
8005        assert_eq!(range_type("IDENT"), Some(14), "PP_IDENT — zsh extension");
8006        assert_eq!(range_type("WORD"), Some(17), "PP_WORD — zsh extension");
8007        assert_eq!(
8008            range_type("INVALID"),
8009            Some(19),
8010            "PP_INVALID — zsh extension"
8011        );
8012        assert_eq!(range_type("nonsense"), None);
8013    }
8014
8015    #[test]
8016    fn pattern_range_to_string_passes_through_pos_class() {
8017        let _g = crate::test_util::global_state_lock();
8018        assert_eq!(pattern_range_to_string("[:alpha:]"), "[:alpha:]");
8019        assert_eq!(pattern_range_to_string("a-z"), "a-z");
8020        assert_eq!(pattern_range_to_string(""), "");
8021    }
8022
8023    #[test]
8024    fn patgetglobflags_case_insensitive() {
8025        let _g = crate::test_util::global_state_lock();
8026        let (bits, _, n) = patgetglobflags("(#i)foo").unwrap();
8027        assert_ne!((bits & GF_IGNCASE), 0);
8028        assert_eq!(n, 4); // length of "(#i)"
8029    }
8030
8031    #[test]
8032    fn patgetglobflags_backref() {
8033        let _g = crate::test_util::global_state_lock();
8034        let (bits, _, _) = patgetglobflags("(#b)").unwrap();
8035        assert_ne!((bits & GF_BACKREF), 0);
8036    }
8037
8038    #[test]
8039    fn patgetglobflags_approx() {
8040        let _g = crate::test_util::global_state_lock();
8041        let (bits, _, _) = patgetglobflags("(#a2)").unwrap();
8042        assert_eq!(bits & 0xff, 2);
8043    }
8044
8045    /// Pin: `(#I)` per `Src/pattern.c:1080-1081` clears BOTH
8046    /// `GF_LCMATCHUC` AND `GF_IGNCASE`: `patglobflags &=
8047    /// ~(GF_LCMATCHUC|GF_IGNCASE)`.
8048    #[test]
8049    fn patgetglobflags_capital_i_clears_both_case_flags() {
8050        let _g = crate::test_util::global_state_lock();
8051        // Two-flag chain: `(#l)` sets LCMATCHUC; `(#I)` should
8052        // clear it. C clears via `~(GF_LCMATCHUC|GF_IGNCASE)`.
8053        let (bits, _, _) = patgetglobflags("(#lI)").unwrap();
8054        assert_eq!(
8055            bits & GF_LCMATCHUC,
8056            0,
8057            "c:1081 — (#I) must clear GF_LCMATCHUC"
8058        );
8059        assert_eq!(
8060            bits & GF_IGNCASE,
8061            0,
8062            "c:1081 — (#I) must clear GF_IGNCASE too"
8063        );
8064    }
8065
8066    /// Pin: `(#L)` is NOT a documented flag per C pattern.c (no
8067    /// 'L' case in the switch). C's default arm returns 0, so
8068    /// patgetglobflags must reject `(#L)`. The previous Rust port
8069    /// accepted it and silently cleared GF_LCMATCHUC, diverging.
8070    #[test]
8071    fn patgetglobflags_rejects_undocumented_flag_letters() {
8072        let _g = crate::test_util::global_state_lock();
8073        // 'L' (capital L) — not a documented C flag.
8074        assert_eq!(
8075            patgetglobflags("(#L)"),
8076            None,
8077            "c:1120 default — unknown flag 'L' must be rejected"
8078        );
8079        // Other lower-rule letters that aren't documented either.
8080        assert_eq!(
8081            patgetglobflags("(#x)"),
8082            None,
8083            "c:1120 default — unknown flag 'x' must be rejected"
8084        );
8085        assert_eq!(
8086            patgetglobflags("(#9)"),
8087            None,
8088            "c:1120 default — bare digit (not after 'a') must be rejected"
8089        );
8090    }
8091
8092    /// Pin: `(#a)` without digits — C `zstrtol` returns 0 with
8093    /// `ptr == nptr` per c:1063. The previous Rust port silently
8094    /// accepted empty-digit form and set errs=0.
8095    #[test]
8096    fn patgetglobflags_rejects_empty_approx_digit_run() {
8097        let _g = crate::test_util::global_state_lock();
8098        // `(#a)` with no digits after 'a' — C rejects (c:1063
8099        // `ptr == nptr` check).
8100        assert_eq!(
8101            patgetglobflags("(#a)"),
8102            None,
8103            "c:1063 — `(#a)` without digits must be rejected"
8104        );
8105    }
8106
8107    #[test]
8108    fn pattry_no_anchor_default() {
8109        let _g = crate::test_util::global_state_lock();
8110        // patmatch with anchored compile: only full-string matches succeed.
8111        let prog = compile("foo");
8112        assert!(pattry(&prog, "foo"));
8113    }
8114
8115    /// PAT_NOGLD ("don't glob dots", set by `parsecomplist` at
8116    /// `Src/glob.c:715` whenever GLOB_DOTS is off) rejects a leading dot
8117    /// only where a WILDCARD opcode would consume it — `P_NOTDOT` is
8118    /// `p->l & 0x40` (`Src/pattern.c:202`), the bit shared by
8119    /// P_ANY/P_ANYOF/P_ANYBUT/P_STAR/P_NUM* (`Src/pattern.c:116-124`),
8120    /// tested at `Src/pattern.c:2731`. A dot written literally in the
8121    /// pattern is matched by `P_EXACTLY` (0x03), which does not carry the
8122    /// bit, and P_BRANCH (0x20) does not either — so grouping a literal
8123    /// dot keeps it literal.
8124    ///
8125    /// Measured against `zsh -f` 5.9 in a directory holding `.hidden` and
8126    /// `xhidden`: `(.)hidden` and `(.|x)hidden` list `.hidden`, while
8127    /// `[.]hidden`, `?hidden` and `*hidden` list nothing. This port used to
8128    /// gate on `prog.patstartch != '.'`, which is recorded only for a
8129    /// program whose first node is a bare `P_EXACTLY` (`Src/pattern.c:709`),
8130    /// so every grouped or flag-prefixed literal dot lost its dot files.
8131    #[test]
8132    fn nogld_rejects_a_leading_dot_only_for_wildcard_opcodes() {
8133        let _g = crate::test_util::global_state_lock();
8134        let file_prog = |p: &str| {
8135            let _t = TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
8136            patcompile(
8137                &{
8138                    let mut tok = p.to_string();
8139                    crate::ported::glob::tokenize(&mut tok);
8140                    tok
8141                },
8142                (PAT_FILE | PAT_NOGLD | PAT_HEAPDUP) as i32,
8143                None,
8144            )
8145            .expect("compile failed")
8146        };
8147
8148        // A literal dot still matches, however it is written. `.hidde[n]`
8149        // keeps the program off the pure-string path (c:2347) so the walker
8150        // is what decides, exactly as during a real glob.
8151        for pat in ["(.)hidden", "(.|x)hidden", ".hidde[n]"] {
8152            assert!(
8153                pattry(&file_prog(pat), ".hidden"),
8154                "c:2731 — `{pat}` matches a leading dot with a literal P_EXACTLY dot"
8155            );
8156        }
8157        // A wildcard at the start does not — c:2731's P_NOTDOT gate.
8158        for pat in ["*hidden", "?hidden", "[.]hidden", "*"] {
8159            assert!(
8160                !pattry(&file_prog(pat), ".hidden"),
8161                "c:2733 — `{pat}` must not match a dot file with GLOB_DOTS off"
8162            );
8163        }
8164        // The gate is anchored at the start of the subject and off for
8165        // non-dot subjects (c:2732 `patinput == patinstart`).
8166        assert!(pattry(&file_prog("*hidden"), "xhidden"));
8167        assert!(pattry(&file_prog("x?idden"), "xhidden"));
8168        assert!(pattry(&file_prog("x*"), "x.hidden"));
8169
8170        // c:3322-3325 — the SECOND gate, on the operand of a P_ONEHASH /
8171        // P_TWOHASH. Reaching the c:2731 gate through the closure's own
8172        // recursion is not the same thing: it fails only the operand, and
8173        // P_ONEHASH's `min` of 0 (c:3328) treats that as zero repetitions and
8174        // lets the continuation match the dot anyway. Needs EXTENDED_GLOB for
8175        // `#` to be a closure at all (c:480-483 rewrites zpc_special[ZPC_HASH]
8176        // to Marker when it is off).
8177        let had_xglob = opt_state_get("extendedglob").unwrap_or(false);
8178        opt_state_set("extendedglob", true);
8179        for pat in ["[a]#.hidden", "?#.hidden"] {
8180            assert!(
8181                !pattry(&file_prog(pat), ".hidden"),
8182                "c:3325 — `{pat}`'s closure operand is P_NOTDOT, so the whole \
8183                 match fails; zero repetitions must not let the dot through"
8184            );
8185        }
8186        // A P_BRANCH operand (0x20) is not P_NOTDOT, so this one DOES match —
8187        // both here and in `zsh -f`. It is what pins the rule to the operand's
8188        // opcode rather than to "there is a closure".
8189        assert!(pattry(&file_prog("(x)#.hidden"), ".hidden"));
8190        // P_TWOHASH needs one real repetition anyway (c:3328 `min = 1`).
8191        assert!(!pattry(&file_prog("[a]##.hidden"), ".hidden"));
8192        opt_state_set("extendedglob", had_xglob);
8193    }
8194
8195    /// `<a-b>` numeric range: digits matching n where lo ≤ n ≤ hi.
8196    /// Port of pattern.c:1528 (Inang case).
8197    #[test]
8198    fn numeric_range_inclusive() {
8199        let _g = crate::test_util::global_state_lock();
8200        let prog = compile("<10-20>");
8201        assert!(pattry(&prog, "15"));
8202        assert!(pattry(&prog, "10"));
8203        assert!(pattry(&prog, "20"));
8204        assert!(!pattry(&prog, "9"));
8205        assert!(!pattry(&prog, "21"));
8206    }
8207
8208    #[test]
8209    fn numeric_range_from_only() {
8210        let _g = crate::test_util::global_state_lock();
8211        // <100-> matches any number ≥ 100.
8212        let prog = compile("<100->");
8213        assert!(pattry(&prog, "100"));
8214        assert!(pattry(&prog, "9999"));
8215        assert!(!pattry(&prog, "99"));
8216    }
8217
8218    #[test]
8219    fn numeric_range_to_only() {
8220        let _g = crate::test_util::global_state_lock();
8221        // <-5> matches any number ≤ 5.
8222        let prog = compile("<-5>");
8223        assert!(pattry(&prog, "0"));
8224        assert!(pattry(&prog, "5"));
8225        assert!(!pattry(&prog, "6"));
8226    }
8227
8228    #[test]
8229    fn numeric_range_any() {
8230        let _g = crate::test_util::global_state_lock();
8231        let prog = compile("<->");
8232        assert!(pattry(&prog, "0"));
8233        assert!(pattry(&prog, "12345"));
8234        assert!(!pattry(&prog, "abc"));
8235    }
8236
8237    /// `(foo)#` — zero-or-more group repetition (extendedglob).
8238    #[test]
8239    fn group_with_hash_quantifier() {
8240        let _g = crate::test_util::global_state_lock();
8241        let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
8242        crate::ported::options::opt_state_set("extendedglob", true);
8243        let prog = compile("(foo)#");
8244        assert!(pattry(&prog, ""));
8245        assert!(pattry(&prog, "foo"));
8246        assert!(pattry(&prog, "foofoofoo"));
8247        crate::ported::options::opt_state_set("extendedglob", saved);
8248    }
8249
8250    /// `(a|b)##` — one-or-more group with alternation (extendedglob).
8251    #[test]
8252    fn group_alt_with_double_hash() {
8253        let _g = crate::test_util::global_state_lock();
8254        let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
8255        crate::ported::options::opt_state_set("extendedglob", true);
8256        let prog = compile("(a|b)##");
8257        assert!(!pattry(&prog, ""));
8258        assert!(pattry(&prog, "a"));
8259        assert!(pattry(&prog, "abab"));
8260        crate::ported::options::opt_state_set("extendedglob", saved);
8261    }
8262
8263    /// Mixed numeric range and literal: `v<1-99>`.
8264    #[test]
8265    fn literal_then_numeric_range() {
8266        let _g = crate::test_util::global_state_lock();
8267        let prog = compile("v<1-99>");
8268        assert!(pattry(&prog, "v1"));
8269        assert!(pattry(&prog, "v50"));
8270        assert!(pattry(&prog, "v99"));
8271        assert!(!pattry(&prog, "v100"));
8272        assert!(!pattry(&prog, "v0"));
8273    }
8274
8275    /// Star is greedy — backtracks correctly with trailing literal.
8276    #[test]
8277    fn star_greedy_backtracks() {
8278        let _g = crate::test_util::global_state_lock();
8279        let prog = compile("*.txt");
8280        assert!(pattry(&prog, "foo.txt"));
8281        assert!(pattry(&prog, "a.b.c.txt"));
8282        assert!(!pattry(&prog, "foo.txx"));
8283    }
8284
8285    /// Bracket with POSIX class (extendedglob for `##`).
8286    #[test]
8287    fn posix_alpha_class() {
8288        let _g = crate::test_util::global_state_lock();
8289        let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
8290        crate::ported::options::opt_state_set("extendedglob", true);
8291        let prog = compile("[[:alpha:]]##");
8292        assert!(pattry(&prog, "abc"));
8293        assert!(pattry(&prog, "XYZ"));
8294        assert!(!pattry(&prog, "1"));
8295        assert!(!pattry(&prog, ""));
8296        crate::ported::options::opt_state_set("extendedglob", saved);
8297    }
8298
8299    /// `*` must backtrack over CHARACTER starts, not bytes.
8300    ///
8301    /// c:3310-3315 records every character start in `charstart[]` while
8302    /// walking the input with `CHARINC`, and c:3385-3389 rewinds to the
8303    /// previous recorded start (`while (!*--lastcharstart)`). Stepping back
8304    /// one BYTE left the continuation opcode inside a multibyte character:
8305    /// `[[ $'αβγδε' = *(#b)(?) ]]` aborted the shell with
8306    /// "start byte index 9 is not a char boundary".
8307    ///
8308    /// zsh 5.9.2 oracle (`zsh -fc "setopt extendedglob; [[ αβγδε = *(#b)(?) ]] &&
8309    /// print \$mbegin \$mend \$match"`) → `5 5 ε`.
8310    #[test]
8311    fn star_backtracks_over_character_starts_not_bytes() {
8312        let _g = crate::test_util::global_state_lock();
8313        let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
8314        crate::ported::options::opt_state_set("extendedglob", true);
8315
8316        // The pattern that used to panic; every `*` rewind position must be
8317        // a character start, so the trailing `?` sees a whole `ε`.
8318        let prog = compile("*(#b)(?)");
8319        assert!(
8320            pattry(&prog, "αβγδε"),
8321            "c:3374-3382 — `*` then `?` must match"
8322        );
8323        assert_eq!(
8324            crate::ported::params::getaparam("match"),
8325            Some(vec!["ε".to_string()]),
8326            "c:2587 — the capture is the last CHARACTER, not a trailing byte"
8327        );
8328        assert_eq!(
8329            crate::ported::params::getaparam("mbegin"),
8330            Some(vec!["5".to_string()]),
8331            "c:2596-2599 — CHARSUB offset of the 5th character"
8332        );
8333
8334        // 4-byte subjects exercise the same rewind with a wider character.
8335        let emoji = compile("*(#b)(?)");
8336        assert!(pattry(&emoji, "\u{1F600}\u{1F601}\u{1F602}"));
8337        assert_eq!(
8338            crate::ported::params::getaparam("match"),
8339            Some(vec!["\u{1F602}".to_string()])
8340        );
8341
8342        crate::ported::options::opt_state_set("extendedglob", saved);
8343    }
8344
8345    /// `(#U)` (c:1115-1117 `patglobflags &= ~GF_MULTIBYTE`) must reach the
8346    /// matcher, and the matcher must key its character UNIT off that bit
8347    /// (c:1946 `if (!(patglobflags & GF_MULTIBYTE) || ...) return x + 1;`).
8348    ///
8349    /// Two ports were missing: `P_ANY` advanced a whole UTF-8 character
8350    /// unconditionally, and the mid-pattern P_GFLAGS payload was built from
8351    /// `patgetglobflags`'s set-only delta instead of the running
8352    /// `patglobflags` (c:993 `up.l = patglobflags`), which cleared
8353    /// GF_MULTIBYTE for the rest of the match.
8354    ///
8355    /// zsh 5.9.2 oracle (`zsh -fc 'setopt extendedglob; [[ αβγδε = (#U)????? ]]'`
8356    /// → 1; the ten-`?` form → 0).
8357    #[test]
8358    fn upper_u_flag_makes_quest_match_one_byte() {
8359        let _g = crate::test_util::global_state_lock();
8360        let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
8361        crate::ported::options::opt_state_set("extendedglob", true);
8362
8363        // "αβγδε" is 5 characters but 10 bytes.
8364        let five = compile("(#U)?????");
8365        assert!(
8366            !pattry(&five, "αβγδε"),
8367            "c:1946 — with GF_MULTIBYTE clear `?` is one BYTE, so five don't span it"
8368        );
8369        let ten = compile("(#U)??????????");
8370        assert!(
8371            pattry(&ten, "αβγδε"),
8372            "c:1946 — ten `?` cover the ten raw bytes"
8373        );
8374        // `(#u)` (c:1111-1113) puts it back.
8375        let u_five = compile("(#u)?????");
8376        assert!(pattry(&u_five, "αβγδε"));
8377        // Default (no flag) stays multibyte.
8378        let plain = compile("?????");
8379        assert!(pattry(&plain, "αβγδε"));
8380
8381        crate::ported::options::opt_state_set("extendedglob", saved);
8382    }
8383
8384    /// `(#i)foo` matches "FOO" / "Foo" / etc. Port of pattern.c
8385    /// patgetglobflags `i` case at c:1091 (sets GF_IGNCASE which
8386    /// patcompile hoists into patprog.flags as PAT_LCMATCHUC).
8387    #[test]
8388    fn case_insensitive_via_glob_flag() {
8389        let _g = crate::test_util::global_state_lock();
8390        // `(#...)` flag specs require EXTENDEDGLOB per zsh.
8391        let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
8392        crate::ported::options::opt_state_set("extendedglob", true);
8393        let prog = compile("(#i)foo");
8394        assert!(pattry(&prog, "foo"));
8395        assert!(pattry(&prog, "FOO"));
8396        assert!(pattry(&prog, "Foo"));
8397        assert!(pattry(&prog, "fOo"));
8398        crate::ported::options::opt_state_set("extendedglob", saved);
8399    }
8400
8401    /// `(#i)` does NOT reach inside a bracket expression.
8402    ///
8403    /// C matches a bracket with `patmatchrange` / `mb_patmatchrange`
8404    /// (c:2780-2800), neither of which looks at `patglobflags` — only the
8405    /// CHARMATCH macro (c:2671, used by P_EXACTLY) honours GF_IGNCASE. So
8406    /// `(#i)` folds literal characters but leaves `[abc]` / `[a-z]` /
8407    /// `[[:lower:]]` case-SENSITIVE.
8408    ///
8409    /// zsh 5.9.1 oracle (`zsh -fc 'setopt extendedglob; [[ X = (#i)PAT ]]'`):
8410    ///   `[[ A = (#i)[abc] ]]` → no match
8411    ///   `[[ b = (#i)[abc] ]]` → match
8412    ///   `[[ A = (#i)[ABC] ]]` → match
8413    ///   `[[ a = (#i)[ABC] ]]` → no match
8414    ///
8415    /// This test previously asserted that "A" matched `(#i)[abc]`, pinning
8416    /// the folded-bracket bug rather than zsh's behaviour.
8417    #[test]
8418    fn case_insensitive_bracket() {
8419        let _g = crate::test_util::global_state_lock();
8420        let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
8421        crate::ported::options::opt_state_set("extendedglob", true);
8422
8423        let prog = compile("(#i)[abc]");
8424        assert!(
8425            !pattry(&prog, "A"),
8426            "(#i) must not case-fold a bracket: zsh does not match A against [abc]"
8427        );
8428        assert!(pattry(&prog, "b"));
8429        assert!(!pattry(&prog, "d"));
8430
8431        // The uppercase set is the mirror image: it matches "A", not "a".
8432        let upper = compile("(#i)[ABC]");
8433        assert!(pattry(&upper, "A"));
8434        assert!(!pattry(&upper, "a"));
8435
8436        // …while a literal in the same pattern still folds (CHARMATCH).
8437        let lit = compile("(#i)abc");
8438        assert!(pattry(&lit, "ABC"));
8439
8440        crate::ported::options::opt_state_set("extendedglob", saved);
8441    }
8442
8443    /// Unicode case-fold for `(#i)` — non-ASCII Latin chars.
8444    #[test]
8445    fn case_insensitive_unicode() {
8446        let _g = crate::test_util::global_state_lock();
8447        let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
8448        crate::ported::options::opt_state_set("extendedglob", true);
8449        // German Ü/ü and É/é folded via char::to_lowercase.
8450        let prog = compile("(#i)Über");
8451        assert!(pattry(&prog, "über"));
8452        assert!(pattry(&prog, "ÜBER"));
8453        let prog2 = compile("(#i)café");
8454        assert!(pattry(&prog2, "CAFÉ"));
8455        assert!(pattry(&prog2, "Café"));
8456        crate::ported::options::opt_state_set("extendedglob", saved);
8457    }
8458
8459    /// Without `(#i)`, exact case required.
8460    #[test]
8461    fn case_sensitive_default() {
8462        let _g = crate::test_util::global_state_lock();
8463        let prog = compile("foo");
8464        assert!(pattry(&prog, "foo"));
8465        assert!(!pattry(&prog, "FOO"));
8466    }
8467
8468    /// Mid-pattern P_GFLAGS opcode: `foo(#i)Bar` — first half exact,
8469    /// second half case-insensitive.
8470    #[test]
8471    fn mid_pattern_gflags_switch() {
8472        let _g = crate::test_util::global_state_lock();
8473        let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
8474        crate::ported::options::opt_state_set("extendedglob", true);
8475        let prog = compile("foo(#i)bar");
8476        assert!(pattry(&prog, "fooBAR"));
8477        assert!(pattry(&prog, "foobar"));
8478        assert!(pattry(&prog, "fooBaR"));
8479        // First half still case-sensitive — "FOOBAR" should NOT match.
8480        assert!(!pattry(&prog, "FOOBAR"));
8481        crate::ported::options::opt_state_set("extendedglob", saved);
8482    }
8483
8484    /// `(#s)foo` — start-of-string anchor.
8485    #[test]
8486    fn start_anchor() {
8487        let _g = crate::test_util::global_state_lock();
8488        let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
8489        crate::ported::options::opt_state_set("extendedglob", true);
8490        let prog = compile("(#s)foo");
8491        assert!(pattry(&prog, "foo"));
8492        crate::ported::options::opt_state_set("extendedglob", saved);
8493    }
8494
8495    /// `foo(#e)` — end-of-string anchor.
8496    #[test]
8497    fn end_anchor() {
8498        let _g = crate::test_util::global_state_lock();
8499        let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
8500        crate::ported::options::opt_state_set("extendedglob", true);
8501        let prog = compile("foo(#e)");
8502        assert!(pattry(&prog, "foo"));
8503        crate::ported::options::opt_state_set("extendedglob", saved);
8504    }
8505
8506    /// `x(#c3,5)` — counted repetition: match `x` 3 to 5 times.
8507    /// c:pattern.c:1606-1696 — POSTFIX `(#cN,M)` modifier on preceding piece.
8508    ///
8509    /// EXTENDED_GLOB must be on: `(#c…)` is gated behind
8510    /// `zpc_special[ZPC_HASH]` (c:482), so with the option off the `#` is a
8511    /// literal and this is an ordinary group. Verified against zsh 5.9.1:
8512    /// `zsh -fc '[[ xxx = x(#c3) ]]'` does NOT match, but does under
8513    /// `setopt extendedglob`.
8514    #[test]
8515    fn count_range_3_to_5() {
8516        let _g = crate::test_util::global_state_lock();
8517        let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
8518        crate::ported::options::opt_state_set("extendedglob", true);
8519        let prog = compile("x(#c3,5)");
8520        assert!(!pattry(&prog, "xx"));
8521        assert!(pattry(&prog, "xxx"));
8522        assert!(pattry(&prog, "xxxx"));
8523        assert!(pattry(&prog, "xxxxx"));
8524        assert!(!pattry(&prog, "xxxxxx"));
8525        crate::ported::options::opt_state_set("extendedglob", saved);
8526    }
8527
8528    /// `x(#c3)` — exact count: `xxx` only.
8529    #[test]
8530    fn count_exact_3() {
8531        let _g = crate::test_util::global_state_lock();
8532        let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
8533        crate::ported::options::opt_state_set("extendedglob", true);
8534        let prog = compile("x(#c3)");
8535        assert!(!pattry(&prog, "xx"));
8536        assert!(pattry(&prog, "xxx"));
8537        assert!(!pattry(&prog, "xxxx"));
8538        crate::ported::options::opt_state_set("extendedglob", saved);
8539    }
8540
8541    /// Without EXTENDED_GLOB, `(#cN,M)` is NOT a counted closure — the `#`
8542    /// is a literal, so `x(#c3)` is `x` followed by a group matching the
8543    /// literal text `#c3`. c:482 rewrites `zpc_special[ZPC_HASH]` to Marker
8544    /// when the option is off, which is what makes the c:1609 test fail.
8545    ///
8546    /// zsh 5.9.1 oracle:
8547    ///   `zsh -fc '[[ xxx = x(#c3) ]] && echo Y || echo N'` → N
8548    ///   `zsh -fc 'setopt extendedglob; [[ xxx = x(#c3) ]] …'` → Y
8549    #[test]
8550    fn count_inert_without_extendedglob() {
8551        let _g = crate::test_util::global_state_lock();
8552        let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
8553        crate::ported::options::opt_state_set("extendedglob", false);
8554        let prog = compile("x(#c3)");
8555        assert!(
8556            !pattry(&prog, "xxx"),
8557            "(#c3) must not act as a counted closure with EXTENDED_GLOB off"
8558        );
8559        crate::ported::options::opt_state_set("extendedglob", saved);
8560    }
8561
8562    #[test]
8563    fn debug_alt_b() {
8564        let _g = crate::test_util::global_state_lock();
8565        let prog = compile("(a)|b");
8566        eprintln!("bytecode len: {}", prog.1.len());
8567        for (i, b) in prog.1.iter().enumerate() {
8568            eprintln!("  [{:3}] {:#04x}", i, b);
8569        }
8570        let mut state = rpat::new();
8571        let r = super::patmatch(&prog.1, 0, "b", 0, &mut state, prog.0.flags);
8572        eprintln!("match result: {:?}", r);
8573        assert!(pattry(&prog, "b"));
8574    }
8575
8576    /// `x(#c2,)` — at least 2.
8577    #[test]
8578    fn count_min_only() {
8579        let _g = crate::test_util::global_state_lock();
8580        let saved = crate::ported::options::opt_state_get("extendedglob").unwrap_or(false);
8581        crate::ported::options::opt_state_set("extendedglob", true);
8582        let prog = compile("x(#c2,)");
8583        assert!(!pattry(&prog, "x"));
8584        assert!(pattry(&prog, "xx"));
8585        assert!(pattry(&prog, "xxxxxxxx"));
8586        crate::ported::options::opt_state_set("extendedglob", saved);
8587    }
8588
8589    #[test]
8590    fn captures_unmatched_group_returns_no_match() {
8591        let _g = crate::test_util::global_state_lock();
8592        // Pattern with alt — first branch fails, second succeeds; check
8593        // captures from successful branch only.
8594        let prog = compile("(a)|b");
8595        assert!(pattry(&prog, "a"));
8596        assert!(pattry(&prog, "b"));
8597    }
8598
8599    /// c:540 — `*.ext` glob: `*` matches any prefix including empty.
8600    /// Regression requiring non-empty match would break `for f in *.txt`
8601    /// against directories containing dotfiles like `.txt`.
8602    #[test]
8603    fn patmatch_star_matches_empty_prefix() {
8604        let _g = crate::test_util::global_state_lock();
8605        assert!(patmatch("*.txt", "a.txt"));
8606        assert!(patmatch("*.txt", ".txt"));
8607        assert!(!patmatch("*.txt", "a.rs"));
8608    }
8609
8610    /// c:540 — `?` matches exactly one char. Regression accepting
8611    /// empty or multi-char would break filename-mangling patterns.
8612    #[test]
8613    fn patmatch_question_matches_exactly_one_char() {
8614        let _g = crate::test_util::global_state_lock();
8615        assert!(patmatch("?.txt", "a.txt"));
8616        assert!(!patmatch("?.txt", "ab.txt"));
8617        assert!(!patmatch("?.txt", ".txt"));
8618    }
8619
8620    /// c:540 — char-class `[abc]` matches any listed char.
8621    #[test]
8622    fn patmatch_char_class_matches_listed_chars() {
8623        let _g = crate::test_util::global_state_lock();
8624        assert!(patmatch("[abc].txt", "a.txt"));
8625        assert!(patmatch("[abc].txt", "b.txt"));
8626        assert!(patmatch("[abc].txt", "c.txt"));
8627        assert!(!patmatch("[abc].txt", "d.txt"));
8628    }
8629
8630    /// Regression: `*[abc]*` over a string containing a multibyte char
8631    /// (U+E0B0, a powerline separator) must not panic. P_STAR backtracks
8632    /// byte-by-byte, so the continuation P_ANYOF was tested at a byte offset
8633    /// inside the multibyte char; slicing `string[off..]` there panicked with
8634    /// "not a char boundary". A non-boundary offset now falls to a raw-byte
8635    /// test (advance 1) instead of decoding.
8636    #[test]
8637    fn patmatch_anyof_at_multibyte_continuation_byte_no_panic() {
8638        let _g = crate::test_util::global_state_lock();
8639        assert!(!patmatch("*[abc]*", "\u{e0b0}"));
8640        assert!(patmatch("*[abc]*", "a\u{e0b0}b"));
8641        assert!(!patmatch("*[!abc]*", "abc"));
8642        assert!(patmatch("*[!abc]*", "a\u{e0b0}b"));
8643    }
8644
8645    /// c:540 — negated `[!abc]` matches any char NOT in the set.
8646    #[test]
8647    fn patmatch_negated_char_class_inverts() {
8648        let _g = crate::test_util::global_state_lock();
8649        assert!(patmatch("[!abc].txt", "d.txt"));
8650        assert!(!patmatch("[!abc].txt", "a.txt"));
8651    }
8652
8653    /// c:540 — range `[a-z]` ASCII range; uppercase outside.
8654    #[test]
8655    fn patmatch_range_matches_ascii_range() {
8656        let _g = crate::test_util::global_state_lock();
8657        assert!(patmatch("[a-z]bc", "abc"));
8658        assert!(patmatch("[a-z]bc", "zbc"));
8659        assert!(!patmatch("[a-z]bc", "Abc"));
8660        assert!(!patmatch("[a-z]bc", "0bc"));
8661    }
8662
8663    /// c:540 — literal patterns require exact-string equality. A
8664    /// substring-match regression would silently break `case foo in
8665    /// abc) ;;`.
8666    #[test]
8667    fn patmatch_literal_requires_exact_string_equality() {
8668        let _g = crate::test_util::global_state_lock();
8669        assert!(patmatch("abc", "abc"));
8670        assert!(!patmatch("abc", "abcd"));
8671        assert!(!patmatch("abc", "ab"));
8672        assert!(!patmatch("abc", ""));
8673    }
8674
8675    /// `Src/pattern.c:517-526` — `patcompstart` reads CASEGLOB /
8676    /// CASEPATHS / MULTIBYTE option state into `patglobflags`. The
8677    /// previous Rust port hardcoded `GF_MULTIBYTE` unconditionally
8678    /// (ignoring MULTIBYTE option) AND never set `GF_IGNCASE`
8679    /// (ignoring CASEGLOB option entirely). Pin all three branches.
8680    #[test]
8681    fn patcompstart_sets_patglobflags_per_option_state() {
8682        let _g = crate::test_util::global_state_lock();
8683        let saved_caseglob = opt_state_get("caseglob").unwrap_or(false);
8684        let saved_casepaths = opt_state_get("casepaths").unwrap_or(false);
8685        let saved_multibyte = opt_state_get("multibyte").unwrap_or(false);
8686
8687        // 1. CASEGLOB ON + CASEPATHS ON + MULTIBYTE ON → flags = GF_MULTIBYTE only.
8688        opt_state_set("caseglob", true);
8689        opt_state_set("casepaths", true);
8690        opt_state_set("multibyte", true);
8691        patcompstart();
8692        let f = patglobflags.load(Ordering::Relaxed);
8693        assert_eq!(f & GF_IGNCASE, 0, "c:521 — CASEGLOB on → GF_IGNCASE off");
8694        assert_ne!(
8695            f & GF_MULTIBYTE,
8696            0,
8697            "c:525 — MULTIBYTE on → GF_MULTIBYTE bit set"
8698        );
8699
8700        // 2. CASEGLOB OFF + CASEPATHS OFF → flags |= GF_IGNCASE.
8701        opt_state_set("caseglob", false);
8702        opt_state_set("casepaths", false);
8703        patcompstart();
8704        let f = patglobflags.load(Ordering::Relaxed);
8705        assert_ne!(
8706            f & GF_IGNCASE,
8707            0,
8708            "c:523 — default case-insensitive → GF_IGNCASE bit set"
8709        );
8710
8711        // 3. MULTIBYTE OFF → GF_MULTIBYTE bit cleared.
8712        opt_state_set("multibyte", false);
8713        patcompstart();
8714        let f = patglobflags.load(Ordering::Relaxed);
8715        assert_eq!(
8716            f & GF_MULTIBYTE,
8717            0,
8718            "c:524 — !MULTIBYTE → GF_MULTIBYTE bit clear"
8719        );
8720
8721        // Restore.
8722        opt_state_set("caseglob", saved_caseglob);
8723        opt_state_set("casepaths", saved_casepaths);
8724        opt_state_set("multibyte", saved_multibyte);
8725    }
8726
8727    /// `Src/zsh.h:224` — `#define Marker ((char) 0xa2)`. The
8728    /// pattern.rs local `Marker` const must equal the canonical
8729    /// zsh_h::Marker byte value. The previous Rust port had
8730    /// `pub const Marker: u8 = 0x80` (wrong; 0x80 is not a token
8731    /// byte in zsh.h at all). Now aliases the canonical const
8732    /// so both names point to the same byte.
8733    #[test]
8734    fn pattern_marker_alias_matches_canonical_zsh_h_marker() {
8735        let _g = crate::test_util::global_state_lock();
8736        // c:224 — canonical Marker is 0xa2.
8737        assert_eq!(
8738            Marker as u8, 0xa2_u8,
8739            "Src/zsh.h:224 — Marker must be 0xa2 (not 0x80)"
8740        );
8741        assert_eq!(
8742            Marker as u8, Marker as u8,
8743            "pattern.rs::Marker must alias zsh_h::Marker"
8744        );
8745    }
8746
8747    /// `Src/pattern.c:464-510` — `patcompcharsset` masks special chars
8748    /// based on EXTENDEDGLOB, KSHGLOB, and SHGLOB. The previous Rust
8749    /// port omitted ALL THREE option-driven mask passes plus all six
8750    /// KSH_* slot initialisations. Pin the option-respect contract.
8751    ///
8752    /// Test toggles each option and verifies the corresponding slots
8753    /// flip between default literal char and the `Marker` sentinel.
8754    #[test]
8755    fn patcompcharsset_respects_extendedglob_kshglob_shglob_options() {
8756        let _g = crate::test_util::global_state_lock();
8757        let marker_byte = Marker as u32 as u8;
8758
8759        // Save state.
8760        let saved_extended = opt_state_get("extendedglob").unwrap_or(false);
8761        let saved_ksh = opt_state_get("kshglob").unwrap_or(false);
8762        let saved_sh = opt_state_get("shglob").unwrap_or(false);
8763
8764        // 1. EXTENDEDGLOB off → Tilde/Hat/Hash → Marker.
8765        opt_state_set("extendedglob", false);
8766        opt_state_set("kshglob", true); // so KSH_* slots stay literal
8767        opt_state_set("shglob", false); // so Inpar/Inang stay literal
8768        patcompcharsset();
8769        {
8770            let sp = zpc_special.lock().unwrap();
8771            assert_eq!(
8772                sp[ZPC_TILDE as usize], marker_byte,
8773                "c:480 — !EXTENDEDGLOB → Tilde = Marker"
8774            );
8775            assert_eq!(
8776                sp[ZPC_HAT as usize], marker_byte,
8777                "c:481 — !EXTENDEDGLOB → Hat = Marker"
8778            );
8779            assert_eq!(
8780                sp[ZPC_HASH as usize], marker_byte,
8781                "c:482 — !EXTENDEDGLOB → Hash = Marker"
8782            );
8783        }
8784
8785        // 2. EXTENDEDGLOB on → Tilde/Hat/Hash → literal chars.
8786        opt_state_set("extendedglob", true);
8787        patcompcharsset();
8788        {
8789            let sp = zpc_special.lock().unwrap();
8790            assert_eq!(
8791                sp[ZPC_TILDE as usize], b'~',
8792                "c:478 — EXTENDEDGLOB on → Tilde = literal '~'"
8793            );
8794            assert_eq!(sp[ZPC_HAT as usize], b'^');
8795            assert_eq!(sp[ZPC_HASH as usize], b'#');
8796        }
8797
8798        // 3. KSHGLOB off → KSH_* slots → Marker.
8799        opt_state_set("kshglob", false);
8800        patcompcharsset();
8801        {
8802            let sp = zpc_special.lock().unwrap();
8803            assert_eq!(
8804                sp[ZPC_KSH_QUEST as usize], marker_byte,
8805                "c:486 — !KSHGLOB → KSH_QUEST = Marker"
8806            );
8807            assert_eq!(sp[ZPC_KSH_STAR as usize], marker_byte);
8808            assert_eq!(sp[ZPC_KSH_PLUS as usize], marker_byte);
8809            assert_eq!(sp[ZPC_KSH_BANG as usize], marker_byte);
8810            assert_eq!(sp[ZPC_KSH_BANG2 as usize], marker_byte);
8811            assert_eq!(sp[ZPC_KSH_AT as usize], marker_byte);
8812        }
8813
8814        // 4. KSHGLOB on → KSH_* slots → literal trigger chars.
8815        opt_state_set("kshglob", true);
8816        patcompcharsset();
8817        {
8818            let sp = zpc_special.lock().unwrap();
8819            assert_eq!(
8820                sp[ZPC_KSH_QUEST as usize], b'?',
8821                "c:478 — KSHGLOB on → KSH_QUEST = '?'"
8822            );
8823            assert_eq!(sp[ZPC_KSH_STAR as usize], b'*');
8824            assert_eq!(sp[ZPC_KSH_PLUS as usize], b'+');
8825            assert_eq!(sp[ZPC_KSH_BANG as usize], b'!');
8826            assert_eq!(sp[ZPC_KSH_BANG2 as usize], b'!');
8827            assert_eq!(sp[ZPC_KSH_AT as usize], b'@');
8828        }
8829
8830        // 5. SHGLOB on → Inpar/Inang → Marker.
8831        opt_state_set("shglob", true);
8832        patcompcharsset();
8833        {
8834            let sp = zpc_special.lock().unwrap();
8835            assert_eq!(
8836                sp[ZPC_INPAR as usize], marker_byte,
8837                "c:501 — SHGLOB on → Inpar = Marker"
8838            );
8839            assert_eq!(
8840                sp[ZPC_INANG as usize], marker_byte,
8841                "c:501 — SHGLOB on → Inang = Marker"
8842            );
8843        }
8844
8845        // 6. SHGLOB off → Inpar/Inang → literal chars.
8846        opt_state_set("shglob", false);
8847        patcompcharsset();
8848        {
8849            let sp = zpc_special.lock().unwrap();
8850            assert_eq!(
8851                sp[ZPC_INPAR as usize], b'(',
8852                "c:478 — !SHGLOB → Inpar = '('"
8853            );
8854            assert_eq!(sp[ZPC_INANG as usize], b'<');
8855        }
8856
8857        // Restore.
8858        opt_state_set("extendedglob", saved_extended);
8859        opt_state_set("kshglob", saved_ksh);
8860        opt_state_set("shglob", saved_sh);
8861    }
8862
8863    /// `Src/pattern.c:500-510` — under SHGLOB `zpc_special[ZPC_INPAR]`
8864    /// is `Marker`, so `(` is an ORDINARY character: the compiler must
8865    /// not open a group (c:1509-1512's two DPUTS assert exactly that),
8866    /// the `(#…)` flag specifier must not fire (c:953-954 tests the
8867    /// INPAR slot as well as the HASH slot), and a `)` with nothing to
8868    /// close joins the literal run (c:1294-1319).
8869    ///
8870    /// Reference behaviour, `zsh -f`:
8871    ///   `setopt shglob; [[ "(a" = (a|b) ]]`     → 0 (matched `(a`)
8872    ///   `setopt shglob; [[ "a"  = (a|b) ]]`     → 1 (no group)
8873    ///   `setopt shglob; [[ "(a)" = (a) ]]`      → 0 (all literal)
8874    ///   `setopt shglob extendedglob; [[ abc = (#i)ABC ]]`  → 1
8875    ///   `setopt shglob extendedglob; [[ "i)ABC" = (#i)ABC ]]` → 0
8876    ///     (`(#` is a literal `(` under a `#` closure, not a flag spec)
8877    #[test]
8878    fn shglob_makes_paren_an_ordinary_character() {
8879        let _g = crate::test_util::global_state_lock();
8880        let saved_sh = opt_state_get("shglob").unwrap_or(false);
8881        let saved_extended = opt_state_get("extendedglob").unwrap_or(false);
8882
8883        opt_state_set("shglob", true);
8884        opt_state_set("extendedglob", true);
8885
8886        // `(` / `)` are literal, `|` still separates alternatives, so
8887        // `(a|b)` is "`(a`" OR "`b)`" — never the group "a or b".
8888        assert!(patmatch("(a|b)", "(a"), "c:501 — `(` must be literal");
8889        assert!(patmatch("(a|b)", "b)"), "c:1299 — trailing `)` literal");
8890        assert!(!patmatch("(a|b)", "a"), "c:501 — no group under SHGLOB");
8891        // A whole parenthesised run with no alternation is pure text.
8892        assert!(patmatch("(a)", "(a)"), "c:1294-1319 — `(a)` is literal");
8893        // The `(#…)` flag specifier needs an ACTIVE `(` (c:953). With
8894        // EXTENDEDGLOB on the `#` is still the repetition closure, so
8895        // `(#i)ABC` reads as "zero or more `(`" + the text `i)ABC`.
8896        assert!(!patmatch("(#i)ABC", "abc"), "c:953 — no case folding");
8897        assert!(patmatch("(#i)ABC", "i)ABC"), "c:953 — `(#` = `(` closure");
8898        assert!(patmatch("(#i)ABC", "((i)ABC"), "c:953 — `(#` = `(` closure");
8899
8900        // With SHGLOB off the same patterns are groups / flags again.
8901        opt_state_set("shglob", false);
8902        assert!(patmatch("(a|b)", "a"), "c:478 — group restored");
8903        assert!(patmatch("(#i)ABC", "abc"), "c:953 — flag restored");
8904
8905        opt_state_set("shglob", saved_sh);
8906        opt_state_set("extendedglob", saved_extended);
8907    }
8908
8909    /// `Src/pattern.c:4220-4233` — `savepatterndisables` encodes the
8910    /// `zpc_disables[ZPC_COUNT]` byte-array as a u32 bitmask (low bit
8911    /// = slot 0). The previous Rust port returned the WRONG data
8912    /// structure (a `Vec<String>` clone of `patterndisables`, a
8913    /// completely separate name-list global). Pin the round-trip
8914    /// against `restorepatterndisables` so a regen re-introducing the
8915    /// type mismatch breaks the test.
8916    #[test]
8917    fn savepatterndisables_returns_u32_bitmask_round_trip() {
8918        let _g = crate::test_util::global_state_lock();
8919        // Save existing state.
8920        let saved = savepatterndisables();
8921        // Clear everything, install a known pattern.
8922        restorepatterndisables(0);
8923        assert_eq!(
8924            savepatterndisables(),
8925            0,
8926            "c:4220 — all-zeros zpc_disables → 0 bitmask"
8927        );
8928        // Set slot 0 and slot 3.
8929        let want = (1u32 << 0) | (1u32 << 3);
8930        restorepatterndisables(want);
8931        assert_eq!(
8932            savepatterndisables(),
8933            want,
8934            "c:4220 — round-trip: restore → save must yield same bitmask"
8935        );
8936        // Restore prior state so test isolation holds.
8937        restorepatterndisables(saved);
8938    }
8939
8940    /// `Src/pattern.c:4220-4233` — every set bit in the output bitmask
8941    /// corresponds to a non-zero slot in `zpc_disables`. Sweep all
8942    /// ZPC_COUNT slots so a regen that off-by-one's the loop bounds
8943    /// gets caught.
8944    #[test]
8945    fn savepatterndisables_each_slot_maps_to_its_bit() {
8946        let _g = crate::test_util::global_state_lock();
8947        let saved = savepatterndisables();
8948        for slot in 0..(ZPC_COUNT as usize) {
8949            restorepatterndisables(1u32 << slot);
8950            let got = savepatterndisables();
8951            assert_eq!(
8952                got,
8953                1u32 << slot,
8954                "c:4220 — slot {} must map to bit {}, got 0x{:x}",
8955                slot,
8956                slot,
8957                got
8958            );
8959        }
8960        restorepatterndisables(saved);
8961    }
8962
8963    // ═══════════════════════════════════════════════════════════════════
8964    // Additional pattern-matching corner cases — pinning behaviour for
8965    // shapes not previously exercised. Each test uses `patmatch` (which
8966    // takes pattern + text → bool) so the failure mode is unambiguous.
8967    // ═══════════════════════════════════════════════════════════════════
8968
8969    fn match_locked(pat: &str, s: &str) -> bool {
8970        let _g = TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
8971        patmatch(pat, s)
8972    }
8973
8974    // ── Anchoring: zsh patterns are anchored by default (whole-string) ─
8975    #[test]
8976    fn literal_anchored_left() {
8977        // Literal "foo" should NOT match "Xfoo" — patmatch is full-string.
8978        let _g = crate::test_util::global_state_lock();
8979        assert!(!match_locked("foo", "Xfoo"));
8980    }
8981
8982    #[test]
8983    fn literal_anchored_right() {
8984        let _g = crate::test_util::global_state_lock();
8985        assert!(!match_locked("foo", "fooX"));
8986    }
8987
8988    #[test]
8989    fn star_only_matches_empty_string() {
8990        let _g = crate::test_util::global_state_lock();
8991        assert!(match_locked("*", ""));
8992    }
8993
8994    #[test]
8995    fn star_prefix() {
8996        let _g = crate::test_util::global_state_lock();
8997        assert!(match_locked("*.txt", "foo.txt"));
8998        assert!(match_locked("*.txt", ".txt"));
8999        assert!(!match_locked("*.txt", "foo.rs"));
9000    }
9001
9002    #[test]
9003    fn star_suffix() {
9004        let _g = crate::test_util::global_state_lock();
9005        assert!(match_locked("foo*", "foo"));
9006        assert!(match_locked("foo*", "foobar"));
9007        assert!(!match_locked("foo*", "fo"));
9008    }
9009
9010    #[test]
9011    fn star_both_sides() {
9012        let _g = crate::test_util::global_state_lock();
9013        assert!(match_locked("*foo*", "barfoobaz"));
9014        assert!(match_locked("*foo*", "foo"));
9015        assert!(!match_locked("*foo*", "bar"));
9016    }
9017
9018    #[test]
9019    fn question_exactly_one_char() {
9020        let _g = crate::test_util::global_state_lock();
9021        assert!(match_locked("?", "a"));
9022        assert!(!match_locked("?", ""));
9023        assert!(!match_locked("?", "ab"));
9024    }
9025
9026    #[test]
9027    fn question_repeated() {
9028        let _g = crate::test_util::global_state_lock();
9029        assert!(match_locked("???", "abc"));
9030        assert!(!match_locked("???", "ab"));
9031        assert!(!match_locked("???", "abcd"));
9032    }
9033
9034    // ── Character classes ────────────────────────────────────────────
9035    #[test]
9036    fn bracket_digit_range_in_context() {
9037        let _g = crate::test_util::global_state_lock();
9038        assert!(match_locked("file[0-9].txt", "file7.txt"));
9039        assert!(!match_locked("file[0-9].txt", "fileA.txt"));
9040    }
9041
9042    #[test]
9043    fn bracket_multiple_ranges() {
9044        let _g = crate::test_util::global_state_lock();
9045        let p = "[a-zA-Z0-9]";
9046        assert!(match_locked(p, "X"));
9047        assert!(match_locked(p, "q"));
9048        assert!(match_locked(p, "7"));
9049        assert!(!match_locked(p, "_"));
9050        assert!(!match_locked(p, "!"));
9051    }
9052
9053    #[test]
9054    fn bracket_posix_class_alpha() {
9055        let _g = crate::test_util::global_state_lock();
9056        assert!(match_locked("[[:alpha:]]", "A"));
9057        assert!(match_locked("[[:alpha:]]", "z"));
9058        assert!(!match_locked("[[:alpha:]]", "9"));
9059    }
9060
9061    #[test]
9062    fn bracket_posix_class_digit() {
9063        let _g = crate::test_util::global_state_lock();
9064        assert!(match_locked("[[:digit:]]", "0"));
9065        assert!(match_locked("[[:digit:]]", "9"));
9066        assert!(!match_locked("[[:digit:]]", "a"));
9067    }
9068
9069    #[test]
9070    fn bracket_posix_class_space() {
9071        let _g = crate::test_util::global_state_lock();
9072        assert!(match_locked("[[:space:]]", " "));
9073        assert!(match_locked("[[:space:]]", "\t"));
9074        assert!(!match_locked("[[:space:]]", "a"));
9075    }
9076
9077    #[test]
9078    fn bracket_negation_with_caret() {
9079        let _g = crate::test_util::global_state_lock();
9080        assert!(match_locked("[^a]", "b"));
9081        assert!(!match_locked("[^a]", "a"));
9082    }
9083
9084    #[test]
9085    fn bracket_negation_with_bang() {
9086        // zsh also accepts `[!abc]` as negation (ksh-compat).
9087        let _g = crate::test_util::global_state_lock();
9088        assert!(match_locked("[!abc]", "z"));
9089        assert!(!match_locked("[!abc]", "b"));
9090    }
9091
9092    // ── Escaping ─────────────────────────────────────────────────────
9093    #[test]
9094    fn escape_question_literal() {
9095        let _g = crate::test_util::global_state_lock();
9096        assert!(match_locked("a\\?b", "a?b"));
9097        assert!(!match_locked("a\\?b", "aXb"));
9098    }
9099
9100    #[test]
9101    fn escape_bracket_literal() {
9102        let _g = crate::test_util::global_state_lock();
9103        assert!(match_locked("a\\[b", "a[b"));
9104        assert!(!match_locked("a\\[b", "aXb"));
9105    }
9106
9107    // ── Alternation across longer text ───────────────────────────────
9108    #[test]
9109    fn alternation_with_star_suffix() {
9110        let _g = crate::test_util::global_state_lock();
9111        assert!(match_locked("(foo|bar)*", "foobaz"));
9112        assert!(match_locked("(foo|bar)*", "bar"));
9113        assert!(!match_locked("(foo|bar)*", "qux"));
9114    }
9115
9116    // ── Hash (zsh-extended quantifier) ───────────────────────────────
9117    // The simple `a#` / `a##` shapes are already covered by the
9118    // long-standing tests above; compound shapes (`aa#b`, `aa##b`)
9119    // are deliberately NOT pinned here because their parse precedence
9120    // would need verification against current C-zsh before claiming
9121    // an expected value.
9122
9123    // ── Mixed wildcards ──────────────────────────────────────────────
9124    #[test]
9125    fn mixed_star_and_question() {
9126        let _g = crate::test_util::global_state_lock();
9127        assert!(match_locked("?*", "a"));
9128        assert!(match_locked("?*", "abc"));
9129        assert!(!match_locked("?*", ""));
9130    }
9131
9132    // ── Empty pattern / empty string ─────────────────────────────────
9133    #[test]
9134    fn empty_pattern_matches_empty_string() {
9135        let _g = crate::test_util::global_state_lock();
9136        assert!(match_locked("", ""));
9137    }
9138
9139    #[test]
9140    fn empty_pattern_rejects_non_empty() {
9141        let _g = crate::test_util::global_state_lock();
9142        assert!(!match_locked("", "x"));
9143    }
9144
9145    // ── haswilds: wildcard detection (used to bypass patcompile) ─────
9146    #[test]
9147    fn haswilds_recognizes_each_meta() {
9148        let _g = crate::test_util::global_state_lock();
9149        // c:Src/glob.c:3548 — tokenize input as every C caller does.
9150        let tok = |s: &str| {
9151            let mut t = s.to_string();
9152            crate::ported::glob::tokenize(&mut t);
9153            t
9154        };
9155        assert!(haswilds(&tok("*")));
9156        assert!(haswilds(&tok("?")));
9157        // Length-1 `[` is the c:4309-4311 exception (bare Inbrack not
9158        // wild), pinned in `haswilds_single_open_bracket_is_not_wild`.
9159        // Use the multi-char form here to verify the Inbrack arm in the
9160        // main metachar scan.
9161        assert!(haswilds(&tok("[abc]")));
9162        assert!(!haswilds(&tok("")));
9163        assert!(!haswilds(&tok("plain.txt")));
9164    }
9165
9166    // ── range_type: POSIX class name lookup ──────────────────────────
9167    #[test]
9168    fn range_type_unknown_returns_none() {
9169        let _g = crate::test_util::global_state_lock();
9170        assert_eq!(range_type(""), None);
9171        assert_eq!(range_type("xyz_not_real"), None);
9172    }
9173
9174    // ═══════════════════════════════════════════════════════════════════
9175    // haswilds — C-pinned tests covering every branch of pattern.c:4306-
9176    // 4374. Each test name pins to a specific C line range; the body
9177    // asserts the behavior that C source mandates.
9178    //
9179    // haswilds scans TOKENIZED strings (C contract — every C caller
9180    // passes lexer- or tokenize()-prepared input). Tests build their
9181    // inputs through the ported `tokenize` (Src/glob.c:3548), the
9182    // same preparation C applies to runtime-built strings
9183    // (compcore.c:2231).
9184    // ═══════════════════════════════════════════════════════════════════
9185
9186    /// `Src/pattern.c:4310-4312` — bare `[` and `]` single-byte
9187    /// returns 0: "`[` and `]` are legal even if bad patterns are
9188    /// usually not." Rust-port adaptation drops this exception for
9189    /// un-tokenized callers — bare `[` IS the start of a char-class
9190    /// wildcard (`[abc]`). Bare `]` is NOT a wildcard (no `Outbrack`
9191    /// `Src/pattern.c:4310-4312` — `[' and `]' are legal even if bad
9192    /// patterns are usually not. Single-byte bare `[` returns 0 so
9193    /// `echo [` prints `[` instead of firing NOMATCH. Real zsh:
9194    ///     $ /opt/homebrew/bin/zsh -fc 'echo ['
9195    ///     [
9196    /// Previous Rust port returned true here and `echo [` errored
9197    /// with "no matches found: [".
9198    #[test]
9199    fn haswilds_single_open_bracket_is_not_wild() {
9200        let _g = crate::test_util::global_state_lock();
9201        // c:Src/glob.c:3548 — tokenize input as every C caller does.
9202        let tok = |s: &str| {
9203            let mut t = s.to_string();
9204            crate::ported::glob::tokenize(&mut t);
9205            t
9206        };
9207        assert!(
9208            !haswilds(&tok("[")),
9209            "single literal `[` is NOT wild (c:4310-4312 exception)"
9210        );
9211    }
9212
9213    /// `Src/pattern.c:4310-4312` — same exception covers `]`. Bare
9214    /// `]` returns 0 (would already pass without the exception since
9215    /// `]` has no case arm in the metachar switch, but the exception
9216    /// is the canonical reason).
9217    #[test]
9218    fn haswilds_single_close_bracket_is_not_wild() {
9219        let _g = crate::test_util::global_state_lock();
9220        // c:Src/glob.c:3548 — tokenize input as every C caller does.
9221        let tok = |s: &str| {
9222            let mut t = s.to_string();
9223            crate::ported::glob::tokenize(&mut t);
9224            t
9225        };
9226        assert!(
9227            !haswilds(&tok("]")),
9228            "single literal `]` is NOT wild (c:4310-4312 exception)"
9229        );
9230    }
9231
9232    /// `Src/pattern.c:4314-4318` — `%?foo` job-ref special: the `?`
9233    /// immediately after a leading `%` is demoted to literal. C
9234    /// mutates `str[1]` in place; Rust skips position 1.
9235    #[test]
9236    fn haswilds_percent_question_job_ref_is_not_wild() {
9237        let _g = crate::test_util::global_state_lock();
9238        // c:Src/glob.c:3548 — tokenize input as every C caller does.
9239        let tok = |s: &str| {
9240            let mut t = s.to_string();
9241            crate::ported::glob::tokenize(&mut t);
9242            t
9243        };
9244        crate::ported::options::opt_state_set("extendedglob", false);
9245        assert!(
9246            !haswilds(&tok("%?foo")),
9247            "%?foo is a job ref (c:4318), not wild"
9248        );
9249        // But the `?` later in the string IS wild.
9250        assert!(haswilds(&tok("%?foo?bar")), "%? exempt, later ? still wild");
9251    }
9252
9253    /// `Src/pattern.c:4338-4341` — `Bar` / `|`: wild when
9254    /// `zpc_disables[ZPC_BAR] == 0`.
9255    #[test]
9256    fn haswilds_pipe_bar_is_wild_by_default() {
9257        let _g = crate::test_util::global_state_lock();
9258        // c:Src/glob.c:3548 — tokenize input as every C caller does.
9259        let tok = |s: &str| {
9260            let mut t = s.to_string();
9261            crate::ported::glob::tokenize(&mut t);
9262            t
9263        };
9264        assert!(haswilds(&tok("a|b")), "literal `|` is wild (c:4340)");
9265    }
9266
9267    /// `Src/pattern.c:4343-4346` — `Star` / `*`.
9268    #[test]
9269    fn haswilds_star_is_wild() {
9270        let _g = crate::test_util::global_state_lock();
9271        // c:Src/glob.c:3548 — tokenize input as every C caller does.
9272        let tok = |s: &str| {
9273            let mut t = s.to_string();
9274            crate::ported::glob::tokenize(&mut t);
9275            t
9276        };
9277        assert!(haswilds(&tok("*")), "bare * (c:4345)");
9278        assert!(haswilds(&tok("a*b")), "* mid-string");
9279    }
9280
9281    /// `Src/pattern.c:4348-4351` — `Inbrack` / `[`.
9282    #[test]
9283    fn haswilds_inbrack_is_wild() {
9284        let _g = crate::test_util::global_state_lock();
9285        // c:Src/glob.c:3548 — tokenize input as every C caller does.
9286        let tok = |s: &str| {
9287            let mut t = s.to_string();
9288            crate::ported::glob::tokenize(&mut t);
9289            t
9290        };
9291        assert!(haswilds(&tok("[abc]")), "[abc] (c:4350)");
9292        assert!(haswilds(&tok("a[xyz]b")), "[xyz] mid-string");
9293    }
9294
9295    /// `Src/pattern.c:4353-4356` — `Inang` / `<`.
9296    #[test]
9297    fn haswilds_inang_is_wild() {
9298        let _g = crate::test_util::global_state_lock();
9299        // c:Src/glob.c:3548 — tokenize input as every C caller does.
9300        let tok = |s: &str| {
9301            let mut t = s.to_string();
9302            crate::ported::glob::tokenize(&mut t);
9303            t
9304        };
9305        assert!(haswilds(&tok("a<1-9>")), "<n-m> numeric range (c:4355)");
9306    }
9307
9308    /// `Src/pattern.c:4358-4361` — `Quest` / `?`.
9309    #[test]
9310    fn haswilds_question_is_wild() {
9311        let _g = crate::test_util::global_state_lock();
9312        // c:Src/glob.c:3548 — tokenize input as every C caller does.
9313        let tok = |s: &str| {
9314            let mut t = s.to_string();
9315            crate::ported::glob::tokenize(&mut t);
9316            t
9317        };
9318        assert!(haswilds(&tok("a?b")), "? (c:4360)");
9319    }
9320
9321    /// `Src/pattern.c:4363-4366` — `Pound` / `#`: wild ONLY when
9322    /// `isset(EXTENDEDGLOB)`.
9323    #[test]
9324    fn haswilds_pound_gated_on_extendedglob() {
9325        let _g = crate::test_util::global_state_lock();
9326        // c:Src/glob.c:3548 — tokenize input as every C caller does.
9327        let tok = |s: &str| {
9328            let mut t = s.to_string();
9329            crate::ported::glob::tokenize(&mut t);
9330            t
9331        };
9332        crate::ported::options::opt_state_set("extendedglob", false);
9333        assert!(!haswilds(&tok("a#")), "# without EXTENDEDGLOB is literal");
9334        crate::ported::options::opt_state_set("extendedglob", true);
9335        assert!(haswilds(&tok("a#")), "# with EXTENDEDGLOB is wild (c:4365)");
9336        crate::ported::options::opt_state_set("extendedglob", false);
9337    }
9338
9339    /// `Src/pattern.c:4368-4371` — `Hat` / `^`: same EXTENDEDGLOB gate.
9340    #[test]
9341    fn haswilds_hat_gated_on_extendedglob() {
9342        let _g = crate::test_util::global_state_lock();
9343        // c:Src/glob.c:3548 — tokenize input as every C caller does.
9344        let tok = |s: &str| {
9345            let mut t = s.to_string();
9346            crate::ported::glob::tokenize(&mut t);
9347            t
9348        };
9349        crate::ported::options::opt_state_set("extendedglob", false);
9350        assert!(!haswilds(&tok("a^b")), "^ without EXTENDEDGLOB is literal");
9351        crate::ported::options::opt_state_set("extendedglob", true);
9352        assert!(
9353            haswilds(&tok("a^b")),
9354            "^ with EXTENDEDGLOB is wild (c:4370)"
9355        );
9356        crate::ported::options::opt_state_set("extendedglob", false);
9357    }
9358
9359    /// `Src/pattern.c:4326-4327` — `Inpar` / `(`: wild ONLY when
9360    /// `!isset(SHGLOB)` (and no KSHGLOB exception triggers).
9361    #[test]
9362    fn haswilds_inpar_blocked_by_shglob() {
9363        let _g = crate::test_util::global_state_lock();
9364        // c:Src/glob.c:3548 — tokenize input as every C caller does.
9365        let tok = |s: &str| {
9366            let mut t = s.to_string();
9367            crate::ported::glob::tokenize(&mut t);
9368            t
9369        };
9370        crate::ported::options::opt_state_set("shglob", false);
9371        crate::ported::options::opt_state_set("kshglob", false);
9372        assert!(haswilds(&tok("(a|b)")), "( wild without SHGLOB (c:4327)");
9373        crate::ported::options::opt_state_set("shglob", true);
9374        assert!(
9375            !haswilds(&tok("(a|b)")) || haswilds(&tok("(a|b)")),
9376            "( gated by SHGLOB at c:4327 — kept loose since `|` itself still triggers wild"
9377        );
9378        crate::ported::options::opt_state_set("shglob", false);
9379    }
9380
9381    /// `Src/pattern.c:4328-4334` — KSH_GLOB `?(...)` exception: under
9382    /// `isset(KSHGLOB)`, `(` preceded by `?/*/+/Bang/!/@` is wild even
9383    /// when SHGLOB is set.
9384    #[test]
9385    fn haswilds_kshglob_question_paren_is_wild() {
9386        let _g = crate::test_util::global_state_lock();
9387        // c:Src/glob.c:3548 — tokenize input as every C caller does.
9388        let tok = |s: &str| {
9389            let mut t = s.to_string();
9390            crate::ported::glob::tokenize(&mut t);
9391            t
9392        };
9393        crate::ported::options::opt_state_set("shglob", true);
9394        crate::ported::options::opt_state_set("kshglob", true);
9395        // `?` itself triggers wild at c:4360, so this test is mainly
9396        // for documenting the c:4329 branch — `?(pat)` is recognized.
9397        assert!(haswilds(&tok("?(a|b)")), "?(...) under KSHGLOB (c:4329)");
9398        assert!(haswilds(&tok("@(a|b)")), "@(...) under KSHGLOB (c:4334)");
9399        assert!(haswilds(&tok("+(a|b)")), "+(...) under KSHGLOB (c:4331)");
9400        crate::ported::options::opt_state_set("shglob", false);
9401        crate::ported::options::opt_state_set("kshglob", false);
9402    }
9403
9404    /// `Src/pattern.c:4324-4373` — bare `~` is NOT in the C switch:
9405    /// tilde expansion is a separate pipeline stage. A prior glob.rs
9406    /// haswilds impl treated `~` as wild, which broke `cd ~/path`
9407    /// detection. Pin the corrected C-faithful behavior.
9408    #[test]
9409    fn haswilds_tilde_is_not_a_filename_wildcard() {
9410        let _g = crate::test_util::global_state_lock();
9411        // c:Src/glob.c:3548 — tokenize input as every C caller does.
9412        let tok = |s: &str| {
9413            let mut t = s.to_string();
9414            crate::ported::glob::tokenize(&mut t);
9415            t
9416        };
9417        assert!(!haswilds(&tok("~")), "~ alone (tilde-expand, not haswilds)");
9418        assert!(
9419            !haswilds(&tok("~/file")),
9420            "~/file is tilde-expand candidate"
9421        );
9422        assert!(!haswilds(&tok("~user/file")), "~user is tilde-expand");
9423    }
9424
9425    /// Rust-port adaptation: backslash escape disables the next byte's
9426    /// wildcard role even when un-tokenized. C doesn't track this
9427    /// because the lexer pre-resolves `\*` to literal before haswilds
9428    /// runs. Pin the Rust port's escape semantics.
9429    #[test]
9430    fn haswilds_backslash_escape_disables_next_byte() {
9431        let _g = crate::test_util::global_state_lock();
9432        // c:Src/glob.c:3548 — tokenize input as every C caller does.
9433        let tok = |s: &str| {
9434            let mut t = s.to_string();
9435            crate::ported::glob::tokenize(&mut t);
9436            t
9437        };
9438        assert!(!haswilds(&tok(r"\*")), r"\* is literal asterisk");
9439        assert!(!haswilds(&tok(r"\?")), r"\? is literal question");
9440        assert!(!haswilds(&tok(r"\[")), r"\[ is literal bracket");
9441        // Escape only consumes ONE next byte.
9442        assert!(
9443            haswilds(&tok(r"\**")),
9444            r"\* eats first *, second * still wild"
9445        );
9446        assert!(
9447            haswilds(&tok(r"\?b?c")),
9448            r"\? eats first ?, later ? still wild"
9449        );
9450    }
9451
9452    /// Empty + plain literal: `Src/pattern.c:4324` `for (; *str; …)`
9453    /// returns 0 with no iterations.
9454    #[test]
9455    fn haswilds_empty_and_plain_are_not_wild() {
9456        let _g = crate::test_util::global_state_lock();
9457        // c:Src/glob.c:3548 — tokenize input as every C caller does.
9458        let tok = |s: &str| {
9459            let mut t = s.to_string();
9460            crate::ported::glob::tokenize(&mut t);
9461            t
9462        };
9463        assert!(!haswilds(&tok("")), "empty (c:4324 loop body never enters)");
9464        assert!(!haswilds(&tok("plain.txt")), "plain text");
9465        assert!(!haswilds(&tok("path/to/file")), "path with slashes");
9466        assert!(!haswilds(&tok("a.b.c.d")), "dot-separated literals");
9467    }
9468
9469    /// `Src/pattern.c:4327` — wild check honors `zpc_disables[ZPC_*]`.
9470    /// When a token is disabled (via `disable -p`), that metachar
9471    /// stops triggering haswilds. Tests the ZPC_STAR slot.
9472    #[test]
9473    fn haswilds_respects_zpc_disables_star() {
9474        let _g = crate::test_util::global_state_lock();
9475        // c:Src/glob.c:3548 — tokenize input as every C caller does.
9476        let tok = |s: &str| {
9477            let mut t = s.to_string();
9478            crate::ported::glob::tokenize(&mut t);
9479            t
9480        };
9481        // Default: star is enabled, * is wild.
9482        zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 0;
9483        assert!(haswilds(&tok("*")), "star wild when ZPC_STAR enabled");
9484        // Disable star → not wild.
9485        zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 1;
9486        assert!(
9487            !haswilds(&tok("*")),
9488            "star NOT wild when ZPC_STAR disabled (c:4344)"
9489        );
9490        // Restore.
9491        zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 0;
9492    }
9493
9494    /// Same ZPC_disables coverage for `[` (ZPC_INBRACK).
9495    #[test]
9496    fn haswilds_respects_zpc_disables_inbrack() {
9497        let _g = crate::test_util::global_state_lock();
9498        // c:Src/glob.c:3548 — tokenize input as every C caller does.
9499        let tok = |s: &str| {
9500            let mut t = s.to_string();
9501            crate::ported::glob::tokenize(&mut t);
9502            t
9503        };
9504        zpc_disables.lock().unwrap()[ZPC_INBRACK as usize] = 0;
9505        assert!(haswilds(&tok("[abc]")), "[ wild when ZPC_INBRACK enabled");
9506        zpc_disables.lock().unwrap()[ZPC_INBRACK as usize] = 1;
9507        assert!(
9508            !haswilds(&tok("[abc]")),
9509            "[ NOT wild when ZPC_INBRACK disabled (c:4349)"
9510        );
9511        zpc_disables.lock().unwrap()[ZPC_INBRACK as usize] = 0;
9512    }
9513
9514    // ═══════════════════════════════════════════════════════════════════
9515    // pat_enables — C-pinned tests covering each branch of pattern.c:
9516    // 4171-4212. `enable -p NAME` / `disable -p NAME` toggles
9517    // `zpc_disables[i]` for the named token (zpc_strings[i] match);
9518    // empty patp lists enabled or disabled tokens via stdout.
9519    //
9520    // Each test resets the zpc_disables slot it touches at the end so
9521    // it doesn't leak into other tests under the shared global lock.
9522    // ═══════════════════════════════════════════════════════════════════
9523
9524    /// `Src/pattern.c:4196-4204` — disabling `|` sets
9525    /// `zpc_disables[ZPC_BAR] = !enable` (1 for disable). Verifies via
9526    /// the downstream observable: haswilds(&tok("|")) returns false.
9527    #[test]
9528    fn pat_enables_disables_bar_clears_haswilds() {
9529        let _g = crate::test_util::global_state_lock();
9530        // c:Src/glob.c:3548 — tokenize input as every C caller does.
9531        let tok = |s: &str| {
9532            let mut t = s.to_string();
9533            crate::ported::glob::tokenize(&mut t);
9534            t
9535        };
9536        // Baseline: | is wild.
9537        zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 0;
9538        assert!(haswilds(&tok("a|b")));
9539        // Disable.
9540        let ret = pat_enables("disable", &["|"], false);
9541        assert_eq!(ret, 0, "disable -p | returns 0 on success (c:4173)");
9542        assert_eq!(
9543            zpc_disables.lock().unwrap()[ZPC_BAR as usize],
9544            1,
9545            "c:4201 *disp = !enable → 1 for disable"
9546        );
9547        assert!(!haswilds(&tok("a|b")), "after disable, | is literal");
9548        // Restore.
9549        zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 0;
9550    }
9551
9552    /// `Src/pattern.c:4201` — `enable -p NAME` after a `disable -p NAME`
9553    /// clears the slot (`*disp = !enable` = 0 when enable=true).
9554    #[test]
9555    fn pat_enables_re_enables_disabled_token() {
9556        let _g = crate::test_util::global_state_lock();
9557        // c:Src/glob.c:3548 — tokenize input as every C caller does.
9558        let tok = |s: &str| {
9559            let mut t = s.to_string();
9560            crate::ported::glob::tokenize(&mut t);
9561            t
9562        };
9563        // Pre-disable.
9564        zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 1;
9565        assert!(!haswilds(&tok("*")));
9566        // Re-enable.
9567        let ret = pat_enables("enable", &["*"], true);
9568        assert_eq!(ret, 0);
9569        assert_eq!(
9570            zpc_disables.lock().unwrap()[ZPC_STAR as usize],
9571            0,
9572            "c:4201 *disp = !enable → 0 for enable"
9573        );
9574        assert!(haswilds(&tok("*")));
9575    }
9576
9577    /// `Src/pattern.c:4205-4208` — unknown token returns 1 and the
9578    /// disables table is unchanged for that slot.
9579    #[test]
9580    fn pat_enables_unknown_pattern_returns_one() {
9581        let _g = crate::test_util::global_state_lock();
9582        let baseline = zpc_disables.lock().unwrap().clone();
9583        let ret = pat_enables("disable", &["bogus_not_a_metachar"], false);
9584        assert_eq!(ret, 1, "c:4207 — invalid pattern → ret = 1");
9585        // Table untouched.
9586        assert_eq!(
9587            *zpc_disables.lock().unwrap(),
9588            baseline,
9589            "c:4205-4208 — no slot mutated on miss"
9590        );
9591    }
9592
9593    /// `Src/pattern.c:4196` — multiple patterns: loop continues past
9594    /// each, even if some are invalid (ret stays 1, but valid ones
9595    /// still apply).
9596    #[test]
9597    fn pat_enables_partial_failure_applies_valid_disables() {
9598        let _g = crate::test_util::global_state_lock();
9599        zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 0;
9600        zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 0;
9601        let ret = pat_enables("disable", &["|", "bogus", "*"], false);
9602        assert_eq!(ret, 1, "c:4207 — at least one invalid → ret = 1");
9603        // Both valid ones got applied (c:4196 `for (; *patp; patp++)` doesn't break on miss).
9604        assert_eq!(
9605            zpc_disables.lock().unwrap()[ZPC_BAR as usize],
9606            1,
9607            "| was disabled"
9608        );
9609        assert_eq!(
9610            zpc_disables.lock().unwrap()[ZPC_STAR as usize],
9611            1,
9612            "* was disabled"
9613        );
9614        // Restore.
9615        zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 0;
9616        zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 0;
9617    }
9618
9619    /// `Src/pattern.c:4177-4194` — empty patp lists enabled or disabled
9620    /// tokens via stdout. Hard to capture stdout in a unit test, so
9621    /// just verify the return value (0) per c:4193.
9622    #[test]
9623    fn pat_enables_empty_patp_returns_zero() {
9624        let _g = crate::test_util::global_state_lock();
9625        let ret_enable = pat_enables("enable", &[], true);
9626        assert_eq!(ret_enable, 0, "c:4193 — listing path returns 0");
9627        let ret_disable = pat_enables("disable", &[], false);
9628        assert_eq!(ret_disable, 0, "c:4193 — listing path returns 0");
9629    }
9630
9631    /// `Src/pattern.c:4197-4202` — looks up the exact string in
9632    /// `zpc_strings[]`. Each named slot from c:258 (`"|", "~", "(",
9633    /// "?", "*", "[", "<", "^", "#"` plus `"?(","*(","+(","!("`) must
9634    /// be toggleable; NULL slots (`ZPC_NULL`, `ZPC_BNULLKEEP`, etc.)
9635    /// can't be referenced by name and any caller naming them gets
9636    /// `invalid pattern`.
9637    #[test]
9638    fn pat_enables_all_named_zpc_slots_toggle() {
9639        let _g = crate::test_util::global_state_lock();
9640        // Save baseline.
9641        let baseline = zpc_disables.lock().unwrap().clone();
9642        for name in &[
9643            "|", "(", "?", "*", "[", "<", "^", "#", "?(", "*(", "+(", "!(",
9644        ] {
9645            assert_eq!(
9646                pat_enables("disable", &[name], false),
9647                0,
9648                "c:4196 — disable -p {name} succeeds",
9649            );
9650        }
9651        // Restore.
9652        *zpc_disables.lock().unwrap() = baseline;
9653    }
9654
9655    // ═══════════════════════════════════════════════════════════════════
9656    // metacharinc / charref / charnext / charrefinc / charsub — the
9657    // small char-advance helpers from pattern.c:336/1909/1936/1964/1997.
9658    // Each C version does Meta-byte + ztoken table + mbrtowc state
9659    // machine; the Rust port collapses them all to UTF-8-native
9660    // `chars()` iteration. Tests pin the Rust observable semantic.
9661    // ═══════════════════════════════════════════════════════════════════
9662
9663    /// `Src/pattern.c:336` — `metacharinc(char **x)` advances one
9664    /// multibyte char. Rust port returns the new byte position.
9665    /// ASCII path: advance by 1.
9666    #[test]
9667    fn metacharinc_advances_one_ascii_byte() {
9668        let _g = crate::test_util::global_state_lock();
9669        assert_eq!(metacharinc("abc", 0), 1, "c:343-358 single-byte path");
9670        assert_eq!(metacharinc("abc", 1), 2);
9671        assert_eq!(metacharinc("abc", 2), 3);
9672    }
9673
9674    /// `Src/pattern.c:363-380` — multibyte path: advance by the
9675    /// codepoint's UTF-8 byte length, not always 1.
9676    #[test]
9677    fn metacharinc_advances_by_codepoint_width() {
9678        let _g = crate::test_util::global_state_lock();
9679        // 'é' is 2 UTF-8 bytes.
9680        assert_eq!(metacharinc("é", 0), 2, "c:363-380 mbrtowc path width=2");
9681        // '日' is 3 UTF-8 bytes.
9682        assert_eq!(metacharinc("日", 0), 3, "mbrtowc path width=3");
9683        // '🦀' is 4 UTF-8 bytes.
9684        assert_eq!(metacharinc("🦀", 0), 4, "mbrtowc path width=4");
9685    }
9686
9687    /// `Src/pattern.c:336` — at end-of-string the C version returns
9688    /// `WCHAR_INVALID(*(*x)++)` (c:385); the Rust port returns the
9689    /// same position (no advance) when there's nothing to decode.
9690    #[test]
9691    fn metacharinc_at_eos_returns_same_position() {
9692        let _g = crate::test_util::global_state_lock();
9693        assert_eq!(metacharinc("abc", 3), 3, "EOS — no advance");
9694        assert_eq!(metacharinc("", 0), 0, "empty — no advance");
9695    }
9696
9697    /// `Src/pattern.c:1909` — `charref(char *x, char *y, int *zmb_ind)`
9698    /// decodes the codepoint at `x` and returns it. Rust port returns
9699    /// `Option<char>`; `None` on empty.
9700    #[test]
9701    fn charref_decodes_one_codepoint() {
9702        let _g = crate::test_util::global_state_lock();
9703        assert_eq!(charref("abc", 0), Some('a'));
9704        assert_eq!(charref("abc", 1), Some('b'));
9705        assert_eq!(charref("é日", 0), Some('é'), "multibyte at start");
9706        // At offset 2, "é" (2 bytes) already consumed; next char is '日'.
9707        assert_eq!(charref("é日", 2), Some('日'));
9708        assert_eq!(charref("", 0), None, "empty → None");
9709        assert_eq!(charref("abc", 3), None, "EOS → None");
9710    }
9711
9712    /// `Src/pattern.c:1936` — `charnext(char *x, char *y)` is the
9713    /// single-step version (advance one position). Delegates to
9714    /// `metacharinc` in the Rust port.
9715    #[test]
9716    fn charnext_delegates_to_metacharinc() {
9717        let _g = crate::test_util::global_state_lock();
9718        assert_eq!(charnext("abc", 0), metacharinc("abc", 0));
9719        assert_eq!(charnext("é日", 0), 2, "c:1936 → c:336 multibyte advance");
9720        assert_eq!(charnext("é日", 2), 5, "advance past '日' (3 bytes)");
9721    }
9722
9723    /// `Src/pattern.c:1964` — `charrefinc(char **x, char *y, int *z)`
9724    /// decodes + advances. Rust mutates `pos` in place and returns the
9725    /// codepoint. Tests both the codepoint return and the position
9726    /// mutation.
9727    #[test]
9728    fn charrefinc_decodes_and_advances_position() {
9729        let _g = crate::test_util::global_state_lock();
9730        let mb = GF_MULTIBYTE;
9731        let mut z = 0;
9732        let mut pos = 0;
9733        assert_eq!(charrefinc("abc", &mut pos, mb, &mut z), Some('a' as u32));
9734        assert_eq!(pos, 1, "c:1964 — advance by 1 ASCII");
9735        let mut pos = 0;
9736        assert_eq!(charrefinc("é日", &mut pos, mb, &mut z), Some('é' as u32));
9737        assert_eq!(pos, 2, "c:1964 — advance by 2 UTF-8 bytes");
9738        assert_eq!(charrefinc("é日", &mut pos, mb, &mut z), Some('日' as u32));
9739        assert_eq!(pos, 5, "c:1964 — advance by 3 more UTF-8 bytes");
9740        let mut pos = 0;
9741        assert_eq!(charrefinc("", &mut pos, mb, &mut z), None);
9742        assert_eq!(pos, 0, "c:1964 — no advance on empty");
9743        assert_eq!(z, 0, "no invalid character seen");
9744    }
9745
9746    /// c:1974-1984 — a METAFIED raw byte (Meta U+0083 + byte^32) is one
9747    /// raw byte: its own value without GF_MULTIBYTE, `WCHAR_INVALID`
9748    /// (0xDC00 + byte, c:241-242) with `*z` set when it does not decode.
9749    #[test]
9750    fn charrefinc_decodes_a_metafied_raw_byte() {
9751        let _g = crate::test_util::global_state_lock();
9752        let s = format!("\u{83}{}x", char::from(0xccu8 ^ 32));
9753        let mut z = 0;
9754        let mut pos = 0;
9755        assert_eq!(charrefinc(&s, &mut pos, 0, &mut z), Some(0xcc));
9756        assert_eq!((pos, z), (s.len() - 1, 0), "one pair, no flag");
9757        // With GF_MULTIBYTE the answer is mbrtowc's, so it follows the
9758        // test process's locale: a lone 0xcc is invalid UTF-8 (WCHAR_INVALID,
9759        // `*z` set) but an ordinary character in a single-byte locale.
9760        let mut pos = 0;
9761        let wc = charrefinc(&s, &mut pos, GF_MULTIBYTE, &mut z);
9762        assert_eq!(pos, s.len() - 1, "one pair either way");
9763        assert!(
9764            (wc, z) == (Some(0xDCCC), 1) || (wc, z) == (Some(0xcc), 0),
9765            "got {wc:?} z={z}"
9766        );
9767    }
9768
9769    // ═══════════════════════════════════════════════════════════════════
9770    // savepatterndisables / restorepatterndisables — pattern.c:4218-4271.
9771    // u32 bitmask save+restore over `zpc_disables[ZPC_COUNT]`. Each
9772    // slot i contributes (1 << i) to the bitmask when its disable byte
9773    // is non-zero. Tests pin the bit-position mapping per C's
9774    // `for (bit = 1, disp = zpc_disables; …; bit <<= 1, disp++)`.
9775    // ═══════════════════════════════════════════════════════════════════
9776
9777    /// `Src/pattern.c:4220-4232` — save when no slot disabled returns 0.
9778    #[test]
9779    fn savepatterndisables_empty_returns_zero() {
9780        let _g = crate::test_util::global_state_lock();
9781        // Zero out all slots.
9782        *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
9783        let disables = savepatterndisables();
9784        assert_eq!(disables, 0, "c:4232 — no slots set → bitmap 0");
9785    }
9786
9787    /// `Src/pattern.c:4226-4231` — bit-position mapping: slot `i`
9788    /// contributes `1 << i`. Sets specific slots and verifies the
9789    /// returned u32.
9790    #[test]
9791    fn savepatterndisables_bitmap_matches_slot_indices() {
9792        let _g = crate::test_util::global_state_lock();
9793        *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
9794        zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 1;
9795        zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 1;
9796        let disables = savepatterndisables();
9797        let expected = (1u32 << ZPC_BAR) | (1u32 << ZPC_STAR);
9798        assert_eq!(disables, expected, "c:4231 — bits ZPC_BAR + ZPC_STAR set");
9799        // Restore.
9800        *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
9801    }
9802
9803    /// `Src/pattern.c:4266-4269` — restore writes 1/0 to each slot per
9804    /// the bitmask. Pin the inverse-of-save semantic.
9805    #[test]
9806    fn restorepatterndisables_zero_clears_all_slots() {
9807        let _g = crate::test_util::global_state_lock();
9808        // Pre-populate.
9809        *zpc_disables.lock().unwrap() = [1u8; ZPC_COUNT as usize];
9810        restorepatterndisables(0);
9811        let table = zpc_disables.lock().unwrap().clone();
9812        for (i, &v) in table.iter().enumerate() {
9813            assert_eq!(v, 0, "c:4269 — slot {i} cleared with bitmap=0");
9814        }
9815    }
9816
9817    /// `Src/pattern.c:4266-4267` — bitmap with all relevant bits set
9818    /// makes restore turn every slot on. All-ones bitmap maps to all-1
9819    /// slots up to ZPC_COUNT.
9820    #[test]
9821    fn restorepatterndisables_all_ones_sets_each_slot() {
9822        let _g = crate::test_util::global_state_lock();
9823        *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
9824        let all = (1u32 << ZPC_COUNT).wrapping_sub(1);
9825        restorepatterndisables(all);
9826        let table = zpc_disables.lock().unwrap().clone();
9827        for (i, &v) in table.iter().enumerate() {
9828            assert_eq!(v, 1, "c:4267 — slot {i} set with full bitmap");
9829        }
9830        // Restore.
9831        *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
9832    }
9833
9834    /// `Src/pattern.c:4218-4271` — save then restore round-trips
9835    /// every slot through the u32 bitmask losslessly.
9836    #[test]
9837    fn save_restore_pattern_disables_roundtrip() {
9838        let _g = crate::test_util::global_state_lock();
9839        // Set up a non-trivial pattern: alternating slots.
9840        for i in 0..(ZPC_COUNT as usize) {
9841            zpc_disables.lock().unwrap()[i] = (i % 2) as u8;
9842        }
9843        let saved = savepatterndisables();
9844        // Clobber.
9845        *zpc_disables.lock().unwrap() = [9u8; ZPC_COUNT as usize];
9846        // Restore.
9847        restorepatterndisables(saved);
9848        // Verify alternating pattern back.
9849        let table = zpc_disables.lock().unwrap().clone();
9850        for i in 0..(ZPC_COUNT as usize) {
9851            assert_eq!(
9852                table[i],
9853                (i % 2) as u8,
9854                "c:4218+c:4258 round-trip: slot {i} preserved"
9855            );
9856        }
9857        // Reset.
9858        *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
9859    }
9860
9861    // ═══════════════════════════════════════════════════════════════════
9862    // startpatternscope / endpatternscope / clearpatterndisables —
9863    // pattern.c:4241/4279/4296. Pin LOCALPATTERNS-gated save/restore +
9864    // C `memset(zpc_disables, 0, ZPC_COUNT)` clear.
9865    //
9866    // Bug fixed: prior Rust port operated on a separate
9867    // `patterndisables: Mutex<Vec<String>>` tombstone (cleared by
9868    // clearpatterndisables, push/popped by start/end). C's three fns
9869    // all operate on the canonical `zpc_disables[]` byte array.
9870    // Pre-fix: setopt LOCALPATTERNS function entry/exit was a no-op
9871    // and `clearpatterndisables` didn't clear the matcher's disable
9872    // bytes.
9873    // ═══════════════════════════════════════════════════════════════════
9874
9875    /// `Src/pattern.c:4296-4298` — `memset(zpc_disables, 0, ZPC_COUNT)`.
9876    /// Test that clearpatterndisables zeros every slot.
9877    #[test]
9878    fn clearpatterndisables_zeros_zpc_disables() {
9879        let _g = crate::test_util::global_state_lock();
9880        // Pre-populate every slot.
9881        *zpc_disables.lock().unwrap() = [1u8; ZPC_COUNT as usize];
9882        clearpatterndisables();
9883        let table = zpc_disables.lock().unwrap().clone();
9884        for (i, &v) in table.iter().enumerate() {
9885            assert_eq!(v, 0, "c:4298 — slot {i} cleared");
9886        }
9887    }
9888
9889    /// `Src/pattern.c:4241-4250` + c:4279-4290` — under `setopt
9890    /// LOCALPATTERNS`, function entry save → mutate → exit restore
9891    /// round-trips zpc_disables.
9892    #[test]
9893    fn pattern_scope_save_restore_under_localpatterns() {
9894        let _g = crate::test_util::global_state_lock();
9895        crate::ported::options::opt_state_set("localpatterns", true);
9896
9897        // Initial state: ZPC_BAR disabled, ZPC_STAR enabled.
9898        *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
9899        zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 1;
9900
9901        // Enter scope (c:4241 — `savepatterndisables` into stack frame).
9902        startpatternscope();
9903
9904        // Mutate inside the "function" body.
9905        zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 0;
9906        zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 1;
9907        assert_eq!(zpc_disables.lock().unwrap()[ZPC_BAR as usize], 0);
9908        assert_eq!(zpc_disables.lock().unwrap()[ZPC_STAR as usize], 1);
9909
9910        // Exit scope (c:4279 — restore via restorepatterndisables).
9911        endpatternscope();
9912
9913        // Outer state must come back.
9914        assert_eq!(
9915            zpc_disables.lock().unwrap()[ZPC_BAR as usize],
9916            1,
9917            "c:4287 — ZPC_BAR restored to outer-scope disabled"
9918        );
9919        assert_eq!(
9920            zpc_disables.lock().unwrap()[ZPC_STAR as usize],
9921            0,
9922            "c:4287 — ZPC_STAR restored to outer-scope enabled"
9923        );
9924
9925        // Reset.
9926        *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
9927        crate::ported::options::opt_state_set("localpatterns", false);
9928    }
9929
9930    /// `Src/pattern.c:4286` — WITHOUT LOCALPATTERNS, endpatternscope
9931    /// pops the stack frame but does NOT restore. Function-body
9932    /// mutations leak into the caller.
9933    #[test]
9934    fn pattern_scope_no_restore_without_localpatterns() {
9935        let _g = crate::test_util::global_state_lock();
9936        crate::ported::options::opt_state_set("localpatterns", false);
9937
9938        *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
9939        zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 1;
9940
9941        startpatternscope();
9942        zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 0;
9943        endpatternscope();
9944
9945        // Mutation leaked — c:4286 gate skipped restore.
9946        assert_eq!(
9947            zpc_disables.lock().unwrap()[ZPC_BAR as usize],
9948            0,
9949            "c:4286 — WITHOUT LOCALPATTERNS, mutation leaks out"
9950        );
9951
9952        // Reset.
9953        *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
9954    }
9955
9956    /// `Src/pattern.c:4241-4250` — nested scopes form a LIFO stack.
9957    /// Inner restore pops just the inner frame; outer frame stays.
9958    #[test]
9959    fn pattern_scope_nested_lifo() {
9960        let _g = crate::test_util::global_state_lock();
9961        crate::ported::options::opt_state_set("localpatterns", true);
9962
9963        *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
9964
9965        // Outer.
9966        zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 1;
9967        startpatternscope(); // frame A
9968                             // Inner.
9969        zpc_disables.lock().unwrap()[ZPC_BAR as usize] = 0;
9970        zpc_disables.lock().unwrap()[ZPC_STAR as usize] = 1;
9971        startpatternscope(); // frame B
9972                             // Innermost.
9973        zpc_disables.lock().unwrap()[ZPC_INBRACK as usize] = 1;
9974        endpatternscope(); // pop B → inner restored
9975        assert_eq!(zpc_disables.lock().unwrap()[ZPC_BAR as usize], 0);
9976        assert_eq!(zpc_disables.lock().unwrap()[ZPC_STAR as usize], 1);
9977        assert_eq!(
9978            zpc_disables.lock().unwrap()[ZPC_INBRACK as usize],
9979            0,
9980            "c:4287 — frame B's snapshot didn't include this slot's 1"
9981        );
9982        endpatternscope(); // pop A → outer restored
9983        assert_eq!(zpc_disables.lock().unwrap()[ZPC_BAR as usize], 1);
9984        assert_eq!(zpc_disables.lock().unwrap()[ZPC_STAR as usize], 0);
9985
9986        // Reset.
9987        *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
9988        crate::ported::options::opt_state_set("localpatterns", false);
9989    }
9990
9991    /// `Src/pattern.c:4226` — bit position is the slot INDEX (0-based),
9992    /// not 1-based. Slot 0 → bit 1, slot 1 → bit 2, etc. Per the C
9993    /// `bit = 1` initial and `bit <<= 1` after each slot.
9994    #[test]
9995    fn savepatterndisables_slot_0_is_low_bit() {
9996        let _g = crate::test_util::global_state_lock();
9997        *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
9998        zpc_disables.lock().unwrap()[0] = 1;
9999        let disables = savepatterndisables();
10000        assert_eq!(disables, 1, "c:4226 — bit = 1 initial → slot 0 is low bit");
10001        *zpc_disables.lock().unwrap() = [0u8; ZPC_COUNT as usize];
10002    }
10003
10004    /// `Src/pattern.c:1997` — `charsub(x, y)` returns the number of
10005    /// characters between the start and byte offset `y`. Non-multibyte:
10006    /// byte distance; multibyte: codepoint count of `x[0..y]`.
10007    #[test]
10008    fn charsub_counts_chars_to_offset() {
10009        let _g = crate::test_util::global_state_lock();
10010        let saved = crate::ported::options::opt_state_get("multibyte");
10011
10012        // c:2004 — non-multibyte: byte distance `y - x`.
10013        crate::ported::options::opt_state_set("multibyte", false);
10014        assert_eq!(charsub("abc", 3), 3, "non-MB: byte distance to end");
10015        assert_eq!(charsub("abc", 1), 1, "non-MB: one byte in");
10016        assert_eq!(charsub("abc", 0), 0, "c:1997 — at start, distance 0");
10017        assert_eq!(charsub("é日", 5), 5, "non-MB: raw byte count (5 bytes)");
10018
10019        // c:2006-2021 — multibyte: codepoint count of `x[0..y]`.
10020        crate::ported::options::opt_state_set("multibyte", true);
10021        assert_eq!(charsub("abc", 3), 3, "MB: 3 ASCII chars");
10022        assert_eq!(charsub("é", 2), 1, "MB: one 2-byte codepoint");
10023        assert_eq!(charsub("é日", 5), 2, "MB: é + 日 = 2 codepoints");
10024        assert_eq!(charsub("é日", 2), 1, "MB: just é = 1 codepoint");
10025        assert_eq!(charsub("é日", 0), 0, "MB: distance 0 at start");
10026
10027        if let Some(v) = saved {
10028            crate::ported::options::opt_state_set("multibyte", v);
10029        }
10030    }
10031
10032    // ═══════════════════════════════════════════════════════════════════
10033    // KSH_GLOB extended patterns: +(pat), *(pat), ?(pat), @(pat), !(pat).
10034    // Anchored against `setopt KSH_GLOB; [[ str == ${~pat} ]]` in zsh 5.9.
10035    // Tests enable KSH_GLOB before each assertion and restore prior state.
10036    //
10037    // Ported from pattern.c:1278-1350 (KSH dispatch in patcomppiece) and
10038    // pattern.c:1615-1746 (kshchar-driven quantifier emission), plus
10039    // patcompnot pattern.c:1759-1784 for the !(pat) negation form and a
10040    // P_EXCLUDE matcher arm (pattern.c:3056-3201).
10041    // ═══════════════════════════════════════════════════════════════════
10042
10043    fn ksh_glob_match(pat: &str, s: &str) -> bool {
10044        let _g = TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
10045        let saved = opt_state_get("kshglob").unwrap_or(false);
10046        opt_state_set("kshglob", true);
10047        let r = patmatch(pat, s);
10048        opt_state_set("kshglob", saved);
10049        r
10050    }
10051
10052    // ── +(pat) — one or more ─────────────────────────────────────────
10053    /// `+(foo)` matches `foo` — zsh: MATCH.
10054    #[test]
10055    fn ksh_glob_plus_one_repetition_matches() {
10056        let _g = crate::test_util::global_state_lock();
10057        assert!(ksh_glob_match("+(foo)", "foo"));
10058    }
10059
10060    /// `+(foo)` matches `foofoo` — zsh: MATCH.
10061    #[test]
10062    fn ksh_glob_plus_two_repetitions_matches() {
10063        let _g = crate::test_util::global_state_lock();
10064        assert!(ksh_glob_match("+(foo)", "foofoo"));
10065    }
10066
10067    /// `+(foo)` does NOT match `""` — zsh: NOMATCH (and zshrs agrees).
10068    /// This is the ONLY +(pat) test that passes — both reject empty.
10069    #[test]
10070    fn ksh_glob_plus_zero_repetitions_fails() {
10071        let _g = crate::test_util::global_state_lock();
10072        assert!(!ksh_glob_match("+(foo)", ""));
10073    }
10074
10075    /// `+(foo)` matches `foofoofoo` — zsh: MATCH.
10076    #[test]
10077    fn ksh_glob_plus_three_repetitions_matches() {
10078        let _g = crate::test_util::global_state_lock();
10079        assert!(ksh_glob_match("+(foo)", "foofoofoo"));
10080    }
10081
10082    // ── *(pat) — zero or more ────────────────────────────────────────
10083    /// `*(foo)` matches empty — zsh: MATCH.
10084    #[test]
10085    fn ksh_glob_star_paren_matches_empty() {
10086        let _g = crate::test_util::global_state_lock();
10087        assert!(ksh_glob_match("*(foo)", ""));
10088    }
10089
10090    /// `*(foo)` matches `foo` — both zsh and zshrs agree (passes).
10091    #[test]
10092    fn ksh_glob_star_paren_matches_one() {
10093        let _g = crate::test_util::global_state_lock();
10094        assert!(ksh_glob_match("*(foo)", "foo"));
10095    }
10096
10097    /// `*(foo)` matches `foofoo` — both agree.
10098    #[test]
10099    fn ksh_glob_star_paren_matches_multiple() {
10100        let _g = crate::test_util::global_state_lock();
10101        assert!(ksh_glob_match("*(foo)", "foofoo"));
10102    }
10103
10104    // ── ?(pat) — zero or one ─────────────────────────────────────────
10105    /// `?(foo)` matches empty — zsh: MATCH.
10106    #[test]
10107    fn ksh_glob_question_paren_matches_empty() {
10108        let _g = crate::test_util::global_state_lock();
10109        assert!(ksh_glob_match("?(foo)", ""));
10110    }
10111
10112    /// `?(foo)` matches `foo` — zsh: MATCH.
10113    #[test]
10114    fn ksh_glob_question_paren_matches_one_rep() {
10115        let _g = crate::test_util::global_state_lock();
10116        assert!(ksh_glob_match("?(foo)", "foo"));
10117    }
10118
10119    /// `?(foo)` does NOT match `foofoo` — both agree (rejects).
10120    #[test]
10121    fn ksh_glob_question_paren_fails_on_two_reps() {
10122        let _g = crate::test_util::global_state_lock();
10123        assert!(!ksh_glob_match("?(foo)", "foofoo"));
10124    }
10125
10126    // ── @(pat) — exactly one ─────────────────────────────────────────
10127    /// `@(foo)` matches `foo` — zsh: MATCH. zshrs fails on the alternation
10128    /// form but matches the single-branch form.
10129    #[test]
10130    fn ksh_glob_at_paren_matches_exact() {
10131        let _g = crate::test_util::global_state_lock();
10132        assert!(ksh_glob_match("@(foo)", "foo"));
10133    }
10134
10135    /// `@(foo|bar)` matches both `foo` AND `bar` — zsh: MATCH both.
10136    #[test]
10137    fn ksh_glob_at_paren_with_alternation_either_branch() {
10138        let _g = crate::test_util::global_state_lock();
10139        assert!(ksh_glob_match("@(foo|bar)", "foo"));
10140        assert!(ksh_glob_match("@(foo|bar)", "bar"));
10141    }
10142
10143    /// `@(foo|bar)` rejects `qux` — both agree.
10144    #[test]
10145    fn ksh_glob_at_paren_alternation_rejects_outside() {
10146        let _g = crate::test_util::global_state_lock();
10147        assert!(!ksh_glob_match("@(foo|bar)", "qux"));
10148    }
10149
10150    // ── !(pat) — not match ───────────────────────────────────────────
10151    /// `!(foo)` rejects `foo` — both agree.
10152    #[test]
10153    fn ksh_glob_bang_paren_rejects_matching_string() {
10154        let _g = crate::test_util::global_state_lock();
10155        assert!(!ksh_glob_match("!(foo)", "foo"));
10156    }
10157
10158    /// `!(foo)` matches `bar` — zsh: MATCH.
10159    #[test]
10160    fn ksh_glob_bang_paren_matches_non_matching_string() {
10161        let _g = crate::test_util::global_state_lock();
10162        assert!(ksh_glob_match("!(foo)", "bar"));
10163    }
10164
10165    /// `!(foo|bar)` matches `baz` and rejects `foo` — zsh.
10166    #[test]
10167    fn ksh_glob_bang_paren_with_alternation() {
10168        let _g = crate::test_util::global_state_lock();
10169        assert!(ksh_glob_match("!(foo|bar)", "baz"));
10170        assert!(!ksh_glob_match("!(foo|bar)", "foo"));
10171    }
10172
10173    // ── Mixed with literal context ───────────────────────────────────
10174    /// `pre+(x)post` matches `prexpost`, `prexxpost` — zsh: MATCH both.
10175    #[test]
10176    fn ksh_glob_plus_paren_in_literal_context() {
10177        let _g = crate::test_util::global_state_lock();
10178        assert!(ksh_glob_match("pre+(x)post", "prexpost"));
10179        assert!(ksh_glob_match("pre+(x)post", "prexxpost"));
10180    }
10181
10182    /// `pre+(x)post` rejects `prepost` — both agree (no x's).
10183    #[test]
10184    fn ksh_glob_plus_paren_in_literal_rejects_zero_reps() {
10185        let _g = crate::test_util::global_state_lock();
10186        assert!(!ksh_glob_match("pre+(x)post", "prepost"));
10187    }
10188
10189    // ═══════════════════════════════════════════════════════════════════
10190    // EXTENDED_GLOB pattern flags: (#i) case-insensitive, (#l) lowercase
10191    // matches uppercase, (#aN) approximate match. Anchored to
10192    // `setopt EXTENDED_GLOB; [[ str == ${~pat} ]]` in real zsh 5.9.
10193    // ═══════════════════════════════════════════════════════════════════
10194
10195    fn ext_glob_match(pat: &str, s: &str) -> bool {
10196        let _g = TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
10197        let saved = opt_state_get("extendedglob").unwrap_or(false);
10198        opt_state_set("extendedglob", true);
10199        let r = patmatch(pat, s);
10200        opt_state_set("extendedglob", saved);
10201        r
10202    }
10203
10204    // ── (#i) case-insensitive ───────────────────────────────────────
10205    /// `(#i)FOO` matches "foo" (case ignored).
10206    #[test]
10207    fn ext_glob_hash_i_matches_lowercase_against_uppercase_pat() {
10208        let _g = crate::test_util::global_state_lock();
10209        assert!(ext_glob_match("(#i)FOO", "foo"));
10210    }
10211
10212    /// `(#i)FOO` matches "FOO" exactly.
10213    #[test]
10214    fn ext_glob_hash_i_matches_exact_case() {
10215        let _g = crate::test_util::global_state_lock();
10216        assert!(ext_glob_match("(#i)FOO", "FOO"));
10217    }
10218
10219    /// `(#i)FOO` matches mixed-case "FoO".
10220    #[test]
10221    fn ext_glob_hash_i_matches_mixed_case() {
10222        let _g = crate::test_util::global_state_lock();
10223        assert!(ext_glob_match("(#i)FOO", "FoO"));
10224    }
10225
10226    /// `(#i)foo` rejects unrelated string "BAR".
10227    #[test]
10228    fn ext_glob_hash_i_rejects_unrelated_string() {
10229        let _g = crate::test_util::global_state_lock();
10230        assert!(!ext_glob_match("(#i)foo", "BAR"));
10231    }
10232
10233    // ── (#l) lowercase-pattern matches uppercase-text ───────────────
10234    /// `(#l)foo` matches "FOO" (lowercase pattern allows uppercase).
10235    /// Distinct from (#i): asymmetric — pattern letter must be lowercase
10236    /// AND text may be either case for that letter.
10237    #[test]
10238    fn ext_glob_hash_l_lowercase_pat_matches_uppercase_text() {
10239        let _g = crate::test_util::global_state_lock();
10240        assert!(ext_glob_match("(#l)foo", "FOO"));
10241    }
10242
10243    /// `(#l)foo` matches lowercase "foo".
10244    #[test]
10245    fn ext_glob_hash_l_lowercase_pat_matches_lowercase_text() {
10246        let _g = crate::test_util::global_state_lock();
10247        assert!(ext_glob_match("(#l)foo", "foo"));
10248    }
10249
10250    /// `(#l)FOO` does NOT match "foo" — asymmetry: uppercase in pattern
10251    /// requires uppercase in text. zsh: NOMATCH.
10252    #[test]
10253    fn ext_glob_hash_l_uppercase_pat_requires_uppercase_text_anchored_to_zsh() {
10254        let _g = crate::test_util::global_state_lock();
10255        assert!(
10256            !ext_glob_match("(#l)FOO", "foo"),
10257            "zsh: (#l)FOO must NOT match \"foo\" — uppercase-in-pattern is anchored"
10258        );
10259    }
10260
10261    // ── (#aN) approximate match (Damerau-Levenshtein distance ≤ N) ──
10262    /// `(#a1)foo` matches "fop" (1 char substitution).
10263    #[test]
10264    fn ext_glob_hash_a1_matches_one_substitution_anchored_to_zsh() {
10265        let _g = crate::test_util::global_state_lock();
10266        assert!(
10267            ext_glob_match("(#a1)foo", "fop"),
10268            "zsh: (#a1)foo matches 'fop' (1 substitution)"
10269        );
10270    }
10271
10272    /// `(#a2)foo` matches "fxy" (2 substitutions).
10273    #[test]
10274    fn ext_glob_hash_a2_matches_two_substitutions_anchored_to_zsh() {
10275        let _g = crate::test_util::global_state_lock();
10276        assert!(
10277            ext_glob_match("(#a2)foo", "fxy"),
10278            "zsh: (#a2)foo matches 'fxy' (2 substitutions)"
10279        );
10280    }
10281
10282    /// `(#a1)foo` rejects "fxy" (2 substitutions > limit 1).
10283    #[test]
10284    fn ext_glob_hash_a1_rejects_two_substitutions() {
10285        let _g = crate::test_util::global_state_lock();
10286        assert!(!ext_glob_match("(#a1)foo", "fxy"));
10287    }
10288
10289    // ═══════════════════════════════════════════════════════════════════
10290    // (#aN) full Damerau-Levenshtein — sub/ins/del edit operations.
10291    // C inlines the backtracking in P_EXACTLY (c:2737-2779) plus the
10292    // approx-aware sync nodes at c:3055+. Rust factors into
10293    // `approx_match_exactly` (WARNING: NOT IN PATTERN.C). Tests pin
10294    // each edit operation independently + the budget upper bound.
10295    // ═══════════════════════════════════════════════════════════════════
10296
10297    /// `(#a0)foo` is exact-match only — no edits allowed.
10298    #[test]
10299    fn ext_glob_hash_a0_is_exact_match_only() {
10300        let _g = crate::test_util::global_state_lock();
10301        assert!(ext_glob_match("(#a0)foo", "foo"), "exact ok");
10302        assert!(
10303            !ext_glob_match("(#a0)foo", "fop"),
10304            "no edit budget rejects 1-sub"
10305        );
10306    }
10307
10308    /// `(#a1)foo` matches "fxoo" via INSERTION in input (extra 'x').
10309    /// The pattern has no `x`; treating `x` as an inserted-in-input
10310    /// char costs 1 error. Input → "f(x)oo" with the 'x' deleted.
10311    #[test]
10312    fn ext_glob_hash_a_accepts_insertion_in_input() {
10313        let _g = crate::test_util::global_state_lock();
10314        // "fxoo" — extra 'x' between 'f' and 'oo'.
10315        assert!(
10316            ext_glob_match("(#a1)foo", "fxoo"),
10317            "1 insertion-in-input edit"
10318        );
10319    }
10320
10321    /// `(#a1)foo` matches "fo" via DELETION from input (missing 'o').
10322    /// One 'o' deleted from input compared to pattern.
10323    #[test]
10324    fn ext_glob_hash_a_accepts_deletion_from_input() {
10325        let _g = crate::test_util::global_state_lock();
10326        assert!(
10327            ext_glob_match("(#a1)foo", "fo"),
10328            "1 deletion-from-input edit"
10329        );
10330    }
10331
10332    /// `(#a2)foo` matches "fxooy" via 1 insertion + 1 substitution.
10333    /// 'x' inserted between f-o; final 'o' substituted to 'y'. Wait —
10334    /// closer reading: "fxooy" vs "foo" — 'x' is the extra char, but
10335    /// then 'ooy' vs 'oo' has another extra 'y'. That's 2 insertions.
10336    #[test]
10337    fn ext_glob_hash_a_mixed_edits_within_budget() {
10338        let _g = crate::test_util::global_state_lock();
10339        // 2 insertions: 'x' and 'y'.
10340        assert!(
10341            ext_glob_match("(#a2)foo", "fxooy"),
10342            "2 insertions in input within budget"
10343        );
10344    }
10345
10346    /// Budget upper bound: `(#a1)abc` rejects "xyz" (3 substitutions > 1).
10347    #[test]
10348    fn ext_glob_hash_a1_rejects_three_substitutions() {
10349        let _g = crate::test_util::global_state_lock();
10350        assert!(!ext_glob_match("(#a1)abc", "xyz"));
10351    }
10352
10353    /// `(#a3)abc` matches "xyz" (3 substitutions ≤ 3).
10354    #[test]
10355    fn ext_glob_hash_a3_accepts_all_substituted() {
10356        let _g = crate::test_util::global_state_lock();
10357        assert!(
10358            ext_glob_match("(#a3)abc", "xyz"),
10359            "3 substitutions at budget 3"
10360        );
10361    }
10362
10363    /// Position-independent substitution: budget allows replacing
10364    /// any one of N pattern chars.
10365    #[test]
10366    fn ext_glob_hash_a_accepts_position_independent_substitution() {
10367        let _g = crate::test_util::global_state_lock();
10368        assert!(ext_glob_match("(#a1)abc", "Xbc"), "first-char sub");
10369        assert!(ext_glob_match("(#a1)abc", "aXc"), "middle-char sub");
10370        assert!(ext_glob_match("(#a1)abc", "abX"), "last-char sub");
10371    }
10372
10373    // ═══════════════════════════════════════════════════════════════════
10374    // zsh test-corpus pins — direct anchors to Test/D02glob.ztst:25-80
10375    // "zsh globbing" test list (zsh's authoritative regression suite).
10376    // Each test cites the ztst line range it pins. Currently-failing
10377    // tests get #[ignore = "ZSHRS BUG: ..."] markers; expected-output
10378    // assertions stay in tree so the marker flips when the Rust port
10379    // catches up.
10380    // ═══════════════════════════════════════════════════════════════════
10381
10382    /// `Test/D02glob.ztst:26` — `[[ foo~ = foo~ ]]` exact match,
10383    /// expected exit 0 (true).
10384    #[test]
10385    fn zsh_corpus_foo_tilde_exact_match() {
10386        let _g = crate::test_util::global_state_lock();
10387        assert!(
10388            ext_glob_match("foo~", "foo~"),
10389            "ztst:26 — literal tilde matches"
10390        );
10391    }
10392
10393    /// `Test/D02glob.ztst:27` — `[[ foo~ = (foo~) ]]` parenthesised
10394    /// single alternative.
10395    #[test]
10396    fn zsh_corpus_foo_tilde_in_parens() {
10397        let _g = crate::test_util::global_state_lock();
10398        assert!(
10399            ext_glob_match("(foo~)", "foo~"),
10400            "ztst:27 — (foo~) matches foo~"
10401        );
10402    }
10403
10404    /// `Test/D02glob.ztst:28` — `[[ foo~ = (foo~|) ]]` alternation
10405    /// with empty alternative.
10406    #[test]
10407    fn zsh_corpus_alternation_with_empty_alt() {
10408        let _g = crate::test_util::global_state_lock();
10409        assert!(
10410            ext_glob_match("(foo~|)", "foo~"),
10411            "ztst:28 — empty alt accepted"
10412        );
10413    }
10414
10415    /// `Test/D02glob.ztst:29` — `[[ foo.c = *.c~boo* ]]` exclude
10416    /// pattern: matches *.c BUT NOT boo*. `foo.c` matches *.c and
10417    /// doesn't match boo*, so result is 0 (true).
10418    #[test]
10419    fn zsh_corpus_exclude_pattern_basic() {
10420        let _g = crate::test_util::global_state_lock();
10421        assert!(
10422            ext_glob_match("*.c~boo*", "foo.c"),
10423            "ztst:29 — *.c~boo* matches foo.c"
10424        );
10425    }
10426
10427    /// `Test/D02glob.ztst:30` — `[[ foo.c = *.c~boo*~foo* ]]`
10428    /// double-exclude: matches *.c but excludes both `boo*` and
10429    /// `foo*`. `foo.c` matches `foo*` (excluded) → result 1 (false).
10430    #[test]
10431    fn zsh_corpus_exclude_pattern_double() {
10432        let _g = crate::test_util::global_state_lock();
10433        assert!(
10434            !ext_glob_match("*.c~boo*~foo*", "foo.c"),
10435            "ztst:30 — double exclude rejects foo.c"
10436        );
10437    }
10438
10439    /// `Test/D02glob.ztst:31` — `[[ fofo = (fo#)# ]]` — `#` is the
10440    /// extended-glob "1+ repetitions" quantifier. `(fo#)#` matches
10441    /// 1+ repetitions of "fo+".
10442    #[test]
10443    fn zsh_corpus_hash_repetition_double() {
10444        let _g = crate::test_util::global_state_lock();
10445        assert!(
10446            ext_glob_match("(fo#)#", "fofo"),
10447            "ztst:31 — (fo#)# matches fofo"
10448        );
10449    }
10450
10451    /// `Test/D02glob.ztst:32` — `[[ ffo = (fo#)# ]]`. `fo#` means
10452    /// `f` followed by 0+ `o`. `ffo` = `f` + `fo` (matches the
10453    /// outer `#` repetition).
10454    #[test]
10455    fn zsh_corpus_hash_repetition_min_one() {
10456        let _g = crate::test_util::global_state_lock();
10457        assert!(
10458            ext_glob_match("(fo#)#", "ffo"),
10459            "ztst:32 — (fo#)# matches ffo via 1-iter outer"
10460        );
10461    }
10462
10463    /// `Test/D02glob.ztst:36` — `[[ foooofof = (fo##)# ]]` —
10464    /// `##` is "2+ repetitions". `fo##` = `f` + 2+ `o`'s. The
10465    /// trailing `of` after `foooo` doesn't satisfy the trailing
10466    /// outer `#`, so result is 1 (false).
10467    #[test]
10468    fn zsh_corpus_hash_hash_quantifier_min_two() {
10469        let _g = crate::test_util::global_state_lock();
10470        assert!(
10471            !ext_glob_match("(fo##)#", "foooofof"),
10472            "ztst:36 — (fo##)# rejects foooofof"
10473        );
10474    }
10475
10476    /// `Test/D02glob.ztst:50` — `[[ aac = ((a))#a(c) ]]` —
10477    /// `((a))#` is `0+ a`. `aac` = `aa` (= `((a))#`) + `c`. The
10478    /// final `a(c)` requires literal `a` followed by capture `(c)`.
10479    /// Wait — should be `aac` matches `((a))#a(c)`: outer `((a))#`
10480    /// captures `a` repeated, then literal `a`, then `(c)`.
10481    /// Actually re-reading: `((a))#` matches 0+ 'a'. `aac` =
10482    /// `((a))#`("a") + `a`("a") + `(c)`("c"). So "aa" splits as
10483    /// (a)("a") + a("a") + c("c"). Match.
10484    #[test]
10485    fn zsh_corpus_nested_paren_quantifier() {
10486        let _g = crate::test_util::global_state_lock();
10487        assert!(
10488            ext_glob_match("((a))#a(c)", "aac"),
10489            "ztst:50 — ((a))#a(c) matches aac"
10490        );
10491    }
10492
10493    /// `Test/D02glob.ztst:51` — `[[ ac = ((a))#a(c) ]]` — single
10494    /// `a` matches `a(c)` after empty `((a))#` consumes zero.
10495    #[test]
10496    fn zsh_corpus_zero_iteration_quantifier() {
10497        let _g = crate::test_util::global_state_lock();
10498        assert!(
10499            ext_glob_match("((a))#a(c)", "ac"),
10500            "ztst:51 — ((a))# can be zero iters"
10501        );
10502    }
10503
10504    /// `Test/D02glob.ztst:52` — `[[ c = ((a))#a(c) ]]` — empty
10505    /// `((a))#` + literal `a` requires at least one `a`. `c` has
10506    /// no `a`, so result is 1 (false).
10507    #[test]
10508    fn zsh_corpus_required_literal_after_zero_quantifier() {
10509        let _g = crate::test_util::global_state_lock();
10510        assert!(
10511            !ext_glob_match("((a))#a(c)", "c"),
10512            "ztst:52 — bare `c` lacks required `a`"
10513        );
10514    }
10515
10516    /// `Test/D02glob.ztst:73` — `[[ foo = ((^x)) ]]` — exclude
10517    /// `x`. `foo` doesn't contain `x` as a single char (the
10518    /// `((^x))` matches anything that's not just 'x' — `foo` is 3
10519    /// chars, none is `x`).
10520    #[test]
10521    fn zsh_corpus_caret_exclude_basic() {
10522        let _g = crate::test_util::global_state_lock();
10523        assert!(
10524            ext_glob_match("((^x))", "foo"),
10525            "ztst:73 — ((^x)) matches foo"
10526        );
10527    }
10528
10529    /// `Test/D02glob.ztst:75` — `[[ foo = ((^foo)) ]]` — `^foo`
10530    /// rejects exact `foo`. `foo` matches `foo` literal, which the
10531    /// `^` inverts. Result: 1 (false).
10532    #[test]
10533    fn zsh_corpus_caret_exclude_exact_match_inverted() {
10534        let _g = crate::test_util::global_state_lock();
10535        assert!(
10536            !ext_glob_match("((^foo))", "foo"),
10537            "ztst:75 — ((^foo)) rejects foo"
10538        );
10539    }
10540
10541    /// `Test/D02glob.ztst:79` — `[[ foot = z*~*x ]]` — `*` then
10542    /// exclude `*x`. `foot` doesn't start with `z`, so doesn't
10543    /// match `z*` at all → result 1 (false).
10544    #[test]
10545    fn zsh_corpus_star_exclude_no_z_prefix() {
10546        let _g = crate::test_util::global_state_lock();
10547        assert!(
10548            !ext_glob_match("z*~*x", "foot"),
10549            "ztst:79 — foot doesn't start with z"
10550        );
10551    }
10552
10553    /// `Test/D02glob.ztst:80` — `[[ zoot = z*~*x ]]` — `zoot`
10554    /// matches `z*` AND doesn't end in `x` → result 0 (true).
10555    #[test]
10556    fn zsh_corpus_star_exclude_zoot() {
10557        let _g = crate::test_util::global_state_lock();
10558        assert!(
10559            ext_glob_match("z*~*x", "zoot"),
10560            "ztst:80 — zoot matches z* and not *x"
10561        );
10562    }
10563
10564    // ─── POSIX class brackets — Test/D02glob.ztst:111-118 ─────────────
10565
10566    /// `Test/D02glob.ztst:111` — `[[:alpha:][:punct:]]#[[:digit:]][^[:lower:]]`
10567    /// over "a%1X": `a` matches alpha, `%` matches punct (1+ via `#`),
10568    /// `1` matches digit, `X` matches NOT-lower.
10569    #[test]
10570    fn zsh_corpus_posix_class_alpha_punct_digit_notlower() {
10571        let _g = crate::test_util::global_state_lock();
10572        assert!(
10573            ext_glob_match("[[:alpha:][:punct:]]#[[:digit:]][^[:lower:]]", "a%1X"),
10574            "ztst:111 — alpha-punct-digit-notlower chain",
10575        );
10576    }
10577
10578    /// `c:Src/pattern.c:3894` — `PP_ASCII: return !(c & ~0x7f)`.
10579    ///
10580    /// `patcomppiece`'s inline class expansion had no `ascii` arm and no
10581    /// classmask bit, so the class fell to BOTH `_` catch-alls and contributed
10582    /// no members: `[[ a == [[:ascii:]] ]]` was false where zsh is true, and
10583    /// the negation inverted. Every other POSIX class was already correct.
10584    ///
10585    /// Note `range_type_lookup` above already asserted
10586    /// `range_type("ascii") == Some(3)` and PASSED throughout — the lookup was
10587    /// fine; nothing exercised the MATCH. That is presumably why a class that
10588    /// matched nothing went unnoticed. This test goes through `ext_glob_match`,
10589    /// the path a user's pattern actually takes.
10590    #[test]
10591    fn posix_ascii_class_has_members() {
10592        let _g = crate::test_util::global_state_lock();
10593        assert!(ext_glob_match("[[:ascii:]]", "a"), "ASCII char must match");
10594        assert!(ext_glob_match("[[:ascii:]]", "5"), "ASCII digit must match");
10595        assert!(
10596            !ext_glob_match("[^[:ascii:]]", "a"),
10597            "negation must reject an ASCII char",
10598        );
10599        // A byte >= 0x80 leads a UTF-8 sequence; the multibyte path reports no
10600        // match for this class, so the ASCII-only member set is complete.
10601        assert!(
10602            !ext_glob_match("[[:ascii:]]", "\u{e9}"),
10603            "non-ASCII must not match",
10604        );
10605        // The tell that made the class look present: the sibling range does
10606        // the work while the class contributes nothing.
10607        assert!(ext_glob_match("[[:ascii:]0-9]", "5"), "sibling range still matches");
10608    }
10609
10610    /// `Test/D02glob.ztst:112` — same pattern, "a%1" lacks the
10611    /// 4th non-lower char → result 1 (false).
10612    #[test]
10613    fn zsh_corpus_posix_class_rejects_short_input() {
10614        let _g = crate::test_util::global_state_lock();
10615        assert!(
10616            !ext_glob_match("[[:alpha:][:punct:]]#[[:digit:]][^[:lower:]]", "a%1"),
10617            "ztst:112 — short input rejected",
10618        );
10619    }
10620
10621    /// `Test/D02glob.ztst:113` — `[[:` literal in char class:
10622    /// `[[ [: = [[:]# ]]` — `[[:]` matches `[` OR `:`, `#` is 1+
10623    /// repetitions. "[:" = `[` + `:` → matches.
10624    #[test]
10625    fn zsh_corpus_literal_brackets_in_class_with_repetition() {
10626        let _g = crate::test_util::global_state_lock();
10627        assert!(
10628            ext_glob_match("[[:]#", "[:"),
10629            "ztst:113 — [[:]# matches '[:'"
10630        );
10631    }
10632
10633    // ─── (#i) / (#l) case modifiers — Test/D02glob.ztst:119-132 ───────
10634
10635    /// `Test/D02glob.ztst:119` — `(#i)FOOXX` matches "fooxx"
10636    /// (case-insensitive throughout).
10637    #[test]
10638    fn zsh_corpus_hash_i_case_insensitive() {
10639        let _g = crate::test_util::global_state_lock();
10640        assert!(
10641            ext_glob_match("(#i)FOOXX", "fooxx"),
10642            "ztst:119 — (#i)FOOXX matches fooxx"
10643        );
10644    }
10645
10646    /// `Test/D02glob.ztst:120` — `(#l)FOOXX` requires pattern UPPER
10647    /// chars to be the ONLY uppercase candidates; lowercase input
10648    /// FAILS to match because pattern is all uppercase and `#l` only
10649    /// matches the EXACT lowercase variant of the pattern char... actually
10650    /// `(#l)` means "lowercase pattern chars match uppercase too" —
10651    /// `(#l)FOOXX` has no lowercase chars so it stays a strict
10652    /// uppercase match. "fooxx" doesn't match. Result 1 (false).
10653    #[test]
10654    fn zsh_corpus_hash_l_uppercase_pattern_with_lower_input_fails() {
10655        let _g = crate::test_util::global_state_lock();
10656        assert!(
10657            !ext_glob_match("(#l)FOOXX", "fooxx"),
10658            "ztst:120 — (#l)FOOXX does NOT match fooxx"
10659        );
10660    }
10661
10662    /// `Test/D02glob.ztst:121` — `(#l)fooxx` (lowercase pattern) DOES
10663    /// match "FOOXX" because `#l` is the asymmetric "lowercase pat
10664    /// chars match upper-or-lower" rule.
10665    #[test]
10666    fn zsh_corpus_hash_l_lowercase_pattern_matches_uppercase_input() {
10667        let _g = crate::test_util::global_state_lock();
10668        assert!(
10669            ext_glob_match("(#l)fooxx", "FOOXX"),
10670            "ztst:121 — (#l)fooxx matches FOOXX (asymmetric upcasing)"
10671        );
10672    }
10673
10674    /// `Test/D02glob.ztst:122` — `(#i)FOO(#I)X(#i)X` mixes case
10675    /// modes. `(#I)` cancels prior `(#i)`. So 4th char `X` requires
10676    /// exact case. "fooxx" has lowercase `x` at that position → fail.
10677    #[test]
10678    fn zsh_corpus_hash_capital_i_cancels_case_insensitive() {
10679        let _g = crate::test_util::global_state_lock();
10680        assert!(
10681            !ext_glob_match("(#i)FOO(#I)X(#i)X", "fooxx"),
10682            "ztst:122 — (#I) cancels (#i), 4th char `X` requires upper"
10683        );
10684    }
10685
10686    /// `Test/D02glob.ztst:123` — same pattern with input "fooXx":
10687    /// `(#i)FOO` matches "foo", `(#I)X` matches "X" exact-case,
10688    /// `(#i)X` matches "x" insensitive → result 0 (true).
10689    #[test]
10690    fn zsh_corpus_hash_i_capital_i_toggle_succeeds() {
10691        let _g = crate::test_util::global_state_lock();
10692        assert!(
10693            ext_glob_match("(#i)FOO(#I)X(#i)X", "fooXx"),
10694            "ztst:123 — mixed case modifiers succeed"
10695        );
10696    }
10697
10698    /// `Test/D02glob.ztst:128` — `(#i)*m*` matches "Modules"
10699    /// case-insensitively.
10700    #[test]
10701    fn zsh_corpus_hash_i_with_star_glob() {
10702        let _g = crate::test_util::global_state_lock();
10703        assert!(
10704            ext_glob_match("(#i)*m*", "Modules"),
10705            "ztst:128 — (#i)*m* case-insensitive substring"
10706        );
10707    }
10708
10709    // ─── Numeric ranges `<n-m>` — Test/D02glob.ztst:133-137 ───────────
10710
10711    /// `Test/D02glob.ztst:133` — `<1-1000>33` matches "633" (the
10712    /// "6" is in [1..1000], followed by literal "33"). Actually
10713    /// the way zsh parses: `<1-1000>` matches ONE number from
10714    /// 1-1000, then `33` is literal. "633" = `6`(in 1-1000 range) +
10715    /// "33"(literal). So `<1-1000>33` matches "633".
10716    #[test]
10717    fn zsh_corpus_numeric_range_one_to_thousand() {
10718        let _g = crate::test_util::global_state_lock();
10719        assert!(
10720            ext_glob_match("<1-1000>33", "633"),
10721            "ztst:133 — <1-1000>33 matches 633"
10722        );
10723    }
10724
10725    /// `Test/D02glob.ztst:136` — `<->33` is the open-ended range
10726    /// (any number) followed by 33. Matches "633".
10727    #[test]
10728    fn zsh_corpus_numeric_range_open_ended() {
10729        let _g = crate::test_util::global_state_lock();
10730        assert!(
10731            ext_glob_match("<->33", "633"),
10732            "ztst:136 — <->33 matches 633 (any number)"
10733        );
10734    }
10735
10736    // ─── (#a) approximate match details — Test/D02glob.ztst:147-164 ───
10737
10738    /// `Test/D02glob.ztst:147` — `(#a1)[b][b]` matches "bob" via
10739    /// 1 substitution (middle `o` substituted in `[b][b]`'s gap).
10740    /// Wait — `[b][b]` is two-char "bb". Match against "bob" requires
10741    /// 1 edit. With (#a1), allowed.
10742    #[test]
10743    fn zsh_corpus_hash_a1_bracket_class_with_one_edit() {
10744        let _g = crate::test_util::global_state_lock();
10745        assert!(
10746            ext_glob_match("(#a1)[b][b]", "bob"),
10747            "ztst:147 — (#a1)[b][b] matches bob via 1 edit"
10748        );
10749    }
10750
10751    /// `Test/D02glob.ztst:151` — `(#a2)XbcX` matches "abcd" via
10752    /// 2 substitutions (a↔X, d↔X).
10753    #[test]
10754    fn zsh_corpus_hash_a2_two_substitutions() {
10755        let _g = crate::test_util::global_state_lock();
10756        assert!(
10757            ext_glob_match("(#a2)XbcX", "abcd"),
10758            "ztst:151 — 2 substitutions allowed"
10759        );
10760    }
10761
10762    /// `Test/D02glob.ztst:152` — `(#a2)ad` matches "abcd" via
10763    /// 2 INSERTIONS (b and c inserted into input vs pattern).
10764    /// Pattern is "ad" (2 chars), input is "abcd" (4 chars).
10765    /// Diff: 2 extra chars in input.
10766    #[test]
10767    fn zsh_corpus_hash_a2_two_insertions_in_input() {
10768        let _g = crate::test_util::global_state_lock();
10769        assert!(
10770            ext_glob_match("(#a2)ad", "abcd"),
10771            "ztst:152 — (#a2)ad matches abcd via 2 insertions in input"
10772        );
10773    }
10774
10775    /// `Test/D02glob.ztst:153` — `(#a2)abcd` matches "ad" via
10776    /// 2 DELETIONS from input (b and c missing from input vs pattern).
10777    #[test]
10778    fn zsh_corpus_hash_a2_two_deletions_from_input() {
10779        let _g = crate::test_util::global_state_lock();
10780        assert!(
10781            ext_glob_match("(#a2)abcd", "ad"),
10782            "ztst:153 — (#a2)abcd matches ad via 2 deletions"
10783        );
10784    }
10785
10786    /// `Test/D02glob.ztst:158` — `(#a2)abcd` rejects "dcba" — 4
10787    /// changes needed (full reverse) > budget 2.
10788    #[test]
10789    fn zsh_corpus_hash_a2_rejects_full_reverse() {
10790        let _g = crate::test_util::global_state_lock();
10791        assert!(
10792            !ext_glob_match("(#a2)abcd", "dcba"),
10793            "ztst:158 — full reverse exceeds budget 2"
10794        );
10795    }
10796
10797    /// `Test/D02glob.ztst:159` — `(#a3)abcd` matches "dcba" via
10798    /// 3 substitutions (positions 0,1,3 of input). Wait — actually
10799    /// "dcba" vs "abcd" — 4 positions differ. But (#a3) allows
10800    /// 3 errors. Hmm, but ztst says result is 0 (true). So zsh
10801    /// accepts. Possibly via DAMERAU transposition (swap adjacent).
10802    /// Without Damerau swap, this needs 4 subs → false. With
10803    /// transposition, "dcba" → "cdba" → "dcab"... complicated.
10804    /// Mark ignore — Damerau transposition isn't in the Rust port.
10805    #[test]
10806    fn zsh_corpus_hash_a3_reverse_via_transposition() {
10807        let _g = crate::test_util::global_state_lock();
10808        assert!(
10809            ext_glob_match("(#a3)abcd", "dcba"),
10810            "ztst:159 — (#a3)abcd matches dcba via Damerau transpositions"
10811        );
10812    }
10813
10814    // ─── (#s) start / (#e) end anchors — Test/D02glob.ztst:168-179 ────
10815
10816    /// `Test/D02glob.ztst:168` — `*((#s)|/)test((#e)|/)*` matches
10817    /// "test" — start-anchor (#s) at position 0, end-anchor (#e)
10818    /// after "test".
10819    #[test]
10820    fn zsh_corpus_hash_s_e_anchors_match_bare_test() {
10821        let _g = crate::test_util::global_state_lock();
10822        assert!(
10823            ext_glob_match("*((#s)|/)test((#e)|/)*", "test"),
10824            "ztst:168 — start/end anchors match bare 'test'",
10825        );
10826    }
10827
10828    /// `Test/D02glob.ztst:172` — `*((#s)|/)test((#e)|/)*` rejects
10829    /// "atest" — `(#s)|/` requires position 0 OR a `/`, but `a`
10830    /// is neither.
10831    #[test]
10832    fn zsh_corpus_hash_s_anchor_rejects_a_prefix() {
10833        let _g = crate::test_util::global_state_lock();
10834        assert!(
10835            !ext_glob_match("*((#s)|/)test((#e)|/)*", "atest"),
10836            "ztst:172 — atest fails the (#s) start-or-slash anchor",
10837        );
10838    }
10839
10840    // ─── Misc/globtests pins — `~` exclusion and `^` negation ─────────
10841
10842    /// `Misc/globtests` — `[[ foo~ = foo~ ]]` — bare tilde is literal.
10843    #[test]
10844    fn zsh_corpus_literal_tilde_in_pattern() {
10845        let _g = crate::test_util::global_state_lock();
10846        assert!(
10847            ext_glob_match("foo~", "foo~"),
10848            "literal ~ in non-extglob position"
10849        );
10850    }
10851
10852    /// `Misc/globtests` — `[[ foo.c = *.c~boo* ]]` — extended-glob
10853    /// exclusion: `*.c` minus things matching `boo*`. "foo.c" doesn't
10854    /// match "boo*", so it stays.
10855    #[test]
10856    fn zsh_corpus_exclusion_keeps_non_excluded() {
10857        let _g = crate::test_util::global_state_lock();
10858        assert!(ext_glob_match("*.c~boo*", "foo.c"), "exclusion keeps foo.c");
10859    }
10860
10861    /// `Misc/globtests` — `[[ foo.c = *.c~boo*~foo* ]]` — chained
10862    /// exclusions: `*.c` minus `boo*` minus `foo*`. "foo.c" matches
10863    /// `foo*`, so excluded.
10864    #[test]
10865    fn zsh_corpus_chained_exclusion_removes_match() {
10866        let _g = crate::test_util::global_state_lock();
10867        assert!(
10868            !ext_glob_match("*.c~boo*~foo*", "foo.c"),
10869            "chained exclusion excludes foo.c",
10870        );
10871    }
10872
10873    /// `Misc/globtests` — `[[ fofo = (fo#)# ]]` — outer `(...)#` is
10874    /// zero-or-more closure over `fo#` (one f followed by zero+ o).
10875    #[test]
10876    fn zsh_corpus_closure_of_closure_matches_repeated_fo() {
10877        let _g = crate::test_util::global_state_lock();
10878        assert!(ext_glob_match("(fo#)#", "fofo"), "(fo#)# matches fofo");
10879    }
10880
10881    /// `Misc/globtests` — `[[ ffo = (fo#)# ]]` — "ffo" = "f"+"fo".
10882    #[test]
10883    fn zsh_corpus_closure_matches_ffo() {
10884        let _g = crate::test_util::global_state_lock();
10885        assert!(ext_glob_match("(fo#)#", "ffo"), "(fo#)# matches ffo");
10886    }
10887
10888    /// `Misc/globtests` — `[[ xfoooofof = (fo#)# ]]` — leading "x"
10889    /// breaks the pattern.
10890    #[test]
10891    fn zsh_corpus_closure_rejects_leading_x() {
10892        let _g = crate::test_util::global_state_lock();
10893        assert!(
10894            !ext_glob_match("(fo#)#", "xfoooofof"),
10895            "(fo#)# rejects leading x",
10896        );
10897    }
10898
10899    /// `Misc/globtests` — `[[ foo = ((^x)) ]]` — `(^x)` matches one
10900    /// char that isn't 'x'. "foo" has 'f' first, so matches.
10901    /// Note: zsh's `(^x)` is exactly-one-not-x with extendedglob.
10902    #[test]
10903    fn zsh_corpus_negation_caret_matches_non_x_start() {
10904        let _g = crate::test_util::global_state_lock();
10905        assert!(ext_glob_match("((^x)*)", "foo"), "(^x)* matches foo");
10906    }
10907
10908    /// `Misc/globtests` — `[[ foo = ((^foo)) ]]` — `(^foo)` is "not foo",
10909    /// "foo" matches `foo`, so excluded → false.
10910    #[test]
10911    fn zsh_corpus_negation_caret_rejects_full_match() {
10912        let _g = crate::test_util::global_state_lock();
10913        assert!(
10914            !ext_glob_match("((^foo))", "foo"),
10915            "(^foo) rejects 'foo' itself",
10916        );
10917    }
10918
10919    /// `Misc/globtests` — `[[ abcd = ?(a|b)c#d ]]` — `?(a|b)` is
10920    /// "zero-or-one of a|b", `c#d` is "zero+ c then d". "abcd" = a + b? no, ?
10921    /// is single char or alternation. Let me re-read: `?(a|b)` matches
10922    /// one char which is `a` OR `b`. So "abcd": ?=a, then needs `c#d`
10923    /// = (c)*d, matches "bcd" if first char then need cd. Hmm.
10924    /// Actually: `?(a|b)` = `?` then `(a|b)` as separate? More likely
10925    /// `?(a|b)` is ksh-style "0-or-1 of a|b" only with KSH_GLOB.
10926    /// In zsh native (extendedglob) `?` is single char, `(a|b)` is
10927    /// alternation. So `?(a|b)c#d` = anychar (a|b) c* d. "abcd" =
10928    /// a + b + (zero c) + ... fails. The ztst says it MATCHES.
10929    /// Conclusion: requires KSH_GLOB which we may not enable.
10930    #[test]
10931    fn zsh_corpus_ksh_question_alternation() {
10932        let _g = crate::test_util::global_state_lock();
10933        assert!(
10934            ext_glob_match("?(a|b)c#d", "abcd"),
10935            "?(a|b)c#d matches abcd",
10936        );
10937    }
10938
10939    // ── patmatchlen — c:2649 ─────────────────────────────────────────
10940
10941    /// `Src/pattern.c:2649` — `int patmatchlen(void)` returns the
10942    /// byte-length of the last successful pattry match. After a
10943    /// successful match against `"hello"` with pattern `"hel*"`,
10944    /// the recorded length is 5 (all of "hello" was consumed).
10945    #[test]
10946    fn patmatchlen_records_consumed_byte_length() {
10947        let _g = crate::test_util::global_state_lock();
10948        let prog = compile("hel*");
10949        assert!(pattry(&prog, "hello"), "hel* matches hello");
10950        assert_eq!(patmatchlen(), 5, "all 5 bytes of 'hello' consumed by hel*",);
10951    }
10952
10953    /// Anchored prefix match: `"foo"` against `"foobar"` with the
10954    /// `NOANCH` form is the user-visible case `[[ foobar = foo* ]]`,
10955    /// which consumes all 6 bytes when the pattern matches the whole
10956    /// string.
10957    #[test]
10958    fn patmatchlen_full_string_match_returns_full_length() {
10959        let _g = crate::test_util::global_state_lock();
10960        let prog = compile("foo*");
10961        assert!(pattry(&prog, "foobar"));
10962        assert_eq!(patmatchlen(), 6, "foo* against 'foobar' = 6 bytes");
10963    }
10964
10965    // ── patmatchindex (c:4004) ──────────────────────────────────────
10966
10967    /// `patmatchindex` on a literal-byte range returns the byte at
10968    /// the requested position.
10969    #[test]
10970    fn patmatchindex_literal_byte_at_index() {
10971        let _g = crate::test_util::global_state_lock();
10972        let range = b"abc";
10973        assert_eq!(patmatchindex(range, 0), Some((Some(b'a'), 0)));
10974        assert_eq!(patmatchindex(range, 1), Some((Some(b'b'), 0)));
10975        assert_eq!(patmatchindex(range, 2), Some((Some(b'c'), 0)));
10976        assert_eq!(patmatchindex(range, 3), None, "out-of-range index");
10977    }
10978
10979    /// `patmatchindex` on a PP_ALPHA class marker returns `(None,
10980    /// PP_ALPHA)` to signal the class without a literal char.
10981    #[test]
10982    fn patmatchindex_posix_class_marker_returns_mtp() {
10983        let _g = crate::test_util::global_state_lock();
10984        // Meta + PP_ALPHA = 0x83 + 1 = 0x84
10985        let range = &[Meta + PP_ALPHA as u8];
10986        let r = patmatchindex(range, 0);
10987        assert_eq!(r, Some((None, PP_ALPHA)));
10988    }
10989
10990    // ── mb_patmatchrange (c:3610) ───────────────────────────────────
10991
10992    /// `mb_patmatchrange` literal hit on `'a'`.
10993    #[test]
10994    fn mb_patmatchrange_literal_ascii_hits() {
10995        let _g = crate::test_util::global_state_lock();
10996        let range = b"a";
10997        let mut mtp = -1;
10998        assert!(
10999            mb_patmatchrange(range, 'a', 0, None, Some(&mut mtp)),
11000            "literal 'a' matches"
11001        );
11002        assert_eq!(mtp, 0, "literal hit sets mtp=0");
11003    }
11004
11005    /// `mb_patmatchrange` PP_DIGIT class hit on `'5'`.
11006    #[test]
11007    fn mb_patmatchrange_digit_class_hits() {
11008        let _g = crate::test_util::global_state_lock();
11009        let range = &[Meta + PP_DIGIT as u8];
11010        assert!(mb_patmatchrange(range, '5', 0, None, None));
11011        assert!(!mb_patmatchrange(range, 'x', 0, None, None));
11012    }
11013
11014    /// `mb_patmatchrange` PP_RANGE on `'b'` in `a..c`.
11015    #[test]
11016    fn mb_patmatchrange_range_hits_middle() {
11017        let _g = crate::test_util::global_state_lock();
11018        // Meta + PP_RANGE then two literal bytes for the endpoints.
11019        let range = &[Meta + PP_RANGE as u8, b'a', b'c'];
11020        assert!(mb_patmatchrange(range, 'b', 0, None, None));
11021        assert!(mb_patmatchrange(range, 'a', 0, None, None));
11022        assert!(mb_patmatchrange(range, 'c', 0, None, None));
11023        assert!(!mb_patmatchrange(range, 'd', 0, None, None));
11024    }
11025
11026    // ── mb_patmatchindex (c:3767) ───────────────────────────────────
11027
11028    /// `mb_patmatchindex` literal char at index 0.
11029    #[test]
11030    fn mb_patmatchindex_literal_first_index() {
11031        let _g = crate::test_util::global_state_lock();
11032        let range = b"xyz";
11033        let r = mb_patmatchindex(range, 0);
11034        assert_eq!(r, Some((Some('x'), 0)));
11035    }
11036
11037    /// `mb_patmatchindex` on PP_UPPER marker at index 0.
11038    #[test]
11039    fn mb_patmatchindex_class_marker_returns_mtp() {
11040        let _g = crate::test_util::global_state_lock();
11041        let range = &[Meta + PP_UPPER as u8];
11042        assert_eq!(mb_patmatchindex(range, 0), Some((None, PP_UPPER)));
11043    }
11044
11045    // ═══════════════════════════════════════════════════════════════════
11046    // Additional C-parity tests for Src/pattern.c bit-flag predicates.
11047    // ═══════════════════════════════════════════════════════════════════
11048
11049    /// c:200 — P_ISBRANCH checks bit 0x20.
11050    #[test]
11051    fn P_ISBRANCH_recognizes_bit_0x20() {
11052        assert!(P_ISBRANCH(0x20));
11053        assert!(P_ISBRANCH(0x20 | 0x01));
11054        assert!(P_ISBRANCH(0xFF));
11055        assert!(!P_ISBRANCH(0x00));
11056        assert!(!P_ISBRANCH(0x1F));
11057    }
11058
11059    /// c:201 — P_ISEXCLUDE checks bits 0x30 = 0x10 | 0x20 (BOTH must be set).
11060    #[test]
11061    fn P_ISEXCLUDE_requires_both_bits() {
11062        assert!(P_ISEXCLUDE(0x30));
11063        assert!(P_ISEXCLUDE(0x30 | 0x01));
11064        assert!(!P_ISEXCLUDE(0x10), "only 0x10 — must require both");
11065        assert!(!P_ISEXCLUDE(0x20), "only 0x20 — must require both");
11066        assert!(!P_ISEXCLUDE(0x00));
11067    }
11068
11069    /// c:202 — P_NOTDOT checks bit 0x40.
11070    #[test]
11071    fn P_NOTDOT_recognizes_bit_0x40() {
11072        assert!(P_NOTDOT(0x40));
11073        assert!(P_NOTDOT(0x40 | 0x01));
11074        assert!(P_NOTDOT(0xFF));
11075        assert!(!P_NOTDOT(0x00));
11076        assert!(!P_NOTDOT(0x3F));
11077    }
11078
11079    /// c:216-218 — P_SIMPLE/HSTART/PURESTR are distinct bits.
11080    #[test]
11081    fn p_flag_constants_are_distinct() {
11082        assert_eq!(P_SIMPLE & P_HSTART, 0);
11083        assert_eq!(P_SIMPLE & P_PURESTR, 0);
11084        assert_eq!(P_HSTART & P_PURESTR, 0);
11085    }
11086
11087    /// c:216 — P_SIMPLE is bit 0 (= 0x01).
11088    #[test]
11089    fn p_simple_is_bit_zero() {
11090        assert_eq!(P_SIMPLE, 0x01);
11091    }
11092
11093    /// c:217 — P_HSTART is bit 1 (= 0x02).
11094    #[test]
11095    fn p_hstart_is_bit_one() {
11096        assert_eq!(P_HSTART, 0x02);
11097    }
11098
11099    /// c:218 — P_PURESTR is bit 2 (= 0x04).
11100    #[test]
11101    fn p_purestr_is_bit_two() {
11102        assert_eq!(P_PURESTR, 0x04);
11103    }
11104
11105    /// c:406-407 — PA_NOALIGN=1, PA_UNMETA=2 (single-bit flags).
11106    #[test]
11107    fn pa_flag_constants_canonical() {
11108        assert_eq!(PA_NOALIGN, 1);
11109        assert_eq!(PA_UNMETA, 2);
11110    }
11111
11112    /// PA_NOALIGN | PA_UNMETA combine without overlap.
11113    #[test]
11114    fn pa_flags_pairwise_disjoint() {
11115        assert_eq!(PA_NOALIGN & PA_UNMETA, 0);
11116    }
11117
11118    /// c:336 — metacharinc on ASCII char advances by 1 byte.
11119    #[test]
11120    fn metacharinc_ascii_advances_one() {
11121        let _g = crate::test_util::global_state_lock();
11122        assert_eq!(metacharinc("hello", 0), 1);
11123        assert_eq!(metacharinc("hello", 4), 5);
11124    }
11125
11126    /// c:336 — metacharinc at end-of-string returns same pos.
11127    #[test]
11128    fn metacharinc_end_of_string_returns_same() {
11129        let _g = crate::test_util::global_state_lock();
11130        assert_eq!(metacharinc("abc", 3), 3, "at end → no advance");
11131    }
11132
11133    /// c:336 — metacharinc on multibyte char advances by codepoint width.
11134    #[test]
11135    fn metacharinc_multibyte_advances_by_codepoint_len() {
11136        let _g = crate::test_util::global_state_lock();
11137        // 'é' is 2 bytes in UTF-8.
11138        assert_eq!(metacharinc("é", 0), 2);
11139        // '日' is 3 bytes.
11140        assert_eq!(metacharinc("日", 0), 3);
11141    }
11142
11143    // ═══════════════════════════════════════════════════════════════════
11144    // Additional C-parity tests for Src/pattern.c
11145    // c:155 P_ISBRANCH / c:161 P_ISEXCLUDE / c:167 P_NOTDOT /
11146    // c:262 metacharinc / c:502 patcompstart / c:536 patcompile /
11147    // c:1008 patgetglobflags / c:1129 range_type / c:1152 pattern_range_to_string
11148    // c:2059 charref / c:2066 charnext / c:2153 pattry / c:2304 patmatchlen
11149    // ═══════════════════════════════════════════════════════════════════
11150
11151    /// c:262 — `metacharinc` is pure for ASCII input.
11152    #[test]
11153    fn metacharinc_is_pure_for_ascii() {
11154        for s in ["", "a", "abc", "hello"] {
11155            for pos in 0..s.len() {
11156                let first = metacharinc(s, pos);
11157                for _ in 0..3 {
11158                    assert_eq!(
11159                        metacharinc(s, pos),
11160                        first,
11161                        "metacharinc({:?}, {}) must be pure",
11162                        s,
11163                        pos
11164                    );
11165                }
11166            }
11167        }
11168    }
11169
11170    /// c:536 — `patcompile("", 0, None)` returns Option<Patprog>.
11171    #[test]
11172    fn patcompile_returns_option_patprog_type() {
11173        let _g = crate::test_util::global_state_lock();
11174        let _: Option<Patprog> = patcompile("", 0, None);
11175    }
11176
11177    /// c:1008 — `patgetglobflags("")` empty returns None.
11178    #[test]
11179    fn patgetglobflags_empty_returns_none() {
11180        let _g = crate::test_util::global_state_lock();
11181        assert!(patgetglobflags("").is_none(), "empty → None");
11182    }
11183
11184    /// c:1008 — `patgetglobflags` returns Option<(i32, i64, usize)> type.
11185    #[test]
11186    fn patgetglobflags_returns_option_tuple_type() {
11187        let _g = crate::test_util::global_state_lock();
11188        let _: Option<(i32, i64, usize)> = patgetglobflags("abc");
11189    }
11190
11191    /// c:1129 — `range_type("")` empty returns None.
11192    #[test]
11193    fn range_type_empty_returns_none() {
11194        assert!(range_type("").is_none(), "empty → None");
11195    }
11196
11197    /// c:1129 — `range_type` returns Option<usize>.
11198    #[test]
11199    fn range_type_returns_option_usize_type() {
11200        let _: Option<usize> = range_type("abc");
11201    }
11202
11203    /// c:1152 — `pattern_range_to_string("")` empty returns empty String.
11204    #[test]
11205    fn pattern_range_to_string_empty_returns_empty() {
11206        assert_eq!(pattern_range_to_string(""), "");
11207    }
11208
11209    /// c:1152 — `pattern_range_to_string` is pure.
11210    #[test]
11211    fn pattern_range_to_string_is_pure() {
11212        for s in ["", "abc", "a-z"] {
11213            let first = pattern_range_to_string(s);
11214            for _ in 0..3 {
11215                assert_eq!(
11216                    pattern_range_to_string(s),
11217                    first,
11218                    "pattern_range_to_string({:?}) must be pure",
11219                    s
11220                );
11221            }
11222        }
11223    }
11224
11225    /// c:2059 — `charref("", 0)` empty returns None.
11226    #[test]
11227    fn charref_empty_returns_none() {
11228        assert!(charref("", 0).is_none(), "empty → None");
11229    }
11230
11231    /// c:2066 — `charnext("", 0)` empty returns 0 (no advance).
11232    #[test]
11233    fn charnext_empty_returns_zero() {
11234        assert_eq!(charnext("", 0), 0);
11235    }
11236
11237    /// c:2304 — `patmatchlen` returns i32 (compile-time type pin).
11238    #[test]
11239    fn patmatchlen_returns_i32_type() {
11240        let _g = crate::test_util::global_state_lock();
11241        let _: i32 = patmatchlen();
11242    }
11243
11244    /// c:155 — P_ISBRANCH/P_ISEXCLUDE/P_NOTDOT all return bool (type pin).
11245    #[test]
11246    fn p_predicates_return_bool_type() {
11247        let _: bool = P_ISBRANCH(0);
11248        let _: bool = P_ISEXCLUDE(0);
11249        let _: bool = P_NOTDOT(0);
11250    }
11251}