// fnmatch — shell wildcard pattern matching. Port of Hare's // fnmatch:: (ref/hare/fnmatch/fnmatch.ha) using ww byte-indexed // cursors in place of Hare's strings::iterator. // // Matching rules (Hare-spec): // // - '?' matches any single byte // - '*' matches any byte sequence (including empty) // - '['…']' bracket expression. Inside: byte ranges (`a-z`), // POSIX character classes ([:alnum:], [:alpha:], // …), leading '!' negates. Hare-style only: '^' // is *not* accepted as a negation marker; it // yields invalid. // - '\\' escape — `` is taken literally. Disabled by // flag.NOESCAPE, in which case '\\' is itself a // literal. // - any other matches itself. // // Flags (see [[flag]]): // // - PATHNAME: '/' in the string is matched only by a literal '/' // in the pattern (segments delimited by '/' are matched // independently with PATHNAME stripped). // - NOESCAPE: '\\' loses its escape meaning. // - PERIOD: a leading '.' (or — under PATHNAME — a '.' // immediately following a '/') must be matched by a literal // '.' in the pattern, not by '?' / '*' / '[…]'. // // Divergence from Hare: // // - ww has no UTF-8 rune iteration on str; matching is byte-wise. // Pattern and string bytes compare directly, bracket ranges // `[a-z]` operate over byte values (ASCII-only meaningful; // multibyte UTF-8 only matches byte-identically, not // codepoint-equivalently). Same precedent as // [[strings.byteindex]]'s rune-needle arm (which matches a // single ASCII byte). When ww grows UTF-8 iteration, this // module graduates "in one go" (lib/CLAUDE.md) and the // byte-only caveat retires. // // - Hare returns invalid up the public surface via `?` so callers // can distinguish bad patterns from non-matches. ww collapses // invalid → false at the public boundary, matching Hare's // fnmatch.ha:53 (`return b is bool && b: bool;`). // // - Hare's algorithm uses strings::iterator (with `next` / `prev` // / `riter` / `slice` / `iterstr`). ww has none of these // yet; the port models a cursor as a bare `i32` byte index // into the source `str` and threads `(pat, *i32)` through // pat_next / match_bracket / match_ctype. The tail-match phase // uses a fresh forward cursor at `string.len - cnt` instead of // a reverse iterator — produces the same byte sequence. // // - Hare's match_ctype builds a `[name, *fn(rune) bool]` table // and dispatches via fn-pointer call; ww uses a sequence of // `streq` comparisons routing to the matching [[ascii.is*]] // predicate. Equivalent, narrower lowering. // // Owning model: pure function — no allocation, no mutation of // caller-owned data. Pattern and string are read-only byte sources; // the returned `bool` is the only outward effect. // // if (fnmatch.fnmatch("*.c", "foo.c", fnmatch.flag.NONE)) { … }; // if (fnmatch.fnmatch("a/*.c", "a/x.c", // fnmatch.flag.PATHNAME)) { … }; package fnmatch; import ascii; import strings; // flag — bitmask altering match semantics. Stored as `enum i32` // to match [[os.flag]] / [[temp.mode]]; the four named values are // small unsigned bit positions where i32-vs-u32 makes no // observable difference. Bitwise AND/OR composition lives in the // public boundary, which casts to i32 once and back. export type flag = enum i32 { NONE = 0, PATHNAME = 1, NOESCAPE = 2, PERIOD = 4, }; // invalid — internal sentinel for a malformed pattern (bad bracket, // unterminated escape, unknown POSIX class, …). Module-private: // Hare exposes this to callers via `?`, ww collapses it to `false` // at the public boundary (mirrors Hare's `fnmatch` line 53). Once // callers need pattern-validity diagnostics, a `try`-shaped public // entry can be added without churning the existing surface. type invalid = !void; // Token tags returned by [[pat_next]]. void aliases so they can sit // alongside `u8` (a literal pattern byte) and `invalid` in one // tagged-union return. Same shape as [[io.eof]] / [[io.closed]]. type star = void; type question = void; type bracket = void; type endt = void; // pat_next — consume one pattern element starting at `*pos`. // Returns the matching token tag for metacharacters (`*` / `?` / // `[`), `endt` at exhaustion, the literal byte otherwise. Under // flag.NOESCAPE, `\\` is itself a literal byte; otherwise `\\` // returns `` and an unterminated `\\` returns `invalid`. // Mirrors Hare's pat_next (fnmatch.ha:313). fn pat_next(pat: str, pos: *i32, fl: flag) (u8 | star | question | bracket | endt | invalid) = { if (*pos >= pat.len) { let e: endt; return e; }; let c: u8 = pat[*pos]; *pos += 1; if (c == '*') { let r: star; return r; }; if (c == '?') { let r: question; return r; }; if (c == '[') { let r: bracket; return r; }; if (c == '\\') { if (((fl as i32) & (flag.NOESCAPE as i32)) == 0) { if (*pos >= pat.len) { let e: invalid; return e; }; let c2: u8 = pat[*pos]; *pos += 1; return c2; }; return '\\'; }; return c; }; // advance_or_err — read one byte from `s` at `*pos`, advancing. // Returns invalid on exhaustion. Mirrors Hare's advance_or_err // (fnmatch.ha:336), narrowed to bytes. fn advance_or_err(s: str, pos: *i32) (u8 | invalid) = { if (*pos >= s.len) { let e: invalid; return e; }; let c: u8 = s[*pos]; *pos += 1; return c; }; // streq — byte-equal compare. ascii is the only character set we // fold against; the POSIX class names are pure ASCII, so a flat // length-checked memcmp matches Hare's behavior here. fn streq(a: str, b: str) bool = { if (a.len != b.len) { return false; }; let i: i32 = 0; for (i < a.len) { if (a[i] != b[i]) { return false; }; i += 1; }; return true; }; // match_ctype — parse a "[:name:]" character class. `*pos` points // just past the leading colon on entry; the function consumes // through the closing "]" and returns whether `c` is a member of // the named class. Mirrors Hare's match_ctype (fnmatch.ha:284), // modulo: ascii.is* are dispatched by direct name compare instead // of through a fn-pointer table. fn match_ctype(pat: str, pos: *i32, c: u8) (bool | invalid) = { let nameoff: i32 = *pos; let i: i32 = 0; let r: u8 = 0u8; for (r != ':') { let rr = advance_or_err(pat, pos); match (rr) { case let e: invalid => return e; case let f: u8 => { r = f; }; }; if (!ascii.valid(r: rune)) { let e: invalid; return e; }; i += 1; }; let rr = advance_or_err(pat, pos); match (rr) { case let e: invalid => return e; case let f: u8 => { if (f != ']') { let e: invalid; return e; }; }; }; let name: str; name.ptr = pat.ptr + (nameoff: u64); name.len = i - 1; let cr: rune = c: rune; if (streq(name, "alnum")) { return ascii.isalnum(cr); }; if (streq(name, "alpha")) { return ascii.isalpha(cr); }; if (streq(name, "blank")) { return ascii.isblank(cr); }; if (streq(name, "cntrl")) { return ascii.iscntrl(cr); }; if (streq(name, "digit")) { return ascii.isdigit(cr); }; if (streq(name, "graph")) { return ascii.isgraph(cr); }; if (streq(name, "lower")) { return ascii.islower(cr); }; if (streq(name, "print")) { return ascii.isprint(cr); }; if (streq(name, "punct")) { return ascii.ispunct(cr); }; if (streq(name, "space")) { return ascii.isspace(cr); }; if (streq(name, "upper")) { return ascii.isupper(cr); }; if (streq(name, "xdigit")) { return ascii.isxdigit(cr); }; let e: invalid; return e; }; // match_bracket — consume one `[…]` group at `*pos` and decide // whether `c` is a member. Mirrors Hare's match_bracket // (fnmatch.ha:220). // // On entry `*pos` is positioned just past the leading '['; on // successful return it sits just past the closing ']'. fn match_bracket(pat: str, pos: *i32, c: u8) (bool | invalid) = { let oldpos: i32 = *pos; let r0 = advance_or_err(pat, pos); let first: u8 = 0u8; match (r0) { case let e: invalid => return e; case let f: u8 => { first = f; }; }; if (first == '^') { let e: invalid; return e; }; // '^' is Hare-invalid let inv: bool = false; if (first == '!') { inv = true; let r1 = advance_or_err(pat, pos); match (r1) { case let e: invalid => return e; case let f: u8 => { first = f; }; }; }; let found: bool = (first != '[') && (first == c); let havelast: bool = true; let last: u8 = first; if (first == ']') { // ']' as first member let r2 = advance_or_err(pat, pos); match (r2) { case let e: invalid => return e; case let f: u8 => { first = f; }; }; }; let r: u8 = first; let loop: bool = true; for (loop) { if (r == ']') { // ']' closes loop = false; } else { if (r == '-') { // '-' range let er = advance_or_err(pat, pos); let endv: u8 = 0u8; match (er) { case let e: invalid => return e; case let f: u8 => { endv = f; }; }; if (endv == ']') { // Trailing '-' matches itself. Un-eat ']' // so the next iteration sees it. *pos -= 1; last = '-'; havelast = true; if (c == '-') { found = true; }; } else { if (!havelast) { let e: invalid; return e; }; if ((last: u32) <= (c: u32) && (c: u32) <= (endv: u32)) { found = true; }; havelast = false; // forbid `a-f-n` }; } else { if (r == '[') { let nx = advance_or_err(pat, pos); let nxv: u8 = 0u8; match (nx) { case let e: invalid => return e; case let f: u8 => { nxv = f; }; }; if (nxv == '=') { let e: invalid; return e; }; if (nxv == '.') { let e: invalid; return e; }; if (nxv == ':') { // ':' class let t = match_ctype(pat, pos, c); match (t) { case let e: invalid => return e; case let v: bool => { if (v) { found = true; }; }; }; } else { // Not a class — un-eat the byte after '['. *pos -= 1; if (c == '[') { found = true; }; }; last = '['; havelast = true; } else { // literal byte if (c == r) { found = true; }; last = r; havelast = true; }; }; }; if (loop) { let rr = advance_or_err(pat, pos); match (rr) { case let e: invalid => return e; case let f: u8 => { r = f; }; }; }; }; // Hare's degeneracy check: `[xyx]` where the bracket spans // exactly `[X..X]` for X in `=`, `.`, `:` is invalid (would // have been an empty collating/equivalence/class). let cnt: i32 = *pos - oldpos; if (havelast && first == last && cnt >= 4) { if (first == '=') { let e: invalid; return e; }; if (first == '.') { let e: invalid; return e; }; if (first == ':') { let e: invalid; return e; }; }; if (inv) { return !found; }; return found; }; // fnmatch_internal — core "sea of stars" matcher. Drives the same // three-phase algorithm as Hare (fnmatch.ha:101): // // 1. Exact match against pattern up to the first '*' (prefix). // 2. Exact match of the remaining pattern after the last '*' // against the corresponding tail of the string. // 3. Greedy left-to-right match of each pattern segment between // stars against the leftover middle of the string. // // Step (3) has no exponential corner because each star anchors a // "match found" at increasing string positions. fn fnmatch_internal(pattern: str, string: str, fl: flag) (bool | invalid) = { if (((fl as i32) & (flag.PERIOD as i32)) != 0) { if (strings.hasprefix(string, ".") && !strings.hasprefix(pattern, ".")) { return false; }; }; let pp: i32 = 0; let sp: i32 = 0; let sawstar: bool = false; for (!sawstar) { let scur: i32 = sp; let isdone: bool = (scur >= string.len); let curs: u8 = 0u8; if (!isdone) { curs = string[scur]; scur += 1; }; let t = pat_next(pattern, &pp, fl); let advance: bool = true; match (t) { case let e: invalid => return e; case star => { sawstar = true; advance = false; }; case endt => { return isdone; }; case question => { if (isdone) { return false; }; }; case bracket => { if (isdone) { return false; }; let mr = match_bracket(pattern, &pp, curs); match (mr) { case let e: invalid => return e; case let b: bool => { if (!b) { return false; }; }; }; }; case let r: u8 => { if (isdone) { return false; }; if (curs != r) { return false; }; }; }; if (advance) { sp = scur; }; }; let pp_copy: i32 = pp; let pp_last: i32 = pp; let pp_last_cnt: i32 = 0; let cnt: i32 = 0; let pdone: bool = false; for (!pdone) { let t = pat_next(pattern, &pp, fl); match (t) { case let e: invalid => return e; case endt => { pdone = true; }; case star => { pp_last = pp; pp_last_cnt = cnt + 1; cnt += 1; }; case bracket => { let mr = match_bracket(pattern, &pp, 0u8); match (mr) { case let e: invalid => return e; case let _b: bool => { }; }; cnt += 1; }; case question => { cnt += 1; }; case let r: u8 => { cnt += 1; }; }; }; pp = pp_last; let tail_cnt: i32 = cnt - pp_last_cnt; // String has to leave room for the post-star tail; the prefix // already consumed up to sp. let sp_copy: i32 = sp; let tail_pos: i32 = string.len - tail_cnt; if (tail_pos < sp_copy) { return false; }; let ts: i32 = tail_pos; let tdone: bool = false; for (!tdone) { let t = pat_next(pattern, &pp, fl); let havec: bool = (ts < string.len); let ch: u8 = 0u8; if (havec) { ch = string[ts]; }; match (t) { case let e: invalid => return e; case endt => { if (havec) { return false; }; tdone = true; }; case star => { // Unreachable — pp starts after pp_last (past the // last star), so pat_next can't yield another star. // Treat defensively as invalid pattern. let e: invalid; return e; }; case question => { if (!havec) { return false; }; ts += 1; }; case bracket => { if (!havec) { return false; }; let mr = match_bracket(pattern, &pp, ch); match (mr) { case let e: invalid => return e; case let b: bool => { if (!b) { return false; }; }; }; ts += 1; }; case let r: u8 => { if (!havec) { return false; }; if (ch != r) { return false; }; ts += 1; }; }; }; let mid_pat: str; mid_pat.ptr = pattern.ptr + (pp_copy: u64); mid_pat.len = pp_last - pp_copy; let mid_str: str; mid_str.ptr = string.ptr + (sp_copy: u64); mid_str.len = tail_pos - sp_copy; let mpp_copy: i32 = 0; let msp_copy: i32 = 0; let outer: bool = true; for (outer) { let mpp: i32 = mpp_copy; if (mpp >= mid_pat.len) { return true; }; let msp: i32 = msp_copy; let inner: bool = true; let sawstarm: bool = false; for (inner) { let scur: i32 = msp; let isdone: bool = (scur >= mid_str.len); let cur: u8 = 0u8; if (!isdone) { cur = mid_str[scur]; scur += 1; }; let t = pat_next(mid_pat, &mpp, fl); let matched: bool = false; let advance: bool = true; match (t) { case let e: invalid => return e; case endt => { // mid_pat by construction never ends with end // here (the last token was a star); treat as // invalid defensively. let e: invalid; return e; }; case question => { matched = !isdone; }; case bracket => { if (isdone) { matched = false; } else { let mr = match_bracket(mid_pat, &mpp, cur); match (mr) { case let e: invalid => return e; case let b: bool => { matched = b; }; }; }; }; case let r: u8 => { matched = (!isdone) && (cur == r); }; case star => { mpp_copy = mpp; msp_copy = msp; inner = false; sawstarm = true; advance = false; }; }; if (advance) { if (!matched) { inner = false; } else { msp = scur; }; }; }; if (!sawstarm) { // Inner failed — advance the outer string anchor by // one byte and retry from the top of mid_pat. if (msp_copy >= mid_str.len) { return false; }; msp_copy += 1; }; }; return false; }; // fnmatch_pathname — PATHNAME variant: split pattern and string on // '/' and match each segment independently with [[fnmatch_internal]]. // Mirrors Hare's fnmatch_pathname (fnmatch.ha:58). fn fnmatch_pathname(pattern: str, string: str, fl: flag) (bool | invalid) = { let pp: i32 = 0; let segstart_pat: i32 = 0; let sp: i32 = 0; let donetokens: bool = false; let final: bool = false; for (!final) { segstart_pat = pp; let inner: bool = true; let kind: i32 = 0; // 0 = saw '/', 1 = saw end for (inner) { let t = pat_next(pattern, &pp, fl); match (t) { case let e: invalid => return e; case endt => { kind = 1; inner = false; }; case let r: u8 => { if (r == '/') { kind = 0; inner = false; }; }; case bracket => { let mr = match_bracket(pattern, &pp, 0u8); match (mr) { case let e: invalid => return e; case let _b: bool => { }; }; }; case question => { }; case star => { }; }; }; // Pull the next string token (Hare's strings::next_token // on `/`). if (donetokens) { return false; }; let segstart_s: i32 = sp; for (sp < string.len) { if (string[sp] == '/') { break; }; sp += 1; }; let seg: str; seg.ptr = string.ptr + (segstart_s: u64); seg.len = sp - segstart_s; let hadslash: bool = (sp < string.len); if (hadslash) { sp += 1; } else { donetokens = true; }; let p_slice: str; if (kind == 0) { // pp is just past the '/'; slice excludes it. p_slice.ptr = pattern.ptr + (segstart_pat: u64); p_slice.len = (pp - 1) - segstart_pat; } else { // kind == 1: end of pattern; this is the final // segment-pattern. Whole tail of pattern from // segstart_pat is the slice. p_slice.ptr = pattern.ptr + (segstart_pat: u64); p_slice.len = pp - segstart_pat; final = true; }; let r = fnmatch_internal(p_slice, seg, fl); match (r) { case let e: invalid => return e; case let b: bool => { if (!b) { return false; }; }; }; if (final) { // Final segment: require string also exhausted // (Hare's `&& strings::next_token(&tok) is done`). if (!donetokens) { return false; }; return true; }; }; return false; }; // fnmatch — public entry. Selects PATHNAME-split or core matcher // based on `flags`; invalid pattern → false (Hare's collapsing // semantic, fnmatch.ha:53). export fn fnmatch(pattern: str, string: str, flags: flag) bool = { let r: (bool | invalid); if (((flags as i32) & (flag.PATHNAME as i32)) != 0) { r = fnmatch_pathname(pattern, string, flags); } else { r = fnmatch_internal(pattern, string, flags); }; match (r) { case invalid => return false; case let b: bool => return b; }; };