lib+test: add fnmatch over Hare sea-of-stars

Port of ref/hare/fnmatch/fnmatch.ha. Public surface mirrors Hare:
`flag` enum (NONE/PATHNAME/NOESCAPE/PERIOD) and `fnmatch(pattern,
string, flags) bool`.

Algorithm is the three-phase sea-of-stars (also used in musl):
exact-match the prefix before the first `*`, exact-match the tail
after the last `*`, then greedily match each star-delimited middle
segment with backtrack on inner failure. No exponential corner —
each star anchors a "match found" at strictly increasing positions.

Bracket expressions: Hare-strict — `!` for negation, `^` rejected
as invalid; `]` as first member legal, trailing `-` literal, all
12 POSIX classes ([:alnum:] … [:xdigit:]) via direct streq + the
ascii.is* predicates.

Divergences from Hare (documented in fnmatch.ww docblock):
  - byte-indexed cursors in place of strings::iterator (no UTF-8
    rune iter yet); ASCII-only meaningful, multibyte matches
    byte-identically. Graduates "in one go" per lib/CLAUDE.md
    when the language stack grows rune iteration.
  - invalid pattern collapses to `false` at the public boundary
    (Hare's `b is bool && b: bool;`); a try-shaped diagnostic
    entry can be added later without churning the surface.
  - tail-match uses a forward cursor at `string.len - cnt`
    instead of riter/prev — same byte sequence either way.

Test fixture follows the project's helper-per-row table-driven
shape (precedent: lib/encoding/base32/base32_test.ww). 8 @test
fns clustered by feature (basic / brackets / ctype / period /
noescape / musl_basic / pathname / combined), ~95 rows total
adapted from Hare's +test.ha plus musl-derived edge cases.

Wired as 972_fnmatch_run alongside 970_fmt_run / 971_log_run in
the stdlib-runtime band.

Unblocked by 7f60ebb (cstage+wwstage SK_USE→SK_X promotion
missing use_alias), which is what let the module name `fnmatch`
coexist with an exported leaf fn `fnmatch`.
This commit is contained in:
2026-05-15 15:11:59 +09:00
parent 7f60ebbe44
commit 1d5ff201ee
4 changed files with 927 additions and 1 deletions

607
lib/fnmatch/fnmatch.ww Normal file
View File

@@ -0,0 +1,607 @@
// 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.
//
// Surface today:
//
// fnmatch.flag — enum i32 bitmask (NONE / PATHNAME /
// NOESCAPE / PERIOD)
// fnmatch.fnmatch(pattern: str, string: str, flags: flag) bool
//
// 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.
// - '\\<c>' escape — `<c>` 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)) { … };
use ascii;
use 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 `\\<c>`
// returns `<c>` 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 == 42u8) { let r: star; return r; }; // '*'
if (c == 63u8) { let r: question; return r; }; // '?'
if (c == 91u8) { let r: bracket; return r; }; // '['
if (c == 92u8) { // '\\'
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 92u8;
};
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 != 58u8) { // ':'
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 != 93u8) { 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 == 94u8) { let e: invalid; return e; }; // '^' is Hare-invalid
let inv: bool = false;
if (first == 33u8) { // '!'
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 != 91u8) && (first == c);
let havelast: bool = true;
let last: u8 = first;
if (first == 93u8) { // ']' 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 == 93u8) { // ']' closes
loop = false;
} else { if (r == 45u8) { // '-' 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 == 93u8) {
// Trailing '-' matches itself. Un-eat ']'
// so the next iteration sees it.
*pos -= 1;
last = 45u8;
havelast = true;
if (c == 45u8) { 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 == 91u8) { // '['
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 == 61u8) { let e: invalid; return e; }; // '='
if (nxv == 46u8) { let e: invalid; return e; }; // '.'
if (nxv == 58u8) { // ':' 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 == 91u8) { found = true; };
};
last = 91u8;
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 == 61u8) { let e: invalid; return e; };
if (first == 46u8) { let e: invalid; return e; };
if (first == 58u8) { 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;
// ---- prefix: match up to the first '*' ----------------------
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; };
};
// ---- find the tail (token count after the last '*') ---------
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; };
// ---- tail: match (from pp) against string[tail_pos..] -------
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;
};
};
};
// ---- middle: greedy match of each star-delimited subpattern --
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;
// Walk pattern until the next '/' (segment break) or end.
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 == 47u8) { 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] == 47u8) { 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;
};
};

263
lib/fnmatch/fnmatchtest.ww Normal file
View File

