lib/regex: fold-2c — findall/result_freeall
findall (regex.ha:923-960) over the memio seeker: one fixed stream for the whole string, per-call suffix substring, absolute io.seek(SET) past the scanner readahead after each match. The append-then-mutate m[0] fix-up is verbatim Hare (the appended header shares m's backing); the zero-length-match rune advancement guard (ha:946-952) carries the infinite-loop protection. search's |success|=2 unwrap is the D13 explicit 3-arm match (ww-core #14); nomem propagates. result_freeall (ha:1119-1124) verbatim, frees no-op (#27). Tests port Hare's own findall table (+test.ha:719-731, the three fold-2a-reachable rows) through run_findall_case's checks, plus field rows pinning adjacency, the one-result overlap pick, multibyte zero-length advancement (utf8sz step != 1), idx != bytesize, the tail-match break, and the empty no-match slice. 989's run gets the conventional timeout-180 wrap: a regression of the zero-length guard would otherwise hang the gate (no-op frees, so no quick OOM exit).
This commit is contained in:
@@ -6,8 +6,9 @@
|
||||
// (delete_thread/is_consuming_inst/add_thread/run_thread); fold 2b
|
||||
// tranche C = search, the first end-to-end match; fold 2b tranche D
|
||||
// = the exec surface (test/find — the D13 explicit-match spelling of
|
||||
// Hare's multi-success `?`, ww-core #14). Every other metacharacter
|
||||
// arm (and replace/findall) is DEFERRED behind compiler fixes —
|
||||
// Hare's multi-success `?`, ww-core #14); fold 2c =
|
||||
// findall/result_freeall over the memio seeker. Every other
|
||||
// metacharacter arm (and replace) is DEFERRED behind compiler fixes —
|
||||
// probed pre-port, pA*/pB*/PB*/PC*/PD* probes. They land with those
|
||||
// fixes.
|
||||
//
|
||||
@@ -642,6 +643,68 @@ export fn find(re: *regex, string: str) (result | nomem) = {
|
||||
};
|
||||
};
|
||||
|
||||
// Attempts to match a [[regex]] against a string and returns all
|
||||
// non-overlapping matches as a slice of [[result]]s. The caller must
|
||||
// free the return value with [[result_freeall]].
|
||||
//
|
||||
// ref/hare/regex/regex.ha:920-960. Hare's ok-flag + `defer if (!ok)
|
||||
// result_freeall(res)` (ha:924-926) is omitted: the flag's only use
|
||||
// is the defer-if, ww defer takes a single expression, and the frees
|
||||
// are the documented no-op (#27) — compile()'s omitted `defer if`
|
||||
// precedent. The `search(...)?` unwrap (ha:933) is the same explicit
|
||||
// D13 lowering as test()/find() (ww-core #14); the nomem arm
|
||||
// propagates verbatim.
|
||||
export fn findall(re: *regex, string: str) ([]result | nomem) = {
|
||||
let res: []result;
|
||||
let str_idx: size = 0;
|
||||
let str_bytesize: size = 0;
|
||||
let strm: memio.stream = memio.fixed(strings.toutf8(string));
|
||||
let str_bytes: []u8 = strings.toutf8(string);
|
||||
for (true) {
|
||||
let substring: str =
|
||||
strings.frombytes(str_bytes[str_bytesize:]);
|
||||
let r: (void | []capture | nomem) =
|
||||
search(re, substring, &strm.vt, true);
|
||||
match (r) {
|
||||
case let m: []capture => {
|
||||
// Hare appends m and THEN fixes m[0] up from
|
||||
// substring- to whole-string-relative (ha:935-939):
|
||||
// the appended header shares m's backing, so the
|
||||
// mutations below are visible through res. Kept
|
||||
// verbatim — the aliasing is the subtle bit.
|
||||
append(res, m);
|
||||
m[0].start += str_idx;
|
||||
m[0].end += str_idx;
|
||||
m[0].start_bytesize += str_bytesize;
|
||||
m[0].end_bytesize += str_bytesize;
|
||||
str_idx = m[0].end;
|
||||
str_bytesize = m[0].end_bytesize;
|
||||
if (m[0].start_bytesize == (len(str_bytes): size)) {
|
||||
// end-of-string reached
|
||||
break;
|
||||
};
|
||||
if (m[0].start_bytesize == m[0].end_bytesize) {
|
||||
// zero-length match: forward rune and byte
|
||||
// indices (ha:946-952 — the guard against
|
||||
// the classic findall infinite loop)
|
||||
str_idx += 1;
|
||||
str_bytesize += (utf8.utf8sz(
|
||||
str_bytes[str_bytesize])!: size);
|
||||
};
|
||||
// ha:953-954: each search call's scanner buffers
|
||||
// past what scanrune consumed; the absolute SET
|
||||
// repositions the underlying stream before the
|
||||
// next call builds a fresh scanner.
|
||||
io.seek(&strm.vt, (str_bytesize: io.off),
|
||||
io.whence.SET)!;
|
||||
};
|
||||
case void => break;
|
||||
case let n: nomem => return n;
|
||||
};
|
||||
};
|
||||
return res;
|
||||
};
|
||||
|
||||
// Frees a [[result]].
|
||||
//
|
||||
// ref/hare/regex/regex.ha:1113-1116, verbatim — the free() builtin is
|
||||
@@ -650,6 +713,17 @@ export fn result_free(s: result) void = {
|
||||
free(s);
|
||||
};
|
||||
|
||||
// Frees a slice of [[result]]s.
|
||||
//
|
||||
// ref/hare/regex/regex.ha:1119-1124, verbatim — both frees are the
|
||||
// documented no-op (#27).
|
||||
export fn result_freeall(s: []result) void = {
|
||||
for (let r .. s) {
|
||||
result_free(r);
|
||||
};
|
||||
free(s);
|
||||
};
|
||||
|
||||
// Converts an [[error]] into a user-friendly string.
|
||||
//
|
||||
// ref/hare/regex/regex.ha:1126-1127 (expression-bodied in Hare; ww
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
// model + finish()), the fold-2a compile() literal core, the
|
||||
// fold-2b tranche-A/B thread machine (thread/newmatch + result_free
|
||||
// + strerror; delete_thread/is_consuming_inst/add_thread/run_thread),
|
||||
// the tranche-C search end-to-end matches, and the tranche-D exec
|
||||
// surface (test/find). Run with
|
||||
// `out/bin/ww run lib/regex/regex_test.ww`.
|
||||
// the tranche-C search end-to-end matches, the tranche-D exec
|
||||
// surface (test/find), and the fold-2c findall/result_freeall. Run
|
||||
// with `out/bin/ww run lib/regex/regex_test.ww`.
|
||||
//
|
||||
// Private symbols (thread, newmatch) are reached unqualified: this
|
||||
// file declares `package regex`, so the import unifies it with the
|
||||
@@ -900,6 +900,157 @@ type fcase = struct {
|
||||
};
|
||||
};
|
||||
|
||||
// findall() (regex.ha:923-960) content/count rows ported from Hare's
|
||||
// OWN findall table (+test.ha:719-731) via run_findall_case's checks
|
||||
// (+test.ha:102-130: result count + results[i][0].content), restricted
|
||||
// to the rows fold-2a can compile (the fo{2,} / a* rows ride the
|
||||
// repeat/star folds). Variable-length expectations live in a flat
|
||||
// targets pool indexed by per-row (toff, tcnt).
|
||||
type facase = struct {
|
||||
expr: str,
|
||||
input: str,
|
||||
toff: i32,
|
||||
tcnt: i32,
|
||||
};
|
||||
|
||||
@test fn findall_content() void = {
|
||||
let targets: [9]str = [
|
||||
"abc", "abあ", "abq",
|
||||
"a", "a",
|
||||
"", "", "", "",
|
||||
];
|
||||
let rows: [3]facase = [
|
||||
// multi-match + inst_any over the 3-byte あ
|
||||
facase { expr = "ab.",
|
||||
input = "hello abc and abあ test abq thanks",
|
||||
toff = 0, tcnt = 3 },
|
||||
// adjacent single-rune matches
|
||||
facase { expr = "a", input = "aa", toff = 3, tcnt = 2 },
|
||||
// zero-length: one empty match per position INCLUDING
|
||||
// end-of-string (the ha:942-945 break appends first)
|
||||
facase { expr = "", input = "abc", toff = 5, tcnt = 4 },
|
||||
];
|
||||
let i: i32 = 0;
|
||||
for (i < len(rows)) {
|
||||
let ex: str = rows[i].expr;
|
||||
let inp: str = rows[i].input;
|
||||
let c: (regex.regex | regex.error | nomem) = regex.compile(ex);
|
||||
match (c) {
|
||||
case let re: regex.regex => {
|
||||
let fr: ([]regex.result | nomem) =
|
||||
regex.findall(&re, inp);
|
||||
if (!(fr is []regex.result)) { fail(); };
|
||||
let results: []regex.result = fr as []regex.result;
|
||||
if (len(results) != rows[i].tcnt) { fail(); };
|
||||
let k: i32 = 0;
|
||||
for (k < rows[i].tcnt) {
|
||||
let want: str = targets[rows[i].toff + k];
|
||||
if (strings.compare(results[k][0].content,
|
||||
want) != 0) {
|
||||
fail();
|
||||
};
|
||||
k += 1;
|
||||
};
|
||||
regex.result_freeall(results);
|
||||
regex.finish(&re);
|
||||
};
|
||||
case => fail();
|
||||
};
|
||||
i += 1;
|
||||
};
|
||||
};
|
||||
|
||||
// findall() field rows: every capture index plus content per result,
|
||||
// against a flat expectation pool. Pins adjacency (non-overlap), the
|
||||
// one-result overlap pick, the multibyte zero-length advancement
|
||||
// (utf8sz step != 1 splits idx from bytesize), the tail-match break,
|
||||
// and the empty no-match slice. result_freeall on every row.
|
||||
type fdcase = struct {
|
||||
expr: str,
|
||||
input: str,
|
||||
eoff: i32,
|
||||
ecnt: i32,
|
||||
};
|
||||
|
||||
type fdexp = struct {
|
||||
start: size,
|
||||
sb: size,
|
||||
end: size,
|
||||
eb: size,
|
||||
content: str,
|
||||
};
|
||||
|
||||
@test fn findall_fields() void = {
|
||||
let exp: [10]fdexp = [
|
||||
// ("ab", "abxab")
|
||||
fdexp { start = 0, sb = 0, end = 2, eb = 2, content = "ab" },
|
||||
fdexp { start = 3, sb = 3, end = 5, eb = 5, content = "ab" },
|
||||
// ("ab", "abab") — adjacent, non-overlapping
|
||||
fdexp { start = 0, sb = 0, end = 2, eb = 2, content = "ab" },
|
||||
fdexp { start = 2, sb = 2, end = 4, eb = 4, content = "ab" },
|
||||
// ("aa", "aaa") — ONE result: leftmost-longest then
|
||||
// advance-past; findall must not re-enter mid-match
|
||||
fdexp { start = 0, sb = 0, end = 2, eb = 2, content = "aa" },
|
||||
// ("", "ßx") — zero-length advancement over a 2-byte rune:
|
||||
// bytesize steps 0→2→3 while idx steps 0→1→2
|
||||
fdexp { start = 0, sb = 0, end = 0, eb = 0, content = "" },
|
||||
fdexp { start = 1, sb = 2, end = 1, eb = 2, content = "" },
|
||||
fdexp { start = 2, sb = 3, end = 2, eb = 3, content = "" },
|
||||
// ("b.d", "aßbxd") — multibyte before the match start
|
||||
// splits every idx from its bytesize
|
||||
fdexp { start = 2, sb = 3, end = 5, eb = 6, content = "bxd" },
|
||||
// ("ab", "xab") — tail match: the post-match seek lands at
|
||||
// end-of-string and the next search returns void
|
||||
fdexp { start = 1, sb = 1, end = 3, eb = 3, content = "ab" },
|
||||
];
|
||||
let rows: [7]fdcase = [
|
||||
fdcase { expr = "ab", input = "abxab", eoff = 0, ecnt = 2 },
|
||||
fdcase { expr = "ab", input = "abab", eoff = 2, ecnt = 2 },
|
||||
fdcase { expr = "aa", input = "aaa", eoff = 4, ecnt = 1 },
|
||||
fdcase { expr = "", input = "ßx", eoff = 5, ecnt = 3 },
|
||||
fdcase { expr = "b.d", input = "aßbxd", eoff = 8, ecnt = 1 },
|
||||
fdcase { expr = "ab", input = "xab", eoff = 9, ecnt = 1 },
|
||||
// no match → empty slice the caller still result_freealls
|
||||
fdcase { expr = "ab", input = "xyz", eoff = 10, ecnt = 0 },
|
||||
];
|
||||
let i: i32 = 0;
|
||||
for (i < len(rows)) {
|
||||
let ex: str = rows[i].expr;
|
||||
let inp: str = rows[i].input;
|
||||
let c: (regex.regex | regex.error | nomem) = regex.compile(ex);
|
||||
match (c) {
|
||||
case let re: regex.regex => {
|
||||
let fr: ([]regex.result | nomem) =
|
||||
regex.findall(&re, inp);
|
||||
if (!(fr is []regex.result)) { fail(); };
|
||||
let results: []regex.result = fr as []regex.result;
|
||||
if (len(results) != rows[i].ecnt) { fail(); };
|
||||
let k: i32 = 0;
|
||||
for (k < rows[i].ecnt) {
|
||||
let w: fdexp = exp[rows[i].eoff + k];
|
||||
if (results[k][0].start != w.start) { fail(); };
|
||||
if (results[k][0].start_bytesize != w.sb) {
|
||||
fail();
|
||||
};
|
||||
if (results[k][0].end != w.end) { fail(); };
|
||||
if (results[k][0].end_bytesize != w.eb) {
|
||||
fail();
|
||||
};
|
||||
if (strings.compare(results[k][0].content,
|
||||
w.content) != 0) {
|
||||
fail();
|
||||
};
|
||||
k += 1;
|
||||
};
|
||||
regex.result_freeall(results);
|
||||
regex.finish(&re);
|
||||
};
|
||||
case => fail();
|
||||
};
|
||||
i += 1;
|
||||
};
|
||||
};
|
||||
|
||||
export fn main() i32 = {
|
||||
signalled = 1; lit_and_match();
|
||||
signalled = 2; size_aliases_distinct();
|
||||
@@ -925,5 +1076,7 @@ export fn main() i32 = {
|
||||
signalled = 22; search_no_match();
|
||||
signalled = 23; test_matches();
|
||||
signalled = 24; find_cases();
|
||||
signalled = 25; findall_content();
|
||||
signalled = 26; findall_fields();
|
||||
return 0;
|
||||
};
|
||||
|
||||
@@ -42,7 +42,12 @@ main(void)
|
||||
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);
|
||||
/* timeout 180 per repo convention (732/775; test/run does not
|
||||
* bound runtime): the fold-2c zero-length findall rows turn a
|
||||
* regression of the ha:946-952 rune-advancement guard into an
|
||||
* infinite loop (frees are no-ops, so OOM is the only other
|
||||
* exit) — timeout converts the hang into a loud 124. */
|
||||
snprintf(cmd, sizeof cmd, "timeout 180 %s/ww run %s", bin, path);
|
||||
int rc = runwait(cmd);
|
||||
if (rc != 0) {
|
||||
fprintf(stderr, "regex_run FAIL: %s exited %d\n", src, rc);
|
||||
|
||||
Reference in New Issue
Block a user