// 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). // // Fold 2a ports compile()'s lit/any/match arms only; exec lives in // later folds, so the compile_* cases pin the emitted inst PROGRAM // (shape + payloads via indexed match-extraction), not matching. // charclass_map's fn-ptr table is deferred behind the array→slice // element-coercion checker gap (see regex.ww), so this test does not // exercise the POSIX-class predicate dispatch yet — it pins variant // discrimination (including the nominally-distinct same-underlying // inst_split/inst_jump/inst_groupstart `size` aliases and the // inst_any/inst_skip/inst_groupend `void` aliases), payload extraction, // the regex/capture struct shapes, and finish(). Same // signalled-then-fail()-with-+10 pattern as the rest of the stdlib // run-tests; the non-zero exit pinpoints the failing case. // // 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. package regex; import regex; import io; import memio; import os; import strings; import types; let signalled: i32 = 0; fn fail() void = { os.exit(signalled + 10); }; // inst_lit / inst_match carry distinguishable payloads (rune / bool). @test fn lit_and_match() void = { let a: regex.inst = ('a': regex.inst_lit); match (a) { case let l: regex.inst_lit => { if ((l: rune) != 'a') { fail(); }; }; case => fail(); }; let m: regex.inst = (true: regex.inst_match); match (m) { case let b: regex.inst_match => { if (!(b: bool)) { fail(); }; }; case => fail(); }; }; // The three `size`-aliased variants are nominally distinct: a value // built as inst_split must match inst_split, never inst_jump / // inst_groupstart, despite identical underlying storage. @test fn size_aliases_distinct() void = { let sp: regex.inst = ((5: size): regex.inst_split); match (sp) { case let s: regex.inst_split => { if ((s: size) != (5: size)) { fail(); }; }; case let j: regex.inst_jump => fail(); case let g: regex.inst_groupstart => fail(); case => fail(); }; let jp: regex.inst = ((9: size): regex.inst_jump); match (jp) { case let j: regex.inst_jump => { if ((j: size) != (9: size)) { fail(); }; }; case let s: regex.inst_split => fail(); case => fail(); }; let gs: regex.inst = ((2: size): regex.inst_groupstart); match (gs) { case let g: regex.inst_groupstart => { if ((g: size) != (2: size)) { fail(); }; }; case let s: regex.inst_split => fail(); case => fail(); }; }; // The `void`-aliased variants are likewise nominally distinct. @test fn void_aliases_distinct() void = { let av: regex.inst_any; let an: regex.inst = av; match (an) { case let a: regex.inst_any => void; case let k: regex.inst_skip => fail(); case let e: regex.inst_groupend => fail(); case => fail(); }; let sv: regex.inst_skip; let sk: regex.inst = sv; match (sk) { case let k: regex.inst_skip => void; case let a: regex.inst_any => fail(); case => fail(); }; let gv: regex.inst_groupend; let ge: regex.inst = gv; match (ge) { case let e: regex.inst_groupend => void; case let a: regex.inst_any => fail(); case let k: regex.inst_skip => fail(); case => fail(); }; }; // inst_charset carries a struct payload; its fields survive the union // round-trip. @test fn charset_payload() void = { let c: regex.inst = (inst_charset { idx = 3, is_positive = true }); match (c) { case let cs: regex.inst_charset => { if (cs.idx != (3: size)) { fail(); }; if (!cs.is_positive) { fail(); }; }; case => fail(); }; }; // inst_repeat round-trips through the inst union with its plain `size` // fields intact. Matching the nested (void | size) min/max bounds back // out is DEFERRED: `match` on a tagged-union-typed struct field // diverges cs≠ww (#26 — the wwstage frames it wider), so // asserting the bounds here would seed a rule-10-divergent fixture. @test fn repeat_payload() void = { let r: regex.inst = (inst_repeat { id = 1, origin = 4, min = (2: size), max = void, }); match (r) { case let rp: regex.inst_repeat => { if (rp.id != (1: size)) { fail(); }; if (rp.origin != (4: size)) { fail(); }; }; case => fail(); }; }; // The regex/capture structs hold their fields; finish() is a no-op // (no-free runtime) and must accept a built regex. @test fn struct_shapes_and_finish() void = { let cap: regex.capture = capture { content = "abc", start = 0, start_bytesize = 0, end = 3, end_bytesize = 3, }; if (cap.content.len != 3) { fail(); }; if (cap.end != (3: size)) { fail(); }; // regex's insts/charsets ([]inst / []charset) are left empty here: // fold 1 ports no compile() to populate them, an empty `[]` literal // is unspellable as a typed slice (#25 — array→slice // element-coercion gap), and a struct-literal slice-field store // drops len/cap (#24). Declaring the regex zeroes both // slice headers to {0,0,0}; only n_reps is set explicitly. let re: regex.regex; re.n_reps = 0; if (re.n_reps != (0: size)) { fail(); }; if (re.insts.len != 0) { fail(); }; regex.finish(&re); }; // compile("abc") emits the 5-inst literal program: the leading // unanchored inst_skip (regex.ha:261-263), one inst_lit per rune, the // epilogue inst_match(false) (ha:475-477). compile()'s // (regex | error | nomem) return is the first >24B tagged payload in // the tree — the receive shapes here double as #38 sret consumers // (typed-let + match here; scrutinee-direct in compile_empty_program). @test fn compile_literal_program() void = { let c: (regex.regex | regex.error | nomem) = regex.compile("abc"); match (c) { case let re: regex.regex => { if (re.insts.len != 5) { fail(); }; match (re.insts[0]) { case let k: regex.inst_skip => void; case => fail(); }; match (re.insts[1]) { case let l: regex.inst_lit => { if ((l: rune) != 'a') { fail(); }; }; case => fail(); }; match (re.insts[2]) { case let l: regex.inst_lit => { if ((l: rune) != 'b') { fail(); }; }; case => fail(); }; match (re.insts[3]) { case let l: regex.inst_lit => { if ((l: rune) != 'c') { fail(); }; }; case => fail(); }; match (re.insts[4]) { case let m: regex.inst_match => { if ((m: bool)) { fail(); }; }; case => fail(); }; if (re.charsets.len != 0) { fail(); }; if (re.n_reps != (0: size)) { fail(); }; regex.finish(&re); }; case => fail(); }; }; // '.' compiles to inst_any between the literals (regex.ha:460-461): // [skip, lit 'a', any, lit 'c', match(false)]. @test fn compile_any_program() void = { let c: (regex.regex | regex.error | nomem) = regex.compile("a.c"); match (c) { case let re: regex.regex => { if (re.insts.len != 5) { fail(); }; match (re.insts[0]) { case let k: regex.inst_skip => void; case => fail(); }; match (re.insts[1]) { case let l: regex.inst_lit => { if ((l: rune) != 'a') { fail(); }; }; case => fail(); }; match (re.insts[2]) { case let a: regex.inst_any => void; case => fail(); }; match (re.insts[3]) { case let l: regex.inst_lit => { if ((l: rune) != 'c') { fail(); }; }; case => fail(); }; match (re.insts[4]) { case let m: regex.inst_match => { if ((m: bool)) { fail(); }; }; case => fail(); }; regex.finish(&re); }; case => fail(); }; }; // compile("") is exactly [inst_match(false)]: the leading skip must // not fire on immediate done (regex.ha:261 gates on `next is rune`), // and the epilogue guard must fire on the empty program. @test fn compile_empty_program() void = { match (regex.compile("")) { case let re: regex.regex => { if (re.insts.len != 1) { fail(); }; match (re.insts[0]) { case let m: regex.inst_match => { if ((m: bool)) { fail(); }; }; case => fail(); }; regex.finish(&re); }; case => fail(); }; }; // Every deferred metacharacter is a LOUD error carrying the exact // fold-boundary text — falling through to the literal default would // silently compile a wrong program, and any OTHER error text would // mean an arm was half-ported. One pattern per deferred arm so the // fold that ports an arm consciously deletes its row. Fold 3 flipped // \ ^ $ | ? * + positive (fold3_* below); fold 4 flipped `[` // (fold4_* below); fold 5a flipped `(`/`)` (fold5_* below — "a("/"a)" // graduated into fold5_compile_errors with their REAL texts); // repetition `{` remains. @test fn compile_metachar_loud() void = { match (regex.compile("a{")) { case let e: regex.error => { if (strings.compare((e: str), "regex: metacharacter not yet ported") != 0) { fail(); }; }; case => fail(); }; }; // 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 }, ]; if (len(threads) != len(want)) { fail(); }; let i: i32 = 0; for (i < len(want)) { if (threads[i].pc != want[i].pc) { fail(); }; if (threads[i].start_idx != want[i].start_idx) { fail(); }; if (threads[i].start_bytesize != want[i].start_bytesize) { fail(); }; if (threads[i].matched != want[i].matched) { fail(); }; if (threads[i].failed != want[i].failed) { fail(); }; if (threads[i].captures.len != want[i].ncaps) { fail(); }; if (threads[i].rep_counters.len != want[i].nreps) { fail(); }; 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); if ((r is newmatch) != rows[i].want_nm) { fail(); }; if ((r is void) != rows[i].want_void) { fail(); }; if ((r is nomem) != rows[i].want_nomem) { fail(); }; 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. // The local is spelled []regex.capture, not the regex.result alias: // wwstage falsely loud-bails appending a struct literal onto an // alias-typed dst (#20); the alias + signature stay exercised by the // result_free call itself. Reverts to `regex.result` when #20 lands. @test fn result_free_noop() void = { let res: []regex.capture; append(res, capture { content = "x", start = 0, start_bytesize = 0, end = 1, end_bytesize = 1, }); regex.result_free(res); if (len(res) != 1) { fail(); }; if (res[0].end != (1: size)) { fail(); }; // The zero-header edge: find()'s no-match path returns an empty // result (regex.ha:915-916) the caller still result_free()s. The // bare decl is alias-typed — the #20 dodge above is append-only, // so the alias stays exercised in value position here. let empty: regex.result; regex.result_free(empty); if (len(empty) != 0) { fail(); }; }; // strerror (regex.ha:1127) is identity on the boundary text — routed // through a REAL compile() error, completing the exported error // surface end to end. @test fn strerror_identity() void = { match (regex.compile("a(")) { case let e: regex.error => { if (strings.compare(regex.strerror(e), "Unmatched '('") != 0) { fail(); }; }; case => fail(); }; }; // 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 = { if (is_consuming_inst(v) != want) { fail(); }; }; @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); if (len(ts) != 2) { fail(); }; if (ts[0].pc != (1: size)) { fail(); }; if (ts[0].start_idx != (11: size)) { fail(); }; if (ts[0].captures.len != 1) { fail(); }; if (ts[1].pc != (3: size)) { fail(); }; if (ts[1].start_idx != (33: size)) { fail(); }; if (ts[1].captures.len != 0) { fail(); }; // 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); if (len(ts) != 1) { fail(); }; if (ts[0].pc != (1: size)) { fail(); }; delete_thread(0, &ts); if (len(ts) != 0) { fail(); }; }; // 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); if (!(r is void)) { fail(); }; if (len(ts) != 2) { fail(); }; if (ts[1].pc != (7: size)) { fail(); }; if (ts[1].start_idx != (5: size)) { fail(); }; if (ts[1].start_bytesize != (4: size)) { fail(); }; if (ts[1].matched) { fail(); }; if (!ts[1].failed) { fail(); }; if (ts[1].captures.len != 0) { fail(); }; if (ts[1].rep_counters.len != 0) { fail(); }; if (ts[1].root_capture.content.len != 0) { fail(); }; if (ts[1].root_capture.end != (0: size)) { fail(); }; // same-pc same-start does NOT suppress (strict <, ha:563-565) let r2: (void | nomem) = add_thread(&ts, 0, 7); if (!(r2 is void)) { fail(); }; if (len(ts) != 3) { fail(); }; // 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); if (!(r3 is void)) { fail(); }; if (len(ts2) != 2) { fail(); }; // 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); if (!(r4 is void)) { fail(); }; if (len(ts3) != 3) { fail(); }; if (ts3[2].pc != (7: size)) { fail(); }; if (ts3[2].start_idx != (5: size)) { fail(); }; }; // 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); if (!(r is void)) { fail(); }; if (len(ts) != 2) { fail(); }; // dup carried the parent's values if (ts[1].captures.len != 2) { fail(); }; if (strings.compare(ts[1].captures[0].content, "ab") != 0) { fail(); }; if (ts[1].captures[0].start != (1: size)) { fail(); }; if (ts[1].captures[1].end != (4: size)) { fail(); }; if (ts[1].rep_counters.len != 2) { fail(); }; if (ts[1].rep_counters[0] != (5: size)) { fail(); }; if (ts[1].rep_counters[1] != (6: size)) { fail(); }; // 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; if (ts[1].captures[0].start != (1: size)) { fail(); }; if (strings.compare(ts[1].captures[0].content, "ab") != 0) { fail(); }; if (ts[1].rep_counters[0] != (5: size)) { fail(); }; // independence, child → parent ts[1].captures[1].end = 200; ts[1].rep_counters[1] = 88; if (ts[0].captures[1].end != (4: size)) { fail(); }; if (ts[0].rep_counters[1] != (6: size)) { fail(); }; // empty parent → empty dup let ts2: []thread = []; append(ts2, thread { pc = 0, ... }); let r2: (void | nomem) = add_thread(&ts2, 0, 3); if (!(r2 is void)) { fail(); }; if (ts2[1].captures.len != 0) { fail(); }; if (ts2[1].rep_counters.len != 0) { fail(); }; }; // 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); if (!(r1 is void)) { fail(); }; if (len(ts) != 2) { fail(); }; if (ts[0].pc != (0: size)) { fail(); }; if (ts[1].pc != (1: size)) { fail(); }; if (ts[1].failed) { fail(); }; // lit match advances pc past 'a' let r2: (void | newmatch | nomem) = run_thread(1, &re, "ab", &ts, ra, 0, 0); if (!(r2 is void)) { fail(); }; if (ts[1].pc != (2: size)) { fail(); }; if (ts[1].failed) { fail(); }; // 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); if (!(r3 is void)) { fail(); }; if (!ts[1].failed) { fail(); }; if (ts[1].pc != (3: size)) { fail(); }; // 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); if (!(r4 is void)) { fail(); }; if (!ts2[0].failed) { fail(); }; if (ts2[0].pc != (1: size)) { fail(); }; // 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); if (!(r5 is newmatch)) { fail(); }; if (!ts3[0].matched) { fail(); }; if (ts3[0].failed) { fail(); }; if (ts3[0].root_capture.start != (0: size)) { fail(); }; if (ts3[0].root_capture.start_bytesize != (0: size)) { fail(); }; if (ts3[0].root_capture.end != (2: size)) { fail(); }; if (ts3[0].root_capture.end_bytesize != (2: size)) { fail(); }; if (strings.compare(ts3[0].root_capture.content, "ab") != 0) { fail(); }; // 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); if (!(r6 is void)) { fail(); }; if (ts3[0].root_capture.end != (2: size)) { fail(); }; // 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); if (!(r7 is newmatch)) { fail(); }; if (ts4[0].root_capture.start != (1: size)) { fail(); }; if (ts4[0].root_capture.start_bytesize != (2: size)) { fail(); }; if (ts4[0].root_capture.end != (3: size)) { fail(); }; if (ts4[0].root_capture.end_bytesize != (4: size)) { fail(); }; if (strings.compare(ts4[0].root_capture.content, "ab") != 0) { fail(); }; regex.finish(&re); }; case => fail(); }; }; // 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); if (!(r1 is void)) { fail(); }; if (!ts[0].failed) { fail(); }; if (ts[0].matched) { fail(); }; 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); if (!(r2 is newmatch)) { fail(); }; if (!ts2[0].matched) { fail(); }; if (ts2[0].root_capture.content.len != 0) { fail(); }; if (ts2[0].root_capture.end != (0: size)) { fail(); }; }; // 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); if (!(r is []capture)) { fail(); }; let caps: []capture = r as []capture; if (len(caps) != 1) { fail(); }; if (caps[0].start != rows[i].start) { fail(); }; if (caps[0].start_bytesize != rows[i].sb) { fail(); }; if (caps[0].end != rows[i].end) { fail(); }; if (caps[0].end_bytesize != rows[i].eb) { fail(); }; let wc: str = rows[i].content; if (strings.compare(caps[0].content, wc) != 0) { fail(); }; regex.result_free(caps); regex.finish(&re); }; case => fail(); }; 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); if (!(r is []capture)) { fail(); }; let caps: []capture = r as []capture; if (len(caps) != 0) { fail(); }; regex.result_free(caps); regex.finish(&re); }; case => fail(); }; }; // 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); if (!(r is void)) { fail(); }; let strm2: memio.stream = memio.fixed(strings.toutf8("a")); let r2: (void | []capture | nomem) = search(&re, "a", &strm2.vt, true); if (!(r2 is void)) { fail(); }; regex.finish(&re); }; case => fail(); }; }; // 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; }; }; // 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: [12]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" }, // ("a*", "baa") — fold-3 rider: search's longest-pick beats // the zero-length candidate at 0 (the b*-over-"aaaabbbb" // semantics), so the greedy (1,3) "aa" leads; the trailing // end-of-string zero-length match takes the ha:942-945 // break, pinning a real splitting pattern through the 2c // zero-length machinery fdexp { start = 1, sb = 1, end = 3, eb = 3, content = "aa" }, fdexp { start = 3, sb = 3, end = 3, eb = 3, content = "" }, ]; let rows: [8]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 }, fdcase { expr = "a*", input = "baa", eoff = 10, ecnt = 2 }, ]; 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; }; }; // ---- fold 3: anchors / escape / postfix / alternation ---------------- // 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: regex.inst) i64 = { match (v) { case let l: regex.inst_lit => return 1000 + ((l: rune): i64); case regex.inst_skip => return 2000; case regex.inst_any => return 3000; case let s: regex.inst_split => return 4000 + ((s: size): i64); case let j: regex.inst_jump => return 5000 + ((j: size): i64); case let m: regex.inst_match => { if ((m: bool)) { return 6001; }; return 6000; }; case let g: regex.inst_groupstart => return 7000 + ((g: size): i64); case regex.inst_groupend => return 8000; case let c: regex.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; }; }; // Emitted-program pins for the fold-3 arms — deterministic, engine- // independent: the exact inst sequence (kinds + jump/split targets) // each metachar must compile to. Derived by hand-executing // regex.ha:286-473 (insert-before + shift + the SIZE_MAX-sentinel // jump fixup); the a|b row pins the whole jump_idxs pipeline // including the sentinel overwrite at ha:470-473. type pgmcase = struct { expr: str, soff: i32, scnt: i32, }; @test fn fold3_programs() void = { let sigs: [25]i64 = [ // "^a": anchored — no leading skip 1097, 6000, // "a$": skip, lit a, match(TRUE) 2000, 1097, 6001, // "a?": split jumps OVER the lit to the match 2000, 4003, 1097, 6000, // "a*": split to match; jump back to the split 2000, 4004, 1097, 5001, 6000, // "a+": split back to the lit 2000, 1097, 4001, 6000, // "a|b": leading split to the second branch's skip; the // first branch's jump lands on the epilogue match (the // fixed-up SIZE_MAX sentinel) 4004, 2000, 1097, 5006, 2000, 1098, 6000, ]; let rows: [6]pgmcase = [ pgmcase { expr = "^a", soff = 0, scnt = 2 }, pgmcase { expr = "a$", soff = 2, scnt = 3 }, pgmcase { expr = "a?", soff = 5, scnt = 4 }, pgmcase { expr = "a*", soff = 9, scnt = 5 }, pgmcase { expr = "a+", soff = 14, scnt = 4 }, pgmcase { expr = "a|b", soff = 18, scnt = 7 }, ]; let i: i32 = 0; for (i < len(rows)) { let ex: str = rows[i].expr; let c: (regex.regex | regex.error | nomem) = regex.compile(ex); match (c) { case let re: regex.regex => { if (re.insts.len != rows[i].scnt) { fail(); }; let k: i32 = 0; for (k < rows[i].scnt) { if (instsig(re.insts[k]) != sigs[rows[i].soff + k]) { fail(); }; k += 1; }; regex.finish(&re); }; case => fail(); }; i += 1; }; }; // The fold-3 compile-error surface, exact texts (regex.ha:289 / 296-299 // / 303-308 / 405-417 / 423-435 / 446-455). The "ab\|^cd" row is // Hare's own ERROR fixture (+test.ha:634) — the escaped '|' must NOT // set was_prev_rune_pipe, so the following '^' misplaces. type cerow = struct { pat: str, want: str, }; @test fn fold3_compile_errors() void = { let rows: [12]cerow = [ cerow { pat = "\\", want = "Trailing backslash '\\'" }, cerow { pat = "a\\", want = "Trailing backslash '\\'" }, cerow { pat = "a^", want = "Anchor '^' not at start of whole pattern or alternation" }, cerow { pat = "$a", want = "Anchor '$' not at end of whole pattern or alternation" }, cerow { pat = "ab\\|^cd", want = "Anchor '^' not at start of whole pattern or alternation" }, cerow { pat = "?", want = "Unused '?'" }, cerow { pat = "*", want = "Unused '*'" }, cerow { pat = "+", want = "Unused '+'" }, // '^' appends nothing, so insts is still empty (ha:404's // len check, not the r_idx one) cerow { pat = "^*", want = "Unused '*'" }, cerow { pat = "a*?", want = "Misused '?'" }, cerow { pat = "a**", want = "Misused '*'" }, cerow { pat = "a*+", want = "Misused '+'" }, ]; let i: i32 = 0; for (i < len(rows)) { let p: str = rows[i].pat; match (regex.compile(p)) { case let e: regex.error => { let w: str = rows[i].want; if (strings.compare((e: str), w) != 0) { fail(); }; }; case => fail(); }; i += 1; }; }; // 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) { fail(); }; }; case => fail(); }; append(insts, ((1: size): regex.inst_groupstart)); append(insts, ('b': regex.inst_lit)); match (find_last_groupstart(insts)) { case let sz: size => { if (sz != 1) { fail(); }; }; case => fail(); }; }; // 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:]); if (instsig(insts[0]) != 5003) { fail(); }; if (instsig(insts[1]) != 1097) { fail(); }; if (instsig(insts[2]) != 4006) { fail(); }; if (instsig(insts[3]) != 5008) { fail(); }; }; // 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 // +test.ha:693-697) plus rob's dedup/leftmost/longest riders: // `a*` over "aaaa" must yield ONE (0,4) (split spawns same-pc threads // every step — the ha:872-889 dedup pin gone observable), `b+` over // "abab" pins the leftmost trim (1,2 not 3,4), `b*`/`^b*` over // "aaaabbbb" pin longest-pick vs anchored zero-length. The multibyte // `b+` row keeps every idx != bytesize (B4). Reuses the fcase shape; // every row also cross-pins test() == (find() matched). @test fn fold3_find_cases() void = { let rows: [42]fcase = [ fcase { expr = "^abc$", input = "abc", matches = true, start = 0, sb = 0, end = 3, eb = 3, content = "abc" }, fcase { expr = "^abc$", input = "axc", matches = false, ... }, fcase { expr = "^.$", input = "x", matches = true, start = 0, sb = 0, end = 1, eb = 1, content = "x" }, fcase { expr = "^.$", input = "", matches = false, ... }, fcase { expr = "^a+$", input = "a", matches = true, start = 0, sb = 0, end = 1, eb = 1, content = "a" }, fcase { expr = "^a+$", input = "aaa", matches = true, start = 0, sb = 0, end = 3, eb = 3, content = "aaa" }, fcase { expr = "^a+$", input = "", matches = false, ... }, fcase { expr = "^a*$", input = "", matches = true, start = 0, sb = 0, end = 0, eb = 0, content = "" }, fcase { expr = "^a*$", input = "aaaa", matches = true, start = 0, sb = 0, end = 4, eb = 4, content = "aaaa" }, fcase { expr = "^a*$", input = "b", matches = false, ... }, fcase { expr = "^a?$", input = "", matches = true, start = 0, sb = 0, end = 0, eb = 0, content = "" }, fcase { expr = "^a?$", input = "a", matches = true, start = 0, sb = 0, end = 1, eb = 1, content = "a" }, fcase { expr = "^a?$", input = "b", matches = false, ... }, fcase { expr = "^a*", input = "aaaa", matches = true, start = 0, sb = 0, end = 4, eb = 4, content = "aaaa" }, fcase { expr = "a*$", input = "aaaa", matches = true, start = 0, sb = 0, end = 4, eb = 4, content = "aaaa" }, fcase { expr = "a*", input = "aaaa", matches = true, start = 0, sb = 0, end = 4, eb = 4, content = "aaaa" }, fcase { expr = "b*", input = "aaaabbbb", matches = true, start = 4, sb = 4, end = 8, eb = 8, content = "bbbb" }, fcase { expr = "^b*", input = "aaaabbbb", matches = true, start = 0, sb = 0, end = 0, eb = 0, content = "" }, fcase { expr = "b*$", input = "aaaabbbb", matches = true, start = 4, sb = 4, end = 8, eb = 8, content = "bbbb" }, fcase { expr = "b+", input = "abab", matches = true, start = 1, sb = 1, end = 2, eb = 2, content = "b" }, // multibyte rider: 2-byte ß before the b's splits every // idx from its bytesize fcase { expr = "b+", input = "aßbb", matches = true, start = 2, sb = 3, end = 4, eb = 5, content = "bb" }, fcase { expr = "ab|cd", input = "cd", matches = true, start = 0, sb = 0, end = 2, eb = 2, content = "cd" }, fcase { expr = "ab|cd", input = "abc", matches = true, start = 0, sb = 0, end = 2, eb = 2, content = "ab" }, fcase { expr = "ab|cd", input = "abcd", matches = true, start = 0, sb = 0, end = 2, eb = 2, content = "ab" }, fcase { expr = "ab|cd", input = "bcd", matches = true, start = 1, sb = 1, end = 3, eb = 3, content = "cd" }, fcase { expr = "^ab|cd", input = "bcd", matches = true, start = 1, sb = 1, end = 3, eb = 3, content = "cd" }, fcase { expr = "^ab|cd", input = "zab", matches = false, ... }, fcase { expr = "ab$|cd", input = "ab", matches = true, start = 0, sb = 0, end = 2, eb = 2, content = "ab" }, fcase { expr = "ab$|cd", input = "abc", matches = false, ... }, fcase { expr = "ab|cd$", input = "cde", matches = false, ... }, fcase { expr = "ab|^cd", input = "bcd", matches = false, ... }, fcase { expr = "ab|^cd", input = "cde", matches = true, start = 0, sb = 0, end = 2, eb = 2, content = "cd" }, fcase { expr = "a|b|c|d|e", input = "e", matches = true, start = 0, sb = 0, end = 1, eb = 1, content = "e" }, fcase { expr = "a|b|c|d|e", input = "xe", matches = true, start = 1, sb = 1, end = 2, eb = 2, content = "e" }, fcase { expr = "a|b$|c$|d$|e", input = "cd", matches = true, start = 1, sb = 1, end = 2, eb = 2, content = "d" }, fcase { expr = "a|b$|c$|d$|e", input = "ax", matches = true, start = 0, sb = 0, end = 1, eb = 1, content = "a" }, fcase { expr = "a|b$|c$|d$|e", input = "cx", matches = false, ... }, fcase { expr = "a|b$|c$|d$|e", input = "ex", matches = true, start = 0, sb = 0, end = 1, eb = 1, content = "e" }, fcase { expr = "a|^b|^c|^d|e", input = "cd", matches = true, start = 0, sb = 0, end = 1, eb = 1, content = "c" }, fcase { expr = "a|^b|^c|^d|e", input = "xa", matches = true, start = 1, sb = 1, end = 2, eb = 2, content = "a" }, fcase { expr = "a|^b|^c|^d|e", input = "xc", matches = false, ... }, fcase { expr = "a|^b|^c|^d|e", input = "xe", matches = true, start = 1, sb = 1, end = 2, eb = 2, content = "e" }, ]; 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(); }; }; 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; }; }; // ---- fold 4: bracket expressions -------------------------------------- // Emitted-program pins for the `[..]` arm: the charset inst lands where // a literal would (and composes with fold-3's postfix/anchors), the // charsets table grows one entry per bracket, negation rides // is_positive. Derived by hand-executing regex.ha:265-275 + 313-314 + // handle_bracket. @test fn fold4_programs() void = { let sigs: [17]i64 = [ // "[abc]": skip, charset 0 positive, match(false) 2000, 10000, 6000, // "^[abc]$": anchored both ends — charset, match(TRUE) 10000, 6001, // "[^ab]": skip, charset 0 NEGATED, match(false) 2000, 11000, 6000, // "[ab][cd]": two brackets — charset 0, charset 1 2000, 10000, 10001, 6000, // "[abc]*": the fold-3 a* shape with charset as the term 2000, 4004, 10000, 5001, 6000, ]; let rows: [5]pgmcase = [ pgmcase { expr = "[abc]", soff = 0, scnt = 3 }, pgmcase { expr = "^[abc]$", soff = 3, scnt = 2 }, pgmcase { expr = "[^ab]", soff = 5, scnt = 3 }, pgmcase { expr = "[ab][cd]", soff = 8, scnt = 4 }, pgmcase { expr = "[abc]*", soff = 12, scnt = 5 }, ]; let i: i32 = 0; for (i < len(rows)) { let ex: str = rows[i].expr; let c: (regex.regex | regex.error | nomem) = regex.compile(ex); match (c) { case let re: regex.regex => { if (re.insts.len != rows[i].scnt) { fail(); }; let k: i32 = 0; for (k < rows[i].scnt) { if (instsig(re.insts[k]) != sigs[rows[i].soff + k]) { fail(); }; k += 1; }; regex.finish(&re); }; case => fail(); }; i += 1; }; }; // cssig — flatten a charset element: lits as 1_000_000 + codepoint, // ranges as start*10000 + end (codepoints stay < 10000 in these rows), // class items can't exist (compile()'s POSIX arm is loud). // The element binds structurally, not via the `charset` alias — an // alias-typed slice local's index read mis-scales in wwstage (#68). fn cssig(cs: [](charset_lit_item | charset_range_item | charset_class_item), k: size) i64 = { let cur: (charset_lit_item | charset_range_item | charset_class_item) = cs[k]; match (cur) { case let l: charset_lit_item => return 1000000 + ((l: rune): i64); case let range: charset_range_item => return (range.0: i64) * 10000 + (range.1: i64); case => return -1; }; }; type cscase = struct { expr: str, eoff: i32, ecnt: i32, }; // charsets-table content pins: literal vs range element discrimination, // the first-char `]`/`[` literal rules, literal dashes, and multibyte // codepoints in both element kinds (regex.ha:172-221 state machine). @test fn fold4_charsets() void = { let exp: [15]i64 = [ // "[abc]" 1000097, 1000098, 1000099, // "[]ab]" — first-char ] is a literal 1000093, 1000097, 1000098, // "[[ab]" — [ inside a bracket is a literal 1000091, 1000097, 1000098, // "[a-c]" 970099, // "[-a-c]" — leading literal dash 1000045, 970099, // "[a-c-]" — trailing literal dash 970099, 1000045, // "[ä-ö]" — multibyte range, codepoints 228..246 2280246, ]; let rows: [7]cscase = [ cscase { expr = "[abc]", eoff = 0, ecnt = 3 }, cscase { expr = "[]ab]", eoff = 3, ecnt = 3 }, cscase { expr = "[[ab]", eoff = 6, ecnt = 3 }, cscase { expr = "[a-c]", eoff = 9, ecnt = 1 }, cscase { expr = "[-a-c]", eoff = 10, ecnt = 2 }, cscase { expr = "[a-c-]", eoff = 12, ecnt = 2 }, cscase { expr = "[ä-ö]", eoff = 14, ecnt = 1 }, ]; let i: i32 = 0; for (i < len(rows)) { let ex: str = rows[i].expr; let c: (regex.regex | regex.error | nomem) = regex.compile(ex); match (c) { case let re: regex.regex => { if (re.charsets.len != 1) { fail(); }; let cs0: [](charset_lit_item | charset_range_item | charset_class_item) = re.charsets[0]; if ((len(cs0): i32) != rows[i].ecnt) { fail(); }; let k: i32 = 0; for (k < rows[i].ecnt) { if (cssig(cs0, (k: size)) != exp[rows[i].eoff + k]) { fail(); }; k += 1; }; regex.finish(&re); }; case => fail(); }; i += 1; }; }; // The fold-4 compile-error surface, exact texts (regex.ha:267 / 211). // The `[[:alpha:]]` POSIX-class arm is a loud ABORT, not an error — // unpinnable in-process (it kills the runner); its boundary is // source-audited (handle_bracket's class arm) until the POSIX fold. @test fn fold4_compile_errors() void = { let rows: [4]cerow = [ cerow { pat = "a[", want = "Unmatched '['" }, cerow { pat = "[abc", want = "Unmatched '['" }, cerow { pat = "[z-a]", want = "Descending bracket expression range '[z-a]'" }, // the escape arm must not eat `[`: "\[" is a literal, the // SECOND `[` opens an unterminated bracket cerow { pat = "\\[[", want = "Unmatched '['" }, ]; let i: i32 = 0; for (i < len(rows)) { let p: str = rows[i].pat; match (regex.compile(p)) { case let e: regex.error => { let w: str = rows[i].want; if (strings.compare((e: str), w) != 0) { fail(); }; }; case => fail(); }; i += 1; }; }; // fold-4 find/test rows — Hare's own bracket block (+test.ha:278-345) // minus the group row (`(` is loud) and the POSIX-class rows (loud // abort), plus multibyte riders (literal and range brackets over // 2-byte runes, idx != bytesize) and an unanchored composition row. // MATCH 0 -1 resolves to (0, runelen, input) per +test.ha:693-697; // every input is ASCII unless noted. Cross-pins test() == find(). @test fn fold4_find_cases() void = { let rows: [72]fcase = [ fcase { expr = "^test[abc]$", input = "testa", matches = true, start = 0, sb = 0, end = 5, eb = 5, content = "testa" }, fcase { expr = "^test[abc]$", input = "testb", matches = true, start = 0, sb = 0, end = 5, eb = 5, content = "testb" }, fcase { expr = "^test[abc]$", input = "testc", matches = true, start = 0, sb = 0, end = 5, eb = 5, content = "testc" }, fcase { expr = "^test[abc]$", input = "testd", matches = false, ... }, fcase { expr = "^test[abc]*$", input = "test", matches = true, start = 0, sb = 0, end = 4, eb = 4, content = "test" }, fcase { expr = "^test[abc]*$", input = "testa", matches = true, start = 0, sb = 0, end = 5, eb = 5, content = "testa" }, fcase { expr = "^test[abc]*$", input = "testaaa", matches = true, start = 0, sb = 0, end = 7, eb = 7, content = "testaaa" }, fcase { expr = "^test[abc]*$", input = "testabc", matches = true, start = 0, sb = 0, end = 7, eb = 7, content = "testabc" }, fcase { expr = "^test[abc]?$", input = "test", matches = true, start = 0, sb = 0, end = 4, eb = 4, content = "test" }, fcase { expr = "^test[abc]?$", input = "testa", matches = true, start = 0, sb = 0, end = 5, eb = 5, content = "testa" }, fcase { expr = "^test[abc]+$", input = "testa", matches = true, start = 0, sb = 0, end = 5, eb = 5, content = "testa" }, fcase { expr = "^test[abc]+$", input = "test", matches = false, ... }, fcase { expr = "^test[]abc]$", input = "test]", matches = true, start = 0, sb = 0, end = 5, eb = 5, content = "test]" }, fcase { expr = "^test[[abc]$", input = "test[", matches = true, start = 0, sb = 0, end = 5, eb = 5, content = "test[" }, fcase { expr = "^test[^abc]$", input = "testd", matches = true, start = 0, sb = 0, end = 5, eb = 5, content = "testd" }, fcase { expr = "^test[^abc]$", input = "test!", matches = true, start = 0, sb = 0, end = 5, eb = 5, content = "test!" }, fcase { expr = "^test[^abc]$", input = "testa", matches = false, ... }, fcase { expr = "^test[^abc]$", input = "testb", matches = false, ... }, fcase { expr = "^test[^abc]$", input = "testc", matches = false, ... }, fcase { expr = "^test[^]abc]$", input = "test]", matches = false, ... }, fcase { expr = "^test[^abc[]$", input = "test[", matches = false, ... }, fcase { expr = "^test[^abc]*$", input = "testd", matches = true, start = 0, sb = 0, end = 5, eb = 5, content = "testd" }, fcase { expr = "^test[^abc]*$", input = "testqqqqq", matches = true, start = 0, sb = 0, end = 9, eb = 9, content = "testqqqqq" }, fcase { expr = "^test[^abc]*$", input = "test", matches = true, start = 0, sb = 0, end = 4, eb = 4, content = "test" }, fcase { expr = "^test[^abc]*$", input = "testc", matches = false, ... }, fcase { expr = "^test[^abc]?$", input = "test", matches = true, start = 0, sb = 0, end = 4, eb = 4, content = "test" }, fcase { expr = "^test[^abc]?$", input = "testd", matches = true, start = 0, sb = 0, end = 5, eb = 5, content = "testd" }, fcase { expr = "^test[^abc]?$", input = "testc", matches = false, ... }, fcase { expr = "^test[^abc]+$", input = "testd", matches = true, start = 0, sb = 0, end = 5, eb = 5, content = "testd" }, fcase { expr = "^test[^abc]+$", input = "testddd", matches = true, start = 0, sb = 0, end = 7, eb = 7, content = "testddd" }, fcase { expr = "^test[^abc]+$", input = "testc", matches = false, ... }, fcase { expr = "^test[^abc]+$", input = "testcccc", matches = false, ... }, fcase { expr = "^test[a-c]$", input = "testa", matches = true, start = 0, sb = 0, end = 5, eb = 5, content = "testa" }, fcase { expr = "^test[a-c]$", input = "testb", matches = true, start = 0, sb = 0, end = 5, eb = 5, content = "testb" }, fcase { expr = "^test[a-c]$", input = "testc", matches = true, start = 0, sb = 0, end = 5, eb = 5, content = "testc" }, fcase { expr = "^test[a-c]$", input = "testd", matches = false, ... }, fcase { expr = "^test[a-c]$", input = "test!", matches = false, ... }, fcase { expr = "^test[a-c]$", input = "test-", matches = false, ... }, fcase { expr = "^test[-a-c]$", input = "test-", matches = true, start = 0, sb = 0, end = 5, eb = 5, content = "test-" }, fcase { expr = "^test[a-c-]$", input = "test-", matches = true, start = 0, sb = 0, end = 5, eb = 5, content = "test-" }, fcase { expr = "^test[a-c]*$", input = "test", matches = true, start = 0, sb = 0, end = 4, eb = 4, content = "test" }, fcase { expr = "^test[a-c]*$", input = "testa", matches = true, start = 0, sb = 0, end = 5, eb = 5, content = "testa" }, fcase { expr = "^test[a-c]*$", input = "testabb", matches = true, start = 0, sb = 0, end = 7, eb = 7, content = "testabb" }, fcase { expr = "^test[a-c]*$", input = "testddd", matches = false, ... }, fcase { expr = "^test[a-c]?$", input = "test", matches = true, start = 0, sb = 0, end = 4, eb = 4, content = "test" }, fcase { expr = "^test[a-c]?$", input = "testb", matches = true, start = 0, sb = 0, end = 5, eb = 5, content = "testb" }, fcase { expr = "^test[a-c]?$", input = "testd", matches = false, ... }, fcase { expr = "^test[a-c]+$", input = "test", matches = false, ... }, fcase { expr = "^test[a-c]+$", input = "testbcbc", matches = true, start = 0, sb = 0, end = 8, eb = 8, content = "testbcbc" }, fcase { expr = "^test[a-c]+$", input = "testd", matches = false, ... }, fcase { expr = "^test[^a-c]$", input = "testa", matches = false, ... }, fcase { expr = "^test[^a-c]$", input = "testb", matches = false, ... }, fcase { expr = "^test[^a-c]$", input = "testc", matches = false, ... }, fcase { expr = "^test[^a-c]$", input = "testd", matches = true, start = 0, sb = 0, end = 5, eb = 5, content = "testd" }, fcase { expr = "^test[^a-c]$", input = "test!", matches = true, start = 0, sb = 0, end = 5, eb = 5, content = "test!" }, fcase { expr = "^test[^a-c]$", input = "test-", matches = true, start = 0, sb = 0, end = 5, eb = 5, content = "test-" }, fcase { expr = "^test[^-a-c]$", input = "test-", matches = false, ... }, fcase { expr = "^test[^a-c-]$", input = "test-", matches = false, ... }, fcase { expr = "^test[^a-c-]*$", input = "test", matches = true, start = 0, sb = 0, end = 4, eb = 4, content = "test" }, fcase { expr = "^test[^a-c-]*$", input = "test--", matches = false, ... }, fcase { expr = "^test[^a-c-]*$", input = "testq", matches = true, start = 0, sb = 0, end = 5, eb = 5, content = "testq" }, fcase { expr = "^test[^a-c-]?$", input = "test", matches = true, start = 0, sb = 0, end = 4, eb = 4, content = "test" }, fcase { expr = "^test[^a-c-]?$", input = "testq", matches = true, start = 0, sb = 0, end = 5, eb = 5, content = "testq" }, fcase { expr = "^test[^a-c-]?$", input = "test-", matches = false, ... }, fcase { expr = "^test[^a-c-]+$", input = "test", matches = false, ... }, fcase { expr = "^test[^a-c-]+$", input = "testb", matches = false, ... }, fcase { expr = "^test[^a-c-]+$", input = "testddd", matches = true, start = 0, sb = 0, end = 7, eb = 7, content = "testddd" }, // multibyte riders: 2-byte runes in a literal bracket and a // codepoint range — idx != bytesize in every field (B4) fcase { expr = "^x[äö]$", input = "xä", matches = true, start = 0, sb = 0, end = 2, eb = 3, content = "xä" }, fcase { expr = "^x[äö]$", input = "xq", matches = false, ... }, fcase { expr = "^[à-ö]$", input = "á", matches = true, start = 0, sb = 0, end = 1, eb = 2, content = "á" }, fcase { expr = "^[à-ö]$", input = "x", matches = false, ... }, // unanchored leftmost-longest composition with fold-3's `+` fcase { expr = "[ab]+", input = "xxabyyba", matches = true, start = 2, sb = 2, end = 4, eb = 4, content = "ab" }, ]; 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(); }; }; 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; }; }; // findall composition: charset × split (`+`) × the 2c machinery — // non-overlapping greedy matches over mixed input. @test fn fold4_findall() void = { let c: (regex.regex | regex.error | nomem) = regex.compile("[ab]+"); match (c) { case let re: regex.regex => { let fr: ([]regex.result | nomem) = regex.findall(&re, "xxabyyba"); if (!(fr is []regex.result)) { fail(); }; let results: []regex.result = fr as []regex.result; if (len(results) != 2) { fail(); }; if (results[0][0].start != (2: size)) { fail(); }; if (results[0][0].end != (4: size)) { fail(); }; if (strings.compare(results[0][0].content, "ab") != 0) { fail(); }; if (results[1][0].start != (6: size)) { fail(); }; if (results[1][0].end != (8: size)) { fail(); }; if (strings.compare(results[1][0].content, "ba") != 0) { fail(); }; regex.result_freeall(results); regex.finish(&re); }; case => fail(); }; }; // ---- fold 5a: capture groups ------------------------------------------ // The fold-5a compile-error surface, exact texts. ")" and "(" are // Hare's own ERROR fixtures (+test.ha:276/606); "a("/"a)" graduate // here from the metachar-loud table with their real texts. The // anchor-in-group and groupstart-postfix rows are ww-added (no Hare // table row carries these texts): they pin that the fold-3 // verbatim-dead arms (regex.ha:295-297/302-304 anchors, // 413-414/434-435/453-454 Unused-on-groupstart) FIRE now that `(` // emits inst_groupstart — the §9a A0 re-verify, as live rows. @test fn fold5_compile_errors() void = { let rows: [9]cerow = [ cerow { pat = ")", want = "Unmatched ')'" }, cerow { pat = "(", want = "Unmatched '('" }, cerow { pat = "a)", want = "Unmatched ')'" }, cerow { pat = "a(", want = "Unmatched '('" }, // ww-added: anchor-in-group arms (regex.ha:295-297/302-304) cerow { pat = "(^a)", want = "Anchor '^' in capture groups is unsupported" }, cerow { pat = "(a$)", want = "Anchor '$' in capture groups is unsupported" }, // ww-added: postfix on a bare groupstart // (regex.ha:413-414/434-435/453-454) cerow { pat = "(?", want = "Unused '?'" }, cerow { pat = "(*", want = "Unused '*'" }, cerow { pat = "(+", want = "Unused '+'" }, ]; let i: i32 = 0; for (i < len(rows)) { let p: str = rows[i].pat; match (regex.compile(p)) { case let e: regex.error => { let w: str = rows[i].want; if (strings.compare((e: str), w) != 0) { fail(); }; }; case => fail(); }; i += 1; }; }; // 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); if (!(r1 is void)) { fail(); }; if (ts[0].captures.len != 2) { fail(); }; // the fill element below idx is zeroed if (ts[0].captures[0].end != (0: size)) { fail(); }; if (ts[0].captures[0].content.len != 0) { fail(); }; // the opened group: start stamped, end still the open sentinel if (ts[0].captures[1].start != (2: size)) { fail(); }; if (ts[0].captures[1].start_bytesize != (3: size)) { fail(); }; if (ts[0].captures[1].end != types.SIZE_MAX) { fail(); }; if (ts[0].captures[1].end_bytesize != types.SIZE_MAX) { fail(); }; // groupstart is non-consuming: pc stepped through it, then the // lit consumed if (ts[0].pc != (2: size)) { fail(); }; // 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); if (!(r2 is void)) { fail(); }; if (ts2[0].captures[1].end != (3: size)) { fail(); }; if (ts2[0].captures[1].end_bytesize != (4: size)) { fail(); }; if (strings.compare(ts2[0].captures[1].content, "cd") != 0) { fail(); }; if (ts2[0].captures[0].end != (3: size)) { fail(); }; if (ts2[0].captures[0].end_bytesize != (4: size)) { fail(); }; if (strings.compare(ts2[0].captures[0].content, "bcd") != 0) { fail(); }; if (ts2[0].pc != (3: size)) { fail(); }; // 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); if (!(r3 is void)) { fail(); }; if (ts3[0].captures.len != 1) { fail(); }; if (ts3[0].captures[0].start != (2: size)) { fail(); }; if (ts3[0].captures[0].end != types.SIZE_MAX) { fail(); }; if (ts3[0].captures[0].content.len != 0) { fail(); }; }; // 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 // POSIX-comment submatch-handling family :610-621 (MATCH-only root // assertions, as Hare; the literally-duplicated (a*) pairs :614-617 // appear once), `a|(b)` :635, `(a|b|c|d|e)f` :640, and the // nested-group block :649-665 minus the `{0,}`/`{1,}`/`{0,1}` twins // (5b) and Hare's own TODO-disabled anchor-in-group rows. end == -1 // resolves to rune length (+test.ha:693-697). ncaps additionally pins // len(res) = last-groupstart payload + 2 (the search length scan + // pad fill — unset groups pad to zeroed captures, so `a|(b)` over "a" // still yields 2). The multibyte row is ww-added (B4: every idx // splits from its bytesize). `(a+|b)*` over "ab" is rob's // thread-explosion stress row (group split × star respawn × // dedup/trim/leftmost pins). Cross-pins test() == (find() matched). type f5case = struct { expr: str, input: str, matches: bool, ncaps: i32, start: size, sb: size, end: size, eb: size, content: str, }; @test fn fold5_find_cases() void = { let rows: [32]f5case = [ // group/alternation (+test.ha:257-275) f5case { expr = "^(cafe|b)x$", input = "cafex", matches = true, ncaps = 2, start = 0, sb = 0, end = 5, eb = 5, content = "cafex" }, f5case { expr = "^(cafe|b)x$", input = "bx", matches = true, ncaps = 2, start = 0, sb = 0, end = 2, eb = 2, content = "bx" }, f5case { expr = "^(cafe|b)x$", input = "XXXx", matches = false, ... }, f5case { expr = "^(Privat|Jagd)(haftpflicht|schaden)versicherungs(police|betrag)$", input = "Jagdhaftpflichtversicherungsbetrag", matches = true, ncaps = 4, start = 0, sb = 0, end = 34, eb = 34, content = "Jagdhaftpflichtversicherungsbetrag" }, f5case { expr = "^(Privat|Jagd)(haftpflicht|schaden)versicherungs(police|betrag)$", input = "Jagdhaftpflichtversicherungsbetrug", matches = false, ... }, f5case { expr = "^(Privat|Jagd)(haftpflicht|schaden)versicherungs(police|betrag)$", input = "Jagdversicherungspolice", matches = false, ... }, // jump bugs (+test.ha:499-503 minus `{,1}`; :607) f5case { expr = "ab?c", input = "ac", matches = true, ncaps = 1, start = 0, sb = 0, end = 2, eb = 2, content = "ac" }, f5case { expr = "ab?c|z", input = "ac", matches = true, ncaps = 1, start = 0, sb = 0, end = 2, eb = 2, content = "ac" }, f5case { expr = "(ab?c)?", input = "ac", matches = true, ncaps = 2, start = 0, sb = 0, end = 2, eb = 2, content = "ac" }, f5case { expr = "(ab?c)*", input = "ac", matches = true, ncaps = 2, start = 0, sb = 0, end = 2, eb = 2, content = "ac" }, f5case { expr = "(x?)?", input = "x", matches = true, ncaps = 2, start = 0, sb = 0, end = 1, eb = 1, content = "x" }, // submatch handling (+test.ha:610-621) — MATCH-only root // assertions, as Hare (POSIX submatch semantics diverge) f5case { expr = "(a|ab)(c|bcd)(d*)", input = "abcd", matches = true, ncaps = 4, start = 0, sb = 0, end = 4, eb = 4, content = "abcd" }, f5case { expr = "(a|ab)(bcd|c)(d*)", input = "abcd", matches = true, ncaps = 4, start = 0, sb = 0, end = 4, eb = 4, content = "abcd" }, f5case { expr = "(ab|a)(c|bcd)(d*)", input = "abcd", matches = true, ncaps = 4, start = 0, sb = 0, end = 4, eb = 4, content = "abcd" }, f5case { expr = "(ab|a)(bcd|c)(d*)", input = "abcd", matches = true, ncaps = 4, start = 0, sb = 0, end = 4, eb = 4, content = "abcd" }, f5case { expr = "(a*)(b|abc)(c*)", input = "abc", matches = true, ncaps = 4, start = 0, sb = 0, end = 3, eb = 3, content = "abc" }, f5case { expr = "(a*)(abc|b)(c*)", input = "abc", matches = true, ncaps = 4, start = 0, sb = 0, end = 3, eb = 3, content = "abc" }, f5case { expr = "(a|ab)(c|bcd)(d|.*)", input = "abcd", matches = true, ncaps = 4, start = 0, sb = 0, end = 4, eb = 4, content = "abcd" }, f5case { expr = "(a|ab)(bcd|c)(d|.*)", input = "abcd", matches = true, ncaps = 4, start = 0, sb = 0, end = 4, eb = 4, content = "abcd" }, f5case { expr = "(ab|a)(c|bcd)(d|.*)", input = "abcd", matches = true, ncaps = 4, start = 0, sb = 0, end = 4, eb = 4, content = "abcd" }, f5case { expr = "(ab|a)(bcd|c)(d|.*)", input = "abcd", matches = true, ncaps = 4, start = 0, sb = 0, end = 4, eb = 4, content = "abcd" }, // alternation with a group (+test.ha:635) — the (b) branch // never runs on "a"; the pad fill still yields 2 captures f5case { expr = "a|(b)", input = "a", matches = true, ncaps = 2, start = 0, sb = 0, end = 1, eb = 1, content = "a" }, // multiple alternation inside a group (+test.ha:640) f5case { expr = "(a|b|c|d|e)f", input = "ef", matches = true, ncaps = 2, start = 0, sb = 0, end = 2, eb = 2, content = "ef" }, // nested groups (+test.ha:649-665 minus the `{…}` twins) f5case { expr = "((a))", input = "abc", matches = true, ncaps = 3, start = 0, sb = 0, end = 1, eb = 1, content = "a" }, f5case { expr = "((a)(b)c)(d)", input = "abcd", matches = true, ncaps = 5, start = 0, sb = 0, end = 4, eb = 4, content = "abcd" }, f5case { expr = "((((((((((a))))))))))", input = "a", matches = true, ncaps = 11, start = 0, sb = 0, end = 1, eb = 1, content = "a" }, f5case { expr = "(((((((((a)))))))))", input = "a", matches = true, ncaps = 10, start = 0, sb = 0, end = 1, eb = 1, content = "a" }, f5case { expr = "(([a-z]+):)?([a-z]+)$", input = "smil", matches = true, ncaps = 4, start = 0, sb = 0, end = 4, eb = 4, content = "smil" }, f5case { expr = "^((a)c)?(ab)$", input = "ab", matches = true, ncaps = 4, start = 0, sb = 0, end = 2, eb = 2, content = "ab" }, f5case { expr = "(a+|b)*", input = "ab", matches = true, ncaps = 2, start = 0, sb = 0, end = 2, eb = 2, content = "ab" }, f5case { expr = "(a+|b)+", input = "ab", matches = true, ncaps = 2, start = 0, sb = 0, end = 2, eb = 2, content = "ab" }, // ww-added multibyte group row (B4) f5case { expr = "(ä|b)x", input = "äx", matches = true, ncaps = 2, start = 0, sb = 0, end = 2, eb = 3, content = "äx" }, ]; 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): i32) != rows[i].ncaps) { 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(); }; }; 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; }; }; // `(a+|b)?` over "ab" (+test.ha:665) matches (0,1) "a" — the // non-greedy END pins that the optional group stops at the leftmost- // longest SINGLE term, not the star/plus whole-string sweep above. @test fn fold5_optional_group() void = { let c: (regex.regex | regex.error | nomem) = regex.compile("(a+|b)?"); match (c) { case let re: regex.regex => { let fr: (regex.result | nomem) = regex.find(&re, "ab"); if (!(fr is regex.result)) { fail(); }; let res: regex.result = fr as regex.result; if (len(res) != 2) { fail(); }; if (res[0].start != (0: size)) { fail(); }; if (res[0].end != (1: size)) { fail(); }; if (strings.compare(res[0].content, "a") != 0) { fail(); }; regex.result_free(res); regex.finish(&re); }; case => fail(); }; }; // submatch rows (+test.ha:704-708 via run_submatch_case :72-100) — // THE 5a acceptance gate: per-group .content equality through find(), // root first. The second row is ww-added (B4): a 2-byte ß inside both // captured groups splits every byte offset the contents are sliced // from. Targets live in a flat pool indexed by (toff, tcnt), the // findall_content shape. type smcase = struct { expr: str, input: str, toff: i32, tcnt: i32, }; @test fn fold5_submatches() void = { let targets: [6]str = [ "aaa bbb ccc", "bbb", "ccc", "aaa bßb cßc", "bßb", "cßc", ]; let rows: [2]smcase = [ smcase { expr = "aaa ([^ ]*) (...)", input = "aaa bbb ccc", toff = 0, tcnt = 3 }, // ww-added multibyte variant smcase { expr = "aaa ([^ ]*) (...)", input = "aaa bßb cßc", toff = 3, tcnt = 3 }, ]; 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 ((len(res): i32) != rows[i].tcnt) { fail(); }; let k: i32 = 0; for (k < rows[i].tcnt) { let want: str = targets[rows[i].toff + k]; if (strings.compare(res[k].content, want) != 0) { fail(); }; k += 1; }; 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(); signalled = 3; void_aliases_distinct(); signalled = 4; charset_payload(); signalled = 5; repeat_payload(); signalled = 6; struct_shapes_and_finish(); signalled = 7; compile_literal_program(); signalled = 8; compile_any_program(); signalled = 9; compile_empty_program(); signalled = 10; compile_metachar_loud(); signalled = 11; thread_shape(); signalled = 12; newmatch_discriminates(); signalled = 13; result_free_noop(); signalled = 14; strerror_identity(); signalled = 15; is_consuming_kinds(); signalled = 16; delete_thread_middle(); signalled = 17; add_thread_dedup_inherit(); signalled = 18; run_thread_literal_program(); signalled = 19; run_thread_anchored_route(); signalled = 20; search_matches(); signalled = 21; search_early_exit(); signalled = 22; search_no_match(); signalled = 23; test_matches(); signalled = 24; find_cases(); signalled = 25; findall_content(); signalled = 26; findall_fields(); signalled = 27; fold3_programs(); signalled = 28; fold3_compile_errors(); signalled = 29; find_last_groupstart_cases(); signalled = 30; shift_direct(); signalled = 31; fold3_find_cases(); signalled = 32; fold4_programs(); signalled = 33; fold4_charsets(); signalled = 34; fold4_compile_errors(); signalled = 35; fold4_find_cases(); signalled = 36; fold4_findall(); signalled = 37; add_thread_dup_independence(); signalled = 38; fold5_compile_errors(); signalled = 39; run_thread_group_arms(); signalled = 40; fold5_find_cases(); signalled = 41; fold5_optional_group(); signalled = 42; fold5_submatches(); return 0; };