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

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