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:
6
Makefile
6
Makefile
@@ -230,7 +230,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \
|
||||
$(BIN)/test_selfhost $(BIN)/test_w6a_ww $(BIN)/test_w6l_ww \
|
||||
$(BIN)/test_w6c_ww $(BIN)/test_ww_ww $(BIN)/test_self_rebuild \
|
||||
$(BIN)/test_dyn_ww $(BIN)/test_selfcheck $(BIN)/test_at_test_ww \
|
||||
$(BIN)/test_fmt_run $(BIN)/test_log_run \
|
||||
$(BIN)/test_fmt_run $(BIN)/test_log_run $(BIN)/test_fnmatch_run \
|
||||
$(BIN)/test_memio_run $(BIN)/test_temp_run $(BIN)/test_getopt_run \
|
||||
$(BIN)/test_base32_run $(BIN)/test_base64_run \
|
||||
$(BIN)/test_adler32_run $(BIN)/test_crc16_run \
|
||||
@@ -419,6 +419,10 @@ $(BIN)/test_log_run: test/wcc/971_log_run.c $(BIN)/ww $(BIN)/w6c \
|
||||
$(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN)
|
||||
$(CC) $(CFLAGS) -o $@ $<
|
||||
|
||||
$(BIN)/test_fnmatch_run: test/wcc/972_fnmatch_run.c $(BIN)/ww $(BIN)/w6c \
|
||||
$(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN)
|
||||
$(CC) $(CFLAGS) -o $@ $<
|
||||
|
||||
$(BIN)/test_memio_run: test/wcc/980_memio_run.c $(BIN)/ww $(BIN)/w6c \
|
||||
$(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN)
|
||||
$(CC) $(CFLAGS) -o $@ $<
|
||||
|
||||
607
lib/fnmatch/fnmatch.ww
Normal file
607
lib/fnmatch/fnmatch.ww
Normal 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
263
lib/fnmatch/fnmatchtest.ww
Normal 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;
|
||||
};
|
||||
52
test/wcc/972_fnmatch_run.c
Normal file
52
test/wcc/972_fnmatch_run.c
Normal file
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 972_fnmatch_run — execute the lib/fnmatch @test fixture under the
|
||||
* C-side `ww run` driver and assert exit 0.
|
||||
*
|
||||
* fnmatch is a pure leaf (depends on ascii + strings only) sitting
|
||||
* alongside fmt (970) and log (971) in the stdlib-runtime band. The
|
||||
* fixture (fnmatchtest.ww) carries its own `export fn main()` that
|
||||
* drives the @test fns and signals which case failed via the exit
|
||||
* code, so this file is a thin wrapper — no @test scanning, no
|
||||
* synthetic main generation.
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/wait.h>
|
||||
|
||||
static int
|
||||
runwait(const char *cmd)
|
||||
{
|
||||
int rc = system(cmd);
|
||||
if (rc == -1) return -1;
|
||||
if (WIFEXITED(rc)) return WEXITSTATUS(rc);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int
|
||||
main(void)
|
||||
{
|
||||
const char *bin = getenv("BIN");
|
||||
if (!bin) bin = "out/bin";
|
||||
char absbin[1024];
|
||||
if (bin[0] != '/') {
|
||||
char cwd[1024];
|
||||
if (getcwd(cwd, sizeof cwd) == NULL) return 1;
|
||||
snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin);
|
||||
bin = absbin;
|
||||
}
|
||||
char cwd[1024];
|
||||
if (getcwd(cwd, sizeof cwd) == NULL) return 1;
|
||||
|
||||
const char *src = "lib/fnmatch/fnmatchtest.ww";
|
||||
char path[1024], cmd[2048];
|
||||
snprintf(path, sizeof path, "%s/%s", cwd, src);
|
||||
snprintf(cmd, sizeof cmd, "%s/ww run %s", bin, path);
|
||||
int rc = runwait(cmd);
|
||||
if (rc != 0) {
|
||||
fprintf(stderr, "fnmatch_run FAIL: %s exited %d\n", src, rc);
|
||||
return 1;
|
||||
}
|
||||
printf("fnmatch_run: %s ok\n", src);
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user