// regex_test — exercises the lib/regex fold-1 data model (the type // model + finish()), the fold-2a compile() literal core, and the // fold-2b tranche-A/B thread machine (thread/newmatch + result_free // + strerror; delete_thread/is_consuming_inst/add_thread; run_thread // and the exec surface are deferred — see regex.ww). 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 os; import strings; 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. '^' leads its // pattern: the r_idx==0 skip gate (regex.ha:261) must compose with // the loud arm, not bypass it. @test fn compile_metachar_loud() void = { let pats: [11]str = [ "a\\", "^a", "a$", "a[", "a(", "a)", "a|", "a{", "a?", "a*", "a+", ]; let i: i32 = 0; for (i < len(pats)) { match (regex.compile(pats[i])) { case let e: regex.error => { if (strings.compare((e: str), "regex: metacharacter not yet ported") != 0) { fail(); }; }; case => fail(); }; i += 1; }; }; // 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), "regex: metacharacter not yet ported") != 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 fresh empty // capture/rep_counter headers and a zeroed root_capture. The // capture-dup loud bound must NOT fire on these empty-caps parents. @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(); }; }; 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(); return 0; };