@@ -0,0 +1,263 @@
// fnmatchtest — exercises lib/fnmatch. Run with
// `out/bin/ww run lib/fnmatch/fnmatchtest.ww`.
//
// Table-driven via the [[check]] helper: every row is the uniform
// 4-tuple `(pattern, string, expected, flags)`. Cases follow Hare's
// ref/hare/fnmatch/+test.ha (the de-facto spec for this port);
// multibyte UTF-8 rows (わたし, ε) are skipped — our matcher is
// byte-wise and those would match byte-identically but obscure the
// ASCII semantics the test is asserting.
//
// Failure path: each @test fn bumps `signalled` to its slot index,
// `check` does `exit(signalled + 10)` on miscompare so the harness
// reports `WEXITSTATUS = 11..N` pointing at the failing scenario.
// Same convention as lib/log/logtest.
use fnmatch;
// Direct rt_syscall binding rather than `use os;` — os exports
// read/write/close, which collide with io.read/write/close under the
// driver's flat-scope concat. Mirrors logtest / fmttest / bufiotest.
@symbol("rt_syscall") fn syscall1ww(num: i64, a: i64) i64;
fn doexit(code: i32) void = {
syscall1ww(60i64, code: i64);
};
let signalled: i32 = 0;
fn fail() void = { doexit(signalled + 10); };
// check — pin one row. `flags` is taken as i32 and cast to
// fnmatch.flag so the test rows can read as integer bitmasks
// without dragging enum literals into every line.
fn check(pat: str, s: str, expected: bool, flags: i32) void = {
let f: fnmatch.flag = flags as fnmatch.flag;
if (fnmatch.fnmatch(pat, s, f) != expected) { fail(); };
};
// ---- basic literal / wildcard cases ---------------------------------
@test fn basic() void = {
check("a", "a", true, 0);
check("b", "b", true, 0);
check("\0", "\0", true, 0);
check("abcde", "abcde", true, 0);
check("aaa", "bbb", false, 0);
check("ab*cde", "abcde", true, 0);
check("g*a", "gordana", true, 0);
check("ab*cde*foo*bar", "abcdefooba", false, 0);
check("*", "foo", true, 0);
check("aa*", "aafoo", true, 0);
check("bb*", "foo", false, 0);
check("*cc", "foocc", true, 0);
check("*dd", "foo", false, 0);
check("ra**ra", "rarara", true, 0);
check("x*yy*x", "xxyyyyyxxx", true, 0);
check("*", "*", true, 0);
check("*", "", true, 0);
check("****", "a", true, 0);
check("**a**", "a", true, 0);
check("****", "", true, 0);
check("?", "*", true, 0);
check("?", "", false, 0);
check("??", "a", false, 0);
check("??", "abc", false, 0);
check("?aa", "bbb", false, 0);
check("**?**", "", false, 0);
check("*?*?*?*?", "abcd", true, 0);
check("*?*?*?*?", "abc", false, 0);
};
// ---- bracket expressions: literal / range / negation ----------------
@test fn brackets() void = {
check("[b]", "b", true, 0);
check("a[b]c", "abc", true, 0);
check("a[b]c", "axc", false, 0);
check("[a-c]", "b", true, 0);
check("[a-z]", "a", true, 0);
check("[c-a]", "b", false, 0);
check("x[a-c]y", "xay", true, 0);
check("x[a-c]y", "xby", true, 0);
check("x[a-c]y", "xcy", true, 0);
check("x[a-c]y", "xzy", false, 0);
check("x[a-c]y", "xy", false, 0);
check("[a-c]*[a-c]", "axxxb", true, 0);
// Special members: '-' at start/end, ']' as first member.
check("[-]", "-", true, 0);
check("[.]", ".", true, 0);
check("[:ias]", ":", true, 0);
check("[-]", "a", false, 0);
check("[-ac]", "a", true, 0);
check("[-ac]", "-", true, 0);
check("[-ac]", "b", false, 0);
check("[ac-]", "a", true, 0);
check("[ac-]", "-", true, 0);
check("[ac-]", "b", false, 0);
// Equivalence-class-like syntax: invalid in Hare/POSIX.
check("[.a.]", "a", false, 0);
// Negation with '!'.
check("[!b]", "b", false, 0);
check("a[!b]c", "abc", false, 0);
check("a[!b]c", "axc", true, 0);
check("[!a-c]", "b", false, 0);
check("[!c-a]", "b", true, 0);
check("x[!a-c]y", "xay", false, 0);
check("x[!a-c]y", "xby", false, 0);
check("x[!a-c]y", "xcy", false, 0);
check("x[!a-c]y", "xzy", true, 0);
check("x[!a-c]y", "xy", false, 0);
check("[!a-c]*[!a-c]", "axxxb", false, 0);
check("[!-]", "-", false, 0);
check("[!-]", "a", true, 0);
check("[!-ac]", "a", false, 0);
check("[!-ac]", "-", false, 0);
check("[!-ac]", "b", true, 0);
};
// ---- POSIX character classes: [[:alnum:]] / [[:alpha:]] / ... -------
@test fn ctype() void = {
check("[[:alnum:]]", "7", true, 0);
check("[[:alpha:]]", "[", false, 0);
check("[[:alpha:]]", "[[", false, 0);
check("[[alpha:]]", "a]", true, 0);
check("[[alpha:]]", ":]", true, 0);
check("[[:alpha:]]", "a", true, 0);
check("[[:blank:]]", " ", true, 0);
check("[[:alnum:]]a", "a]a", false, 0);
check("[[:alnum:][[:digit:]]", "a", true, 0);
check("[![:alnum:]]", "a", false, 0);
check("[![:alpha:]]", "[", true, 0);
check("[![:alnum:]]a", "a]a", false, 0);
check("[![:alpha:]]a", "[a", true, 0);
check("[![:alnum:][:digit:]]", "a", false, 0);
};
// ---- flag.PERIOD: leading '.' must be literal in the pattern --------
@test fn period() void = {
let fp: i32 = 4; // flag.PERIOD
check(".", ".", true, fp);
check("*", ".", false, fp);
check("?", ".", false, fp);
check("[.]", ".", false, fp);
check(".*", ".asdf", true, fp);
check(".*", "asdf", false, fp);
};
// ---- flag.NOESCAPE: '\\' loses its escape meaning -------------------
@test fn noescape() void = {
let nesc: i32 = 2; // flag.NOESCAPE
check("\\", "\\", true, nesc);
check("\\*", "\\asdf", true, nesc);
};
// ---- musl-adapted cases (no flags) ---------------------------------
@test fn musl_basic() void = {
check("*.c", "foo.c", true, 0);
check("*.c", ".c", true, 0);
check("*.a", "foo.c", false, 0);
check("*.c", ".foo.c", true, 0);
check("a\\*.c", "ax.c", false, 0);
check("a[xy].c", "ax.c", true, 0);
check("a[!y].c", "ax.c", true, 0);
check("-O[01]", "-O1", true, 0);
check("[[?*\\]", "\\", true, 0);
check("[]?*\\]", "]", true, 0);
check("[!]a-]", "b", true, 0);
check("[]-_]", "^", true, 0);
check("[!]-_]", "X", true, 0);
check("??", "-", false, 0);
// Without PATHNAME, `[...]` and `?` happily eat '/'.
check("[*]/b", "a/b", false, 0);
check("[*]/b", "*/b", true, 0);
check("[?]/b", "a/b", false, 0);
check("[?]/b", "?/b", true, 0);
check("[[a]/b", "a/b", true, 0);
check("[[a]/b", "[/b", true, 0);
check("\\*/b", "a/b", false, 0);
check("\\*/b", "*/b", true, 0);
check("\\?/b", "a/b", false, 0);
check("\\?/b", "?/b", true, 0);
check("[/b", "[/b", false, 0);
check("\\[/b", "[/b", true, 0);
check("a[/]b", "a/b", true, 0);
// "[![:d-d]" — bracket carrying a class-name start without
// proper close-bracket is treated as a non-class literal set.
// Hare's table asserts false in all three rows.
check("[![:d-d]", "b", false, 0);
check("[[:d-d]", "[", false, 0);
check("[![:d-d]", "[", false, 0);
};
// ---- flag.PATHNAME: '/' must be matched by literal '/' --------------
@test fn pathname() void = {
let fp: i32 = 1; // flag.PATHNAME
check("[a-z]/[a-z]", "a/b", true, fp);
check("a[/]b", "a/b", false, fp);
check("*", "a/b", false, fp);
check("*[/]b", "a/b", false, fp);
check("*[b]", "a/b", false, fp);
check("a[a/z]*.c", "a/x.c", false, fp);
check("a/*.c", "a/x.c", true, fp);
check("a*.c", "a/x.c", false, fp);
check("*/foo", "/foo", true, fp);
check("???b", "aa/b", false, fp);
};
// ---- flag.PERIOD + flag.PATHNAME combined ---------------------------
@test fn combined() void = {
let pp: i32 = 1; // PATHNAME
let fp: i32 = 4; // PERIOD
let pf: i32 = pp | fp;
check("?a/b", ".a/b", false, pf);
check("a/?b", "a/.b", false, pf);
check("*a/b", ".a/b", false, pf);
check("a/*b", "a/.b", false, pf);
check("[.]a/b", ".a/b", false, pf);
check("a/[.]b", "a/.b", false, pf);
check("*/?", "a/b", true, pf);
check("?/*", "a/b", true, pf);
check(".*/?", ".a/b", true, pf);
check("*/.?", "a/.b", true, pf);
check("*/*", "a/.b", false, pf);
check("*?*/*", "a/.b", true, fp);
check("*[.]/b", "a./b", true, pf);
check("*[[:alpha:]]/*[[:alnum:]]", "a/b", true, pp);
check("a?b", "a.b", true, pf);
check("a*b", "a.b", true, pf);
check("a[.]b", "a.b", true, pf);
check("*.c", ".foo.c", false, fp);
check("*.c", "foo.c", true, fp);
let nesc: i32 = 2; // NOESCAPE
check("a\\*.c", "a*.c", false, nesc);
};
export fn main() i32 = {
signalled = 1; basic();
signalled = 2; brackets();
signalled = 3; ctype();
signalled = 4; period();
signalled = 5; noescape();
signalled = 6; musl_basic();
signalled = 7; pathname();
signalled = 8; combined();
return 0;
};