'ww test' gains the istest build path (-T injection in build_one/ buildone) and do_test/dotest accept -I, mirroring do_run - both twins. The 35 converted lib tests drop their interim bare mains (-T synthesizes the entry from @test fns and rejects a user main); their 35 C run-drivers flip 'ww run' -> 'ww test'; 989_lib_byteid compiles lib tests under -T (8 user-main probe fixtures stay non-T, gated on the fixture field). Abort-on-first-failure stands until the deferred record-and-continue harness lands with the multi-package arc.
2571 lines
95 KiB
Plaintext
2571 lines
95 KiB
Plaintext
// 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.
|
||
// 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).
|
||
//
|
||
// 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_test;
|
||
|
||
import regex;
|
||
import io;
|
||
import memio;
|
||
import strings;
|
||
import types;
|
||
|
||
|
||
// 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 c: regex.inst = (inst_charset { idx = 3, is_positive = true });
|
||
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 r: regex.inst = (inst_repeat {
|
||
id = 1, origin = 4, min = (2: size), max = void,
|
||
});
|
||
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 = capture {
|
||
content = "abc",
|
||
start = 0,
|
||
start_bytesize = 0,
|
||
end = 3,
|
||
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();
|
||
};
|
||
};
|
||
|
||
// The thread struct (regex.ha:55-64) is in tree ahead of its engine
|
||
// consumers so the pending #15/#17 fix probes exercise the real type.
|
||
// Pin the field layout via the P6-proven wide-literal append + a
|
||
// depth-1 read-back row per appended thread; the Hare `...` partial
|
||
// fill (P5) must zero everything the second row's literal omits.
|
||
// root_capture has NO row: every read route into it is
|
||
// compiler-blocked today — the depth-2 chain behind the index links
|
||
// the field as a global (#6 F4), the element let-copy is #7 F5, and
|
||
// a probed `&threads[i].root_capture` deref segfaults byte-id on
|
||
// both stages — so its row lands with those fixes.
|
||
type texp = struct {
|
||
pc: size,
|
||
start_idx: size,
|
||
start_bytesize: size,
|
||
matched: bool,
|
||
failed: bool,
|
||
// .len reads as i32 (check.c:1239), so the count columns match it
|
||
ncaps: i32,
|
||
nreps: i32,
|
||
};
|
||
|
||
@test fn thread_shape() void = {
|
||
let rc: capture = capture {
|
||
content = "ab", start = 1, start_bytesize = 1,
|
||
end = 2, end_bytesize = 2,
|
||
};
|
||
let pcaps: []capture = [];
|
||
append(pcaps, rc);
|
||
let prep: []size = [];
|
||
append(prep, (7: size));
|
||
let threads: []thread = [];
|
||
append(threads, thread {
|
||
pc = 5,
|
||
start_idx = 6,
|
||
start_bytesize = 7,
|
||
root_capture = rc,
|
||
captures = pcaps,
|
||
rep_counters = prep,
|
||
matched = false,
|
||
failed = true,
|
||
});
|
||
append(threads, thread { pc = 9, ... });
|
||
let want: [2]texp = [
|
||
texp { pc = 5, start_idx = 6, start_bytesize = 7,
|
||
matched = false, failed = true,
|
||
ncaps = 1, nreps = 1 },
|
||
texp { pc = 9, start_idx = 0, start_bytesize = 0,
|
||
matched = false, failed = false,
|
||
ncaps = 0, nreps = 0 },
|
||
];
|
||
assert(!(len(threads) != len(want)));
|
||
let i: i32 = 0;
|
||
for (i < len(want)) {
|
||
assert(!(threads[i].pc != want[i].pc));
|
||
assert(!(threads[i].start_idx != want[i].start_idx));
|
||
if (threads[i].start_bytesize != want[i].start_bytesize) {
|
||
abort();
|
||
};
|
||
assert(!(threads[i].matched != want[i].matched));
|
||
assert(!(threads[i].failed != want[i].failed));
|
||
assert(!(threads[i].captures.len != want[i].ncaps));
|
||
assert(!(threads[i].rep_counters.len != want[i].nreps));
|
||
i += 1;
|
||
};
|
||
};
|
||
|
||
// newmatch (regex.ha:66) must discriminate nominally against plain
|
||
// void — and against nomem, the third payload-free member — across
|
||
// run_thread's (void | newmatch | nomem) return boundary: the P8
|
||
// shape on the real lib type, one row per returned member.
|
||
fn nm_probe(x: i32) (void | newmatch | nomem) = {
|
||
if (x == 1) {
|
||
let nm: newmatch;
|
||
return nm;
|
||
};
|
||
if (x == 2) {
|
||
let n: nomem;
|
||
return n;
|
||
};
|
||
return;
|
||
};
|
||
|
||
type nmexp = struct {
|
||
arg: i32,
|
||
want_nm: bool,
|
||
want_void: bool,
|
||
want_nomem: bool,
|
||
};
|
||
|
||
@test fn newmatch_discriminates() void = {
|
||
let rows: [3]nmexp = [
|
||
nmexp { arg = 1, want_nm = true, want_void = false,
|
||
want_nomem = false },
|
||
nmexp { arg = 0, want_nm = false, want_void = true,
|
||
want_nomem = false },
|
||
nmexp { arg = 2, want_nm = false, want_void = false,
|
||
want_nomem = true },
|
||
];
|
||
let i: i32 = 0;
|
||
for (i < len(rows)) {
|
||
let r: (void | newmatch | nomem) = nm_probe(rows[i].arg);
|
||
assert(!((r is newmatch) != rows[i].want_nm));
|
||
assert(!((r is void) != rows[i].want_void));
|
||
assert(!((r is nomem) != rows[i].want_nomem));
|
||
i += 1;
|
||
};
|
||
};
|
||
|
||
// 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);
|
||
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();
|
||
};
|
||
};
|
||
|
||
// 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 = {
|
||
assert(!(is_consuming_inst(v) != want));
|
||
};
|
||
|
||
@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);
|
||
assert(!(len(ts) != 2));
|
||
assert(!(ts[0].pc != (1: size)));
|
||
assert(!(ts[0].start_idx != (11: size)));
|
||
assert(!(ts[0].captures.len != 1));
|
||
assert(!(ts[1].pc != (3: size)));
|
||
assert(!(ts[1].start_idx != (33: size)));
|
||
assert(!(ts[1].captures.len != 0));
|
||
// boundary rows: delete at the last index, then at index 0 down
|
||
// to empty — the failed-sweep loop (regex.ha:891-896) deletes at
|
||
// every position including both ends.
|
||
delete_thread(1, &ts);
|
||
assert(!(len(ts) != 1));
|
||
assert(!(ts[0].pc != (1: size)));
|
||
delete_thread(0, &ts);
|
||
assert(!(len(ts) != 0));
|
||
};
|
||
|
||
// add_thread (regex.ha:557-587): same-pc dedup suppression fires only
|
||
// when the existing thread is unmatched AND started strictly earlier
|
||
// than the parent (ha:561-565); otherwise the child appends,
|
||
// inheriting the parent's start/matched/failed with DUPLICATED
|
||
// capture/rep_counter slices (empty parent → empty dup, ha:569/572)
|
||
// and a zeroed root_capture.
|
||
@test fn add_thread_dedup_inherit() void = {
|
||
let ts: []thread = [];
|
||
append(ts, thread { pc = 0, start_idx = 5, start_bytesize = 4,
|
||
matched = false, failed = true, ... });
|
||
// inherit: fresh pc, parent fields copied, rest zeroed
|
||
let r: (void | nomem) = add_thread(&ts, 0, 7);
|
||
assert(!(!(r is void)));
|
||
assert(!(len(ts) != 2));
|
||
assert(!(ts[1].pc != (7: size)));
|
||
assert(!(ts[1].start_idx != (5: size)));
|
||
assert(!(ts[1].start_bytesize != (4: size)));
|
||
assert(!(ts[1].matched));
|
||
assert(!(!ts[1].failed));
|
||
assert(!(ts[1].captures.len != 0));
|
||
assert(!(ts[1].rep_counters.len != 0));
|
||
assert(!(ts[1].root_capture.content.len != 0));
|
||
assert(!(ts[1].root_capture.end != (0: size)));
|
||
// same-pc same-start does NOT suppress (strict <, ha:563-565)
|
||
let r2: (void | nomem) = add_thread(&ts, 0, 7);
|
||
assert(!(!(r2 is void)));
|
||
assert(!(len(ts) != 3));
|
||
// an earlier-started unmatched existing thread DOES suppress
|
||
let ts2: []thread = [];
|
||
append(ts2, thread { pc = 0, start_idx = 5, ... });
|
||
append(ts2, thread { pc = 7, start_idx = 2, ... });
|
||
let r3: (void | nomem) = add_thread(&ts2, 0, 7);
|
||
assert(!(!(r3 is void)));
|
||
assert(!(len(ts2) != 2));
|
||
// a MATCHED existing thread never suppresses
|
||
let ts3: []thread = [];
|
||
append(ts3, thread { pc = 0, start_idx = 5, ... });
|
||
append(ts3, thread { pc = 7, start_idx = 2, matched = true, ... });
|
||
let r4: (void | nomem) = add_thread(&ts3, 0, 7);
|
||
assert(!(!(r4 is void)));
|
||
assert(!(len(ts3) != 3));
|
||
assert(!(ts3[2].pc != (7: size)));
|
||
assert(!(ts3[2].start_idx != (5: size)));
|
||
};
|
||
|
||
// add_thread dup (regex.ha:568-573): the child carries a COPY of the
|
||
// parent's captures/rep_counters — values equal, backing independent
|
||
// in both directions (mutate parent → child unchanged, mutate child →
|
||
// parent unchanged). Empty parent → empty dup (the pre-flip rows above
|
||
// stay byte-for-byte). Driven directly, the dedup-test precedent.
|
||
@test fn add_thread_dup_independence() void = {
|
||
let caps: []capture = [];
|
||
append(caps, capture {
|
||
content = "ab", start = 1, start_bytesize = 1,
|
||
end = 2, end_bytesize = 2,
|
||
});
|
||
append(caps, capture {
|
||
content = "c", start = 3, start_bytesize = 3,
|
||
end = 4, end_bytesize = 4,
|
||
});
|
||
let reps: []size = [];
|
||
append(reps, (5: size));
|
||
append(reps, (6: size));
|
||
let ts: []thread = [];
|
||
append(ts, thread { pc = 0, start_idx = 1, captures = caps,
|
||
rep_counters = reps, ... });
|
||
let r: (void | nomem) = add_thread(&ts, 0, 9);
|
||
assert(!(!(r is void)));
|
||
assert(!(len(ts) != 2));
|
||
// dup carried the parent's values
|
||
assert(!(ts[1].captures.len != 2));
|
||
assert(!(strings.compare(ts[1].captures[0].content, "ab") != 0));
|
||
assert(!(ts[1].captures[0].start != (1: size)));
|
||
assert(!(ts[1].captures[1].end != (4: size)));
|
||
assert(!(ts[1].rep_counters.len != 2));
|
||
assert(!(ts[1].rep_counters[0] != (5: size)));
|
||
assert(!(ts[1].rep_counters[1] != (6: size)));
|
||
// independence, parent → child: mutate the parent post-add
|
||
ts[0].captures[0].start = 100;
|
||
ts[0].captures[0].content = "zz";
|
||
ts[0].rep_counters[0] = 77;
|
||
assert(!(ts[1].captures[0].start != (1: size)));
|
||
assert(!(strings.compare(ts[1].captures[0].content, "ab") != 0));
|
||
assert(!(ts[1].rep_counters[0] != (5: size)));
|
||
// independence, child → parent
|
||
ts[1].captures[1].end = 200;
|
||
ts[1].rep_counters[1] = 88;
|
||
assert(!(ts[0].captures[1].end != (4: size)));
|
||
assert(!(ts[0].rep_counters[1] != (6: size)));
|
||
// empty parent → empty dup
|
||
let ts2: []thread = [];
|
||
append(ts2, thread { pc = 0, ... });
|
||
let r2: (void | nomem) = add_thread(&ts2, 0, 3);
|
||
assert(!(!(r2 is void)));
|
||
assert(!(ts2[1].captures.len != 0));
|
||
assert(!(ts2[1].rep_counters.len != 0));
|
||
};
|
||
|
||
// run_thread (regex.ha:589-742) driven directly over compile("ab")'s
|
||
// real program [skip, lit 'a', lit 'b', match(false)] — the arms
|
||
// fold-2a can emit. Phases: parked-skip spawn (len 1→2, parent pc
|
||
// unmoved — the unanchored-restart engine), lit advance, lit
|
||
// mismatch (failed=true AND pc still steps — ha:741 runs regardless
|
||
// of the arm's verdict), EOF on a consuming pc (failed, pc frozen),
|
||
// match arm (root_capture spans start_bytesize..str_bytesize +
|
||
// matched + `is newmatch`), and the matched-thread early return
|
||
// (ha:599-601).
|
||
@test fn run_thread_literal_program() void = {
|
||
// typed-let + match receive, the compile_literal_program shape
|
||
let c: (regex.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);
|
||
assert(!(!(r1 is void)));
|
||
assert(!(len(ts) != 2));
|
||
assert(!(ts[0].pc != (0: size)));
|
||
assert(!(ts[1].pc != (1: size)));
|
||
assert(!(ts[1].failed));
|
||
|
||
// lit match advances pc past 'a'
|
||
let r2: (void | newmatch | nomem) = run_thread(1, &re, "ab", &ts, ra, 0, 0);
|
||
assert(!(!(r2 is void)));
|
||
assert(!(ts[1].pc != (2: size)));
|
||
assert(!(ts[1].failed));
|
||
|
||
// lit mismatch fails the thread; pc steps anyway (ha:741)
|
||
let rx: (rune | io.eof) = 'x';
|
||
let r3: (void | newmatch | nomem) = run_thread(1, &re, "ab", &ts, rx, 1, 1);
|
||
assert(!(!(r3 is void)));
|
||
assert(!(!ts[1].failed));
|
||
assert(!(ts[1].pc != (3: size)));
|
||
|
||
// EOF on a consuming pc fails the thread before pc steps
|
||
let ev: io.eof;
|
||
let reof: (rune | io.eof) = ev;
|
||
let ts2: []thread = [];
|
||
append(ts2, thread { pc = 1, ... });
|
||
let r4: (void | newmatch | nomem) = run_thread(0, &re, "ab", &ts2, reof, 2, 2);
|
||
assert(!(!(r4 is void)));
|
||
assert(!(!ts2[0].failed));
|
||
assert(!(ts2[0].pc != (1: size)));
|
||
|
||
// match arm: root_capture spans start_bytesize..str_bytesize,
|
||
// matched set, newmatch returned
|
||
let ts3: []thread = [];
|
||
append(ts3, thread { pc = 3, ... });
|
||
let r5: (void | newmatch | nomem) = run_thread(0, &re, "ab", &ts3, reof, 2, 2);
|
||
assert(!(!(r5 is newmatch)));
|
||
assert(!(!ts3[0].matched));
|
||
assert(!(ts3[0].failed));
|
||
assert(!(ts3[0].root_capture.start != (0: size)));
|
||
assert(!(ts3[0].root_capture.start_bytesize != (0: size)));
|
||
assert(!(ts3[0].root_capture.end != (2: size)));
|
||
assert(!(ts3[0].root_capture.end_bytesize != (2: size)));
|
||
assert(!(strings.compare(ts3[0].root_capture.content, "ab") != 0));
|
||
|
||
// an already-matched thread is inert (ha:599-601): void
|
||
// return, state untouched
|
||
let r6: (void | newmatch | nomem) = run_thread(0, &re, "ab", &ts3, ra, 3, 3);
|
||
assert(!(!(r6 is void)));
|
||
assert(!(ts3[0].root_capture.end != (2: size)));
|
||
|
||
// idx/bytesize split: every all-ASCII row has idx ==
|
||
// bytesize, so a port swapping start/start_bytesize (or
|
||
// end/end_bytesize) in root_capture passes them. One 2-byte
|
||
// rune ('ß') consumed before the match start makes all four
|
||
// values distinct: start=1 start_bytesize=2 end=3
|
||
// end_bytesize=4; content = bytes[2:4] = "ab".
|
||
let ts4: []thread = [];
|
||
append(ts4, thread { pc = 3, start_idx = 1, start_bytesize = 2, ... });
|
||
let r7: (void | newmatch | nomem) = run_thread(0, &re, "ßab", &ts4, reof, 3, 4);
|
||
assert(!(!(r7 is newmatch)));
|
||
assert(!(ts4[0].root_capture.start != (1: size)));
|
||
assert(!(ts4[0].root_capture.start_bytesize != (2: size)));
|
||
assert(!(ts4[0].root_capture.end != (3: size)));
|
||
assert(!(ts4[0].root_capture.end_bytesize != (4: size)));
|
||
assert(!(strings.compare(ts4[0].root_capture.content, "ab") != 0));
|
||
regex.finish(&re);
|
||
};
|
||
case => abort();
|
||
};
|
||
};
|
||
|
||
// The anchored route (ha:621-624) needs a (true: inst_match) program
|
||
// — compile() can't emit `$` yet, so it is HAND-BUILT — pinned from
|
||
// both sides: anchored + string-not-exhausted fails the thread;
|
||
// anchored + EOF falls through to the match (empty content).
|
||
@test fn run_thread_anchored_route() void = {
|
||
let insts: []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);
|
||
assert(!(!(r1 is void)));
|
||
assert(!(!ts[0].failed));
|
||
assert(!(ts[0].matched));
|
||
|
||
let ev: io.eof;
|
||
let reof: (rune | io.eof) = ev;
|
||
let ts2: []thread = [];
|
||
append(ts2, thread { pc = 0, ... });
|
||
let r2: (void | newmatch | nomem) = run_thread(0, &re, "", &ts2, reof, 0, 0);
|
||
assert(!(!(r2 is newmatch)));
|
||
assert(!(!ts2[0].matched));
|
||
assert(!(ts2[0].root_capture.content.len != 0));
|
||
assert(!(ts2[0].root_capture.end != (0: size)));
|
||
};
|
||
|
||
// search (regex.ha:746-898) driven DIRECTLY (private fn, package-regex
|
||
// test) over memio-backed streams — the exec surface (test/find) is
|
||
// tranche D. Each match row pins the root capture's four indices plus
|
||
// content; the multibyte row keeps idx != bytesize honest. Rows share
|
||
// (expr, input, need_captures, want) shape — the P12 struct-row table.
|
||
type scase = struct {
|
||
expr: str,
|
||
input: str,
|
||
nc: bool,
|
||
start: size,
|
||
sb: size,
|
||
end: size,
|
||
eb: size,
|
||
content: str,
|
||
};
|
||
|
||
@test fn search_matches() void = {
|
||
let rows: [6]scase = [
|
||
// full match mid-string: skip-respawn + dispatch +
|
||
// all_matched exit
|
||
scase { expr = "ab", input = "xab", nc = true,
|
||
start = 1, sb = 1, end = 3, eb = 3, content = "ab" },
|
||
// mismatch-restart: the idx-0 child fails and is swept; the
|
||
// restarted thread wins (failed-sweep interplay)
|
||
scase { expr = "bcd", input = "abcd", nc = true,
|
||
start = 1, sb = 1, end = 4, eb = 4, content = "bcd" },
|
||
// leftmost-longest best-pick + first_match_idx trim
|
||
scase { expr = "aa", input = "aaa", nc = true,
|
||
start = 0, sb = 0, end = 2, eb = 2, content = "aa" },
|
||
// zero-length: the all_matched path with matchlen 0 must
|
||
// NOT take the need_captures=false early-exit (ha:845
|
||
// requires matchlen > 0) — hence nc=false expecting the
|
||
// FULL one-capture result, not the empty early-exit slice
|
||
scase { expr = "", input = "", nc = false,
|
||
start = 0, sb = 0, end = 0, eb = 0, content = "" },
|
||
// multibyte: the 2-byte ß before the match start splits
|
||
// every idx from its bytesize; inst_any consumes 'x'
|
||
scase { expr = "b.d", input = "aßbxd", nc = true,
|
||
start = 2, sb = 3, end = 5, eb = 6, content = "bxd" },
|
||
// dedup-heavy: same-pc threads spawn on every step across
|
||
// >=3 passes (ha:872-889); the pick must stay stable.
|
||
// Result stability is the only external pin available this
|
||
// fold: 2a programs are all fixed-length, every match ties
|
||
// on match_len, and best-pick's insertion-order tiebreak
|
||
// alone yields leftmost — so the dedup sweep and the
|
||
// leftmost trim are result-invisible (mutation-verified:
|
||
// disabling either still passes this table; disabling the
|
||
// failed sweep hangs). Both turn result- and
|
||
// termination-visible with the split/star fold.
|
||
scase { expr = "aa", input = "aaaa", nc = true,
|
||
start = 0, sb = 0, end = 2, eb = 2, content = "aa" },
|
||
];
|
||
let i: i32 = 0;
|
||
for (i < len(rows)) {
|
||
let ex: str = rows[i].expr;
|
||
let inp: str = rows[i].input;
|
||
let c: (regex.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);
|
||
assert(!(!(r is []capture)));
|
||
let caps: []capture = r as []capture;
|
||
assert(!(len(caps) != 1));
|
||
assert(!(caps[0].start != rows[i].start));
|
||
assert(!(caps[0].start_bytesize != rows[i].sb));
|
||
assert(!(caps[0].end != rows[i].end));
|
||
assert(!(caps[0].end_bytesize != rows[i].eb));
|
||
let wc: str = rows[i].content;
|
||
if (strings.compare(caps[0].content, wc) != 0) {
|
||
abort();
|
||
};
|
||
regex.result_free(caps);
|
||
regex.finish(&re);
|
||
};
|
||
case => abort();
|
||
};
|
||
i += 1;
|
||
};
|
||
};
|
||
|
||
// ha:845-847: a non-zero-length newmatch with need_captures=false
|
||
// returns the empty result immediately, skipping the best-pick pass.
|
||
@test fn search_early_exit() void = {
|
||
let c: (regex.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);
|
||
assert(!(!(r is []capture)));
|
||
let caps: []capture = r as []capture;
|
||
assert(!(len(caps) != 0));
|
||
regex.result_free(caps);
|
||
regex.finish(&re);
|
||
};
|
||
case => abort();
|
||
};
|
||
};
|
||
|
||
// void rows: no match anywhere ("ab" over "xyz" — every thread fails,
|
||
// the list drains, ha:777-779) and EOF mid-pattern ("ab" over "a" —
|
||
// the consuming-inst EOF fail).
|
||
@test fn search_no_match() void = {
|
||
let c: (regex.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);
|
||
assert(!(!(r is void)));
|
||
let strm2: memio.stream = memio.fixed(strings.toutf8("a"));
|
||
let r2: (void | []capture | nomem) =
|
||
search(&re, "a", &strm2.vt, true);
|
||
assert(!(!(r2 is void)));
|
||
regex.finish(&re);
|
||
};
|
||
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;
|
||
};
|
||
};
|
||
|
||
|
||
// ---- 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 => {
|
||
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;
|
||
};
|
||
};
|
||
|
||
// 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) {
|
||
abort();
|
||
};
|
||
};
|
||
case => abort();
|
||
};
|
||
append(insts, ((1: size): regex.inst_groupstart));
|
||
append(insts, ('b': regex.inst_lit));
|
||
match (find_last_groupstart(insts)) {
|
||
case let sz: size => { assert(!(sz != 1)); };
|
||
case => abort();
|
||
};
|
||
};
|
||
|
||
// 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:]);
|
||
assert(!(instsig(insts[0]) != 5003));
|
||
assert(!(instsig(insts[1]) != 1097));
|
||
assert(!(instsig(insts[2]) != 4006));
|
||
assert(!(instsig(insts[3]) != 5008));
|
||
};
|
||
|
||
// 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;
|
||
};
|
||
};
|
||
|
||
// ---- 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 => {
|
||
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: [](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 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: [](charset_lit_item | charset_range_item |
|
||
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();
|
||
};
|
||
};
|
||
|
||
// ---- 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;
|
||
assert(!(strings.compare((e: str), w) != 0));
|
||
};
|
||
case => abort();
|
||
};
|
||
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);
|
||
assert(!(!(r1 is void)));
|
||
assert(!(ts[0].captures.len != 2));
|
||
// the fill element below idx is zeroed
|
||
assert(!(ts[0].captures[0].end != (0: size)));
|
||
assert(!(ts[0].captures[0].content.len != 0));
|
||
// the opened group: start stamped, end still the open sentinel
|
||
assert(!(ts[0].captures[1].start != (2: size)));
|
||
assert(!(ts[0].captures[1].start_bytesize != (3: size)));
|
||
assert(!(ts[0].captures[1].end != types.SIZE_MAX));
|
||
assert(!(ts[0].captures[1].end_bytesize != types.SIZE_MAX));
|
||
// groupstart is non-consuming: pc stepped through it, then the
|
||
// lit consumed
|
||
assert(!(ts[0].pc != (2: size)));
|
||
|
||
// groupend (ha:653-668): two open groups — the INNERMOST
|
||
// (highest index) closes first; back-to-back groupends close
|
||
// inner then outer in ONE call; content = str_bytes[
|
||
// start_bytesize:end_bytesize]
|
||
let insts2: []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);
|
||
assert(!(!(r2 is void)));
|
||
assert(!(ts2[0].captures[1].end != (3: size)));
|
||
assert(!(ts2[0].captures[1].end_bytesize != (4: size)));
|
||
assert(!(strings.compare(ts2[0].captures[1].content, "cd") != 0));
|
||
assert(!(ts2[0].captures[0].end != (3: size)));
|
||
assert(!(ts2[0].captures[0].end_bytesize != (4: size)));
|
||
assert(!(strings.compare(ts2[0].captures[0].content, "bcd") != 0));
|
||
assert(!(ts2[0].pc != (3: size)));
|
||
|
||
// closed-group re-entry: groupstart over an already-CLOSED idx
|
||
// passes the ha:642 assert (end != SIZE_MAX) and re-opens fresh
|
||
let insts3: []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);
|
||
assert(!(!(r3 is void)));
|
||
assert(!(ts3[0].captures.len != 1));
|
||
assert(!(ts3[0].captures[0].start != (2: size)));
|
||
assert(!(ts3[0].captures[0].end != types.SIZE_MAX));
|
||
assert(!(ts3[0].captures[0].content.len != 0));
|
||
};
|
||
|
||
// 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;
|
||
};
|
||
};
|
||
|
||
// ---- fold 5b: repetition ----------------------------------------------
|
||
|
||
// parse_repetition rows (regex.ha:486-545) — DIRECT private-fn table
|
||
// (the leaf fn lands ahead of its `{`-arm consumer; tranche-A
|
||
// precedent). Expectations hand-executed from Hare's own code paths:
|
||
// the input is everything AFTER `{` (compile passes iterstr's rest),
|
||
// single-arg `{n}` sets max = min and replen = len(n) (ha:500-503,
|
||
// 539-541); two-arg replen = len(min) + 1 + len(max) (ha:543); an
|
||
// empty min is 0 (ha:524) while an empty max stays void (ha:527 —
|
||
// the `{n,}` open bound); a `,` BEYOND the first `}` is not a
|
||
// two-arg form (ha:499). The two error texts are byte-exact
|
||
// (ha:492/521/531-536). max_void distinguishes "expect void" from
|
||
// "expect maxv"; err != "" rows expect that exact error.
|
||
type prrow = struct {
|
||
input: str,
|
||
minv: size,
|
||
maxv: size,
|
||
max_void: bool,
|
||
replen: size,
|
||
err: str,
|
||
};
|
||
|
||
@test fn parse_repetition_cases() void = {
|
||
let rows: [13]prrow = [
|
||
prrow { input = "2}", minv = 2, maxv = 2, max_void = false,
|
||
replen = 1, err = "" },
|
||
prrow { input = "2}$", minv = 2, maxv = 2, max_void = false,
|
||
replen = 1, err = "" },
|
||
// comma AFTER the endbrace — still single-arg (ha:499)
|
||
prrow { input = "2},5", minv = 2, maxv = 2, max_void = false,
|
||
replen = 1, err = "" },
|
||
prrow { input = "1,2}", minv = 1, maxv = 2, max_void = false,
|
||
replen = 3, err = "" },
|
||
prrow { input = ",2}", minv = 0, maxv = 2, max_void = false,
|
||
replen = 2, err = "" },
|
||
prrow { input = ",0}", minv = 0, maxv = 0, max_void = false,
|
||
replen = 2, err = "" },
|
||
prrow { input = "2,}", minv = 2, maxv = 0, max_void = true,
|
||
replen = 2, err = "" },
|
||
prrow { input = ",}", minv = 0, maxv = 0, max_void = true,
|
||
replen = 1, err = "" },
|
||
prrow { input = "12,34}xyz", minv = 12, maxv = 34,
|
||
max_void = false, replen = 5, err = "" },
|
||
prrow { input = "-1,2}", minv = 0, maxv = 0, max_void = false,
|
||
replen = 0, err = "Negative repetition count '{-n}'" },
|
||
prrow { input = "x,2}", minv = 0, maxv = 0, max_void = false,
|
||
replen = 0,
|
||
err = "Repetition expression syntax error '{n}'" },
|
||
prrow { input = "0,-2}", minv = 0, maxv = 0, max_void = false,
|
||
replen = 0, err = "Negative repetition count '{-n}'" },
|
||
// no endbrace at all (ha:491-493)
|
||
prrow { input = "2", minv = 0, maxv = 0, max_void = false,
|
||
replen = 0,
|
||
err = "Repetition expression syntax error '{n}'" },
|
||
];
|
||
let i: i32 = 0;
|
||
for (i < len(rows)) {
|
||
let r: (repparts | error) = parse_repetition(rows[i].input);
|
||
match (r) {
|
||
case let t: repparts => {
|
||
assert(!(rows[i].err.len > 0));
|
||
// .min is always size after a successful parse
|
||
// (ha:523-525 — empty min defaults to 0)
|
||
assert(!(!(t.min is size)));
|
||
assert(!(t.min as size != rows[i].minv));
|
||
if (rows[i].max_void) {
|
||
assert(!(!(t.max is void)));
|
||
} else {
|
||
assert(!(!(t.max is size)));
|
||
assert(!(t.max as size != rows[i].maxv));
|
||
};
|
||
assert(!(t.replen != rows[i].replen));
|
||
};
|
||
case let e: regex.error => {
|
||
assert(!(rows[i].err.len == 0));
|
||
if (strings.compare((e: str), rows[i].err) != 0) {
|
||
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;
|
||
};
|
||
};
|
||
|
||
// ---- fold 6: POSIX character classes ---------------------------------
|
||
|
||
// 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: [](charset_lit_item | charset_range_item |
|
||
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;
|
||
};
|
||
};
|