diff --git a/CLAUDE.md b/CLAUDE.md index 960b5a02..0c70ee37 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,7 +6,7 @@ 6. ref/hare and ref/plan9front are read-only references — consult before inventing data shapes or syntax 7. No workarounds. If a bug forces a workaround, STOP and report with a precise repro. Document any retained divergence at the site with a pointer to the filed task. Never silent. 8. Comments are WHY-only. Never narrate WHAT the code does — names carry the WHAT. Comment only non-obvious WHY: a constraint, a divergence from a reference, a citation to a filed task. -9. Hare-fidelity over convenience. No ad-hoc extensions, renames, or convenience wrappers in lib/. Cite ref/hare// for every signature ported. Carve-out: the Hare `_unsafe` suffix convention is dropped wholesale (ww is C/Plan-9-lineage, an unmanaged systems language — no GC, no "safe" baseline to be unsafe relative to). bytes→str is a pure reinterpret (`strings.frombytes`, renamed from Hare's `fromutf8_unsafe`); validation is opt-in via `utf8.validate(b)?` at the IO source, never wrapped per-construction. Rationale: ref/hare/strings/utf8.ha:10,22 — the suffix flags Hare's managed-bytes-safety axis, which ww doesn't have. The honest name (`frombytes`) reserves `fromutf8` for a future true validating helper. Carve-out: lib/ tests use Go's external-test-package idiom (`package _test;` + `import ;`), a sanctioned Go-over-Hare departure — ww hard-rejects self-import (full Go), and Hare's same-package `@test` colocation has no ww analogue yet (in-package white-box testing is deferred pending tasks #5/#8, not forbidden). Rule 5/9 binds lib/ *API* surface, not test-authoring style. Ref: Go `foo_test` convention; task #16. +9. Hare-fidelity over convenience. No ad-hoc extensions, renames, or convenience wrappers in lib/. Cite ref/hare// for every signature ported. Carve-out: the Hare `_unsafe` suffix convention is dropped wholesale (ww is C/Plan-9-lineage, an unmanaged systems language — no GC, no "safe" baseline to be unsafe relative to). bytes→str is a pure reinterpret (`strings.frombytes`, renamed from Hare's `fromutf8_unsafe`); validation is opt-in via `utf8.validate(b)?` at the IO source, never wrapped per-construction. Rationale: ref/hare/strings/utf8.ha:10,22 — the suffix flags Hare's managed-bytes-safety axis, which ww doesn't have. The honest name (`frombytes`) reserves `fromutf8` for a future true validating helper. Carve-out: lib/ tests use Go's external-test-package idiom (`package _test;` + `import ;`), a sanctioned Go-over-Hare departure — ww hard-rejects self-import (full Go), and Hare's same-package `@test` colocation now has a ww analogue: in-package white-box `@test` files (`package ;`, colocated) are sanctioned alongside the external black-box `package _test;` split — Go's `foo`/`foo_test` model — now that the non-T `@test` drop landed (#6, harec check.c:3941, 08a76cf; #5 closed). Rule 5/9 binds lib/ *API* surface, not test-authoring style. Ref: Go `foo_test` convention; task #16. 10. Symmetric stages. cstage and wwstage MUST emit byte-identical asm for the same input. When inference power differs, align the richer side DOWN to the leaner side, not the other way. 11. Split commits when they bundle unrelated concerns. Bisect-cleanliness is the default. Multi-fix commits need a body paragraph explaining why they couldn't split. 12. Simple data, simple algorithms. Sea-of-stars style. Mirror Hare's structural choices over clever alternatives. diff --git a/lib/regex/regex_test.ww b/lib/regex/regex_test.ww index 9c346e3f..6413aaa8 100644 --- a/lib/regex/regex_test.ww +++ b/lib/regex/regex_test.ww @@ -1,15 +1,14 @@ -// regex_test — exercises the lib/regex fold-1 data model (the type -// 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, 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 -// lib sources (the decimaltest precedent, -// lib/strconv/test/decimaltest.ww). +// regex_test — BLACK-BOX @test probes for lib/regex: the exported API +// only (compile / test / find / findall / strerror / result_free / +// result_freeall / finish) plus exported-type variant discrimination, +// payload extraction, and the regex/capture/charset struct shapes. +// This is the external `package regex_test` half of the Go foo / foo_test +// split (CLAUDE.md rule-9). The WHITE-BOX half — probes that bare-call +// unexported internals (thread/newmatch, run_thread / add_thread / +// delete_thread / search, find_last_groupstart / shift / +// parse_repetition, is_consuming_inst) — lives in +// lib/regex/regex_whitebox.ww (`package regex`, unified with regex.ww), +// run via the lib/regex/wb/ driver. // // Fold 2a ports compile()'s lit/any/match arms only; exec lives in // later folds, so the compile_* cases pin the emitted inst PROGRAM @@ -24,15 +23,14 @@ // // Struct literals below name the type UNQUALIFIED (`inst_charset { … }`, // not `regex.inst_charset { … }`): the parser rejects a module-qualified -// name in struct-literal position (#29), and the imported type -// is in scope unqualified. +// name in struct-literal position (#29), and the imported EXPORTED type +// is in scope unqualified. These reach only exported types — the #29 +// axis, distinct from the white-box privacy boundary handled by the +// regex_whitebox.ww split above. package regex_test; import regex; -import io; -import memio; import strings; -import types; // inst_lit / inst_match carry distinguishable payloads (rune / bool). @@ -254,114 +252,6 @@ import types; }; }; -// The thread struct (regex.ha:55-64) is in tree ahead of its engine -// consumers so the pending #15/#17 fix probes exercise the real type. -// Pin the field layout via the P6-proven wide-literal append + a -// depth-1 read-back row per appended thread; the Hare `...` partial -// fill (P5) must zero everything the second row's literal omits. -// root_capture has NO row: every read route into it is -// compiler-blocked today — the depth-2 chain behind the index links -// the field as a global (#6 F4), the element let-copy is #7 F5, and -// a probed `&threads[i].root_capture` deref segfaults byte-id on -// both stages — so its row lands with those fixes. -type texp = struct { - pc: size, - start_idx: size, - start_bytesize: size, - matched: bool, - failed: bool, - // .len reads as i32 (check.c:1239), so the count columns match it - ncaps: i32, - nreps: i32, -}; - -@test fn thread_shape() void = { - let rc: capture = capture { - content = "ab", start = 1, start_bytesize = 1, - end = 2, end_bytesize = 2, - }; - let pcaps: []capture = []; - append(pcaps, rc); - let prep: []size = []; - append(prep, (7: size)); - let threads: []thread = []; - append(threads, thread { - pc = 5, - start_idx = 6, - start_bytesize = 7, - root_capture = rc, - captures = pcaps, - rep_counters = prep, - matched = false, - failed = true, - }); - append(threads, thread { pc = 9, ... }); - let want: [2]texp = [ - texp { pc = 5, start_idx = 6, start_bytesize = 7, - matched = false, failed = true, - ncaps = 1, nreps = 1 }, - texp { pc = 9, start_idx = 0, start_bytesize = 0, - matched = false, failed = false, - ncaps = 0, nreps = 0 }, - ]; - assert(!(len(threads) != len(want))); - let i: i32 = 0; - for (i < len(want)) { - assert(!(threads[i].pc != want[i].pc)); - assert(!(threads[i].start_idx != want[i].start_idx)); - if (threads[i].start_bytesize != want[i].start_bytesize) { - abort(); - }; - assert(!(threads[i].matched != want[i].matched)); - assert(!(threads[i].failed != want[i].failed)); - assert(!(threads[i].captures.len != want[i].ncaps)); - assert(!(threads[i].rep_counters.len != want[i].nreps)); - i += 1; - }; -}; - -// newmatch (regex.ha:66) must discriminate nominally against plain -// void — and against nomem, the third payload-free member — across -// run_thread's (void | newmatch | nomem) return boundary: the P8 -// shape on the real lib type, one row per returned member. -fn nm_probe(x: i32) (void | newmatch | nomem) = { - if (x == 1) { - let nm: newmatch; - return nm; - }; - if (x == 2) { - let n: nomem; - return n; - }; - return; -}; - -type nmexp = struct { - arg: i32, - want_nm: bool, - want_void: bool, - want_nomem: bool, -}; - -@test fn newmatch_discriminates() void = { - let rows: [3]nmexp = [ - nmexp { arg = 1, want_nm = true, want_void = false, - want_nomem = false }, - nmexp { arg = 0, want_nm = false, want_void = true, - want_nomem = false }, - nmexp { arg = 2, want_nm = false, want_void = false, - want_nomem = true }, - ]; - let i: i32 = 0; - for (i < len(rows)) { - let r: (void | newmatch | nomem) = nm_probe(rows[i].arg); - assert(!((r is newmatch) != rows[i].want_nm)); - assert(!((r is void) != rows[i].want_void)); - assert(!((r is nomem) != rows[i].want_nomem)); - i += 1; - }; -}; - // result_free (regex.ha:1114-1116) accepts a built result; free() is // the documented no-op (no-free runtime), so the header must stay // readable after — a future real free changes this row consciously. @@ -402,417 +292,6 @@ type nmexp = struct { }; }; -// is_consuming_inst must discriminate the three consuming kinds from -// the seven non-consuming ones across all 10 inst variants -// (regex.ha:553-555) — the tranche-A-deferred row, graduated by the -// #19 >48B by-value arg wiring. Sequential typed-let + helper calls, -// not a [10](inst, bool) table: tagged-element array literals -// under-copy (#12), and a cast/literal rvalue arg source is -// #38b-unwired, so each value goes through a typed let (the -// #19-landed ident source). -fn ic_one(v: regex.inst, want: bool) void = { - assert(!(is_consuming_inst(v) != want)); -}; - -@test fn is_consuming_kinds() void = { - let lit: regex.inst = ('a': regex.inst_lit); - ic_one(lit, true); - let av: regex.inst_any; - let any: regex.inst = av; - ic_one(any, true); - let cs: regex.inst = (inst_charset { idx = 0, is_positive = true }); - ic_one(cs, true); - let kv: regex.inst_skip; - let sk: regex.inst = kv; - ic_one(sk, false); - let sp: regex.inst = ((5: size): regex.inst_split); - ic_one(sp, false); - let jm: regex.inst = ((6: size): regex.inst_jump); - ic_one(jm, false); - let mt: regex.inst = (false: regex.inst_match); - ic_one(mt, false); - let gs: regex.inst = ((2: size): regex.inst_groupstart); - ic_one(gs, false); - let gv: regex.inst_groupend; - let ge: regex.inst = gv; - ic_one(ge, false); - let rp: regex.inst = (inst_repeat { - id = 1, origin = 4, min = (2: size), max = void, - }); - ic_one(rp, false); -}; - -// delete_thread (regex.ha:547-551) removes exactly the indexed -// element and preserves order; its frees are no-ops (no-free -// runtime), so the survivors' capture headers stay readable. -@test fn delete_thread_middle() void = { - let caps: []regex.capture = []; - append(caps, capture { - content = "x", start = 0, start_bytesize = 0, - end = 1, end_bytesize = 1, - }); - let ts: []thread = []; - append(ts, thread { pc = 1, start_idx = 11, captures = caps, ... }); - append(ts, thread { pc = 2, start_idx = 22, ... }); - append(ts, thread { pc = 3, start_idx = 33, ... }); - delete_thread(1, &ts); - assert(!(len(ts) != 2)); - assert(!(ts[0].pc != (1: size))); - assert(!(ts[0].start_idx != (11: size))); - assert(!(ts[0].captures.len != 1)); - assert(!(ts[1].pc != (3: size))); - assert(!(ts[1].start_idx != (33: size))); - assert(!(ts[1].captures.len != 0)); - // boundary rows: delete at the last index, then at index 0 down - // to empty — the failed-sweep loop (regex.ha:891-896) deletes at - // every position including both ends. - delete_thread(1, &ts); - assert(!(len(ts) != 1)); - assert(!(ts[0].pc != (1: size))); - delete_thread(0, &ts); - assert(!(len(ts) != 0)); -}; - -// add_thread (regex.ha:557-587): same-pc dedup suppression fires only -// when the existing thread is unmatched AND started strictly earlier -// than the parent (ha:561-565); otherwise the child appends, -// inheriting the parent's start/matched/failed with DUPLICATED -// capture/rep_counter slices (empty parent → empty dup, ha:569/572) -// and a zeroed root_capture. -@test fn add_thread_dedup_inherit() void = { - let ts: []thread = []; - append(ts, thread { pc = 0, start_idx = 5, start_bytesize = 4, - matched = false, failed = true, ... }); - // inherit: fresh pc, parent fields copied, rest zeroed - let r: (void | nomem) = add_thread(&ts, 0, 7); - assert(!(!(r is void))); - assert(!(len(ts) != 2)); - assert(!(ts[1].pc != (7: size))); - assert(!(ts[1].start_idx != (5: size))); - assert(!(ts[1].start_bytesize != (4: size))); - assert(!(ts[1].matched)); - assert(!(!ts[1].failed)); - assert(!(ts[1].captures.len != 0)); - assert(!(ts[1].rep_counters.len != 0)); - assert(!(ts[1].root_capture.content.len != 0)); - assert(!(ts[1].root_capture.end != (0: size))); - // same-pc same-start does NOT suppress (strict <, ha:563-565) - let r2: (void | nomem) = add_thread(&ts, 0, 7); - assert(!(!(r2 is void))); - assert(!(len(ts) != 3)); - // an earlier-started unmatched existing thread DOES suppress - let ts2: []thread = []; - append(ts2, thread { pc = 0, start_idx = 5, ... }); - append(ts2, thread { pc = 7, start_idx = 2, ... }); - let r3: (void | nomem) = add_thread(&ts2, 0, 7); - assert(!(!(r3 is void))); - assert(!(len(ts2) != 2)); - // a MATCHED existing thread never suppresses - let ts3: []thread = []; - append(ts3, thread { pc = 0, start_idx = 5, ... }); - append(ts3, thread { pc = 7, start_idx = 2, matched = true, ... }); - let r4: (void | nomem) = add_thread(&ts3, 0, 7); - assert(!(!(r4 is void))); - assert(!(len(ts3) != 3)); - assert(!(ts3[2].pc != (7: size))); - assert(!(ts3[2].start_idx != (5: size))); -}; - -// add_thread dup (regex.ha:568-573): the child carries a COPY of the -// parent's captures/rep_counters — values equal, backing independent -// in both directions (mutate parent → child unchanged, mutate child → -// parent unchanged). Empty parent → empty dup (the pre-flip rows above -// stay byte-for-byte). Driven directly, the dedup-test precedent. -@test fn add_thread_dup_independence() void = { - let caps: []capture = []; - append(caps, capture { - content = "ab", start = 1, start_bytesize = 1, - end = 2, end_bytesize = 2, - }); - append(caps, capture { - content = "c", start = 3, start_bytesize = 3, - end = 4, end_bytesize = 4, - }); - let reps: []size = []; - append(reps, (5: size)); - append(reps, (6: size)); - let ts: []thread = []; - append(ts, thread { pc = 0, start_idx = 1, captures = caps, - rep_counters = reps, ... }); - let r: (void | nomem) = add_thread(&ts, 0, 9); - assert(!(!(r is void))); - assert(!(len(ts) != 2)); - // dup carried the parent's values - assert(!(ts[1].captures.len != 2)); - assert(!(strings.compare(ts[1].captures[0].content, "ab") != 0)); - assert(!(ts[1].captures[0].start != (1: size))); - assert(!(ts[1].captures[1].end != (4: size))); - assert(!(ts[1].rep_counters.len != 2)); - assert(!(ts[1].rep_counters[0] != (5: size))); - assert(!(ts[1].rep_counters[1] != (6: size))); - // independence, parent → child: mutate the parent post-add - ts[0].captures[0].start = 100; - ts[0].captures[0].content = "zz"; - ts[0].rep_counters[0] = 77; - assert(!(ts[1].captures[0].start != (1: size))); - assert(!(strings.compare(ts[1].captures[0].content, "ab") != 0)); - assert(!(ts[1].rep_counters[0] != (5: size))); - // independence, child → parent - ts[1].captures[1].end = 200; - ts[1].rep_counters[1] = 88; - assert(!(ts[0].captures[1].end != (4: size))); - assert(!(ts[0].rep_counters[1] != (6: size))); - // empty parent → empty dup - let ts2: []thread = []; - append(ts2, thread { pc = 0, ... }); - let r2: (void | nomem) = add_thread(&ts2, 0, 3); - assert(!(!(r2 is void))); - assert(!(ts2[1].captures.len != 0)); - assert(!(ts2[1].rep_counters.len != 0)); -}; - -// run_thread (regex.ha:589-742) driven directly over compile("ab")'s -// real program [skip, lit 'a', lit 'b', match(false)] — the arms -// fold-2a can emit. Phases: parked-skip spawn (len 1→2, parent pc -// unmoved — the unanchored-restart engine), lit advance, lit -// mismatch (failed=true AND pc still steps — ha:741 runs regardless -// of the arm's verdict), EOF on a consuming pc (failed, pc frozen), -// match arm (root_capture spans start_bytesize..str_bytesize + -// matched + `is newmatch`), and the matched-thread early return -// (ha:599-601). -@test fn run_thread_literal_program() void = { - // typed-let + match receive, the compile_literal_program shape - let c: (regex.regex | regex.error | nomem) = regex.compile("ab"); - match (c) { - case let re: regex.regex => { - // skip spawn: thread 0 parks on the skip, child enters at pc 1 - let ra: (rune | io.eof) = 'a'; - let ts: []thread = []; - append(ts, thread { pc = 0, ... }); - let r1: (void | newmatch | nomem) = run_thread(0, &re, "ab", &ts, ra, 0, 0); - assert(!(!(r1 is void))); - assert(!(len(ts) != 2)); - assert(!(ts[0].pc != (0: size))); - assert(!(ts[1].pc != (1: size))); - assert(!(ts[1].failed)); - - // lit match advances pc past 'a' - let r2: (void | newmatch | nomem) = run_thread(1, &re, "ab", &ts, ra, 0, 0); - assert(!(!(r2 is void))); - assert(!(ts[1].pc != (2: size))); - assert(!(ts[1].failed)); - - // lit mismatch fails the thread; pc steps anyway (ha:741) - let rx: (rune | io.eof) = 'x'; - let r3: (void | newmatch | nomem) = run_thread(1, &re, "ab", &ts, rx, 1, 1); - assert(!(!(r3 is void))); - assert(!(!ts[1].failed)); - assert(!(ts[1].pc != (3: size))); - - // EOF on a consuming pc fails the thread before pc steps - let ev: io.eof; - let reof: (rune | io.eof) = ev; - let ts2: []thread = []; - append(ts2, thread { pc = 1, ... }); - let r4: (void | newmatch | nomem) = run_thread(0, &re, "ab", &ts2, reof, 2, 2); - assert(!(!(r4 is void))); - assert(!(!ts2[0].failed)); - assert(!(ts2[0].pc != (1: size))); - - // match arm: root_capture spans start_bytesize..str_bytesize, - // matched set, newmatch returned - let ts3: []thread = []; - append(ts3, thread { pc = 3, ... }); - let r5: (void | newmatch | nomem) = run_thread(0, &re, "ab", &ts3, reof, 2, 2); - assert(!(!(r5 is newmatch))); - assert(!(!ts3[0].matched)); - assert(!(ts3[0].failed)); - assert(!(ts3[0].root_capture.start != (0: size))); - assert(!(ts3[0].root_capture.start_bytesize != (0: size))); - assert(!(ts3[0].root_capture.end != (2: size))); - assert(!(ts3[0].root_capture.end_bytesize != (2: size))); - assert(!(strings.compare(ts3[0].root_capture.content, "ab") != 0)); - - // an already-matched thread is inert (ha:599-601): void - // return, state untouched - let r6: (void | newmatch | nomem) = run_thread(0, &re, "ab", &ts3, ra, 3, 3); - assert(!(!(r6 is void))); - assert(!(ts3[0].root_capture.end != (2: size))); - - // idx/bytesize split: every all-ASCII row has idx == - // bytesize, so a port swapping start/start_bytesize (or - // end/end_bytesize) in root_capture passes them. One 2-byte - // rune ('ß') consumed before the match start makes all four - // values distinct: start=1 start_bytesize=2 end=3 - // end_bytesize=4; content = bytes[2:4] = "ab". - let ts4: []thread = []; - append(ts4, thread { pc = 3, start_idx = 1, start_bytesize = 2, ... }); - let r7: (void | newmatch | nomem) = run_thread(0, &re, "ßab", &ts4, reof, 3, 4); - assert(!(!(r7 is newmatch))); - assert(!(ts4[0].root_capture.start != (1: size))); - assert(!(ts4[0].root_capture.start_bytesize != (2: size))); - assert(!(ts4[0].root_capture.end != (3: size))); - assert(!(ts4[0].root_capture.end_bytesize != (4: size))); - assert(!(strings.compare(ts4[0].root_capture.content, "ab") != 0)); - regex.finish(&re); - }; - case => abort(); - }; -}; - -// The anchored route (ha:621-624) needs a (true: inst_match) program -// — compile() can't emit `$` yet, so it is HAND-BUILT — pinned from -// both sides: anchored + string-not-exhausted fails the thread; -// anchored + EOF falls through to the match (empty content). -@test fn run_thread_anchored_route() void = { - let insts: []regex.inst = []; - append(insts, (true: regex.inst_match)); - let re: regex.regex; - re.insts = insts; - re.n_reps = 0; - - let ra: (rune | io.eof) = 'a'; - let ts: []thread = []; - append(ts, thread { pc = 0, ... }); - let r1: (void | newmatch | nomem) = run_thread(0, &re, "ab", &ts, ra, 0, 0); - assert(!(!(r1 is void))); - assert(!(!ts[0].failed)); - assert(!(ts[0].matched)); - - let ev: io.eof; - let reof: (rune | io.eof) = ev; - let ts2: []thread = []; - append(ts2, thread { pc = 0, ... }); - let r2: (void | newmatch | nomem) = run_thread(0, &re, "", &ts2, reof, 0, 0); - assert(!(!(r2 is newmatch))); - assert(!(!ts2[0].matched)); - assert(!(ts2[0].root_capture.content.len != 0)); - assert(!(ts2[0].root_capture.end != (0: size))); -}; - -// search (regex.ha:746-898) driven DIRECTLY (private fn, package-regex -// test) over memio-backed streams — the exec surface (test/find) is -// tranche D. Each match row pins the root capture's four indices plus -// content; the multibyte row keeps idx != bytesize honest. Rows share -// (expr, input, need_captures, want) shape — the P12 struct-row table. -type scase = struct { - expr: str, - input: str, - nc: bool, - start: size, - sb: size, - end: size, - eb: size, - content: str, -}; - -@test fn search_matches() void = { - let rows: [6]scase = [ - // full match mid-string: skip-respawn + dispatch + - // all_matched exit - scase { expr = "ab", input = "xab", nc = true, - start = 1, sb = 1, end = 3, eb = 3, content = "ab" }, - // mismatch-restart: the idx-0 child fails and is swept; the - // restarted thread wins (failed-sweep interplay) - scase { expr = "bcd", input = "abcd", nc = true, - start = 1, sb = 1, end = 4, eb = 4, content = "bcd" }, - // leftmost-longest best-pick + first_match_idx trim - scase { expr = "aa", input = "aaa", nc = true, - start = 0, sb = 0, end = 2, eb = 2, content = "aa" }, - // zero-length: the all_matched path with matchlen 0 must - // NOT take the need_captures=false early-exit (ha:845 - // requires matchlen > 0) — hence nc=false expecting the - // FULL one-capture result, not the empty early-exit slice - scase { expr = "", input = "", nc = false, - start = 0, sb = 0, end = 0, eb = 0, content = "" }, - // multibyte: the 2-byte ß before the match start splits - // every idx from its bytesize; inst_any consumes 'x' - scase { expr = "b.d", input = "aßbxd", nc = true, - start = 2, sb = 3, end = 5, eb = 6, content = "bxd" }, - // dedup-heavy: same-pc threads spawn on every step across - // >=3 passes (ha:872-889); the pick must stay stable. - // Result stability is the only external pin available this - // fold: 2a programs are all fixed-length, every match ties - // on match_len, and best-pick's insertion-order tiebreak - // alone yields leftmost — so the dedup sweep and the - // leftmost trim are result-invisible (mutation-verified: - // disabling either still passes this table; disabling the - // failed sweep hangs). Both turn result- and - // termination-visible with the split/star fold. - scase { expr = "aa", input = "aaaa", nc = true, - start = 0, sb = 0, end = 2, eb = 2, content = "aa" }, - ]; - 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 strm: memio.stream = - memio.fixed(strings.toutf8(inp)); - let r: (void | []capture | nomem) = - search(&re, inp, &strm.vt, rows[i].nc); - assert(!(!(r is []capture))); - let caps: []capture = r as []capture; - assert(!(len(caps) != 1)); - assert(!(caps[0].start != rows[i].start)); - assert(!(caps[0].start_bytesize != rows[i].sb)); - assert(!(caps[0].end != rows[i].end)); - assert(!(caps[0].end_bytesize != rows[i].eb)); - let wc: str = rows[i].content; - if (strings.compare(caps[0].content, wc) != 0) { - abort(); - }; - regex.result_free(caps); - regex.finish(&re); - }; - case => abort(); - }; - i += 1; - }; -}; - -// ha:845-847: a non-zero-length newmatch with need_captures=false -// returns the empty result immediately, skipping the best-pick pass. -@test fn search_early_exit() void = { - let c: (regex.regex | regex.error | nomem) = regex.compile("ab"); - match (c) { - case let re: regex.regex => { - let strm: memio.stream = memio.fixed(strings.toutf8("xab")); - let r: (void | []capture | nomem) = - search(&re, "xab", &strm.vt, false); - assert(!(!(r is []capture))); - let caps: []capture = r as []capture; - assert(!(len(caps) != 0)); - regex.result_free(caps); - regex.finish(&re); - }; - case => abort(); - }; -}; - -// void rows: no match anywhere ("ab" over "xyz" — every thread fails, -// the list drains, ha:777-779) and EOF mid-pattern ("ab" over "a" — -// the consuming-inst EOF fail). -@test fn search_no_match() void = { - let c: (regex.regex | regex.error | nomem) = regex.compile("ab"); - match (c) { - case let re: regex.regex => { - let strm: memio.stream = memio.fixed(strings.toutf8("xyz")); - let r: (void | []capture | nomem) = - search(&re, "xyz", &strm.vt, true); - assert(!(!(r is void))); - let strm2: memio.stream = memio.fixed(strings.toutf8("a")); - let r2: (void | []capture | nomem) = - search(&re, "a", &strm2.vt, true); - assert(!(!(r2 is void))); - regex.finish(&re); - }; - case => abort(); - }; -}; - // 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 { @@ -1215,45 +694,6 @@ type cerow = struct { }; }; -// find_last_groupstart (regex.ha:104-119) — driven directly (private -// fn): no inst_groupstart exists in any fold-3 program, so the error -// arm is the live one; pin its exact text. A hand-built groupstart -// row pins the success arm the group fold will rely on. -@test fn find_last_groupstart_cases() void = { - let insts: []regex.inst = []; - append(insts, ('a': regex.inst_lit)); - match (find_last_groupstart(insts)) { - case let e: regex.error => { - if (strings.compare((e: str), "Unmatched ')'") != 0) { - abort(); - }; - }; - case => abort(); - }; - append(insts, ((1: size): regex.inst_groupstart)); - append(insts, ('b': regex.inst_lit)); - match (find_last_groupstart(insts)) { - case let sz: size => { assert(!(sz != 1)); }; - case => abort(); - }; -}; - -// shift (regex.ha:123-133) — driven directly over a sub-slice view: -// jump/split payloads in the view bump by one, the element before the -// view and non-jump kinds are untouched (the PE3/PE4 shapes). -@test fn shift_direct() void = { - let insts: []regex.inst = []; - append(insts, ((3: size): regex.inst_jump)); - append(insts, ('a': regex.inst_lit)); - append(insts, ((5: size): regex.inst_split)); - append(insts, ((7: size): regex.inst_jump)); - shift(insts[1:]); - assert(!(instsig(insts[0]) != 5003)); - assert(!(instsig(insts[1]) != 1097)); - assert(!(instsig(insts[2]) != 4006)); - assert(!(instsig(insts[3]) != 5008)); -}; - // fold-3 find/test rows — the group-free subset of Hare's own table // (+test.ha:221-256 anchors/postfix, :622-650 whole-expression and // multiple alternation; end == -1 resolved to rune-length per @@ -1774,96 +1214,6 @@ type cscase = struct { }; }; -// run_thread inst_groupstart / inst_groupend driven over HAND-BUILT -// programs (the anchored-route precedent): compile() composition is -// pinned by the find/submatch tables below; these pin the arm -// mechanics — fill-grow to idx+1, the SIZE_MAX open sentinel, -// innermost-unclosed close order, content from the bytesize span, -// and the closed-group re-entry overwrite (the ha:642 assert's -// PASSING direction). -@test fn run_thread_group_arms() void = { - // groupstart (ha:636-652): grows captures to idx+1 (zero-filled - // below idx), stamps start/start_bytesize, opens with - // end = end_bytesize = SIZE_MAX - let insts: []regex.inst = []; - append(insts, ((1: size): regex.inst_groupstart)); - append(insts, ('a': regex.inst_lit)); - append(insts, (false: regex.inst_match)); - let re: regex.regex; - re.insts = insts; - re.n_reps = 0; - let ra: (rune | io.eof) = 'a'; - let ts: []thread = []; - append(ts, thread { pc = 0, ... }); - let r1: (void | newmatch | nomem) = run_thread(0, &re, "ab", &ts, ra, 2, 3); - assert(!(!(r1 is void))); - assert(!(ts[0].captures.len != 2)); - // the fill element below idx is zeroed - assert(!(ts[0].captures[0].end != (0: size))); - assert(!(ts[0].captures[0].content.len != 0)); - // the opened group: start stamped, end still the open sentinel - assert(!(ts[0].captures[1].start != (2: size))); - assert(!(ts[0].captures[1].start_bytesize != (3: size))); - assert(!(ts[0].captures[1].end != types.SIZE_MAX)); - assert(!(ts[0].captures[1].end_bytesize != types.SIZE_MAX)); - // groupstart is non-consuming: pc stepped through it, then the - // lit consumed - assert(!(ts[0].pc != (2: size))); - - // groupend (ha:653-668): two open groups — the INNERMOST - // (highest index) closes first; back-to-back groupends close - // inner then outer in ONE call; content = str_bytes[ - // start_bytesize:end_bytesize] - let insts2: []regex.inst = []; - let gv: regex.inst_groupend; - let ge: regex.inst = gv; - append(insts2, ge); - append(insts2, ge); - append(insts2, ('x': regex.inst_lit)); - append(insts2, (false: regex.inst_match)); - let re2: regex.regex; - re2.insts = insts2; - re2.n_reps = 0; - let caps: []capture = []; - append(caps, capture { content = "", start = 1, start_bytesize = 1, - end = types.SIZE_MAX, end_bytesize = types.SIZE_MAX }); - append(caps, capture { content = "", start = 2, start_bytesize = 2, - end = types.SIZE_MAX, end_bytesize = types.SIZE_MAX }); - let ts2: []thread = []; - append(ts2, thread { pc = 0, captures = caps, ... }); - let rx: (rune | io.eof) = 'x'; - let r2: (void | newmatch | nomem) = run_thread(0, &re2, "abcd", &ts2, rx, 3, 4); - assert(!(!(r2 is void))); - assert(!(ts2[0].captures[1].end != (3: size))); - assert(!(ts2[0].captures[1].end_bytesize != (4: size))); - assert(!(strings.compare(ts2[0].captures[1].content, "cd") != 0)); - assert(!(ts2[0].captures[0].end != (3: size))); - assert(!(ts2[0].captures[0].end_bytesize != (4: size))); - assert(!(strings.compare(ts2[0].captures[0].content, "bcd") != 0)); - assert(!(ts2[0].pc != (3: size))); - - // closed-group re-entry: groupstart over an already-CLOSED idx - // passes the ha:642 assert (end != SIZE_MAX) and re-opens fresh - let insts3: []regex.inst = []; - append(insts3, ((0: size): regex.inst_groupstart)); - append(insts3, ('a': regex.inst_lit)); - append(insts3, (false: regex.inst_match)); - let re3: regex.regex; - re3.insts = insts3; - re3.n_reps = 0; - let caps3: []capture = []; - append(caps3, capture { content = "ab", start = 0, start_bytesize = 0, - end = 2, end_bytesize = 2 }); - let ts3: []thread = []; - append(ts3, thread { pc = 0, captures = caps3, ... }); - let r3: (void | newmatch | nomem) = run_thread(0, &re3, "aba", &ts3, ra, 2, 2); - assert(!(!(r3 is void))); - assert(!(ts3[0].captures.len != 1)); - assert(!(ts3[0].captures[0].start != (2: size))); - assert(!(ts3[0].captures[0].end != types.SIZE_MAX)); - assert(!(ts3[0].captures[0].content.len != 0)); -}; - // fold-5a find/test rows — the group rows of Hare's own table: // group/alternation +test.ha:257-275, the jump-bug group rows // :499-503 minus the `{,1}` form (5b) plus `(x?)?` :607, the @@ -2116,88 +1466,6 @@ type smcase = struct { // ---- fold 5b: repetition ---------------------------------------------- -// parse_repetition rows (regex.ha:486-545) — DIRECT private-fn table -// (the leaf fn lands ahead of its `{`-arm consumer; tranche-A -// precedent). Expectations hand-executed from Hare's own code paths: -// the input is everything AFTER `{` (compile passes iterstr's rest), -// single-arg `{n}` sets max = min and replen = len(n) (ha:500-503, -// 539-541); two-arg replen = len(min) + 1 + len(max) (ha:543); an -// empty min is 0 (ha:524) while an empty max stays void (ha:527 — -// the `{n,}` open bound); a `,` BEYOND the first `}` is not a -// two-arg form (ha:499). The two error texts are byte-exact -// (ha:492/521/531-536). max_void distinguishes "expect void" from -// "expect maxv"; err != "" rows expect that exact error. -type prrow = struct { - input: str, - minv: size, - maxv: size, - max_void: bool, - replen: size, - err: str, -}; - -@test fn parse_repetition_cases() void = { - let rows: [13]prrow = [ - prrow { input = "2}", minv = 2, maxv = 2, max_void = false, - replen = 1, err = "" }, - prrow { input = "2}$", minv = 2, maxv = 2, max_void = false, - replen = 1, err = "" }, - // comma AFTER the endbrace — still single-arg (ha:499) - prrow { input = "2},5", minv = 2, maxv = 2, max_void = false, - replen = 1, err = "" }, - prrow { input = "1,2}", minv = 1, maxv = 2, max_void = false, - replen = 3, err = "" }, - prrow { input = ",2}", minv = 0, maxv = 2, max_void = false, - replen = 2, err = "" }, - prrow { input = ",0}", minv = 0, maxv = 0, max_void = false, - replen = 2, err = "" }, - prrow { input = "2,}", minv = 2, maxv = 0, max_void = true, - replen = 2, err = "" }, - prrow { input = ",}", minv = 0, maxv = 0, max_void = true, - replen = 1, err = "" }, - prrow { input = "12,34}xyz", minv = 12, maxv = 34, - max_void = false, replen = 5, err = "" }, - prrow { input = "-1,2}", minv = 0, maxv = 0, max_void = false, - replen = 0, err = "Negative repetition count '{-n}'" }, - prrow { input = "x,2}", minv = 0, maxv = 0, max_void = false, - replen = 0, - err = "Repetition expression syntax error '{n}'" }, - prrow { input = "0,-2}", minv = 0, maxv = 0, max_void = false, - replen = 0, err = "Negative repetition count '{-n}'" }, - // no endbrace at all (ha:491-493) - prrow { input = "2", minv = 0, maxv = 0, max_void = false, - replen = 0, - err = "Repetition expression syntax error '{n}'" }, - ]; - let i: i32 = 0; - for (i < len(rows)) { - let r: (repparts | error) = parse_repetition(rows[i].input); - match (r) { - case let t: repparts => { - assert(!(rows[i].err.len > 0)); - // .min is always size after a successful parse - // (ha:523-525 — empty min defaults to 0) - assert(!(!(t.min is size))); - assert(!(t.min as size != rows[i].minv)); - if (rows[i].max_void) { - assert(!(!(t.max is void))); - } else { - assert(!(!(t.max is size))); - assert(!(t.max as size != rows[i].maxv)); - }; - assert(!(t.replen != rows[i].replen)); - }; - case let e: regex.error => { - assert(!(rows[i].err.len == 0)); - if (strings.compare((e: str), rows[i].err) != 0) { - abort(); - }; - }; - }; - i += 1; - }; -}; - // 5b compile-error rows end-to-end through compile() — the two // parse_repetition texts surface verbatim (+test.ha:461-463 ERROR // rows). "a{" is the GRADUATED metachar-loud row: with `{` ported the diff --git a/lib/regex/regex_whitebox.ww b/lib/regex/regex_whitebox.ww new file mode 100644 index 00000000..417ba239 --- /dev/null +++ b/lib/regex/regex_whitebox.ww @@ -0,0 +1,783 @@ +// regex_whitebox — in-package white-box @test probes for lib/regex +// internals NOT reachable from the black-box package regex_test: the +// thread/NFA-stepping engine (thread/newmatch, run_thread / add_thread / +// delete_thread / search), the program-shape leaves (find_last_groupstart +// / shift / parse_repetition), and is_consuming_inst. These bare-call +// unexported regex symbols, so they must live in `package regex` (unified +// with regex.ww) — Hare's same-package white-box model +// (ref/hare/regex/+test.ha colocates its internal tests inside the regex +// module; Hare excludes them from normal builds via build tags, ww via +// the non-T @test drop, #6). Load-bearing ww-compiler coverage (drove +// #34/#38/#44/#45/#48), not black-box-reachable. +// +// `_whitebox.ww` (NOT *_test.ww) is the ww convention for an in-package +// white-box test file: the enumerator skips *test.ww, so the non-suffixed +// name is what keeps this a bundled regex module source. It is exercised +// by the sibling external driver lib/regex/wb/regex_test.ww, whose +// different-dir `import regex` dir-resolves and bundles this file (a +// same-dir import file-resolves regex.ww only — probe E; task #33 would +// delete the driver). The public-API @tests stay black-box in +// lib/regex/regex_test.ww (Go's foo / foo_test split; CLAUDE.md rule-9). +package regex; + +import io; +import memio; +import strings; +import types; + +// The thread struct (regex.ha:55-64) is in tree ahead of its engine +// consumers so the pending #15/#17 fix probes exercise the real type. +// Pin the field layout via the P6-proven wide-literal append + a +// depth-1 read-back row per appended thread; the Hare `...` partial +// fill (P5) must zero everything the second row's literal omits. +// root_capture has NO row: every read route into it is +// compiler-blocked today — the depth-2 chain behind the index links +// the field as a global (#6 F4), the element let-copy is #7 F5, and +// a probed `&threads[i].root_capture` deref segfaults byte-id on +// both stages — so its row lands with those fixes. +type texp = struct { + pc: size, + start_idx: size, + start_bytesize: size, + matched: bool, + failed: bool, + // .len reads as i32 (check.c:1239), so the count columns match it + ncaps: i32, + nreps: i32, +}; + +@test fn thread_shape() void = { + let rc: capture = capture { + content = "ab", start = 1, start_bytesize = 1, + end = 2, end_bytesize = 2, + }; + let pcaps: []capture = []; + append(pcaps, rc); + let prep: []size = []; + append(prep, (7: size)); + let threads: []thread = []; + append(threads, thread { + pc = 5, + start_idx = 6, + start_bytesize = 7, + root_capture = rc, + captures = pcaps, + rep_counters = prep, + matched = false, + failed = true, + }); + append(threads, thread { pc = 9, ... }); + let want: [2]texp = [ + texp { pc = 5, start_idx = 6, start_bytesize = 7, + matched = false, failed = true, + ncaps = 1, nreps = 1 }, + texp { pc = 9, start_idx = 0, start_bytesize = 0, + matched = false, failed = false, + ncaps = 0, nreps = 0 }, + ]; + assert(!(len(threads) != len(want))); + let i: i32 = 0; + for (i < len(want)) { + assert(!(threads[i].pc != want[i].pc)); + assert(!(threads[i].start_idx != want[i].start_idx)); + if (threads[i].start_bytesize != want[i].start_bytesize) { + abort(); + }; + assert(!(threads[i].matched != want[i].matched)); + assert(!(threads[i].failed != want[i].failed)); + assert(!(threads[i].captures.len != want[i].ncaps)); + assert(!(threads[i].rep_counters.len != want[i].nreps)); + i += 1; + }; +}; + +// newmatch (regex.ha:66) must discriminate nominally against plain +// void — and against nomem, the third payload-free member — across +// run_thread's (void | newmatch | nomem) return boundary: the P8 +// shape on the real lib type, one row per returned member. +fn nm_probe(x: i32) (void | newmatch | nomem) = { + if (x == 1) { + let nm: newmatch; + return nm; + }; + if (x == 2) { + let n: nomem; + return n; + }; + return; +}; + +type nmexp = struct { + arg: i32, + want_nm: bool, + want_void: bool, + want_nomem: bool, +}; + +@test fn newmatch_discriminates() void = { + let rows: [3]nmexp = [ + nmexp { arg = 1, want_nm = true, want_void = false, + want_nomem = false }, + nmexp { arg = 0, want_nm = false, want_void = true, + want_nomem = false }, + nmexp { arg = 2, want_nm = false, want_void = false, + want_nomem = true }, + ]; + let i: i32 = 0; + for (i < len(rows)) { + let r: (void | newmatch | nomem) = nm_probe(rows[i].arg); + assert(!((r is newmatch) != rows[i].want_nm)); + assert(!((r is void) != rows[i].want_void)); + assert(!((r is nomem) != rows[i].want_nomem)); + i += 1; + }; +}; + +// is_consuming_inst must discriminate the three consuming kinds from +// the seven non-consuming ones across all 10 inst variants +// (regex.ha:553-555) — the tranche-A-deferred row, graduated by the +// #19 >48B by-value arg wiring. Sequential typed-let + helper calls, +// not a [10](inst, bool) table: tagged-element array literals +// under-copy (#12), and a cast/literal rvalue arg source is +// #38b-unwired, so each value goes through a typed let (the +// #19-landed ident source). +fn ic_one(v: inst, want: bool) void = { + assert(!(is_consuming_inst(v) != want)); +}; + +@test fn is_consuming_kinds() void = { + let lit: inst = ('a': inst_lit); + ic_one(lit, true); + let av: inst_any; + let any: inst = av; + ic_one(any, true); + let cs: inst = (inst_charset { idx = 0, is_positive = true }); + ic_one(cs, true); + let kv: inst_skip; + let sk: inst = kv; + ic_one(sk, false); + let sp: inst = ((5: size): inst_split); + ic_one(sp, false); + let jm: inst = ((6: size): inst_jump); + ic_one(jm, false); + let mt: inst = (false: inst_match); + ic_one(mt, false); + let gs: inst = ((2: size): inst_groupstart); + ic_one(gs, false); + let gv: inst_groupend; + let ge: inst = gv; + ic_one(ge, false); + let rp: inst = (inst_repeat { + id = 1, origin = 4, min = (2: size), max = void, + }); + ic_one(rp, false); +}; + +// delete_thread (regex.ha:547-551) removes exactly the indexed +// element and preserves order; its frees are no-ops (no-free +// runtime), so the survivors' capture headers stay readable. +@test fn delete_thread_middle() void = { + let caps: []capture = []; + append(caps, capture { + content = "x", start = 0, start_bytesize = 0, + end = 1, end_bytesize = 1, + }); + let ts: []thread = []; + append(ts, thread { pc = 1, start_idx = 11, captures = caps, ... }); + append(ts, thread { pc = 2, start_idx = 22, ... }); + append(ts, thread { pc = 3, start_idx = 33, ... }); + delete_thread(1, &ts); + assert(!(len(ts) != 2)); + assert(!(ts[0].pc != (1: size))); + assert(!(ts[0].start_idx != (11: size))); + assert(!(ts[0].captures.len != 1)); + assert(!(ts[1].pc != (3: size))); + assert(!(ts[1].start_idx != (33: size))); + assert(!(ts[1].captures.len != 0)); + // boundary rows: delete at the last index, then at index 0 down + // to empty — the failed-sweep loop (regex.ha:891-896) deletes at + // every position including both ends. + delete_thread(1, &ts); + assert(!(len(ts) != 1)); + assert(!(ts[0].pc != (1: size))); + delete_thread(0, &ts); + assert(!(len(ts) != 0)); +}; + +// add_thread (regex.ha:557-587): same-pc dedup suppression fires only +// when the existing thread is unmatched AND started strictly earlier +// than the parent (ha:561-565); otherwise the child appends, +// inheriting the parent's start/matched/failed with DUPLICATED +// capture/rep_counter slices (empty parent → empty dup, ha:569/572) +// and a zeroed root_capture. +@test fn add_thread_dedup_inherit() void = { + let ts: []thread = []; + append(ts, thread { pc = 0, start_idx = 5, start_bytesize = 4, + matched = false, failed = true, ... }); + // inherit: fresh pc, parent fields copied, rest zeroed + let r: (void | nomem) = add_thread(&ts, 0, 7); + assert(!(!(r is void))); + assert(!(len(ts) != 2)); + assert(!(ts[1].pc != (7: size))); + assert(!(ts[1].start_idx != (5: size))); + assert(!(ts[1].start_bytesize != (4: size))); + assert(!(ts[1].matched)); + assert(!(!ts[1].failed)); + assert(!(ts[1].captures.len != 0)); + assert(!(ts[1].rep_counters.len != 0)); + assert(!(ts[1].root_capture.content.len != 0)); + assert(!(ts[1].root_capture.end != (0: size))); + // same-pc same-start does NOT suppress (strict <, ha:563-565) + let r2: (void | nomem) = add_thread(&ts, 0, 7); + assert(!(!(r2 is void))); + assert(!(len(ts) != 3)); + // an earlier-started unmatched existing thread DOES suppress + let ts2: []thread = []; + append(ts2, thread { pc = 0, start_idx = 5, ... }); + append(ts2, thread { pc = 7, start_idx = 2, ... }); + let r3: (void | nomem) = add_thread(&ts2, 0, 7); + assert(!(!(r3 is void))); + assert(!(len(ts2) != 2)); + // a MATCHED existing thread never suppresses + let ts3: []thread = []; + append(ts3, thread { pc = 0, start_idx = 5, ... }); + append(ts3, thread { pc = 7, start_idx = 2, matched = true, ... }); + let r4: (void | nomem) = add_thread(&ts3, 0, 7); + assert(!(!(r4 is void))); + assert(!(len(ts3) != 3)); + assert(!(ts3[2].pc != (7: size))); + assert(!(ts3[2].start_idx != (5: size))); +}; + +// add_thread dup (regex.ha:568-573): the child carries a COPY of the +// parent's captures/rep_counters — values equal, backing independent +// in both directions (mutate parent → child unchanged, mutate child → +// parent unchanged). Empty parent → empty dup (the pre-flip rows above +// stay byte-for-byte). Driven directly, the dedup-test precedent. +@test fn add_thread_dup_independence() void = { + let caps: []capture = []; + append(caps, capture { + content = "ab", start = 1, start_bytesize = 1, + end = 2, end_bytesize = 2, + }); + append(caps, capture { + content = "c", start = 3, start_bytesize = 3, + end = 4, end_bytesize = 4, + }); + let reps: []size = []; + append(reps, (5: size)); + append(reps, (6: size)); + let ts: []thread = []; + append(ts, thread { pc = 0, start_idx = 1, captures = caps, + rep_counters = reps, ... }); + let r: (void | nomem) = add_thread(&ts, 0, 9); + assert(!(!(r is void))); + assert(!(len(ts) != 2)); + // dup carried the parent's values + assert(!(ts[1].captures.len != 2)); + assert(!(strings.compare(ts[1].captures[0].content, "ab") != 0)); + assert(!(ts[1].captures[0].start != (1: size))); + assert(!(ts[1].captures[1].end != (4: size))); + assert(!(ts[1].rep_counters.len != 2)); + assert(!(ts[1].rep_counters[0] != (5: size))); + assert(!(ts[1].rep_counters[1] != (6: size))); + // independence, parent → child: mutate the parent post-add + ts[0].captures[0].start = 100; + ts[0].captures[0].content = "zz"; + ts[0].rep_counters[0] = 77; + assert(!(ts[1].captures[0].start != (1: size))); + assert(!(strings.compare(ts[1].captures[0].content, "ab") != 0)); + assert(!(ts[1].rep_counters[0] != (5: size))); + // independence, child → parent + ts[1].captures[1].end = 200; + ts[1].rep_counters[1] = 88; + assert(!(ts[0].captures[1].end != (4: size))); + assert(!(ts[0].rep_counters[1] != (6: size))); + // empty parent → empty dup + let ts2: []thread = []; + append(ts2, thread { pc = 0, ... }); + let r2: (void | nomem) = add_thread(&ts2, 0, 3); + assert(!(!(r2 is void))); + assert(!(ts2[1].captures.len != 0)); + assert(!(ts2[1].rep_counters.len != 0)); +}; + +// run_thread (regex.ha:589-742) driven directly over compile("ab")'s +// real program [skip, lit 'a', lit 'b', match(false)] — the arms +// fold-2a can emit. Phases: parked-skip spawn (len 1→2, parent pc +// unmoved — the unanchored-restart engine), lit advance, lit +// mismatch (failed=true AND pc still steps — ha:741 runs regardless +// of the arm's verdict), EOF on a consuming pc (failed, pc frozen), +// match arm (root_capture spans start_bytesize..str_bytesize + +// matched + `is newmatch`), and the matched-thread early return +// (ha:599-601). +@test fn run_thread_literal_program() void = { + // typed-let + match receive, the compile_literal_program shape + let c: (regex | error | nomem) = compile("ab"); + match (c) { + case let re: regex => { + // skip spawn: thread 0 parks on the skip, child enters at pc 1 + let ra: (rune | io.eof) = 'a'; + let ts: []thread = []; + append(ts, thread { pc = 0, ... }); + let r1: (void | newmatch | nomem) = run_thread(0, &re, "ab", &ts, ra, 0, 0); + assert(!(!(r1 is void))); + assert(!(len(ts) != 2)); + assert(!(ts[0].pc != (0: size))); + assert(!(ts[1].pc != (1: size))); + assert(!(ts[1].failed)); + + // lit match advances pc past 'a' + let r2: (void | newmatch | nomem) = run_thread(1, &re, "ab", &ts, ra, 0, 0); + assert(!(!(r2 is void))); + assert(!(ts[1].pc != (2: size))); + assert(!(ts[1].failed)); + + // lit mismatch fails the thread; pc steps anyway (ha:741) + let rx: (rune | io.eof) = 'x'; + let r3: (void | newmatch | nomem) = run_thread(1, &re, "ab", &ts, rx, 1, 1); + assert(!(!(r3 is void))); + assert(!(!ts[1].failed)); + assert(!(ts[1].pc != (3: size))); + + // EOF on a consuming pc fails the thread before pc steps + let ev: io.eof; + let reof: (rune | io.eof) = ev; + let ts2: []thread = []; + append(ts2, thread { pc = 1, ... }); + let r4: (void | newmatch | nomem) = run_thread(0, &re, "ab", &ts2, reof, 2, 2); + assert(!(!(r4 is void))); + assert(!(!ts2[0].failed)); + assert(!(ts2[0].pc != (1: size))); + + // match arm: root_capture spans start_bytesize..str_bytesize, + // matched set, newmatch returned + let ts3: []thread = []; + append(ts3, thread { pc = 3, ... }); + let r5: (void | newmatch | nomem) = run_thread(0, &re, "ab", &ts3, reof, 2, 2); + assert(!(!(r5 is newmatch))); + assert(!(!ts3[0].matched)); + assert(!(ts3[0].failed)); + assert(!(ts3[0].root_capture.start != (0: size))); + assert(!(ts3[0].root_capture.start_bytesize != (0: size))); + assert(!(ts3[0].root_capture.end != (2: size))); + assert(!(ts3[0].root_capture.end_bytesize != (2: size))); + assert(!(strings.compare(ts3[0].root_capture.content, "ab") != 0)); + + // an already-matched thread is inert (ha:599-601): void + // return, state untouched + let r6: (void | newmatch | nomem) = run_thread(0, &re, "ab", &ts3, ra, 3, 3); + assert(!(!(r6 is void))); + assert(!(ts3[0].root_capture.end != (2: size))); + + // idx/bytesize split: every all-ASCII row has idx == + // bytesize, so a port swapping start/start_bytesize (or + // end/end_bytesize) in root_capture passes them. One 2-byte + // rune ('ß') consumed before the match start makes all four + // values distinct: start=1 start_bytesize=2 end=3 + // end_bytesize=4; content = bytes[2:4] = "ab". + let ts4: []thread = []; + append(ts4, thread { pc = 3, start_idx = 1, start_bytesize = 2, ... }); + let r7: (void | newmatch | nomem) = run_thread(0, &re, "ßab", &ts4, reof, 3, 4); + assert(!(!(r7 is newmatch))); + assert(!(ts4[0].root_capture.start != (1: size))); + assert(!(ts4[0].root_capture.start_bytesize != (2: size))); + assert(!(ts4[0].root_capture.end != (3: size))); + assert(!(ts4[0].root_capture.end_bytesize != (4: size))); + assert(!(strings.compare(ts4[0].root_capture.content, "ab") != 0)); + finish(&re); + }; + case => abort(); + }; +}; + +// The anchored route (ha:621-624) needs a (true: inst_match) program +// — compile() can't emit `$` yet, so it is HAND-BUILT — pinned from +// both sides: anchored + string-not-exhausted fails the thread; +// anchored + EOF falls through to the match (empty content). +@test fn run_thread_anchored_route() void = { + let insts: []inst = []; + append(insts, (true: inst_match)); + let re: regex; + re.insts = insts; + re.n_reps = 0; + + let ra: (rune | io.eof) = 'a'; + let ts: []thread = []; + append(ts, thread { pc = 0, ... }); + let r1: (void | newmatch | nomem) = run_thread(0, &re, "ab", &ts, ra, 0, 0); + assert(!(!(r1 is void))); + assert(!(!ts[0].failed)); + assert(!(ts[0].matched)); + + let ev: io.eof; + let reof: (rune | io.eof) = ev; + let ts2: []thread = []; + append(ts2, thread { pc = 0, ... }); + let r2: (void | newmatch | nomem) = run_thread(0, &re, "", &ts2, reof, 0, 0); + assert(!(!(r2 is newmatch))); + assert(!(!ts2[0].matched)); + assert(!(ts2[0].root_capture.content.len != 0)); + assert(!(ts2[0].root_capture.end != (0: size))); +}; + +// search (regex.ha:746-898) driven DIRECTLY (private fn, package-regex +// test) over memio-backed streams — the exec surface (test/find) is +// tranche D. Each match row pins the root capture's four indices plus +// content; the multibyte row keeps idx != bytesize honest. Rows share +// (expr, input, need_captures, want) shape — the P12 struct-row table. +type scase = struct { + expr: str, + input: str, + nc: bool, + start: size, + sb: size, + end: size, + eb: size, + content: str, +}; + +@test fn search_matches() void = { + let rows: [6]scase = [ + // full match mid-string: skip-respawn + dispatch + + // all_matched exit + scase { expr = "ab", input = "xab", nc = true, + start = 1, sb = 1, end = 3, eb = 3, content = "ab" }, + // mismatch-restart: the idx-0 child fails and is swept; the + // restarted thread wins (failed-sweep interplay) + scase { expr = "bcd", input = "abcd", nc = true, + start = 1, sb = 1, end = 4, eb = 4, content = "bcd" }, + // leftmost-longest best-pick + first_match_idx trim + scase { expr = "aa", input = "aaa", nc = true, + start = 0, sb = 0, end = 2, eb = 2, content = "aa" }, + // zero-length: the all_matched path with matchlen 0 must + // NOT take the need_captures=false early-exit (ha:845 + // requires matchlen > 0) — hence nc=false expecting the + // FULL one-capture result, not the empty early-exit slice + scase { expr = "", input = "", nc = false, + start = 0, sb = 0, end = 0, eb = 0, content = "" }, + // multibyte: the 2-byte ß before the match start splits + // every idx from its bytesize; inst_any consumes 'x' + scase { expr = "b.d", input = "aßbxd", nc = true, + start = 2, sb = 3, end = 5, eb = 6, content = "bxd" }, + // dedup-heavy: same-pc threads spawn on every step across + // >=3 passes (ha:872-889); the pick must stay stable. + // Result stability is the only external pin available this + // fold: 2a programs are all fixed-length, every match ties + // on match_len, and best-pick's insertion-order tiebreak + // alone yields leftmost — so the dedup sweep and the + // leftmost trim are result-invisible (mutation-verified: + // disabling either still passes this table; disabling the + // failed sweep hangs). Both turn result- and + // termination-visible with the split/star fold. + scase { expr = "aa", input = "aaaa", nc = true, + start = 0, sb = 0, end = 2, eb = 2, content = "aa" }, + ]; + let i: i32 = 0; + for (i < len(rows)) { + let ex: str = rows[i].expr; + let inp: str = rows[i].input; + let c: (regex | error | nomem) = compile(ex); + match (c) { + case let re: regex => { + let strm: memio.stream = + memio.fixed(strings.toutf8(inp)); + let r: (void | []capture | nomem) = + search(&re, inp, &strm.vt, rows[i].nc); + assert(!(!(r is []capture))); + let caps: []capture = r as []capture; + assert(!(len(caps) != 1)); + assert(!(caps[0].start != rows[i].start)); + assert(!(caps[0].start_bytesize != rows[i].sb)); + assert(!(caps[0].end != rows[i].end)); + assert(!(caps[0].end_bytesize != rows[i].eb)); + let wc: str = rows[i].content; + if (strings.compare(caps[0].content, wc) != 0) { + abort(); + }; + result_free(caps); + finish(&re); + }; + case => abort(); + }; + i += 1; + }; +}; + +// ha:845-847: a non-zero-length newmatch with need_captures=false +// returns the empty result immediately, skipping the best-pick pass. +@test fn search_early_exit() void = { + let c: (regex | error | nomem) = compile("ab"); + match (c) { + case let re: regex => { + let strm: memio.stream = memio.fixed(strings.toutf8("xab")); + let r: (void | []capture | nomem) = + search(&re, "xab", &strm.vt, false); + assert(!(!(r is []capture))); + let caps: []capture = r as []capture; + assert(!(len(caps) != 0)); + result_free(caps); + finish(&re); + }; + case => abort(); + }; +}; + +// void rows: no match anywhere ("ab" over "xyz" — every thread fails, +// the list drains, ha:777-779) and EOF mid-pattern ("ab" over "a" — +// the consuming-inst EOF fail). +@test fn search_no_match() void = { + let c: (regex | error | nomem) = compile("ab"); + match (c) { + case let re: regex => { + let strm: memio.stream = memio.fixed(strings.toutf8("xyz")); + let r: (void | []capture | nomem) = + search(&re, "xyz", &strm.vt, true); + assert(!(!(r is void))); + let strm2: memio.stream = memio.fixed(strings.toutf8("a")); + let r2: (void | []capture | nomem) = + search(&re, "a", &strm2.vt, true); + assert(!(!(r2 is void))); + finish(&re); + }; + case => abort(); + }; +}; + +// find_last_groupstart (regex.ha:104-119) — driven directly (private +// fn): no inst_groupstart exists in any fold-3 program, so the error +// arm is the live one; pin its exact text. A hand-built groupstart +// row pins the success arm the group fold will rely on. +@test fn find_last_groupstart_cases() void = { + let insts: []inst = []; + append(insts, ('a': inst_lit)); + match (find_last_groupstart(insts)) { + case let e: error => { + if (strings.compare((e: str), "Unmatched ')'") != 0) { + abort(); + }; + }; + case => abort(); + }; + append(insts, ((1: size): inst_groupstart)); + append(insts, ('b': inst_lit)); + match (find_last_groupstart(insts)) { + case let sz: size => { assert(!(sz != 1)); }; + case => abort(); + }; +}; + +// instsig (copy; the external black-box file keeps the +// original for its fold3/fold4 program-shape pins). +// instsig — flatten an inst for the table-driven program pins below: +// kind base + payload. Takes the 56B inst by value (the #19-landed +// is_consuming_inst shape). +fn instsig(v: inst) i64 = { + match (v) { + case let l: inst_lit => return 1000 + ((l: rune): i64); + case inst_skip => return 2000; + case inst_any => return 3000; + case let s: inst_split => return 4000 + ((s: size): i64); + case let j: inst_jump => return 5000 + ((j: size): i64); + case let m: inst_match => { + if ((m: bool)) { return 6001; }; + return 6000; + }; + case let g: inst_groupstart => return 7000 + ((g: size): i64); + case inst_groupend => return 8000; + case let c: inst_charset => { + // fold 4: 10xxx positive / 11xxx negated, + charset index + if (c.is_positive) { return 10000 + (c.idx: i64); }; + return 11000 + (c.idx: i64); + }; + case => return 9999; + }; +}; + +// shift (regex.ha:123-133) — driven directly over a sub-slice view: +// jump/split payloads in the view bump by one, the element before the +// view and non-jump kinds are untouched (the PE3/PE4 shapes). +@test fn shift_direct() void = { + let insts: []inst = []; + append(insts, ((3: size): inst_jump)); + append(insts, ('a': inst_lit)); + append(insts, ((5: size): inst_split)); + append(insts, ((7: size): inst_jump)); + shift(insts[1:]); + assert(!(instsig(insts[0]) != 5003)); + assert(!(instsig(insts[1]) != 1097)); + assert(!(instsig(insts[2]) != 4006)); + assert(!(instsig(insts[3]) != 5008)); +}; + +// run_thread inst_groupstart / inst_groupend driven over HAND-BUILT +// programs (the anchored-route precedent): compile() composition is +// pinned by the find/submatch tables below; these pin the arm +// mechanics — fill-grow to idx+1, the SIZE_MAX open sentinel, +// innermost-unclosed close order, content from the bytesize span, +// and the closed-group re-entry overwrite (the ha:642 assert's +// PASSING direction). +@test fn run_thread_group_arms() void = { + // groupstart (ha:636-652): grows captures to idx+1 (zero-filled + // below idx), stamps start/start_bytesize, opens with + // end = end_bytesize = SIZE_MAX + let insts: []inst = []; + append(insts, ((1: size): inst_groupstart)); + append(insts, ('a': inst_lit)); + append(insts, (false: inst_match)); + let re: regex; + re.insts = insts; + re.n_reps = 0; + let ra: (rune | io.eof) = 'a'; + let ts: []thread = []; + append(ts, thread { pc = 0, ... }); + let r1: (void | newmatch | nomem) = run_thread(0, &re, "ab", &ts, ra, 2, 3); + assert(!(!(r1 is void))); + assert(!(ts[0].captures.len != 2)); + // the fill element below idx is zeroed + assert(!(ts[0].captures[0].end != (0: size))); + assert(!(ts[0].captures[0].content.len != 0)); + // the opened group: start stamped, end still the open sentinel + assert(!(ts[0].captures[1].start != (2: size))); + assert(!(ts[0].captures[1].start_bytesize != (3: size))); + assert(!(ts[0].captures[1].end != types.SIZE_MAX)); + assert(!(ts[0].captures[1].end_bytesize != types.SIZE_MAX)); + // groupstart is non-consuming: pc stepped through it, then the + // lit consumed + assert(!(ts[0].pc != (2: size))); + + // groupend (ha:653-668): two open groups — the INNERMOST + // (highest index) closes first; back-to-back groupends close + // inner then outer in ONE call; content = str_bytes[ + // start_bytesize:end_bytesize] + let insts2: []inst = []; + let gv: inst_groupend; + let ge: inst = gv; + append(insts2, ge); + append(insts2, ge); + append(insts2, ('x': inst_lit)); + append(insts2, (false: inst_match)); + let re2: regex; + re2.insts = insts2; + re2.n_reps = 0; + let caps: []capture = []; + append(caps, capture { content = "", start = 1, start_bytesize = 1, + end = types.SIZE_MAX, end_bytesize = types.SIZE_MAX }); + append(caps, capture { content = "", start = 2, start_bytesize = 2, + end = types.SIZE_MAX, end_bytesize = types.SIZE_MAX }); + let ts2: []thread = []; + append(ts2, thread { pc = 0, captures = caps, ... }); + let rx: (rune | io.eof) = 'x'; + let r2: (void | newmatch | nomem) = run_thread(0, &re2, "abcd", &ts2, rx, 3, 4); + assert(!(!(r2 is void))); + assert(!(ts2[0].captures[1].end != (3: size))); + assert(!(ts2[0].captures[1].end_bytesize != (4: size))); + assert(!(strings.compare(ts2[0].captures[1].content, "cd") != 0)); + assert(!(ts2[0].captures[0].end != (3: size))); + assert(!(ts2[0].captures[0].end_bytesize != (4: size))); + assert(!(strings.compare(ts2[0].captures[0].content, "bcd") != 0)); + assert(!(ts2[0].pc != (3: size))); + + // closed-group re-entry: groupstart over an already-CLOSED idx + // passes the ha:642 assert (end != SIZE_MAX) and re-opens fresh + let insts3: []inst = []; + append(insts3, ((0: size): inst_groupstart)); + append(insts3, ('a': inst_lit)); + append(insts3, (false: inst_match)); + let re3: regex; + re3.insts = insts3; + re3.n_reps = 0; + let caps3: []capture = []; + append(caps3, capture { content = "ab", start = 0, start_bytesize = 0, + end = 2, end_bytesize = 2 }); + let ts3: []thread = []; + append(ts3, thread { pc = 0, captures = caps3, ... }); + let r3: (void | newmatch | nomem) = run_thread(0, &re3, "aba", &ts3, ra, 2, 2); + assert(!(!(r3 is void))); + assert(!(ts3[0].captures.len != 1)); + assert(!(ts3[0].captures[0].start != (2: size))); + assert(!(ts3[0].captures[0].end != types.SIZE_MAX)); + assert(!(ts3[0].captures[0].content.len != 0)); +}; + +// parse_repetition rows (regex.ha:486-545) — DIRECT private-fn table +// (the leaf fn lands ahead of its `{`-arm consumer; tranche-A +// precedent). Expectations hand-executed from Hare's own code paths: +// the input is everything AFTER `{` (compile passes iterstr's rest), +// single-arg `{n}` sets max = min and replen = len(n) (ha:500-503, +// 539-541); two-arg replen = len(min) + 1 + len(max) (ha:543); an +// empty min is 0 (ha:524) while an empty max stays void (ha:527 — +// the `{n,}` open bound); a `,` BEYOND the first `}` is not a +// two-arg form (ha:499). The two error texts are byte-exact +// (ha:492/521/531-536). max_void distinguishes "expect void" from +// "expect maxv"; err != "" rows expect that exact error. +type prrow = struct { + input: str, + minv: size, + maxv: size, + max_void: bool, + replen: size, + err: str, +}; + +@test fn parse_repetition_cases() void = { + let rows: [13]prrow = [ + prrow { input = "2}", minv = 2, maxv = 2, max_void = false, + replen = 1, err = "" }, + prrow { input = "2}$", minv = 2, maxv = 2, max_void = false, + replen = 1, err = "" }, + // comma AFTER the endbrace — still single-arg (ha:499) + prrow { input = "2},5", minv = 2, maxv = 2, max_void = false, + replen = 1, err = "" }, + prrow { input = "1,2}", minv = 1, maxv = 2, max_void = false, + replen = 3, err = "" }, + prrow { input = ",2}", minv = 0, maxv = 2, max_void = false, + replen = 2, err = "" }, + prrow { input = ",0}", minv = 0, maxv = 0, max_void = false, + replen = 2, err = "" }, + prrow { input = "2,}", minv = 2, maxv = 0, max_void = true, + replen = 2, err = "" }, + prrow { input = ",}", minv = 0, maxv = 0, max_void = true, + replen = 1, err = "" }, + prrow { input = "12,34}xyz", minv = 12, maxv = 34, + max_void = false, replen = 5, err = "" }, + prrow { input = "-1,2}", minv = 0, maxv = 0, max_void = false, + replen = 0, err = "Negative repetition count '{-n}'" }, + prrow { input = "x,2}", minv = 0, maxv = 0, max_void = false, + replen = 0, + err = "Repetition expression syntax error '{n}'" }, + prrow { input = "0,-2}", minv = 0, maxv = 0, max_void = false, + replen = 0, err = "Negative repetition count '{-n}'" }, + // no endbrace at all (ha:491-493) + prrow { input = "2", minv = 0, maxv = 0, max_void = false, + replen = 0, + err = "Repetition expression syntax error '{n}'" }, + ]; + let i: i32 = 0; + for (i < len(rows)) { + let r: (repparts | error) = parse_repetition(rows[i].input); + match (r) { + case let t: repparts => { + assert(!(rows[i].err.len > 0)); + // .min is always size after a successful parse + // (ha:523-525 — empty min defaults to 0) + assert(!(!(t.min is size))); + assert(!(t.min as size != rows[i].minv)); + if (rows[i].max_void) { + assert(!(!(t.max is void))); + } else { + assert(!(!(t.max is size))); + assert(!(t.max as size != rows[i].maxv)); + }; + assert(!(t.replen != rows[i].replen)); + }; + case let e: error => { + assert(!(rows[i].err.len == 0)); + if (strings.compare((e: str), rows[i].err) != 0) { + abort(); + }; + }; + }; + i += 1; + }; +}; diff --git a/lib/regex/wb/regex_test.ww b/lib/regex/wb/regex_test.ww new file mode 100644 index 00000000..23c7a482 --- /dev/null +++ b/lib/regex/wb/regex_test.ww @@ -0,0 +1,20 @@ +// wb/regex_test — DOCUMENTED-TEMPORARY scaffolding (rule-7), NOT part of +// the blessed white-box convention: the permanent shape is the colocated +// lib/regex/regex_whitebox.ww (`package regex`) file alone. This driver +// only exists to RUN it, and task #33 ("resolver: dir-enumerate same-dir +// same-package siblings on import") DELETES this file when it lands — the +// true E-endpoint. +// +// Why it is load-bearing until then, not incidental: a `import regex` from +// a file sitting IN lib/regex/ (e.g. regex_test.ww) FILE-resolves to the +// sibling regex.ww and pulls ONLY that file, so the colocated +// regex_whitebox.ww is never bundled and its @tests are never collected +// (probe E, negative). Placed in a DIFFERENT dir with no regex.ww to +// shadow, this driver's `import regex` instead DIRECTORY-resolves the +// lib/regex package and bundles every non-*test.ww member — regex.ww + +// regex_whitebox.ww — unifying them so -T collects the white-box @tests. +// The dir is `wb/`, NOT `test/`: a lib//test/ subdir would shadow the +// lib/test runner module in the -T auto-bundle search path (task #32). +package regex_test; + +import regex; diff --git a/test/wcc/989_lib_byteid.c b/test/wcc/989_lib_byteid.c index 1b08296a..7b74604e 100644 --- a/test/wcc/989_lib_byteid.c +++ b/test/wcc/989_lib_byteid.c @@ -85,6 +85,10 @@ static const struct ent ents[] = { { .fixture = "lib/memio/memiotest.ww", .mode = M_ID }, { .fixture = "lib/os/ostest.ww", .mode = M_ID }, { .fixture = "lib/regex/regex_test.ww", .mode = M_ID }, + /* in-package white-box half (#9): the wb/ driver dir-resolves + * `import regex` and bundles lib/regex/regex_whitebox.ww (package + * regex), so this fixture byte-ids the moved engine probes. */ + { .fixture = "lib/regex/wb/regex_test.ww", .mode = M_ID }, { .fixture = "lib/strconv/test/ftostest.ww", .mode = M_ID }, /* graduated from #59.10 DIVERGE by the #62 float-literal fold fix * (wwstage lexer now folds through strconv.stof64, matching diff --git a/test/wcc/989_regex_run.c b/test/wcc/989_regex_run.c index f61e338a..420cc51a 100644 --- a/test/wcc/989_regex_run.c +++ b/test/wcc/989_regex_run.c @@ -1,14 +1,17 @@ /* - * 989_regex_run — execute the lib/regex @test fixture (fold-1 data - * model + fold-2a compile() + fold-2b tranche-A scaffolding) under - * the C-side `ww run` driver and assert exit 0. + * 989_regex_run — execute BOTH lib/regex @test halves under the C-side + * `ww test` driver and assert exit 0: + * 1. lib/regex/regex_test.ww — BLACK-BOX, package regex_test. + * 2. lib/regex/wb/regex_test.ww — the WHITE-BOX driver (#9): its + * `import regex` dir-resolves and bundles lib/regex/regex_whitebox.ww + * (package regex), so -T collects the in-package engine probes. + * Both are main-less @test files; -T synthesizes the entry, runs the + * fork-per-test record-and-continue harness, and exits nonzero iff any + * @test fails. * * Sibling to 984_base64_run / 989_sha256_run (9xx is full so this * shares the 989 prefix — the `short` name keys the binary, cf the - * 949_* / 989_sha256 precedent). regex_test.ww carries its own - * `export fn main()` that drives the data-model @test fns and signals - * which case failed via the exit code, so this file is a thin wrapper - * — no @test scanning, no synthetic main generation. + * 949_* / 989_sha256 precedent). */ #include #include @@ -39,20 +42,26 @@ main(void) char cwd[1024]; if (getcwd(cwd, sizeof cwd) == NULL) return 1; - const char *src = "lib/regex/regex_test.ww"; - char path[1024], cmd[2048]; - snprintf(path, sizeof path, "%s/%s", cwd, src); - /* 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 test %s", bin, path); - int rc = runwait(cmd); - if (rc != 0) { - fprintf(stderr, "regex_run FAIL: %s exited %d\n", src, rc); - return 1; + const char *srcs[] = { + "lib/regex/regex_test.ww", + "lib/regex/wb/regex_test.ww", + }; + for (size_t i = 0; i < sizeof srcs / sizeof srcs[0]; i++) { + const char *src = srcs[i]; + char path[1024], cmd[2048]; + snprintf(path, sizeof path, "%s/%s", cwd, src); + /* 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 test %s", bin, path); + int rc = runwait(cmd); + if (rc != 0) { + fprintf(stderr, "regex_run FAIL: %s exited %d\n", src, rc); + return 1; + } + printf("regex_run: %s ok\n", src); } - printf("regex_run: %s ok\n", src); return 0; }