Files
ww/lib/regex/regex_test.ww
Hojun-Cho a2c2bbc6b1 regex: fold 5a — capture groups (compile ( ) arms + run_thread groupstart/groupend + search spread)
compile: '(' (ha:317-323) appends inst_groupstart(capture_idx) and
grows jump_idxs per level; ')' (ha:324-334) appends the void
inst_groupend, fixes up the level's pending alternation jumps (#70
range + #58 assert), range-deletes them (#8) and closes the level;
the loop-exit done arm gains the Unmatched-'(' check (ha:277-282).
The anchors' group_level arms, the postfix inst_groupend/groupstart
arms and find_last_groupstart's success path go live unchanged.

run_thread: inst_groupstart (ha:636-652) fill-grows captures to
idx+1 (count-loop spelling of Hare's 3-arg fill-append) and opens
the group with the SIZE_MAX end sentinel via the #20-fixed indexed
struct store; inst_groupend (ha:653-668) closes the innermost
unclosed capture (ha:655's 2-clause for respelled — ww has no
cond;post form) and slices content from the bytesize span through
the addr-of-element pointer.

search: the ha:820 loud bound flips to the real capture spread
(#35/#25); the pad fill self-activates for unset trailing groups.

Tests: ( ) graduate from the metachar-loud table into real-text
error rows (+ ww-added anchor-in-group / Unused-on-groupstart
re-verify rows); hand-built groupstart/groupend arm cases; the 5a
find table (+test.ha:257-275 group/alternation, :499-503/:607 jump
bugs, :610-621 submatch family, :635/:640 alternation-group,
:649-665 nested minus the 5b {m,n} twins, + ww-added multibyte
row) with len(res) pinned per row; submatch content rows
(+test.ha:704-708 + ww-added multibyte) — the 5a acceptance gate.
2026-06-05 00:49:42 +09:00

2185 lines
81 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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;
};