From bc048ebe659bb055cd314e2a036c22924e237d87 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Thu, 4 Jun 2026 15:14:54 +0900 Subject: [PATCH] =?UTF-8?q?lib/regex:=20fold-2b=20tranche=20D=20=E2=80=94?= =?UTF-8?q?=20test/find=20(the=20exec=20surface)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- lib/regex/regex.ww | 55 +++++++++++++++++-- lib/regex/regex_test.ww | 117 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 165 insertions(+), 7 deletions(-) diff --git a/lib/regex/regex.ww b/lib/regex/regex.ww index 2d7fe170..e900a1b2 100644 --- a/lib/regex/regex.ww +++ b/lib/regex/regex.ww @@ -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 diff --git a/lib/regex/regex_test.ww b/lib/regex/regex_test.ww index 733eedeb..cdedb81d 100644 --- a/lib/regex/regex_test.ww +++ b/lib/regex/regex_test.ww @@ -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; };