diff --git a/Makefile b/Makefile index 86cc6c17..7801efd8 100644 --- a/Makefile +++ b/Makefile @@ -408,6 +408,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_adler32_run $(BIN)/test_crc16_run \ $(BIN)/test_crc32_run $(BIN)/test_crc64_run \ $(BIN)/test_siphash_run $(BIN)/test_sha256_run \ + $(BIN)/test_regex_run \ $(BIN)/test_checked_run \ $(BIN)/test_floatarr_run \ $(BIN)/test_deref_narrow_run \ @@ -1665,6 +1666,10 @@ $(BIN)/test_sha256_run: test/wcc/989_sha256_run.c $(BIN)/ww $(BIN)/w6c \ $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_regex_run: test/wcc/989_regex_run.c $(BIN)/ww $(BIN)/w6c \ + $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_bufio_run: test/wcc/998_bufio_run.c $(BIN)/ww $(BIN)/w6c \ $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< diff --git a/lib/regex/regex.ww b/lib/regex/regex.ww new file mode 100644 index 00000000..9caa4ca7 --- /dev/null +++ b/lib/regex/regex.ww @@ -0,0 +1,98 @@ +// regex — POSIX extended regular expressions. Port of +// ref/hare/regex/regex.ha. Fold 1 = the data model only; compile() / +// exec / find / replace are DEFERRED to later folds. +// +// Two fold-1 constructs are held back behind filed compiler/fidelity +// gaps (see the charclass_map and finish() sites below): +// - charclass_map (regex.ha:74-87) — a module-level const slice of +// (str, *fn(rune) bool) tuples. Blocked on the array-literal→slice +// element-coercion checker gap (#25; type.c:402-404 #258 borrow +// uses exact type_eq, no element decay). +// - finish() (regex.ha:96-102) — frees; ww is a no-free runtime (#27). +package regex; + +// ref/hare/regex/regex.ha:14 — an error string describing a compilation +// error. +export type error = !str; + +// ref/hare/regex/regex.ha:16-30. +export type inst_lit = rune; +export type inst_charset = struct { idx: size, is_positive: bool }; +export type inst_any = void; +export type inst_split = size; +export type inst_jump = size; +export type inst_skip = void; +export type inst_match = bool; +export type inst_groupstart = size; +export type inst_groupend = void; +export type inst_repeat = struct { + id: size, + origin: size, + min: (void | size), + max: (void | size), +}; + +// ref/hare/regex/regex.ha:32-35. +export type inst = (inst_lit | inst_any | inst_split | inst_jump | + inst_skip | inst_match | inst_charset | + inst_groupstart | inst_groupend | + inst_repeat); + +// The resulting match of a [[regex]] applied to a string. +// +// The first [[capture]] corresponds to the implicit zeroth capture +// group, i.e. the whole expression. +// +// The rest of the [[capture]]s correspond to the rest of the capture +// groups, i.e. the sub-expressions. +// ref/hare/regex/regex.ha:44. +export type result = []capture; + +// A (sub)match corresponding to a regular expression's capture group. +// ref/hare/regex/regex.ha:47-53. +export type capture = struct { + content: str, + start: size, + start_bytesize: size, + end: size, + end_bytesize: size, +}; + +// ref/hare/regex/regex.ha:68-72. +export type charset = [](charset_lit_item | charset_range_item | + charset_class_item); +export type charset_lit_item = rune; +export type charset_range_item = (u32, u32); +export type charset_class_item = (str, *fn(c: rune) bool); + +// ref/hare/regex/regex.ha:74-87 — charclass_map: the const +// [](str, *fn(rune) bool) table mapping POSIX class tokens to the +// matching ascii predicate. DEFERRED: the array-literal→slice +// assignability check (type.c:402-404, the #258 borrow) compares +// element types with exact type_eq and applies NO element coercion, so +// the literal `[(":alnum:]", &ascii.isalnum), ...]` (typed +// `[N](untyped_str, *fn(rune) bool)`) is rejected against the declared +// `[](str, *fn(rune) bool)`. Minimal repro: `let xs: [](size, size) = +// [(1, 2)];`. Reshaping to a fixed `[12](...)` array would compile but +// is an unfaithful workaround (CLAUDE.md rule-7), so the table — and +// the `import ascii;` it needs — land with the consuming fold (compile) +// once the checker gap is fixed. + +// ref/hare/regex/regex.ha:89-93. +export type regex = struct { + insts: []inst, + charsets: []charset, + n_reps: size, +}; + +// Frees resources associated with a [[regex]]. +// +// ref/hare/regex/regex.ha:96-102 frees re.insts / each charset / +// re.charsets. ww is a no-free runtime (rt/alloc.s:30 — rt_free is a +// no-op; the bump allocator can't reclaim, process-exit does), so the +// faithful ww body drops the frees, matching how the port drops every +// Hare free(). Kept for API parity with the Hare surface. Temporary +// empty body: #27 makes the free() builtin compile to a documented +// no-op, after which this ports the Hare frees VERBATIM (the no-op +// builtin reclaims nothing, same end state). +export fn finish(re: *regex) void = { }; diff --git a/lib/regex/regex_test.ww b/lib/regex/regex_test.ww new file mode 100644 index 00000000..63b6c31e --- /dev/null +++ b/lib/regex/regex_test.ww @@ -0,0 +1,163 @@ +// regex_test — exercises the lib/regex fold-1 data model (the type +// model + finish()). Run with `out/bin/ww run lib/regex/regex_test.ww`. +// +// Fold 1 ports the data model only; compile()/exec live in later folds. +// charclass_map's fn-ptr table is deferred behind the array→slice +// element-coercion checker gap (see regex.ww), so this test does not +// exercise the POSIX-class predicate dispatch yet — it pins variant +// discrimination (including the nominally-distinct same-underlying +// inst_split/inst_jump/inst_groupstart `size` aliases and the +// inst_any/inst_skip/inst_groupend `void` aliases), payload extraction, +// the regex/capture struct shapes, and finish(). Same +// signalled-then-fail()-with-+10 pattern as the rest of the stdlib +// run-tests; the non-zero exit pinpoints the failing case. +// +// Struct literals below name the type UNQUALIFIED (`inst_charset { … }`, +// not `regex.inst_charset { … }`): the parser rejects a module-qualified +// name in struct-literal position (#29), and the imported type +// is in scope unqualified. +package regex; + +import regex; +import os; + +let signalled: i32 = 0; +fn fail() void = { os.exit(signalled + 10); }; + +// inst_lit / inst_match carry distinguishable payloads (rune / bool). +@test fn lit_and_match() void = { + let a: regex.inst = ('a': regex.inst_lit); + match (a) { + case let l: regex.inst_lit => { if ((l: rune) != 'a') { fail(); }; }; + case => fail(); + }; + + let m: regex.inst = (true: regex.inst_match); + match (m) { + case let b: regex.inst_match => { if (!(b: bool)) { fail(); }; }; + case => fail(); + }; +}; + +// The three `size`-aliased variants are nominally distinct: a value +// built as inst_split must match inst_split, never inst_jump / +// inst_groupstart, despite identical underlying storage. +@test fn size_aliases_distinct() void = { + let sp: regex.inst = ((5: size): regex.inst_split); + match (sp) { + case let s: regex.inst_split => { if ((s: size) != (5: size)) { fail(); }; }; + case let j: regex.inst_jump => fail(); + case let g: regex.inst_groupstart => fail(); + case => fail(); + }; + + let jp: regex.inst = ((9: size): regex.inst_jump); + match (jp) { + case let j: regex.inst_jump => { if ((j: size) != (9: size)) { fail(); }; }; + case let s: regex.inst_split => fail(); + case => fail(); + }; + + let gs: regex.inst = ((2: size): regex.inst_groupstart); + match (gs) { + case let g: regex.inst_groupstart => { if ((g: size) != (2: size)) { fail(); }; }; + case let s: regex.inst_split => fail(); + case => fail(); + }; +}; + +// The `void`-aliased variants are likewise nominally distinct. +@test fn void_aliases_distinct() void = { + let av: regex.inst_any; + let an: regex.inst = av; + match (an) { + case let a: regex.inst_any => void; + case let k: regex.inst_skip => fail(); + case let e: regex.inst_groupend => fail(); + case => fail(); + }; + + let sv: regex.inst_skip; + let sk: regex.inst = sv; + match (sk) { + case let k: regex.inst_skip => void; + case let a: regex.inst_any => fail(); + case => fail(); + }; + + let gv: regex.inst_groupend; + let ge: regex.inst = gv; + match (ge) { + case let e: regex.inst_groupend => void; + case let a: regex.inst_any => fail(); + case let k: regex.inst_skip => fail(); + case => fail(); + }; +}; + +// inst_charset carries a struct payload; its fields survive the union +// round-trip. +@test fn charset_payload() void = { + let c: regex.inst = (inst_charset { idx = 3, is_positive = true }); + match (c) { + case let cs: regex.inst_charset => { + if (cs.idx != (3: size)) { fail(); }; + if (!cs.is_positive) { fail(); }; + }; + case => fail(); + }; +}; + +// inst_repeat round-trips through the inst union with its plain `size` +// fields intact. Matching the nested (void | size) min/max bounds back +// out is DEFERRED: `match` on a tagged-union-typed struct field +// diverges cs≠ww (#26 — the wwstage frames it wider), so +// asserting the bounds here would seed a rule-10-divergent fixture. +@test fn repeat_payload() void = { + let r: regex.inst = (inst_repeat { + id = 1, origin = 4, min = (2: size), max = void, + }); + match (r) { + case let rp: regex.inst_repeat => { + if (rp.id != (1: size)) { fail(); }; + if (rp.origin != (4: size)) { fail(); }; + }; + case => fail(); + }; +}; + +// The regex/capture structs hold their fields; finish() is a no-op +// (no-free runtime) and must accept a built regex. +@test fn struct_shapes_and_finish() void = { + let cap: regex.capture = capture { + content = "abc", + start = 0, + start_bytesize = 0, + end = 3, + end_bytesize = 3, + }; + if (cap.content.len != 3) { fail(); }; + if (cap.end != (3: size)) { fail(); }; + + // regex's insts/charsets ([]inst / []charset) are left empty here: + // fold 1 ports no compile() to populate them, an empty `[]` literal + // is unspellable as a typed slice (#25 — array→slice + // element-coercion gap), and a struct-literal slice-field store + // drops len/cap (#24). Declaring the regex zeroes both + // slice headers to {0,0,0}; only n_reps is set explicitly. + let re: regex.regex; + re.n_reps = 0; + if (re.n_reps != (0: size)) { fail(); }; + if (re.insts.len != 0) { fail(); }; + regex.finish(&re); +}; + +export fn main() i32 = { + signalled = 1; lit_and_match(); + signalled = 2; size_aliases_distinct(); + signalled = 3; void_aliases_distinct(); + signalled = 4; charset_payload(); + signalled = 5; repeat_payload(); + signalled = 6; struct_shapes_and_finish(); + return 0; +}; diff --git a/test/wcc/989_regex_run.c b/test/wcc/989_regex_run.c new file mode 100644 index 00000000..219496ab --- /dev/null +++ b/test/wcc/989_regex_run.c @@ -0,0 +1,52 @@ +/* + * 989_regex_run — execute the lib/regex fold-1 @test fixture under the + * C-side `ww run` driver and assert exit 0. + * + * Sibling to 984_base64_run / 989_sha256_run (9xx is full so this + * shares the 989 prefix — the `short` name keys the binary, cf the + * 949_* / 989_sha256 precedent). regex_test.ww carries its own + * `export fn main()` that drives the data-model @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 +#include +#include +#include + +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/regex/regex_test.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, "regex_run FAIL: %s exited %d\n", src, rc); + return 1; + } + printf("regex_run: %s ok\n", src); + return 0; +}