Ports ref/hare/regex/regex.ha:227-263 literal arms: leading unanchored inst_skip, inst_lit / inst_any, epilogue inst_match(false). Every deferred metacharacter arm returns a loud not-yet-ported error (the fold boundary); state serving only deferred arms drops with them. Hare free()/defer-if cleanup omitted (no-free runtime, #27); bare append per #36. 4 new @test rows pin the emitted programs incl. the empty-input and loud-boundary cases; compile()'s >24B tagged return doubles as a #38 sret consumer. Rides #34/#38/#44/#45/#48 — all five fold-2a blockers now closed on master.
269 lines
8.4 KiB
Plaintext
269 lines
8.4 KiB
Plaintext
// regex_test — exercises the lib/regex fold-1 data model (the type
|
|
// model + finish()) and the fold-2a compile() literal core. Run with
|
|
// `out/bin/ww run lib/regex/regex_test.ww`.
|
|
//
|
|
// Fold 2a ports compile()'s lit/any/match arms only; exec lives in
|
|
// later folds, so the compile_* cases pin the emitted inst PROGRAM
|
|
// (shape + payloads via indexed match-extraction), not matching.
|
|
// charclass_map's fn-ptr table is deferred behind the array→slice
|
|
// element-coercion checker gap (see regex.ww), so this test does not
|
|
// exercise the POSIX-class predicate dispatch yet — it pins variant
|
|
// discrimination (including the nominally-distinct same-underlying
|
|
// inst_split/inst_jump/inst_groupstart `size` aliases and the
|
|
// inst_any/inst_skip/inst_groupend `void` aliases), payload extraction,
|
|
// the regex/capture struct shapes, and finish(). Same
|
|
// signalled-then-fail()-with-+10 pattern as the rest of the stdlib
|
|
// run-tests; the non-zero exit pinpoints the failing case.
|
|
//
|
|
// Struct literals below name the type UNQUALIFIED (`inst_charset { … }`,
|
|
// not `regex.inst_charset { … }`): the parser rejects a module-qualified
|
|
// name in struct-literal position (#29), and the imported type
|
|
// is in scope unqualified.
|
|
package regex;
|
|
|
|
import regex;
|
|
import os;
|
|
|
|
let signalled: i32 = 0;
|
|
fn fail() void = { os.exit(signalled + 10); };
|
|
|
|
// inst_lit / inst_match carry distinguishable payloads (rune / bool).
|
|
@test fn lit_and_match() void = {
|
|
let a: regex.inst = ('a': regex.inst_lit);
|
|
match (a) {
|
|
case let l: regex.inst_lit => { if ((l: rune) != 'a') { fail(); }; };
|
|
case => fail();
|
|
};
|
|
|
|
let m: regex.inst = (true: regex.inst_match);
|
|
match (m) {
|
|
case let b: regex.inst_match => { if (!(b: bool)) { fail(); }; };
|
|
case => fail();
|
|
};
|
|
};
|
|
|
|
// The three `size`-aliased variants are nominally distinct: a value
|
|
// built as inst_split must match inst_split, never inst_jump /
|
|
// inst_groupstart, despite identical underlying storage.
|
|
@test fn size_aliases_distinct() void = {
|
|
let sp: regex.inst = ((5: size): regex.inst_split);
|
|
match (sp) {
|
|
case let s: regex.inst_split => { if ((s: size) != (5: size)) { fail(); }; };
|
|
case let j: regex.inst_jump => fail();
|
|
case let g: regex.inst_groupstart => fail();
|
|
case => fail();
|
|
};
|
|
|
|
let jp: regex.inst = ((9: size): regex.inst_jump);
|
|
match (jp) {
|
|
case let j: regex.inst_jump => { if ((j: size) != (9: size)) { fail(); }; };
|
|
case let s: regex.inst_split => fail();
|
|
case => fail();
|
|
};
|
|
|
|
let gs: regex.inst = ((2: size): regex.inst_groupstart);
|
|
match (gs) {
|
|
case let g: regex.inst_groupstart => { if ((g: size) != (2: size)) { fail(); }; };
|
|
case let s: regex.inst_split => fail();
|
|
case => fail();
|
|
};
|
|
};
|
|
|
|
// The `void`-aliased variants are likewise nominally distinct.
|
|
@test fn void_aliases_distinct() void = {
|
|
let av: regex.inst_any;
|
|
let an: regex.inst = av;
|
|
match (an) {
|
|
case let a: regex.inst_any => void;
|
|
case let k: regex.inst_skip => fail();
|
|
case let e: regex.inst_groupend => fail();
|
|
case => fail();
|
|
};
|
|
|
|
let sv: regex.inst_skip;
|
|
let sk: regex.inst = sv;
|
|
match (sk) {
|
|
case let k: regex.inst_skip => void;
|
|
case let a: regex.inst_any => fail();
|
|
case => fail();
|
|
};
|
|
|
|
let gv: regex.inst_groupend;
|
|
let ge: regex.inst = gv;
|
|
match (ge) {
|
|
case let e: regex.inst_groupend => void;
|
|
case let a: regex.inst_any => fail();
|
|
case let k: regex.inst_skip => fail();
|
|
case => fail();
|
|
};
|
|
};
|
|
|
|
// inst_charset carries a struct payload; its fields survive the union
|
|
// round-trip.
|
|
@test fn charset_payload() void = {
|
|
let c: regex.inst = (inst_charset { idx = 3, is_positive = true });
|
|
match (c) {
|
|
case let cs: regex.inst_charset => {
|
|
if (cs.idx != (3: size)) { fail(); };
|
|
if (!cs.is_positive) { fail(); };
|
|
};
|
|
case => fail();
|
|
};
|
|
};
|
|
|
|
// inst_repeat round-trips through the inst union with its plain `size`
|
|
// fields intact. Matching the nested (void | size) min/max bounds back
|
|
// out is DEFERRED: `match` on a tagged-union-typed struct field
|
|
// diverges cs≠ww (#26 — the wwstage frames it wider), so
|
|
// asserting the bounds here would seed a rule-10-divergent fixture.
|
|
@test fn repeat_payload() void = {
|
|
let r: regex.inst = (inst_repeat {
|
|
id = 1, origin = 4, min = (2: size), max = void,
|
|
});
|
|
match (r) {
|
|
case let rp: regex.inst_repeat => {
|
|
if (rp.id != (1: size)) { fail(); };
|
|
if (rp.origin != (4: size)) { fail(); };
|
|
};
|
|
case => fail();
|
|
};
|
|
};
|
|
|
|
// The regex/capture structs hold their fields; finish() is a no-op
|
|
// (no-free runtime) and must accept a built regex.
|
|
@test fn struct_shapes_and_finish() void = {
|
|
let cap: regex.capture = capture {
|
|
content = "abc",
|
|
start = 0,
|
|
start_bytesize = 0,
|
|
end = 3,
|
|
end_bytesize = 3,
|
|
};
|
|
if (cap.content.len != 3) { fail(); };
|
|
if (cap.end != (3: size)) { fail(); };
|
|
|
|
// regex's insts/charsets ([]inst / []charset) are left empty here:
|
|
// fold 1 ports no compile() to populate them, an empty `[]` literal
|
|
// is unspellable as a typed slice (#25 — array→slice
|
|
// element-coercion gap), and a struct-literal slice-field store
|
|
// drops len/cap (#24). Declaring the regex zeroes both
|
|
// slice headers to {0,0,0}; only n_reps is set explicitly.
|
|
let re: regex.regex;
|
|
re.n_reps = 0;
|
|
if (re.n_reps != (0: size)) { fail(); };
|
|
if (re.insts.len != 0) { fail(); };
|
|
regex.finish(&re);
|
|
};
|
|
|
|
// compile("abc") emits the 5-inst literal program: the leading
|
|
// unanchored inst_skip (regex.ha:261-263), one inst_lit per rune, the
|
|
// epilogue inst_match(false) (ha:475-477). compile()'s
|
|
// (regex | error | nomem) return is the first >24B tagged payload in
|
|
// the tree — the receive shapes here double as #38 sret consumers
|
|
// (typed-let + match here; scrutinee-direct in compile_empty_program).
|
|
@test fn compile_literal_program() void = {
|
|
let c: (regex.regex | regex.error | nomem) = regex.compile("abc");
|
|
match (c) {
|
|
case let re: regex.regex => {
|
|
if (re.insts.len != 5) { fail(); };
|
|
match (re.insts[0]) {
|
|
case let k: regex.inst_skip => void;
|
|
case => fail();
|
|
};
|
|
match (re.insts[1]) {
|
|
case let l: regex.inst_lit => { if ((l: rune) != 'a') { fail(); }; };
|
|
case => fail();
|
|
};
|
|
match (re.insts[2]) {
|
|
case let l: regex.inst_lit => { if ((l: rune) != 'b') { fail(); }; };
|
|
case => fail();
|
|
};
|
|
match (re.insts[3]) {
|
|
case let l: regex.inst_lit => { if ((l: rune) != 'c') { fail(); }; };
|
|
case => fail();
|
|
};
|
|
match (re.insts[4]) {
|
|
case let m: regex.inst_match => { if ((m: bool)) { fail(); }; };
|
|
case => fail();
|
|
};
|
|
if (re.charsets.len != 0) { fail(); };
|
|
if (re.n_reps != (0: size)) { fail(); };
|
|
regex.finish(&re);
|
|
};
|
|
case => fail();
|
|
};
|
|
};
|
|
|
|
// '.' compiles to inst_any between the literals (regex.ha:460-461):
|
|
// [skip, lit 'a', any, lit 'c', match(false)].
|
|
@test fn compile_any_program() void = {
|
|
let c: (regex.regex | regex.error | nomem) = regex.compile("a.c");
|
|
match (c) {
|
|
case let re: regex.regex => {
|
|
if (re.insts.len != 5) { fail(); };
|
|
match (re.insts[0]) {
|
|
case let k: regex.inst_skip => void;
|
|
case => fail();
|
|
};
|
|
match (re.insts[1]) {
|
|
case let l: regex.inst_lit => { if ((l: rune) != 'a') { fail(); }; };
|
|
case => fail();
|
|
};
|
|
match (re.insts[2]) {
|
|
case let a: regex.inst_any => void;
|
|
case => fail();
|
|
};
|
|
match (re.insts[3]) {
|
|
case let l: regex.inst_lit => { if ((l: rune) != 'c') { fail(); }; };
|
|
case => fail();
|
|
};
|
|
match (re.insts[4]) {
|
|
case let m: regex.inst_match => { if ((m: bool)) { fail(); }; };
|
|
case => fail();
|
|
};
|
|
regex.finish(&re);
|
|
};
|
|
case => fail();
|
|
};
|
|
};
|
|
|
|
// compile("") is exactly [inst_match(false)]: the leading skip must
|
|
// not fire on immediate done (regex.ha:261 gates on `next is rune`),
|
|
// and the epilogue guard must fire on the empty program.
|
|
@test fn compile_empty_program() void = {
|
|
match (regex.compile("")) {
|
|
case let re: regex.regex => {
|
|
if (re.insts.len != 1) { fail(); };
|
|
match (re.insts[0]) {
|
|
case let m: regex.inst_match => { if ((m: bool)) { fail(); }; };
|
|
case => fail();
|
|
};
|
|
regex.finish(&re);
|
|
};
|
|
case => fail();
|
|
};
|
|
};
|
|
|
|
// Any deferred metacharacter is a LOUD error — pins the fold-2a
|
|
// boundary so the fold that ports '*' consciously deletes this row.
|
|
@test fn compile_metachar_loud() void = {
|
|
match (regex.compile("a*")) {
|
|
case let e: regex.error => { if ((e: str).len == 0) { fail(); }; };
|
|
case => fail();
|
|
};
|
|
};
|
|
|
|
export fn main() i32 = {
|
|
signalled = 1; lit_and_match();
|
|
signalled = 2; size_aliases_distinct();
|
|
signalled = 3; void_aliases_distinct();
|
|
signalled = 4; charset_payload();
|
|
signalled = 5; repeat_payload();
|
|
signalled = 6; struct_shapes_and_finish();
|
|
signalled = 7; compile_literal_program();
|
|
signalled = 8; compile_any_program();
|
|
signalled = 9; compile_empty_program();
|
|
signalled = 10; compile_metachar_loud();
|
|
return 0;
|
|
};
|