lib/regex: fold-2b tranche D — test/find (the exec surface)

Port of test (ref/hare/regex/regex.ha:901-904) and find (ha:907-918)
— the exported exec surface over tranche C's search. fold 2b is
COMPLETE: compile → test/find runs end to end on the 2a literal
programs.

Both are |success|=2 `?` sites in Hare (`search(...)?` over
(void | []capture | nomem)); ww's `?` is gated to single-success
unions (the C6 interim), so each spells the propagation as the
explicit 3-arm match harec lowers `?` into (ref/harec/src/check.c:
2780) — the ratified D13 spelling, on task #14's acceptance list for
reversion when subset-union `?` lands. find's no-match `return [];`
(ha:916) binds a zero header first (#25/#31 ruling) — a valid empty
result the caller still result_frees. io::handle args are the landed
memio→io cast (&strm.vt); import memio added.

PD1 probed first (scratch/pd1.ww): []capture values returning
through (result | nomem) — the nominal-alias member — plus the
(bool | nomem) round-trip; build/run/byte-id green at base, so no
new compiler surface was crossed.

regex_test: +2 @test fns (signalled 23-24). test_matches = 8-row
tcase table (the six search-table inputs → true, thread-drain and
EOF-mid-pattern → false). find_cases = 8-row fcase table: the six
match rows reuse the search table's root-capture expectations (all
four indices + content), the two no-match rows pin the empty result;
result_free on every row, matched or not. Every fcase row also
cross-pins test/find agreement (test(re, s) == find(re, s) matched),
so an arm-swap in either D13 match is caught by the other surface.

Both drivers run the fixture exit 0; w6c vs w6c_ww on the
regenerated combined byte-identical (FC0).
This commit is contained in:
2026-06-04 15:14:54 +09:00
parent 6160277098
commit bc048ebe65
2 changed files with 165 additions and 7 deletions

View File

@@ -4,11 +4,12 @@
// tranche A = the thread-machine scaffolding (thread/newmatch types,
// result_free, strerror); fold 2b tranche B = the engine
// (delete_thread/is_consuming_inst/add_thread/run_thread); fold 2b
// tranche C = search, the first end-to-end match. Every other
// metacharacter arm and the exec surface (test/find on the C6
// multi-success `?` gate, tranche D; replace) are DEFERRED behind
// compiler fixes — probed pre-port, pA*/pB*/PB*/PC* probes. They
// land with those fixes.
// 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 —
// probed pre-port, pA*/pB*/PB*/PC*/PD* probes. They land with those
// fixes.
//
// One fold-1 construct is held back behind a filed compiler/fidelity
// gap (see the charclass_map site below):
@@ -20,6 +21,7 @@ package regex;
import bufio;
import io;
import memio;
import strings;
import types;
import encoding.utf8;
@@ -597,6 +599,49 @@ fn search(
};
};
// Returns whether or not a [[regex]] matches any part of a given
// string.
//
// ref/hare/regex/regex.ha:900-904. Hare's io::handle arg `&strm` is
// the landed memio→io cast `&strm.vt`. Hare unwraps with
// `search(...)? is []capture` — a |success|=2 union; ww's `?` is
// gated to single-success unions (the C6 interim, ww-core #14), so
// the propagation is spelled as the explicit match harec lowers `?`
// into (ref/harec/src/check.c:2780) — the ratified D13 spelling,
// reverts with #14.
export fn test(re: *regex, string: str) (bool | nomem) = {
let strm: memio.stream = memio.fixed(strings.toutf8(string));
let r: (void | []capture | nomem) =
search(re, string, &strm.vt, false);
match (r) {
case let m: []capture => return true;
case void => return false;
case let n: nomem => return n;
};
};
// Attempts to match a [[regex]] against a string and returns the
// longest leftmost match as a [[result]]. The caller must free the
// return value with [[result_free]].
//
// ref/hare/regex/regex.ha:907-918. Same explicit `?` lowering as
// test() (D13, ww-core #14); the no-match `return [];` (ha:916)
// binds a zero header first (#25/#31 ruling) — a valid empty result
// the caller still result_frees.
export fn find(re: *regex, string: str) (result | nomem) = {
let strm: memio.stream = memio.fixed(strings.toutf8(string));
let r: (void | []capture | nomem) =
search(re, string, &strm.vt, true);
match (r) {
case let m: []capture => return m;
case void => {
let empty: []capture;
return empty;
};
case let n: nomem => return n;
};
};
// Frees a [[result]].
//
// ref/hare/regex/regex.ha:1113-1116, verbatim — the free() builtin is

