lib/regex: type model + finish() (regex port fold 1, partial)
Port of ref/hare/regex/regex.ha fold 1 (the data model). Lands the
full type model — error, the inst_* variants + 10-variant inst union
(the nominally-distinct same-underlying size/void aliases included),
result/capture, charset + items, the regex struct — plus finish().
Test 989_regex_run pins variant discrimination, payload extraction,
struct shapes, and finish() on cstage; w6c == w6c_ww byte-identical.
Two fold-1 constructs are held back behind filed compiler/fidelity
gaps, documented at their sites (regex tasks A–D):
- charclass_map (regex.ha:74-87): const [](str, *fn(rune) bool)
table — blocked on the array-literal->slice element-coercion
checker gap (type.c:402-404 #258 borrow uses exact type_eq,
no element decay). It needs `import ascii;`, so both land with
the consuming fold (compile) once the gap is fixed.
- finish() free()s; ww is a no-free runtime (rt/alloc.s:30), so the
faithful body drops the frees, as the port drops every Hare
free(). Kept as a no-op for API parity.
DEFERRED to later folds: compile()/exec/find/replace.
This commit is contained in:
5
Makefile
5
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 $@ $<
|
||||
|
||||
98
lib/regex/regex.ww
Normal file
98
lib/regex/regex.ww
Normal file
@@ -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 = { };
|
||||
163
lib/regex/regex_test.ww
Normal file
163
lib/regex/regex_test.ww
Normal file
@@ -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;
|
||||
};
|
||||
52
test/wcc/989_regex_run.c
Normal file
52
test/wcc/989_regex_run.c
Normal file
@@ -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 <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/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;
|
||||
}
|
||||
Reference in New Issue
Block a user