Files
ww/lib/regex/regex_test.ww

1835 lines
68 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 — BLACK-BOX @test probes for lib/regex: the exported API
// only (compile / test / find / findall / strerror / result_free /
// result_freeall / finish) plus exported-type variant discrimination,
// payload extraction, and the regex/capture/charset struct shapes.
// This is the external `package regex_test` half of the Go foo / foo_test
// split (CLAUDE.md rule-9). The WHITE-BOX half — probes that bare-call
// unexported internals (thread/newmatch, run_thread / add_thread /
// delete_thread / search, find_last_groupstart / shift /
// parse_repetition, is_consuming_inst) — lives in
// lib/regex/whitebox_test.ww (`package regex`, unified with regex.ww) and
// runs as a separate root-package binary.
//
// 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.
// fold-6 tests exercise POSIX character classes (charclass_map +
// compile/exec arms). Earlier folds pin 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(). A failing row aborts
// via the assert/abort builtin (task #5 @test conversion).
//
// The parser rejects a qualified name in struct-literal position (#29), so
// the few struct payload rows below initialize a qualified typed local before
// injecting it into the exported union. This stays distinct from the
// white-box privacy boundary handled by the whitebox_test.ww split above.
package regex_test;
import regex;
import strings;
// 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 => { assert(!((l: rune) != 'a')); };
case => abort();
};
let m: regex.inst = (true: regex.inst_match);
match (m) {
case let b: regex.inst_match => { assert(!(!(b: bool))); };
case => abort();
};
};
// 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 => { assert(!((s: size) != (5: size))); };
case let j: regex.inst_jump => abort();
case let g: regex.inst_groupstart => abort();
case => abort();
};
let jp: regex.inst = ((9: size): regex.inst_jump);
match (jp) {
case let j: regex.inst_jump => { assert(!((j: size) != (9: size))); };
case let s: regex.inst_split => abort();
case => abort();
};
let gs: regex.inst = ((2: size): regex.inst_groupstart);
match (gs) {
case let g: regex.inst_groupstart => { assert(!((g: size) != (2: size))); };
case let s: regex.inst_split => abort();
case => abort();
};
};
// 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 => abort();
case let e: regex.inst_groupend => abort();
case => abort();
};
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 => abort();
case => abort();
};
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 => abort();
case let k: regex.inst_skip => abort();
case => abort();
};
};
// inst_charset carries a struct payload; its fields survive the union
// round-trip.
@test fn charset_payload() void = {
let value: regex.inst_charset;
value.idx = 3;
value.is_positive = true;
let c: regex.inst = value;
match (c) {
case let cs: regex.inst_charset => {
assert(!(cs.idx != (3: size)));
assert(!(!cs.is_positive));
};
case => abort();
};
};
// 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 value: regex.inst_repeat;
value.id = 1;
value.origin = 4;
value.min = (2: size);
value.max = void;
let r: regex.inst = value;
match (r) {
case let rp: regex.inst_repeat => {
assert(!(rp.id != (1: size)));
assert(!(rp.origin != (4: size)));
};
case => abort();
};
};
// 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;
cap.content = "abc";
cap.start = 0;
cap.start_bytesize = 0;
cap.end = 3;
cap.end_bytesize = 3;
assert(!(cap.content.len != 3));
assert(!(cap.end != (3: size)));
// 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;
assert(!(re.n_reps != (0: size)));
assert(!(re.insts.len != 0));
regex.finish(&re);
};
// rule-7 divergence: white-box probes of regex internals; mislabeled _test pending task #9 (in-pkg test harness via #5/-T dir-enum, #8 boundary).
// 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 => {
assert(!(re.insts.len != 5));
match (re.insts[0]) {
case let k: regex.inst_skip => void;
case => abort();
};
match (re.insts[1]) {
case let l: regex.inst_lit => { assert(!((l: rune) != 'a')); };
case => abort();
};
match (re.insts[2]) {
case let l: regex.inst_lit => { assert(!((l: rune) != 'b')); };
case => abort();
};
match (re.insts[3]) {
case let l: regex.inst_lit => { assert(!((l: rune) != 'c')); };
case => abort();
};
match (re.insts[4]) {
case let m: regex.inst_match => { assert(!((m: bool))); };
case => abort();
};
assert(!(re.charsets.len != 0));
assert(!(re.n_reps != (0: size)));
regex.finish(&re);
};
case => abort();
};
};
// '.' 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 => {
assert(!(re.insts.len != 5));
match (re.insts[0]) {
case let k: regex.inst_skip => void;
case => abort();
};
match (re.insts[1]) {
case let l: regex.inst_lit => { assert(!((l: rune) != 'a')); };
case => abort();
};
match (re.insts[2]) {
case let a: regex.inst_any => void;
case => abort();
};
match (re.insts[3]) {
case let l: regex.inst_lit => { assert(!((l: rune) != 'c')); };
case => abort();
};
match (re.insts[4]) {
case let m: regex.inst_match => { assert(!((m: bool))); };
case => abort();
};
regex.finish(&re);
};
case => abort();
};
};
// 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 => {
assert(!(re.insts.len != 1));
match (re.insts[0]) {
case let m: regex.inst_match => { assert(!((m: bool))); };
case => abort();
};
regex.finish(&re);
};
case => abort();
};
};
// 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 capture value onto an alias-typed
// dst (#20); the alias + signature stay exercised by result_free itself.
// Reverts to `regex.result` when #20 lands.
@test fn result_free_noop() void = {
let res: []regex.capture;
let value: regex.capture;
value.content = "x";
value.start = 0;
value.start_bytesize = 0;
value.end = 1;
value.end_bytesize = 1;
append(res, value);
regex.result_free(res);
assert(!(len(res) != 1));
assert(!(res[0].end != (1: size)));
// 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);
assert(!(len(empty) != 0));
};
// 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) {
abort();
};
};
case => abort();
};
};
// test() (regex.ha:901-904) — the exported boolean surface over the
// same inputs the search table pins, plus the two void rows.
type tcase = struct {
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);
assert(!(!(tr is bool)));
assert(!((tr as bool) != rows[i].want));
regex.finish(&re);
};
case => abort();
};
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);
assert(!(!(fr is regex.result)));
let res: regex.result = fr as regex.result;
if (rows[i].matches) {
assert(!(len(res) != 1));
assert(!(res[0].start != rows[i].start));
if (res[0].start_bytesize != rows[i].sb) {
abort();
};
assert(!(res[0].end != rows[i].end));
if (res[0].end_bytesize != rows[i].eb) {
abort();
};
let wc: str = rows[i].content;
if (strings.compare(res[0].content, wc) != 0) {
abort();
};
} else {
assert(!(len(res) != 0));
};
// 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);
assert(!(!(tr is bool)));
assert(!((tr as bool) != (len(res) != 0)));
regex.result_free(res);
regex.finish(&re);
};
case => abort();
};
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);
assert(!(!(fr is []regex.result)));
let results: []regex.result = fr as []regex.result;
assert(!(len(results) != rows[i].tcnt));
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) {
abort();
};
k += 1;
};
regex.result_freeall(results);
regex.finish(&re);
};
case => abort();
};
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);
assert(!(!(fr is []regex.result)));
let results: []regex.result = fr as []regex.result;
assert(!(len(results) != rows[i].ecnt));
let k: i32 = 0;
for (k < rows[i].ecnt) {
let w: fdexp = exp[rows[i].eoff + k];
assert(!(results[k][0].start != w.start));
if (results[k][0].start_bytesize != w.sb) {
abort();
};
assert(!(results[k][0].end != w.end));
if (results[k][0].end_bytesize != w.eb) {
abort();
};
if (strings.compare(results[k][0].content,
w.content) != 0) {
abort();
};
k += 1;
};
regex.result_freeall(results);
regex.finish(&re);
};
case => abort();
};
i += 1;
};
};
// 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 => {
assert(!(re.insts.len != rows[i].scnt));
let k: i32 = 0;
for (k < rows[i].scnt) {
if (instsig(re.insts[k])
!= sigs[rows[i].soff + k]) {
abort();
};
k += 1;
};
regex.finish(&re);
};
case => abort();
};
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;
assert(!(strings.compare((e: str), w) != 0));
};
case => abort();
};
i += 1;
};
};
// 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);
assert(!(!(fr is regex.result)));
let res: regex.result = fr as regex.result;
if (rows[i].matches) {
assert(!(len(res) != 1));
assert(!(res[0].start != rows[i].start));
if (res[0].start_bytesize != rows[i].sb) {
abort();
};
assert(!(res[0].end != rows[i].end));
if (res[0].end_bytesize != rows[i].eb) {
abort();
};
let wc: str = rows[i].content;
if (strings.compare(res[0].content, wc) != 0) {
abort();
};
} else {
assert(!(len(res) != 0));
};
let tr: (bool | nomem) = regex.test(&re, inp);
assert(!(!(tr is bool)));
assert(!((tr as bool) != (len(res) != 0)));
regex.result_free(res);
regex.finish(&re);
};
case => abort();
};
i += 1;
};
};
// 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 => {
assert(!(re.insts.len != rows[i].scnt));
let k: i32 = 0;
for (k < rows[i].scnt) {
if (instsig(re.insts[k])
!= sigs[rows[i].soff + k]) {
abort();
};
k += 1;
};
regex.finish(&re);
};
case => abort();
};
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 as -2 (presence sentinel — fn-ptr address not stable).
// 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: [](regex.charset_lit_item | regex.charset_range_item |
regex.charset_class_item), k: size) i64 = {
let cur: (regex.charset_lit_item | regex.charset_range_item |
regex.charset_class_item) = cs[k];
match (cur) {
case let l: regex.charset_lit_item =>
return 1000000 + ((l: rune): i64);
case let range: regex.charset_range_item =>
return (range.0: i64) * 10000 + (range.1: i64);
case regex.charset_class_item => return -2; // fn-ptr address not stable
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 => {
assert(!(re.charsets.len != 1));
let cs0: [](regex.charset_lit_item | regex.charset_range_item |
regex.charset_class_item) = re.charsets[0];
assert(!((len(cs0): i32) != rows[i].ecnt));
let k: i32 = 0;
for (k < rows[i].ecnt) {
if (cssig(cs0, (k: size))
!= exp[rows[i].eoff + k]) {
abort();
};
k += 1;
};
regex.finish(&re);
};
case => abort();
};
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;
assert(!(strings.compare((e: str), w) != 0));
};
case => abort();
};
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);
assert(!(!(fr is regex.result)));
let res: regex.result = fr as regex.result;
if (rows[i].matches) {
assert(!(len(res) != 1));
assert(!(res[0].start != rows[i].start));
if (res[0].start_bytesize != rows[i].sb) {
abort();
};
assert(!(res[0].end != rows[i].end));
if (res[0].end_bytesize != rows[i].eb) {
abort();
};
let wc: str = rows[i].content;
if (strings.compare(res[0].content, wc) != 0) {
abort();
};
} else {
assert(!(len(res) != 0));
};
let tr: (bool | nomem) = regex.test(&re, inp);
assert(!(!(tr is bool)));
assert(!((tr as bool) != (len(res) != 0)));
regex.result_free(res);
regex.finish(&re);
};
case => abort();
};
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");
assert(!(!(fr is []regex.result)));
let results: []regex.result = fr as []regex.result;
assert(!(len(results) != 2));
assert(!(results[0][0].start != (2: size)));
assert(!(results[0][0].end != (4: size)));
if (strings.compare(results[0][0].content, "ab") != 0) {
abort();
};
assert(!(results[1][0].start != (6: size)));
assert(!(results[1][0].end != (8: size)));
if (strings.compare(results[1][0].content, "ba") != 0) {
abort();
};
regex.result_freeall(results);
regex.finish(&re);
};
case => abort();
};
};
// 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;
assert(!(strings.compare((e: str), w) != 0));
};
case => abort();
};
i += 1;
};
};
// 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);
assert(!(!(fr is regex.result)));
let res: regex.result = fr as regex.result;
if (rows[i].matches) {
assert(!((len(res): i32) != rows[i].ncaps));
assert(!(res[0].start != rows[i].start));
if (res[0].start_bytesize != rows[i].sb) {
abort();
};
assert(!(res[0].end != rows[i].end));
if (res[0].end_bytesize != rows[i].eb) {
abort();
};
let wc: str = rows[i].content;
if (strings.compare(res[0].content, wc) != 0) {
abort();
};
} else {
assert(!(len(res) != 0));
};
let tr: (bool | nomem) = regex.test(&re, inp);
assert(!(!(tr is bool)));
assert(!((tr as bool) != (len(res) != 0)));
regex.result_free(res);
regex.finish(&re);
};
case => abort();
};
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");
assert(!(!(fr is regex.result)));
let res: regex.result = fr as regex.result;
assert(!(len(res) != 2));
assert(!(res[0].start != (0: size)));
assert(!(res[0].end != (1: size)));
assert(!(strings.compare(res[0].content, "a") != 0));
regex.result_free(res);
regex.finish(&re);
};
case => abort();
};
};
// 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);
assert(!(!(fr is regex.result)));
let res: regex.result = fr as regex.result;
assert(!((len(res): i32) != rows[i].tcnt));
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) {
abort();
};
k += 1;
};
regex.result_free(res);
regex.finish(&re);
};
case => abort();
};
i += 1;
};
};
// 5b compile-error rows end-to-end through compile() — the two
// parse_repetition texts surface verbatim (+test.ha:461-463 ERROR
// rows). "a{" is the GRADUATED metachar-loud row: with `{` ported the
// deferred-metachar table empties — every metacharacter compiles, and
// the only loud surface left in lib/regex is the POSIX-class runtime
// abort (a fold-4 pin, not an error row). "a{" has no `}` in the
// rest, so it lands on the ha:491-493 syntax text.
@test fn fold5b_compile_errors() void = {
let rows: [4]cerow = [
cerow { pat = "^x(abc){-1,2}$",
want = "Negative repetition count '{-n}'" },
cerow { pat = "^x(abc){x,2}$",
want = "Repetition expression syntax error '{n}'" },
cerow { pat = "^x(abc){0,-2}$",
want = "Negative repetition count '{-n}'" },
cerow { pat = "a{",
want = "Repetition expression syntax error '{n}'" },
];
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;
assert(!(strings.compare((e: str), w) != 0));
};
case => abort();
};
i += 1;
};
};
// fold-5b find/test rows — Hare's own {m,n} matrix (+test.ha:443-460
// incl. the open-ended (0,7) row and the `{,0}de` pair), the `\{`/`\}`
// escape pair :437-438 (pins that ESCAPED braces never enter the `{`
// arm), the 5a carve-out :635 `(a|(b?|c*){,1}|d+|e)` come home, the
// `{0,}`/`{1,}`/`{0,1}` nested twins :649-665 (each must agree with
// its 5a `*`/`+`/`?` sibling's result on the same input — the
// cross-spelling invariant), and the "Various" composed rows :462-485
// minus the [[:class:]] row (POSIX class is a runtime loud abort, the
// fold-4 ruling — excluded until the POSIX fold). end == -1 resolves
// to rune length (+test.ha:693-697). The multibyte `{2}` row and the
// long-input `{1,}` thread/counter-explosion stress row are ww-added
// (B4 lesson; rob's rider). Cross-pins test() == (find() matched);
// the 5a/4/3 tables above re-run untouched in the same binary — the
// dedup/trim/leftmost regression net under the new repeat spawns.
@test fn fold5b_find_cases() void = {
let rows: [30]f5case = [
// {m,n} matrix (+test.ha:443-460)
f5case { expr = "^x(abc){2}$", input = "xabcabc",
matches = true, ncaps = 2, start = 0, sb = 0,
end = 7, eb = 7, content = "xabcabc" },
f5case { expr = "^x(abc){3}$", input = "xabcabc",
matches = false, ... },
f5case { expr = "^x(abc){1,2}$", input = "xabc",
matches = true, ncaps = 2, start = 0, sb = 0,
end = 4, eb = 4, content = "xabc" },
f5case { expr = "^x(abc){1,2}$", input = "xabcabc",
matches = true, ncaps = 2, start = 0, sb = 0,
end = 7, eb = 7, content = "xabcabc" },
f5case { expr = "^x(abc){1,2}$", input = "xabcabcabc",
matches = false, ... },
f5case { expr = "^x(abc){,2}$", input = "xabc",
matches = true, ncaps = 2, start = 0, sb = 0,
end = 4, eb = 4, content = "xabc" },
f5case { expr = "^x(abc){,2}$", input = "xabcabc",
matches = true, ncaps = 2, start = 0, sb = 0,
end = 7, eb = 7, content = "xabcabc" },
// the open-ended (0,7) row :450
f5case { expr = "^x(abc){,2}", input = "xabcabcabc",
matches = true, ncaps = 2, start = 0, sb = 0,
end = 7, eb = 7, content = "xabcabc" },
f5case { expr = "^x(abc){,0}de", input = "xde",
matches = true, ncaps = 2, start = 0, sb = 0,
end = 3, eb = 3, content = "xde" },
f5case { expr = "^x(abc){,0}de", input = "xe",
matches = false, ... },
f5case { expr = "^x(abc){,2}$", input = "xabcabcabc",
matches = false, ... },
f5case { expr = "^x(abc){1,}$", input = "xabc",
matches = true, ncaps = 2, start = 0, sb = 0,
end = 4, eb = 4, content = "xabc" },
f5case { expr = "^x(abc){1,}$", input = "xabcabc",
matches = true, ncaps = 2, start = 0, sb = 0,
end = 7, eb = 7, content = "xabcabc" },
f5case { expr = "^x(abc){3,}$", input = "xabcabc",
matches = false, ... },
f5case { expr = "^x(abc){3,}$", input = "xabcabcabc",
matches = true, ncaps = 2, start = 0, sb = 0,
end = 10, eb = 10, content = "xabcabcabc" },
f5case { expr = "^x(abc){2,2}$", input = "xabcabc",
matches = true, ncaps = 2, start = 0, sb = 0,
end = 7, eb = 7, content = "xabcabc" },
f5case { expr = "^x(abc){2,2}$", input = "xabc",
matches = false, ... },
f5case { expr = "^x(abc){2,2}$", input = "xabcabcabc",
matches = false, ... },
// escaped braces stay literal (+test.ha:437-438)
f5case { expr = "^x(abc)\\{,2\\}$", input = "xabc{,2}",
matches = true, ncaps = 2, start = 0, sb = 0,
end = 8, eb = 8, content = "xabc{,2}" },
f5case { expr = "^x(abc)\\{,2\\}$", input = "xabcabc{,2}",
matches = false, ... },
// the 5a carve-out comes home (+test.ha:635)
f5case { expr = "(a|(b?|c*){,1}|d+|e)", input = "e",
matches = true, ncaps = 3, start = 0, sb = 0,
end = 1, eb = 1, content = "e" },
// nested twins: each agrees with its 5a sibling
// (+test.ha:662-666: {0,} vs *, {1,} vs +, {0,1} vs ?)
f5case { expr = "(a+|b){0,}", input = "ab", matches = true,
ncaps = 2, start = 0, sb = 0, end = 2, eb = 2,
content = "ab" },
f5case { expr = "(a+|b){1,}", input = "ab", matches = true,
ncaps = 2, start = 0, sb = 0, end = 2, eb = 2,
content = "ab" },
f5case { expr = "(a+|b){0,1}", input = "ab", matches = true,
ncaps = 2, start = 0, sb = 0, end = 1, eb = 1,
content = "a" },
// "Various" composed rows (+test.ha:462-485; the M15 4QN
// [[:class:]] row is excluded — POSIX class is a runtime
// loud abort until the POSIX fold)
f5case { expr = "^.(1024)?(face)*(1024)*ca*(f+e?cafe)(babe)+$",
input = "X1024facefacecaaaaafffcafebabebabe",
matches = true, ncaps = 6, start = 0, sb = 0,
end = 34, eb = 34,
content = "X1024facefacecaaaaafffcafebabebabe" },
f5case { expr = ".(1024)?(face)*(1024)*ca*(f+e?cafe)(babe)+",
input = "X1024facefacecaaaaafffcafebabebabe",
matches = true, ncaps = 6, start = 0, sb = 0,
end = 34, eb = 34,
content = "X1024facefacecaaaaafffcafebabebabe" },
f5case { expr = "^.(1024)?(face)*(1024)*ca*(f+e?cafe)(babe)+$",
input = "1024facefacecaaaaafffcafebabebabe",
matches = false, ... },
f5case { expr = ".(1024)?(face)*(1024)*ca*(f+e?cafe)(babe)+",
input = "1024facefacecaaaaafffcafebabebabe",
matches = true, ncaps = 6, start = 3, sb = 3,
end = 33, eb = 33,
content = "4facefacecaaaaafffcafebabebabe" },
// ww-added multibyte repeat row (B4)
f5case { expr = "^(ä|b){2}x$", input = "äbx", matches = true,
ncaps = 2, start = 0, sb = 0, end = 3, eb = 4,
content = "äbx" },
// ww-added (rob's rider): long-input {1,} — thread/counter
// explosion stress under repeat spawns
f5case { expr = "(a+|b){1,}", input = "aaaabbbbaaaabbbb",
matches = true, ncaps = 2, start = 0, sb = 0,
end = 16, eb = 16, content = "aaaabbbbaaaabbbb" },
];
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);
assert(!(!(fr is regex.result)));
let res: regex.result = fr as regex.result;
if (rows[i].matches) {
assert(!((len(res): i32) != rows[i].ncaps));
assert(!(res[0].start != rows[i].start));
if (res[0].start_bytesize != rows[i].sb) {
abort();
};
assert(!(res[0].end != rows[i].end));
if (res[0].end_bytesize != rows[i].eb) {
abort();
};
let wc: str = rows[i].content;
if (strings.compare(res[0].content, wc) != 0) {
abort();
};
} else {
assert(!(len(res) != 0));
};
let tr: (bool | nomem) = regex.test(&re, inp);
assert(!(!(tr is bool)));
assert(!((tr as bool) != (len(res) != 0)));
regex.result_free(res);
regex.finish(&re);
};
case => abort();
};
i += 1;
};
};
// findall × repeat composition (+test.ha:724 `fo{2,}`) — the greedy
// per-position longest pick plus the non-overlap advance over a
// repeat-counted program.
@test fn fold5b_findall() void = {
let targets: [4]str = ["foo", "fooo", "foo", "foo"];
let rows: [1]facase = [
facase { expr = "fo{2,}", input = "fo foo fooofoof oofoo",
toff = 0, 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);
assert(!(!(fr is []regex.result)));
let results: []regex.result = fr as []regex.result;
assert(!(len(results) != rows[i].tcnt));
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) {
abort();
};
k += 1;
};
regex.result_freeall(results);
regex.finish(&re);
};
case => abort();
};
i += 1;
};
};
// compile error: `[[:` with no valid class name
// ref/hare/regex/+test.ha — no direct cite; error string from
// regex.ha:203 "No character class after '[:'".
@test fn fold6_compile_errors() void = {
let rows: [1]cerow = [
cerow { pat = "[[:xyz", want = "No character class after '[:'" },
];
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;
assert(!(strings.compare((e: str), w) != 0));
};
case => abort();
};
i += 1;
};
};
// charsets-table content pin: a POSIX class bracket emits ONE
// charset_class_item element (cssig → -2), not a literal expansion.
// ref/hare/regex/+test.ha:305-308 POSIX class rows.
@test fn fold6_charsets() void = {
// "[[:digit:]]" → 1 charset, 1 class elem
let c: (regex.regex | regex.error | nomem) = regex.compile("[[:digit:]]");
match (c) {
case let re: regex.regex => {
assert(!(re.charsets.len != 1));
let cs0: [](regex.charset_lit_item | regex.charset_range_item |
regex.charset_class_item) = re.charsets[0];
assert(!((len(cs0): i32) != 1));
assert(!(cssig(cs0, 0) != -2));
regex.finish(&re);
};
case => abort();
};
};
// end-to-end find/test rows for all 12 POSIX classes + negation.
// ref/hare/regex/+test.ha:278-345 POSIX subset.
@test fn fold6_find_cases() void = {
let rows: [28]fcase = [
// digit
fcase { expr = "[[:digit:]]", input = "5", matches = true,
start = 0, sb = 0, end = 1, eb = 1, content = "5" },
fcase { expr = "[[:digit:]]", input = "a", matches = false, ... },
// alpha
fcase { expr = "^[[:alpha:]]+$", input = "abc", matches = true,
start = 0, sb = 0, end = 3, eb = 3, content = "abc" },
fcase { expr = "^[[:alpha:]]+$", input = "abc1", matches = false, ... },
// alnum
fcase { expr = "^[[:alnum:]]+$", input = "abc9", matches = true,
start = 0, sb = 0, end = 4, eb = 4, content = "abc9" },
fcase { expr = "^[[:alnum:]]+$", input = "abc!", matches = false, ... },
// space
fcase { expr = "[[:space:]]", input = " ", matches = true,
start = 0, sb = 0, end = 1, eb = 1, content = " " },
fcase { expr = "[[:space:]]", input = "a", matches = false, ... },
// upper
fcase { expr = "^[[:upper:]]+$", input = "ABC", matches = true,
start = 0, sb = 0, end = 3, eb = 3, content = "ABC" },
fcase { expr = "^[[:upper:]]+$", input = "ABc", matches = false, ... },
// lower
fcase { expr = "^[[:lower:]]+$", input = "abc", matches = true,
start = 0, sb = 0, end = 3, eb = 3, content = "abc" },
fcase { expr = "^[[:lower:]]+$", input = "abC", matches = false, ... },
// xdigit
fcase { expr = "^[[:xdigit:]]+$", input = "0aF", matches = true,
start = 0, sb = 0, end = 3, eb = 3, content = "0aF" },
fcase { expr = "^[[:xdigit:]]+$", input = "0g", matches = false, ... },
// blank (space or tab)
fcase { expr = "[[:blank:]]", input = "\t", matches = true,
start = 0, sb = 0, end = 1, eb = 1, content = "\t" },
fcase { expr = "[[:blank:]]", input = "a", matches = false, ... },
// punct
fcase { expr = "[[:punct:]]", input = ".", matches = true,
start = 0, sb = 0, end = 1, eb = 1, content = "." },
fcase { expr = "[[:punct:]]", input = "a", matches = false, ... },
// graph
fcase { expr = "[[:graph:]]", input = "!", matches = true,
start = 0, sb = 0, end = 1, eb = 1, content = "!" },
fcase { expr = "[[:graph:]]", input = " ", matches = false, ... },
// print
fcase { expr = "[[:print:]]", input = " ", matches = true,
start = 0, sb = 0, end = 1, eb = 1, content = " " },
fcase { expr = "[[:print:]]", input = "\x01", matches = false, ... },
// cntrl
fcase { expr = "[[:cntrl:]]", input = "\x01", matches = true,
start = 0, sb = 0, end = 1, eb = 1, content = "\x01" },
fcase { expr = "[[:cntrl:]]", input = "a", matches = false, ... },
// negated class
fcase { expr = "^[^[:digit:]]+$", input = "abc", matches = true,
start = 0, sb = 0, end = 3, eb = 3, content = "abc" },
fcase { expr = "^[^[:digit:]]+$", input = "ab5", matches = false, ... },
// composition: digit+ in surrounding text
fcase { expr = "[[:digit:]]+", input = "abc123def",
matches = true, start = 3, sb = 3, end = 6, eb = 6,
content = "123" },
fcase { expr = "[[:digit:]]+", input = "nodigits",
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);
assert(!(!(fr is regex.result)));
let res: regex.result = fr as regex.result;
if (rows[i].matches) {
assert(!(len(res) != 1));
assert(!(res[0].start != rows[i].start));
if (res[0].start_bytesize != rows[i].sb) {
abort();
};
assert(!(res[0].end != rows[i].end));
if (res[0].end_bytesize != rows[i].eb) {
abort();
};
let wc: str = rows[i].content;
if (strings.compare(res[0].content, wc) != 0) {
abort();
};
} else {
assert(!(len(res) != 0));
};
let tr: (bool | nomem) = regex.test(&re, inp);
assert(!(!(tr is bool)));
assert(!((tr as bool) != (len(res) != 0)));
regex.result_free(res);
regex.finish(&re);
};
case => abort();
};
i += 1;
};
};