lib/regex: rehome private-touching @tests to in-package regex_whitebox.ww
White-box @tests move into a colocated 'package regex' file (Hare +test.ha analogue; #6 non-T drop plays the build-tag role), black-box @tests stay in package regex_test — Go's foo/foo_test split. The wb/ stub driver is documented-temporary scaffolding: a same-dir importer file-resolves to regex.ww before dir-enumeration, so only a different-dir import bundles the whitebox sibling (task #33 retires it). CLAUDE.md rule-9 carve-out updated (#5 closed, gate was #6).
This commit is contained in:
@@ -1,15 +1,14 @@
|
||||
// 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).
|
||||
// regex_test — BLACK-BOX @test probes for lib/regex: the exported API
|
||||
// only (compile / test / find / findall / strerror / result_free /
|
||||
// result_freeall / finish) plus exported-type variant discrimination,
|
||||
// payload extraction, and the regex/capture/charset struct shapes.
|
||||
// This is the external `package regex_test` half of the Go foo / foo_test
|
||||
// split (CLAUDE.md rule-9). The WHITE-BOX half — probes that bare-call
|
||||
// unexported internals (thread/newmatch, run_thread / add_thread /
|
||||
// delete_thread / search, find_last_groupstart / shift /
|
||||
// parse_repetition, is_consuming_inst) — lives in
|
||||
// lib/regex/regex_whitebox.ww (`package regex`, unified with regex.ww),
|
||||
// run via the lib/regex/wb/ driver.
|
||||
//
|
||||
// Fold 2a ports compile()'s lit/any/match arms only; exec lives in
|
||||
// later folds, so the compile_* cases pin the emitted inst PROGRAM
|
||||
@@ -24,15 +23,14 @@
|
||||
//
|
||||
// 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.
|
||||
// name in struct-literal position (#29), and the imported EXPORTED type
|
||||
// is in scope unqualified. These reach only exported types — the #29
|
||||
// axis, distinct from the white-box privacy boundary handled by the
|
||||
// regex_whitebox.ww split above.
|
||||
package regex_test;
|
||||
|
||||
import regex;
|
||||
import io;
|
||||
import memio;
|
||||
import strings;
|
||||
import types;
|
||||
|
||||
|
||||
// inst_lit / inst_match carry distinguishable payloads (rune / bool).
|
||||
@@ -254,114 +252,6 @@ import types;
|
||||
};
|
||||
};
|
||||
|
||||
// The thread struct (regex.ha:55-64) is in tree ahead of its engine
|
||||
// consumers so the pending #15/#17 fix probes exercise the real type.
|
||||
// Pin the field layout via the P6-proven wide-literal append + a
|
||||
// depth-1 read-back row per appended thread; the Hare `...` partial
|
||||
// fill (P5) must zero everything the second row's literal omits.
|
||||
// root_capture has NO row: every read route into it is
|
||||
// compiler-blocked today — the depth-2 chain behind the index links
|
||||
// the field as a global (#6 F4), the element let-copy is #7 F5, and
|
||||
// a probed `&threads[i].root_capture` deref segfaults byte-id on
|
||||
// both stages — so its row lands with those fixes.
|
||||
type texp = struct {
|
||||
pc: size,
|
||||
start_idx: size,
|
||||
start_bytesize: size,
|
||||
matched: bool,
|
||||
failed: bool,
|
||||
// .len reads as i32 (check.c:1239), so the count columns match it
|
||||
ncaps: i32,
|
||||
nreps: i32,
|
||||
};
|
||||
|
||||
@test fn thread_shape() void = {
|
||||
let rc: capture = capture {
|
||||
content = "ab", start = 1, start_bytesize = 1,
|
||||
end = 2, end_bytesize = 2,
|
||||
};
|
||||
let pcaps: []capture = [];
|
||||
append(pcaps, rc);
|
||||
let prep: []size = [];
|
||||
append(prep, (7: size));
|
||||
let threads: []thread = [];
|
||||
append(threads, thread {
|
||||
pc = 5,
|
||||
start_idx = 6,
|
||||
start_bytesize = 7,
|
||||
root_capture = rc,
|
||||
captures = pcaps,
|
||||
rep_counters = prep,
|
||||
matched = false,
|
||||
failed = true,
|
||||
});
|
||||
append(threads, thread { pc = 9, ... });
|
||||
let want: [2]texp = [
|
||||
texp { pc = 5, start_idx = 6, start_bytesize = 7,
|
||||
matched = false, failed = true,
|
||||
ncaps = 1, nreps = 1 },
|
||||
texp { pc = 9, start_idx = 0, start_bytesize = 0,
|
||||
matched = false, failed = false,
|
||||
ncaps = 0, nreps = 0 },
|
||||
];
|
||||
assert(!(len(threads) != len(want)));
|
||||
let i: i32 = 0;
|
||||
for (i < len(want)) {
|
||||
assert(!(threads[i].pc != want[i].pc));
|
||||
assert(!(threads[i].start_idx != want[i].start_idx));
|
||||
if (threads[i].start_bytesize != want[i].start_bytesize) {
|
||||
abort();
|
||||
};
|
||||
assert(!(threads[i].matched != want[i].matched));
|
||||
assert(!(threads[i].failed != want[i].failed));
|
||||
assert(!(threads[i].captures.len != want[i].ncaps));
|
||||
assert(!(threads[i].rep_counters.len != want[i].nreps));
|
||||
i += 1;
|
||||
};
|
||||
};
|
||||
|
||||
// newmatch (regex.ha:66) must discriminate nominally against plain
|
||||
// void — and against nomem, the third payload-free member — across
|
||||
// run_thread's (void | newmatch | nomem) return boundary: the P8
|
||||
// shape on the real lib type, one row per returned member.
|
||||
fn nm_probe(x: i32) (void | newmatch | nomem) = {
|
||||
if (x == 1) {
|
||||
let nm: newmatch;
|
||||
return nm;
|
||||
};
|
||||
if (x == 2) {
|
||||
let n: nomem;
|
||||
return n;
|
||||
};
|
||||
return;
|
||||
};
|
||||
|
||||
type nmexp = struct {
|
||||
arg: i32,
|
||||
want_nm: bool,
|
||||
want_void: bool,
|
||||
want_nomem: bool,
|
||||
};
|
||||
|
||||
@test fn newmatch_discriminates() void = {
|
||||
let rows: [3]nmexp = [
|
||||
nmexp { arg = 1, want_nm = true, want_void = false,
|
||||
want_nomem = false },
|
||||
nmexp { arg = 0, want_nm = false, want_void = true,
|
||||
want_nomem = false },
|
||||
nmexp { arg = 2, want_nm = false, want_void = false,
|
||||
want_nomem = true },
|
||||
];
|
||||
let i: i32 = 0;
|
||||
for (i < len(rows)) {
|
||||
let r: (void | newmatch | nomem) = nm_probe(rows[i].arg);
|
||||
assert(!((r is newmatch) != rows[i].want_nm));
|
||||
assert(!((r is void) != rows[i].want_void));
|
||||
assert(!((r is nomem) != rows[i].want_nomem));
|
||||
i += 1;
|
||||
};
|
||||
};
|
||||
|
||||
// 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.
|
||||
@@ -402,417 +292,6 @@ type nmexp = struct {
|
||||
};
|
||||
};
|
||||
|
||||
// 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 {
|
||||
@@ -1215,45 +694,6 @@ type cerow = struct {
|
||||
};
|
||||
};
|
||||
|
||||
// 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
|
||||
@@ -1774,96 +1214,6 @@ type cscase = struct {
|
||||
};
|
||||
};
|
||||
|
||||
// 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
|
||||
@@ -2116,88 +1466,6 @@ type smcase = struct {
|
||||
|
||||
// ---- 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
|
||||
|
||||
783
lib/regex/regex_whitebox.ww
Normal file
783
lib/regex/regex_whitebox.ww
Normal file
@@ -0,0 +1,783 @@
|
||||
// regex_whitebox — in-package white-box @test probes for lib/regex
|
||||
// internals NOT reachable from the black-box package regex_test: the
|
||||
// thread/NFA-stepping engine (thread/newmatch, run_thread / add_thread /
|
||||
// delete_thread / search), the program-shape leaves (find_last_groupstart
|
||||
// / shift / parse_repetition), and is_consuming_inst. These bare-call
|
||||
// unexported regex symbols, so they must live in `package regex` (unified
|
||||
// with regex.ww) — Hare's same-package white-box model
|
||||
// (ref/hare/regex/+test.ha colocates its internal tests inside the regex
|
||||
// module; Hare excludes them from normal builds via build tags, ww via
|
||||
// the non-T @test drop, #6). Load-bearing ww-compiler coverage (drove
|
||||
// #34/#38/#44/#45/#48), not black-box-reachable.
|
||||
//
|
||||
// `_whitebox.ww` (NOT *_test.ww) is the ww convention for an in-package
|
||||
// white-box test file: the enumerator skips *test.ww, so the non-suffixed
|
||||
// name is what keeps this a bundled regex module source. It is exercised
|
||||
// by the sibling external driver lib/regex/wb/regex_test.ww, whose
|
||||
// different-dir `import regex` dir-resolves and bundles this file (a
|
||||
// same-dir import file-resolves regex.ww only — probe E; task #33 would
|
||||
// delete the driver). The public-API @tests stay black-box in
|
||||
// lib/regex/regex_test.ww (Go's foo / foo_test split; CLAUDE.md rule-9).
|
||||
package regex;
|
||||
|
||||
import io;
|
||||
import memio;
|
||||
import strings;
|
||||
import types;
|
||||
|
||||
// The thread struct (regex.ha:55-64) is in tree ahead of its engine
|
||||
// consumers so the pending #15/#17 fix probes exercise the real type.
|
||||
// Pin the field layout via the P6-proven wide-literal append + a
|
||||
// depth-1 read-back row per appended thread; the Hare `...` partial
|
||||
// fill (P5) must zero everything the second row's literal omits.
|
||||
// root_capture has NO row: every read route into it is
|
||||
// compiler-blocked today — the depth-2 chain behind the index links
|
||||
// the field as a global (#6 F4), the element let-copy is #7 F5, and
|
||||
// a probed `&threads[i].root_capture` deref segfaults byte-id on
|
||||
// both stages — so its row lands with those fixes.
|
||||
type texp = struct {
|
||||
pc: size,
|
||||
start_idx: size,
|
||||
start_bytesize: size,
|
||||
matched: bool,
|
||||
failed: bool,
|
||||
// .len reads as i32 (check.c:1239), so the count columns match it
|
||||
ncaps: i32,
|
||||
nreps: i32,
|
||||
};
|
||||
|
||||
@test fn thread_shape() void = {
|
||||
let rc: capture = capture {
|
||||
content = "ab", start = 1, start_bytesize = 1,
|
||||
end = 2, end_bytesize = 2,
|
||||
};
|
||||
let pcaps: []capture = [];
|
||||
append(pcaps, rc);
|
||||
let prep: []size = [];
|
||||
append(prep, (7: size));
|
||||
let threads: []thread = [];
|
||||
append(threads, thread {
|
||||
pc = 5,
|
||||
start_idx = 6,
|
||||
start_bytesize = 7,
|
||||
root_capture = rc,
|
||||
captures = pcaps,
|
||||
rep_counters = prep,
|
||||
matched = false,
|
||||
failed = true,
|
||||
});
|
||||
append(threads, thread { pc = 9, ... });
|
||||
let want: [2]texp = [
|
||||
texp { pc = 5, start_idx = 6, start_bytesize = 7,
|
||||
matched = false, failed = true,
|
||||
ncaps = 1, nreps = 1 },
|
||||
texp { pc = 9, start_idx = 0, start_bytesize = 0,
|
||||
matched = false, failed = false,
|
||||
ncaps = 0, nreps = 0 },
|
||||
];
|
||||
assert(!(len(threads) != len(want)));
|
||||
let i: i32 = 0;
|
||||
for (i < len(want)) {
|
||||
assert(!(threads[i].pc != want[i].pc));
|
||||
assert(!(threads[i].start_idx != want[i].start_idx));
|
||||
if (threads[i].start_bytesize != want[i].start_bytesize) {
|
||||
abort();
|
||||
};
|
||||
assert(!(threads[i].matched != want[i].matched));
|
||||
assert(!(threads[i].failed != want[i].failed));
|
||||
assert(!(threads[i].captures.len != want[i].ncaps));
|
||||
assert(!(threads[i].rep_counters.len != want[i].nreps));
|
||||
i += 1;
|
||||
};
|
||||
};
|
||||
|
||||
// newmatch (regex.ha:66) must discriminate nominally against plain
|
||||
// void — and against nomem, the third payload-free member — across
|
||||
// run_thread's (void | newmatch | nomem) return boundary: the P8
|
||||
// shape on the real lib type, one row per returned member.
|
||||
fn nm_probe(x: i32) (void | newmatch | nomem) = {
|
||||
if (x == 1) {
|
||||
let nm: newmatch;
|
||||
return nm;
|
||||
};
|
||||
if (x == 2) {
|
||||
let n: nomem;
|
||||
return n;
|
||||
};
|
||||
return;
|
||||
};
|
||||
|
||||
type nmexp = struct {
|
||||
arg: i32,
|
||||
want_nm: bool,
|
||||
want_void: bool,
|
||||
want_nomem: bool,
|
||||
};
|
||||
|
||||
@test fn newmatch_discriminates() void = {
|
||||
let rows: [3]nmexp = [
|
||||
nmexp { arg = 1, want_nm = true, want_void = false,
|
||||
want_nomem = false },
|
||||
nmexp { arg = 0, want_nm = false, want_void = true,
|
||||
want_nomem = false },
|
||||
nmexp { arg = 2, want_nm = false, want_void = false,
|
||||
want_nomem = true },
|
||||
];
|
||||
let i: i32 = 0;
|
||||
for (i < len(rows)) {
|
||||
let r: (void | newmatch | nomem) = nm_probe(rows[i].arg);
|
||||
assert(!((r is newmatch) != rows[i].want_nm));
|
||||
assert(!((r is void) != rows[i].want_void));
|
||||
assert(!((r is nomem) != rows[i].want_nomem));
|
||||
i += 1;
|
||||
};
|
||||
};
|
||||
|
||||
// is_consuming_inst must discriminate the three consuming kinds from
|
||||
// the seven non-consuming ones across all 10 inst variants
|
||||
// (regex.ha:553-555) — the tranche-A-deferred row, graduated by the
|
||||
// #19 >48B by-value arg wiring. Sequential typed-let + helper calls,
|
||||
// not a [10](inst, bool) table: tagged-element array literals
|
||||
// under-copy (#12), and a cast/literal rvalue arg source is
|
||||
// #38b-unwired, so each value goes through a typed let (the
|
||||
// #19-landed ident source).
|
||||
fn ic_one(v: inst, want: bool) void = {
|
||||
assert(!(is_consuming_inst(v) != want));
|
||||
};
|
||||
|
||||
@test fn is_consuming_kinds() void = {
|
||||
let lit: inst = ('a': inst_lit);
|
||||
ic_one(lit, true);
|
||||
let av: inst_any;
|
||||
let any: inst = av;
|
||||
ic_one(any, true);
|
||||
let cs: inst = (inst_charset { idx = 0, is_positive = true });
|
||||
ic_one(cs, true);
|
||||
let kv: inst_skip;
|
||||
let sk: inst = kv;
|
||||
ic_one(sk, false);
|
||||
let sp: inst = ((5: size): inst_split);
|
||||
ic_one(sp, false);
|
||||
let jm: inst = ((6: size): inst_jump);
|
||||
ic_one(jm, false);
|
||||
let mt: inst = (false: inst_match);
|
||||
ic_one(mt, false);
|
||||
let gs: inst = ((2: size): inst_groupstart);
|
||||
ic_one(gs, false);
|
||||
let gv: inst_groupend;
|
||||
let ge: inst = gv;
|
||||
ic_one(ge, false);
|
||||
let rp: inst = (inst_repeat {
|
||||
id = 1, origin = 4, min = (2: size), max = void,
|
||||
});
|
||||
ic_one(rp, false);
|
||||
};
|
||||
|
||||
// delete_thread (regex.ha:547-551) removes exactly the indexed
|
||||
// element and preserves order; its frees are no-ops (no-free
|
||||
// runtime), so the survivors' capture headers stay readable.
|
||||
@test fn delete_thread_middle() void = {
|
||||
let caps: []capture = [];
|
||||
append(caps, capture {
|
||||
content = "x", start = 0, start_bytesize = 0,
|
||||
end = 1, end_bytesize = 1,
|
||||
});
|
||||
let ts: []thread = [];
|
||||
append(ts, thread { pc = 1, start_idx = 11, captures = caps, ... });
|
||||
append(ts, thread { pc = 2, start_idx = 22, ... });
|
||||
append(ts, thread { pc = 3, start_idx = 33, ... });
|
||||
delete_thread(1, &ts);
|
||||
assert(!(len(ts) != 2));
|
||||
assert(!(ts[0].pc != (1: size)));
|
||||
assert(!(ts[0].start_idx != (11: size)));
|
||||
assert(!(ts[0].captures.len != 1));
|
||||
assert(!(ts[1].pc != (3: size)));
|
||||
assert(!(ts[1].start_idx != (33: size)));
|
||||
assert(!(ts[1].captures.len != 0));
|
||||
// boundary rows: delete at the last index, then at index 0 down
|
||||
// to empty — the failed-sweep loop (regex.ha:891-896) deletes at
|
||||
// every position including both ends.
|
||||
delete_thread(1, &ts);
|
||||
assert(!(len(ts) != 1));
|
||||
assert(!(ts[0].pc != (1: size)));
|
||||
delete_thread(0, &ts);
|
||||
assert(!(len(ts) != 0));
|
||||
};
|
||||
|
||||
// add_thread (regex.ha:557-587): same-pc dedup suppression fires only
|
||||
// when the existing thread is unmatched AND started strictly earlier
|
||||
// than the parent (ha:561-565); otherwise the child appends,
|
||||
// inheriting the parent's start/matched/failed with DUPLICATED
|
||||
// capture/rep_counter slices (empty parent → empty dup, ha:569/572)
|
||||
// and a zeroed root_capture.
|
||||
@test fn add_thread_dedup_inherit() void = {
|
||||
let ts: []thread = [];
|
||||
append(ts, thread { pc = 0, start_idx = 5, start_bytesize = 4,
|
||||
matched = false, failed = true, ... });
|
||||
// inherit: fresh pc, parent fields copied, rest zeroed
|
||||
let r: (void | nomem) = add_thread(&ts, 0, 7);
|
||||
assert(!(!(r is void)));
|
||||
assert(!(len(ts) != 2));
|
||||
assert(!(ts[1].pc != (7: size)));
|
||||
assert(!(ts[1].start_idx != (5: size)));
|
||||
assert(!(ts[1].start_bytesize != (4: size)));
|
||||
assert(!(ts[1].matched));
|
||||
assert(!(!ts[1].failed));
|
||||
assert(!(ts[1].captures.len != 0));
|
||||
assert(!(ts[1].rep_counters.len != 0));
|
||||
assert(!(ts[1].root_capture.content.len != 0));
|
||||
assert(!(ts[1].root_capture.end != (0: size)));
|
||||
// same-pc same-start does NOT suppress (strict <, ha:563-565)
|
||||
let r2: (void | nomem) = add_thread(&ts, 0, 7);
|
||||
assert(!(!(r2 is void)));
|
||||
assert(!(len(ts) != 3));
|
||||
// an earlier-started unmatched existing thread DOES suppress
|
||||
let ts2: []thread = [];
|
||||
append(ts2, thread { pc = 0, start_idx = 5, ... });
|
||||
append(ts2, thread { pc = 7, start_idx = 2, ... });
|
||||
let r3: (void | nomem) = add_thread(&ts2, 0, 7);
|
||||
assert(!(!(r3 is void)));
|
||||
assert(!(len(ts2) != 2));
|
||||
// a MATCHED existing thread never suppresses
|
||||
let ts3: []thread = [];
|
||||
append(ts3, thread { pc = 0, start_idx = 5, ... });
|
||||
append(ts3, thread { pc = 7, start_idx = 2, matched = true, ... });
|
||||
let r4: (void | nomem) = add_thread(&ts3, 0, 7);
|
||||
assert(!(!(r4 is void)));
|
||||
assert(!(len(ts3) != 3));
|
||||
assert(!(ts3[2].pc != (7: size)));
|
||||
assert(!(ts3[2].start_idx != (5: size)));
|
||||
};
|
||||
|
||||
// add_thread dup (regex.ha:568-573): the child carries a COPY of the
|
||||
// parent's captures/rep_counters — values equal, backing independent
|
||||
// in both directions (mutate parent → child unchanged, mutate child →
|
||||
// parent unchanged). Empty parent → empty dup (the pre-flip rows above
|
||||
// stay byte-for-byte). Driven directly, the dedup-test precedent.
|
||||
@test fn add_thread_dup_independence() void = {
|
||||
let caps: []capture = [];
|
||||
append(caps, capture {
|
||||
content = "ab", start = 1, start_bytesize = 1,
|
||||
end = 2, end_bytesize = 2,
|
||||
});
|
||||
append(caps, capture {
|
||||
content = "c", start = 3, start_bytesize = 3,
|
||||
end = 4, end_bytesize = 4,
|
||||
});
|
||||
let reps: []size = [];
|
||||
append(reps, (5: size));
|
||||
append(reps, (6: size));
|
||||
let ts: []thread = [];
|
||||
append(ts, thread { pc = 0, start_idx = 1, captures = caps,
|
||||
rep_counters = reps, ... });
|
||||
let r: (void | nomem) = add_thread(&ts, 0, 9);
|
||||
assert(!(!(r is void)));
|
||||
assert(!(len(ts) != 2));
|
||||
// dup carried the parent's values
|
||||
assert(!(ts[1].captures.len != 2));
|
||||
assert(!(strings.compare(ts[1].captures[0].content, "ab") != 0));
|
||||
assert(!(ts[1].captures[0].start != (1: size)));
|
||||
assert(!(ts[1].captures[1].end != (4: size)));
|
||||
assert(!(ts[1].rep_counters.len != 2));
|
||||
assert(!(ts[1].rep_counters[0] != (5: size)));
|
||||
assert(!(ts[1].rep_counters[1] != (6: size)));
|
||||
// independence, parent → child: mutate the parent post-add
|
||||
ts[0].captures[0].start = 100;
|
||||
ts[0].captures[0].content = "zz";
|
||||
ts[0].rep_counters[0] = 77;
|
||||
assert(!(ts[1].captures[0].start != (1: size)));
|
||||
assert(!(strings.compare(ts[1].captures[0].content, "ab") != 0));
|
||||
assert(!(ts[1].rep_counters[0] != (5: size)));
|
||||
// independence, child → parent
|
||||
ts[1].captures[1].end = 200;
|
||||
ts[1].rep_counters[1] = 88;
|
||||
assert(!(ts[0].captures[1].end != (4: size)));
|
||||
assert(!(ts[0].rep_counters[1] != (6: size)));
|
||||
// empty parent → empty dup
|
||||
let ts2: []thread = [];
|
||||
append(ts2, thread { pc = 0, ... });
|
||||
let r2: (void | nomem) = add_thread(&ts2, 0, 3);
|
||||
assert(!(!(r2 is void)));
|
||||
assert(!(ts2[1].captures.len != 0));
|
||||
assert(!(ts2[1].rep_counters.len != 0));
|
||||
};
|
||||
|
||||
// run_thread (regex.ha:589-742) driven directly over compile("ab")'s
|
||||
// real program [skip, lit 'a', lit 'b', match(false)] — the arms
|
||||
// fold-2a can emit. Phases: parked-skip spawn (len 1→2, parent pc
|
||||
// unmoved — the unanchored-restart engine), lit advance, lit
|
||||
// mismatch (failed=true AND pc still steps — ha:741 runs regardless
|
||||
// of the arm's verdict), EOF on a consuming pc (failed, pc frozen),
|
||||
// match arm (root_capture spans start_bytesize..str_bytesize +
|
||||
// matched + `is newmatch`), and the matched-thread early return
|
||||
// (ha:599-601).
|
||||
@test fn run_thread_literal_program() void = {
|
||||
// typed-let + match receive, the compile_literal_program shape
|
||||
let c: (regex | error | nomem) = compile("ab");
|
||||
match (c) {
|
||||
case let re: regex => {
|
||||
// skip spawn: thread 0 parks on the skip, child enters at pc 1
|
||||
let ra: (rune | io.eof) = 'a';
|
||||
let ts: []thread = [];
|
||||
append(ts, thread { pc = 0, ... });
|
||||
let r1: (void | newmatch | nomem) = run_thread(0, &re, "ab", &ts, ra, 0, 0);
|
||||
assert(!(!(r1 is void)));
|
||||
assert(!(len(ts) != 2));
|
||||
assert(!(ts[0].pc != (0: size)));
|
||||
assert(!(ts[1].pc != (1: size)));
|
||||
assert(!(ts[1].failed));
|
||||
|
||||
// lit match advances pc past 'a'
|
||||
let r2: (void | newmatch | nomem) = run_thread(1, &re, "ab", &ts, ra, 0, 0);
|
||||
assert(!(!(r2 is void)));
|
||||
assert(!(ts[1].pc != (2: size)));
|
||||
assert(!(ts[1].failed));
|
||||
|
||||
// lit mismatch fails the thread; pc steps anyway (ha:741)
|
||||
let rx: (rune | io.eof) = 'x';
|
||||
let r3: (void | newmatch | nomem) = run_thread(1, &re, "ab", &ts, rx, 1, 1);
|
||||
assert(!(!(r3 is void)));
|
||||
assert(!(!ts[1].failed));
|
||||
assert(!(ts[1].pc != (3: size)));
|
||||
|
||||
// EOF on a consuming pc fails the thread before pc steps
|
||||
let ev: io.eof;
|
||||
let reof: (rune | io.eof) = ev;
|
||||
let ts2: []thread = [];
|
||||
append(ts2, thread { pc = 1, ... });
|
||||
let r4: (void | newmatch | nomem) = run_thread(0, &re, "ab", &ts2, reof, 2, 2);
|
||||
assert(!(!(r4 is void)));
|
||||
assert(!(!ts2[0].failed));
|
||||
assert(!(ts2[0].pc != (1: size)));
|
||||
|
||||
// match arm: root_capture spans start_bytesize..str_bytesize,
|
||||
// matched set, newmatch returned
|
||||
let ts3: []thread = [];
|
||||
append(ts3, thread { pc = 3, ... });
|
||||
let r5: (void | newmatch | nomem) = run_thread(0, &re, "ab", &ts3, reof, 2, 2);
|
||||
assert(!(!(r5 is newmatch)));
|
||||
assert(!(!ts3[0].matched));
|
||||
assert(!(ts3[0].failed));
|
||||
assert(!(ts3[0].root_capture.start != (0: size)));
|
||||
assert(!(ts3[0].root_capture.start_bytesize != (0: size)));
|
||||
assert(!(ts3[0].root_capture.end != (2: size)));
|
||||
assert(!(ts3[0].root_capture.end_bytesize != (2: size)));
|
||||
assert(!(strings.compare(ts3[0].root_capture.content, "ab") != 0));
|
||||
|
||||
// an already-matched thread is inert (ha:599-601): void
|
||||
// return, state untouched
|
||||
let r6: (void | newmatch | nomem) = run_thread(0, &re, "ab", &ts3, ra, 3, 3);
|
||||
assert(!(!(r6 is void)));
|
||||
assert(!(ts3[0].root_capture.end != (2: size)));
|
||||
|
||||
// idx/bytesize split: every all-ASCII row has idx ==
|
||||
// bytesize, so a port swapping start/start_bytesize (or
|
||||
// end/end_bytesize) in root_capture passes them. One 2-byte
|
||||
// rune ('ß') consumed before the match start makes all four
|
||||
// values distinct: start=1 start_bytesize=2 end=3
|
||||
// end_bytesize=4; content = bytes[2:4] = "ab".
|
||||
let ts4: []thread = [];
|
||||
append(ts4, thread { pc = 3, start_idx = 1, start_bytesize = 2, ... });
|
||||
let r7: (void | newmatch | nomem) = run_thread(0, &re, "ßab", &ts4, reof, 3, 4);
|
||||
assert(!(!(r7 is newmatch)));
|
||||
assert(!(ts4[0].root_capture.start != (1: size)));
|
||||
assert(!(ts4[0].root_capture.start_bytesize != (2: size)));
|
||||
assert(!(ts4[0].root_capture.end != (3: size)));
|
||||
assert(!(ts4[0].root_capture.end_bytesize != (4: size)));
|
||||
assert(!(strings.compare(ts4[0].root_capture.content, "ab") != 0));
|
||||
finish(&re);
|
||||
};
|
||||
case => abort();
|
||||
};
|
||||
};
|
||||
|
||||
// The anchored route (ha:621-624) needs a (true: inst_match) program
|
||||
// — compile() can't emit `$` yet, so it is HAND-BUILT — pinned from
|
||||
// both sides: anchored + string-not-exhausted fails the thread;
|
||||
// anchored + EOF falls through to the match (empty content).
|
||||
@test fn run_thread_anchored_route() void = {
|
||||
let insts: []inst = [];
|
||||
append(insts, (true: inst_match));
|
||||
let re: regex;
|
||||
re.insts = insts;
|
||||
re.n_reps = 0;
|
||||
|
||||
let ra: (rune | io.eof) = 'a';
|
||||
let ts: []thread = [];
|
||||
append(ts, thread { pc = 0, ... });
|
||||
let r1: (void | newmatch | nomem) = run_thread(0, &re, "ab", &ts, ra, 0, 0);
|
||||
assert(!(!(r1 is void)));
|
||||
assert(!(!ts[0].failed));
|
||||
assert(!(ts[0].matched));
|
||||
|
||||
let ev: io.eof;
|
||||
let reof: (rune | io.eof) = ev;
|
||||
let ts2: []thread = [];
|
||||
append(ts2, thread { pc = 0, ... });
|
||||
let r2: (void | newmatch | nomem) = run_thread(0, &re, "", &ts2, reof, 0, 0);
|
||||
assert(!(!(r2 is newmatch)));
|
||||
assert(!(!ts2[0].matched));
|
||||
assert(!(ts2[0].root_capture.content.len != 0));
|
||||
assert(!(ts2[0].root_capture.end != (0: size)));
|
||||
};
|
||||
|
||||
// search (regex.ha:746-898) driven DIRECTLY (private fn, package-regex
|
||||
// test) over memio-backed streams — the exec surface (test/find) is
|
||||
// tranche D. Each match row pins the root capture's four indices plus
|
||||
// content; the multibyte row keeps idx != bytesize honest. Rows share
|
||||
// (expr, input, need_captures, want) shape — the P12 struct-row table.
|
||||
type scase = struct {
|
||||
expr: str,
|
||||
input: str,
|
||||
nc: bool,
|
||||
start: size,
|
||||
sb: size,
|
||||
end: size,
|
||||
eb: size,
|
||||
content: str,
|
||||
};
|
||||
|
||||
@test fn search_matches() void = {
|
||||
let rows: [6]scase = [
|
||||
// full match mid-string: skip-respawn + dispatch +
|
||||
// all_matched exit
|
||||
scase { expr = "ab", input = "xab", nc = true,
|
||||
start = 1, sb = 1, end = 3, eb = 3, content = "ab" },
|
||||
// mismatch-restart: the idx-0 child fails and is swept; the
|
||||
// restarted thread wins (failed-sweep interplay)
|
||||
scase { expr = "bcd", input = "abcd", nc = true,
|
||||
start = 1, sb = 1, end = 4, eb = 4, content = "bcd" },
|
||||
// leftmost-longest best-pick + first_match_idx trim
|
||||
scase { expr = "aa", input = "aaa", nc = true,
|
||||
start = 0, sb = 0, end = 2, eb = 2, content = "aa" },
|
||||
// zero-length: the all_matched path with matchlen 0 must
|
||||
// NOT take the need_captures=false early-exit (ha:845
|
||||
// requires matchlen > 0) — hence nc=false expecting the
|
||||
// FULL one-capture result, not the empty early-exit slice
|
||||
scase { expr = "", input = "", nc = false,
|
||||
start = 0, sb = 0, end = 0, eb = 0, content = "" },
|
||||
// multibyte: the 2-byte ß before the match start splits
|
||||
// every idx from its bytesize; inst_any consumes 'x'
|
||||
scase { expr = "b.d", input = "aßbxd", nc = true,
|
||||
start = 2, sb = 3, end = 5, eb = 6, content = "bxd" },
|
||||
// dedup-heavy: same-pc threads spawn on every step across
|
||||
// >=3 passes (ha:872-889); the pick must stay stable.
|
||||
// Result stability is the only external pin available this
|
||||
// fold: 2a programs are all fixed-length, every match ties
|
||||
// on match_len, and best-pick's insertion-order tiebreak
|
||||
// alone yields leftmost — so the dedup sweep and the
|
||||
// leftmost trim are result-invisible (mutation-verified:
|
||||
// disabling either still passes this table; disabling the
|
||||
// failed sweep hangs). Both turn result- and
|
||||
// termination-visible with the split/star fold.
|
||||
scase { expr = "aa", input = "aaaa", nc = true,
|
||||
start = 0, sb = 0, end = 2, eb = 2, content = "aa" },
|
||||
];
|
||||
let i: i32 = 0;
|
||||
for (i < len(rows)) {
|
||||
let ex: str = rows[i].expr;
|
||||
let inp: str = rows[i].input;
|
||||
let c: (regex | error | nomem) = compile(ex);
|
||||
match (c) {
|
||||
case let re: regex => {
|
||||
let strm: memio.stream =
|
||||
memio.fixed(strings.toutf8(inp));
|
||||
let r: (void | []capture | nomem) =
|
||||
search(&re, inp, &strm.vt, rows[i].nc);
|
||||
assert(!(!(r is []capture)));
|
||||
let caps: []capture = r as []capture;
|
||||
assert(!(len(caps) != 1));
|
||||
assert(!(caps[0].start != rows[i].start));
|
||||
assert(!(caps[0].start_bytesize != rows[i].sb));
|
||||
assert(!(caps[0].end != rows[i].end));
|
||||
assert(!(caps[0].end_bytesize != rows[i].eb));
|
||||
let wc: str = rows[i].content;
|
||||
if (strings.compare(caps[0].content, wc) != 0) {
|
||||
abort();
|
||||
};
|
||||
result_free(caps);
|
||||
finish(&re);
|
||||
};
|
||||
case => abort();
|
||||
};
|
||||
i += 1;
|
||||
};
|
||||
};
|
||||
|
||||
// ha:845-847: a non-zero-length newmatch with need_captures=false
|
||||
// returns the empty result immediately, skipping the best-pick pass.
|
||||
@test fn search_early_exit() void = {
|
||||
let c: (regex | error | nomem) = compile("ab");
|
||||
match (c) {
|
||||
case let re: regex => {
|
||||
let strm: memio.stream = memio.fixed(strings.toutf8("xab"));
|
||||
let r: (void | []capture | nomem) =
|
||||
search(&re, "xab", &strm.vt, false);
|
||||
assert(!(!(r is []capture)));
|
||||
let caps: []capture = r as []capture;
|
||||
assert(!(len(caps) != 0));
|
||||
result_free(caps);
|
||||
finish(&re);
|
||||
};
|
||||
case => abort();
|
||||
};
|
||||
};
|
||||
|
||||
// void rows: no match anywhere ("ab" over "xyz" — every thread fails,
|
||||
// the list drains, ha:777-779) and EOF mid-pattern ("ab" over "a" —
|
||||
// the consuming-inst EOF fail).
|
||||
@test fn search_no_match() void = {
|
||||
let c: (regex | error | nomem) = compile("ab");
|
||||
match (c) {
|
||||
case let re: regex => {
|
||||
let strm: memio.stream = memio.fixed(strings.toutf8("xyz"));
|
||||
let r: (void | []capture | nomem) =
|
||||
search(&re, "xyz", &strm.vt, true);
|
||||
assert(!(!(r is void)));
|
||||
let strm2: memio.stream = memio.fixed(strings.toutf8("a"));
|
||||
let r2: (void | []capture | nomem) =
|
||||
search(&re, "a", &strm2.vt, true);
|
||||
assert(!(!(r2 is void)));
|
||||
finish(&re);
|
||||
};
|
||||
case => abort();
|
||||
};
|
||||
};
|
||||
|
||||
// find_last_groupstart (regex.ha:104-119) — driven directly (private
|
||||
// fn): no inst_groupstart exists in any fold-3 program, so the error
|
||||
// arm is the live one; pin its exact text. A hand-built groupstart
|
||||
// row pins the success arm the group fold will rely on.
|
||||
@test fn find_last_groupstart_cases() void = {
|
||||
let insts: []inst = [];
|
||||
append(insts, ('a': inst_lit));
|
||||
match (find_last_groupstart(insts)) {
|
||||
case let e: error => {
|
||||
if (strings.compare((e: str), "Unmatched ')'") != 0) {
|
||||
abort();
|
||||
};
|
||||
};
|
||||
case => abort();
|
||||
};
|
||||
append(insts, ((1: size): inst_groupstart));
|
||||
append(insts, ('b': inst_lit));
|
||||
match (find_last_groupstart(insts)) {
|
||||
case let sz: size => { assert(!(sz != 1)); };
|
||||
case => abort();
|
||||
};
|
||||
};
|
||||
|
||||
// instsig (copy; the external black-box file keeps the
|
||||
// original for its fold3/fold4 program-shape pins).
|
||||
// instsig — flatten an inst for the table-driven program pins below:
|
||||
// kind base + payload. Takes the 56B inst by value (the #19-landed
|
||||
// is_consuming_inst shape).
|
||||
fn instsig(v: inst) i64 = {
|
||||
match (v) {
|
||||
case let l: inst_lit => return 1000 + ((l: rune): i64);
|
||||
case inst_skip => return 2000;
|
||||
case inst_any => return 3000;
|
||||
case let s: inst_split => return 4000 + ((s: size): i64);
|
||||
case let j: inst_jump => return 5000 + ((j: size): i64);
|
||||
case let m: inst_match => {
|
||||
if ((m: bool)) { return 6001; };
|
||||
return 6000;
|
||||
};
|
||||
case let g: inst_groupstart => return 7000 + ((g: size): i64);
|
||||
case inst_groupend => return 8000;
|
||||
case let c: inst_charset => {
|
||||
// fold 4: 10xxx positive / 11xxx negated, + charset index
|
||||
if (c.is_positive) { return 10000 + (c.idx: i64); };
|
||||
return 11000 + (c.idx: i64);
|
||||
};
|
||||
case => return 9999;
|
||||
};
|
||||
};
|
||||
|
||||
// shift (regex.ha:123-133) — driven directly over a sub-slice view:
|
||||
// jump/split payloads in the view bump by one, the element before the
|
||||
// view and non-jump kinds are untouched (the PE3/PE4 shapes).
|
||||
@test fn shift_direct() void = {
|
||||
let insts: []inst = [];
|
||||
append(insts, ((3: size): inst_jump));
|
||||
append(insts, ('a': inst_lit));
|
||||
append(insts, ((5: size): inst_split));
|
||||
append(insts, ((7: size): inst_jump));
|
||||
shift(insts[1:]);
|
||||
assert(!(instsig(insts[0]) != 5003));
|
||||
assert(!(instsig(insts[1]) != 1097));
|
||||
assert(!(instsig(insts[2]) != 4006));
|
||||
assert(!(instsig(insts[3]) != 5008));
|
||||
};
|
||||
|
||||
// run_thread inst_groupstart / inst_groupend driven over HAND-BUILT
|
||||
// programs (the anchored-route precedent): compile() composition is
|
||||
// pinned by the find/submatch tables below; these pin the arm
|
||||
// mechanics — fill-grow to idx+1, the SIZE_MAX open sentinel,
|
||||
// innermost-unclosed close order, content from the bytesize span,
|
||||
// and the closed-group re-entry overwrite (the ha:642 assert's
|
||||
// PASSING direction).
|
||||
@test fn run_thread_group_arms() void = {
|
||||
// groupstart (ha:636-652): grows captures to idx+1 (zero-filled
|
||||
// below idx), stamps start/start_bytesize, opens with
|
||||
// end = end_bytesize = SIZE_MAX
|
||||
let insts: []inst = [];
|
||||
append(insts, ((1: size): inst_groupstart));
|
||||
append(insts, ('a': inst_lit));
|
||||
append(insts, (false: inst_match));
|
||||
let re: regex;
|
||||
re.insts = insts;
|
||||
re.n_reps = 0;
|
||||
let ra: (rune | io.eof) = 'a';
|
||||
let ts: []thread = [];
|
||||
append(ts, thread { pc = 0, ... });
|
||||
let r1: (void | newmatch | nomem) = run_thread(0, &re, "ab", &ts, ra, 2, 3);
|
||||
assert(!(!(r1 is void)));
|
||||
assert(!(ts[0].captures.len != 2));
|
||||
// the fill element below idx is zeroed
|
||||
assert(!(ts[0].captures[0].end != (0: size)));
|
||||
assert(!(ts[0].captures[0].content.len != 0));
|
||||
// the opened group: start stamped, end still the open sentinel
|
||||
assert(!(ts[0].captures[1].start != (2: size)));
|
||||
assert(!(ts[0].captures[1].start_bytesize != (3: size)));
|
||||
assert(!(ts[0].captures[1].end != types.SIZE_MAX));
|
||||
assert(!(ts[0].captures[1].end_bytesize != types.SIZE_MAX));
|
||||
// groupstart is non-consuming: pc stepped through it, then the
|
||||
// lit consumed
|
||||
assert(!(ts[0].pc != (2: size)));
|
||||
|
||||
// groupend (ha:653-668): two open groups — the INNERMOST
|
||||
// (highest index) closes first; back-to-back groupends close
|
||||
// inner then outer in ONE call; content = str_bytes[
|
||||
// start_bytesize:end_bytesize]
|
||||
let insts2: []inst = [];
|
||||
let gv: inst_groupend;
|
||||
let ge: inst = gv;
|
||||
append(insts2, ge);
|
||||
append(insts2, ge);
|
||||
append(insts2, ('x': inst_lit));
|
||||
append(insts2, (false: inst_match));
|
||||
let re2: regex;
|
||||
re2.insts = insts2;
|
||||
re2.n_reps = 0;
|
||||
let caps: []capture = [];
|
||||
append(caps, capture { content = "", start = 1, start_bytesize = 1,
|
||||
end = types.SIZE_MAX, end_bytesize = types.SIZE_MAX });
|
||||
append(caps, capture { content = "", start = 2, start_bytesize = 2,
|
||||
end = types.SIZE_MAX, end_bytesize = types.SIZE_MAX });
|
||||
let ts2: []thread = [];
|
||||
append(ts2, thread { pc = 0, captures = caps, ... });
|
||||
let rx: (rune | io.eof) = 'x';
|
||||
let r2: (void | newmatch | nomem) = run_thread(0, &re2, "abcd", &ts2, rx, 3, 4);
|
||||
assert(!(!(r2 is void)));
|
||||
assert(!(ts2[0].captures[1].end != (3: size)));
|
||||
assert(!(ts2[0].captures[1].end_bytesize != (4: size)));
|
||||
assert(!(strings.compare(ts2[0].captures[1].content, "cd") != 0));
|
||||
assert(!(ts2[0].captures[0].end != (3: size)));
|
||||
assert(!(ts2[0].captures[0].end_bytesize != (4: size)));
|
||||
assert(!(strings.compare(ts2[0].captures[0].content, "bcd") != 0));
|
||||
assert(!(ts2[0].pc != (3: size)));
|
||||
|
||||
// closed-group re-entry: groupstart over an already-CLOSED idx
|
||||
// passes the ha:642 assert (end != SIZE_MAX) and re-opens fresh
|
||||
let insts3: []inst = [];
|
||||
append(insts3, ((0: size): inst_groupstart));
|
||||
append(insts3, ('a': inst_lit));
|
||||
append(insts3, (false: inst_match));
|
||||
let re3: regex;
|
||||
re3.insts = insts3;
|
||||
re3.n_reps = 0;
|
||||
let caps3: []capture = [];
|
||||
append(caps3, capture { content = "ab", start = 0, start_bytesize = 0,
|
||||
end = 2, end_bytesize = 2 });
|
||||
let ts3: []thread = [];
|
||||
append(ts3, thread { pc = 0, captures = caps3, ... });
|
||||
let r3: (void | newmatch | nomem) = run_thread(0, &re3, "aba", &ts3, ra, 2, 2);
|
||||
assert(!(!(r3 is void)));
|
||||
assert(!(ts3[0].captures.len != 1));
|
||||
assert(!(ts3[0].captures[0].start != (2: size)));
|
||||
assert(!(ts3[0].captures[0].end != types.SIZE_MAX));
|
||||
assert(!(ts3[0].captures[0].content.len != 0));
|
||||
};
|
||||
|
||||
// parse_repetition rows (regex.ha:486-545) — DIRECT private-fn table
|
||||
// (the leaf fn lands ahead of its `{`-arm consumer; tranche-A
|
||||
// precedent). Expectations hand-executed from Hare's own code paths:
|
||||
// the input is everything AFTER `{` (compile passes iterstr's rest),
|
||||
// single-arg `{n}` sets max = min and replen = len(n) (ha:500-503,
|
||||
// 539-541); two-arg replen = len(min) + 1 + len(max) (ha:543); an
|
||||
// empty min is 0 (ha:524) while an empty max stays void (ha:527 —
|
||||
// the `{n,}` open bound); a `,` BEYOND the first `}` is not a
|
||||
// two-arg form (ha:499). The two error texts are byte-exact
|
||||
// (ha:492/521/531-536). max_void distinguishes "expect void" from
|
||||
// "expect maxv"; err != "" rows expect that exact error.
|
||||
type prrow = struct {
|
||||
input: str,
|
||||
minv: size,
|
||||
maxv: size,
|
||||
max_void: bool,
|
||||
replen: size,
|
||||
err: str,
|
||||
};
|
||||
|
||||
@test fn parse_repetition_cases() void = {
|
||||
let rows: [13]prrow = [
|
||||
prrow { input = "2}", minv = 2, maxv = 2, max_void = false,
|
||||
replen = 1, err = "" },
|
||||
prrow { input = "2}$", minv = 2, maxv = 2, max_void = false,
|
||||
replen = 1, err = "" },
|
||||
// comma AFTER the endbrace — still single-arg (ha:499)
|
||||
prrow { input = "2},5", minv = 2, maxv = 2, max_void = false,
|
||||
replen = 1, err = "" },
|
||||
prrow { input = "1,2}", minv = 1, maxv = 2, max_void = false,
|
||||
replen = 3, err = "" },
|
||||
prrow { input = ",2}", minv = 0, maxv = 2, max_void = false,
|
||||
replen = 2, err = "" },
|
||||
prrow { input = ",0}", minv = 0, maxv = 0, max_void = false,
|
||||
replen = 2, err = "" },
|
||||
prrow { input = "2,}", minv = 2, maxv = 0, max_void = true,
|
||||
replen = 2, err = "" },
|
||||
prrow { input = ",}", minv = 0, maxv = 0, max_void = true,
|
||||
replen = 1, err = "" },
|
||||
prrow { input = "12,34}xyz", minv = 12, maxv = 34,
|
||||
max_void = false, replen = 5, err = "" },
|
||||
prrow { input = "-1,2}", minv = 0, maxv = 0, max_void = false,
|
||||
replen = 0, err = "Negative repetition count '{-n}'" },
|
||||
prrow { input = "x,2}", minv = 0, maxv = 0, max_void = false,
|
||||
replen = 0,
|
||||
err = "Repetition expression syntax error '{n}'" },
|
||||
prrow { input = "0,-2}", minv = 0, maxv = 0, max_void = false,
|
||||
replen = 0, err = "Negative repetition count '{-n}'" },
|
||||
// no endbrace at all (ha:491-493)
|
||||
prrow { input = "2", minv = 0, maxv = 0, max_void = false,
|
||||
replen = 0,
|
||||
err = "Repetition expression syntax error '{n}'" },
|
||||
];
|
||||
let i: i32 = 0;
|
||||
for (i < len(rows)) {
|
||||
let r: (repparts | error) = parse_repetition(rows[i].input);
|
||||
match (r) {
|
||||
case let t: repparts => {
|
||||
assert(!(rows[i].err.len > 0));
|
||||
// .min is always size after a successful parse
|
||||
// (ha:523-525 — empty min defaults to 0)
|
||||
assert(!(!(t.min is size)));
|
||||
assert(!(t.min as size != rows[i].minv));
|
||||
if (rows[i].max_void) {
|
||||
assert(!(!(t.max is void)));
|
||||
} else {
|
||||
assert(!(!(t.max is size)));
|
||||
assert(!(t.max as size != rows[i].maxv));
|
||||
};
|
||||
assert(!(t.replen != rows[i].replen));
|
||||
};
|
||||
case let e: error => {
|
||||
assert(!(rows[i].err.len == 0));
|
||||
if (strings.compare((e: str), rows[i].err) != 0) {
|
||||
abort();
|
||||
};
|
||||
};
|
||||
};
|
||||
i += 1;
|
||||
};
|
||||
};
|
||||
20
lib/regex/wb/regex_test.ww
Normal file
20
lib/regex/wb/regex_test.ww
Normal file
@@ -0,0 +1,20 @@
|
||||
// wb/regex_test — DOCUMENTED-TEMPORARY scaffolding (rule-7), NOT part of
|
||||
// the blessed white-box convention: the permanent shape is the colocated
|
||||
// lib/regex/regex_whitebox.ww (`package regex`) file alone. This driver
|
||||
// only exists to RUN it, and task #33 ("resolver: dir-enumerate same-dir
|
||||
// same-package siblings on import") DELETES this file when it lands — the
|
||||
// true E-endpoint.
|
||||
//
|
||||
// Why it is load-bearing until then, not incidental: a `import regex` from
|
||||
// a file sitting IN lib/regex/ (e.g. regex_test.ww) FILE-resolves to the
|
||||
// sibling regex.ww and pulls ONLY that file, so the colocated
|
||||
// regex_whitebox.ww is never bundled and its @tests are never collected
|
||||
// (probe E, negative). Placed in a DIFFERENT dir with no regex.ww to
|
||||
// shadow, this driver's `import regex` instead DIRECTORY-resolves the
|
||||
// lib/regex package and bundles every non-*test.ww member — regex.ww +
|
||||
// regex_whitebox.ww — unifying them so -T collects the white-box @tests.
|
||||
// The dir is `wb/`, NOT `test/`: a lib/<mod>/test/ subdir would shadow the
|
||||
// lib/test runner module in the -T auto-bundle search path (task #32).
|
||||
package regex_test;
|
||||
|
||||
import regex;
|
||||
Reference in New Issue
Block a user