From 48904e5ef34e6e7bcb9ef7e219957458db6efc8e Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Thu, 4 Jun 2026 15:49:38 +0900 Subject: [PATCH] =?UTF-8?q?lib/regex:=20fold-2c=20=E2=80=94=20findall/resu?= =?UTF-8?q?lt=5Ffreeall?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- lib/regex/regex.ww | 78 ++++++++++++++++++- lib/regex/regex_test.ww | 159 ++++++++++++++++++++++++++++++++++++++- test/wcc/989_regex_run.c | 7 +- 3 files changed, 238 insertions(+), 6 deletions(-) diff --git a/lib/regex/regex.ww b/lib/regex/regex.ww index e900a1b2..e4cef86b 100644 --- a/lib/regex/regex.ww +++ b/lib/regex/regex.ww @@ -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 diff --git a/lib/regex/regex_test.ww b/lib/regex/regex_test.ww index cdedb81d..4f864c96 100644 --- a/lib/regex/regex_test.ww +++ b/lib/regex/regex_test.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; }; diff --git a/test/wcc/989_regex_run.c b/test/wcc/989_regex_run.c index 6b5fc84a..77fa211a 100644 --- a/test/wcc/989_regex_run.c +++ b/test/wcc/989_regex_run.c @@ -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);