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:
2026-06-04 15:49:38 +09:00
parent 532b0a88ae
commit 48904e5ef3
3 changed files with 238 additions and 6 deletions

View File

@@ -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

View File

@@ -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;
};

View File

@@ -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);