// whitebox_test — 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. // // Canonical in-package white-box shape: `package regex;` in a *_test.ww // file, assembled with the production `regex` sources by the package // planner. Importing regex as a dependency still strips these tests. // Public-API tests remain a separate `package regex_test` binary in // lib/regex/regex_test.ww // (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; }; };