View File

@@ -2,8 +2,8 @@
// 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),
// and the tranche-C search end-to-end matches (the exec surface
// test/find is tranche D — see regex.ww). Run with
// 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`.
//
// Private symbols (thread, newmatch) are reached unqualified: this
@@ -789,6 +789,117 @@ type scase = struct {
};
};
// test() (regex.ha:901-904) — the exported boolean surface over the
// same inputs the search table pins, plus the two void rows.
type tcase = struct {
expr: str,
input: str,
want: bool,
};
@test fn test_matches() void = {
let rows: [8]tcase = [
tcase { expr = "ab", input = "xab", want = true },
tcase { expr = "bcd", input = "abcd", want = true },
tcase { expr = "aa", input = "aaa", want = true },
tcase { expr = "", input = "", want = true },
tcase { expr = "b.d", input = "aßbxd", want = true },
tcase { expr = "aa", input = "aaaa", want = true },
tcase { expr = "ab", input = "xyz", want = false },
tcase { expr = "ab", input = "a", want = false },
];
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 tr: (bool | nomem) = regex.test(&re, inp);
if (!(tr is bool)) { fail(); };
if ((tr as bool) != rows[i].want) { fail(); };
regex.finish(&re);
};
case => fail();
};
i += 1;
};
};
// find() (regex.ha:910-918) — the exported result surface: match rows
// reuse the search table's expectations; no-match rows return the
// empty result (ha:916) the caller still result_frees. Every row also
// cross-pins test() == (find() matched).
type fcase = struct {
expr: str,
input: str,
matches: bool,
start: size,
sb: size,
end: size,
eb: size,
content: str,
};
@test fn find_cases() void = {
let rows: [8]fcase = [
fcase { expr = "ab", input = "xab", matches = true,
start = 1, sb = 1, end = 3, eb = 3, content = "ab" },
fcase { expr = "bcd", input = "abcd", matches = true,
start = 1, sb = 1, end = 4, eb = 4, content = "bcd" },
fcase { expr = "aa", input = "aaa", matches = true,
start = 0, sb = 0, end = 2, eb = 2, content = "aa" },
fcase { expr = "", input = "", matches = true,
start = 0, sb = 0, end = 0, eb = 0, content = "" },
fcase { expr = "b.d", input = "aßbxd", matches = true,
start = 2, sb = 3, end = 5, eb = 6, content = "bxd" },
fcase { expr = "aa", input = "aaaa", matches = true,
start = 0, sb = 0, end = 2, eb = 2, content = "aa" },
fcase { expr = "ab", input = "xyz", matches = false, ... },
fcase { expr = "ab", input = "a", matches = false, ... },
];
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.find(&re, inp);
if (!(fr is regex.result)) { fail(); };
let res: regex.result = fr as regex.result;
if (rows[i].matches) {
if (len(res) != 1) { fail(); };
if (res[0].start != rows[i].start) { fail(); };
if (res[0].start_bytesize != rows[i].sb) {
fail();
};
if (res[0].end != rows[i].end) { fail(); };
if (res[0].end_bytesize != rows[i].eb) {
fail();
};
let wc: str = rows[i].content;
if (strings.compare(res[0].content, wc) != 0) {
fail();
};
} else {
if (len(res) != 0) { fail(); };
};
// the two surfaces share search; pin their
// agreement so an arm-swap in either D13 match
// can't hide behind a one-sided table
let tr: (bool | nomem) = regex.test(&re, inp);
if (!(tr is bool)) { fail(); };
if ((tr as bool) != (len(res) != 0)) { fail(); };
regex.result_free(res);
regex.finish(&re);
};
case => fail();
};
i += 1;
};
};
export fn main() i32 = {
signalled = 1; lit_and_match();
signalled = 2; size_aliases_distinct();
@@ -812,5 +923,7 @@ export fn main() i32 = {
signalled = 20; search_matches();
signalled = 21; search_early_exit();
signalled = 22; search_no_match();
signalled = 23; test_matches();
signalled = 24; find_cases();
return 0;
};