diff --git a/lib/regex/regex.ww b/lib/regex/regex.ww index e4cef86b..6ebbc46c 100644 --- a/lib/regex/regex.ww +++ b/lib/regex/regex.ww @@ -7,10 +7,13 @@ // tranche C = search, the first end-to-end match; fold 2b tranche D // = the exec surface (test/find — the D13 explicit-match spelling of // Hare's multi-success `?`, ww-core #14); fold 2c = -// findall/result_freeall over the memio seeker. Every other -// metacharacter arm (and replace) is DEFERRED behind compiler fixes — -// probed pre-port, pA*/pB*/PB*/PC*/PD* probes. They land with those -// fixes. +// findall/result_freeall over the memio seeker; fold 3 = anchors +// `^`/`$`, the `\` escape, postfix `?`/`*`/`+`, alternation `|` +// (jump_idxs state + find_last_groupstart/shift + the insert() +// builtin) and the run_thread split/jump arms. Remaining +// metacharacter arms (`[` bracket, `(`/`)` groups, `{` repetition — +// and replace) are DEFERRED — probed pre-port, pA*/pB*/PB*/PC*/PD*/ +// PE* probes. They land with their folds. // // One fold-1 construct is held back behind a filed compiler/fidelity // gap (see the charclass_map site below): @@ -132,16 +135,65 @@ export fn finish(re: *regex) void = { free(re.charsets); }; +// ref/hare/regex/regex.ha:104-119, verbatim. The error arm is ALWAYS +// taken until the group fold ('(' is loud, no inst_groupstart can +// exist); the '|' arm consumes it as origin = 0 (whole-expression +// alternation). Match-on-indexed scrutinee binds through a typed let +// (the run_thread spelling). +fn find_last_groupstart(insts: []inst) (size | error) = { + let nested: uint = 0; + for (let i: size = (len(insts): size); i > 0; i -= 1) { + let cur: inst = insts[i - 1]; + match (cur) { + case inst_groupstart => { + if (nested == 0) { + return i - 1; + }; + nested -= 1; + }; + case inst_groupend => { nested += 1; }; + case => void; + }; + }; + return "Unmatched ')'": error; +}; + +// Increments all inst_jump and inst_split instructions to account +// for a newly inserted instruction before the given slice. +// +// ref/hare/regex/regex.ha:123-133. Hare's by-ref `&..` range has no +// ww spelling (D9, the add_thread/search precedent) — index loop; +// the write-through is a tagged element store via index (PE3), and +// the sub-slice arg at the call sites (`shift(insts[k:])`) is a +// borrowed VIEW, so the stores land in the caller's backing (PE4). +fn shift(sl: []inst) void = { + for (let i: size = 0; i < (len(sl): size); i += 1) { + let cur: inst = sl[i]; + match (cur) { + case let z: inst_jump => + sl[i] = (((z: size) + 1): inst_jump); + case let z: inst_split => + sl[i] = (((z: size) + 1): inst_split); + case => void; + }; + }; +}; + // Compiles a regular expression string into a [[regex]]. // -// ref/hare/regex/regex.ha:227-263. Fold 2a ports the literal core: -// inst_lit / inst_any / inst_match plus the leading inst_skip -// (regex.ha:261-263 — unanchored exec depends on it). Every other -// metacharacter arm is one loud not-yet-ported error — the explicit -// fold boundary; falling to literal would be a silent semantic lie. -// State serving only the deferred arms is dropped with them: -// jump_idxs (ha:241-248), the bracket quad (ha:249-252), -// was_prev_rune_pipe / group_level / capture_idx (ha:253-256). +// ref/hare/regex/regex.ha:227-263 + the fold-3 arms: `\` escape +// (ha:286-293), anchors `^` (294-300) / `$` (301-312), alternation +// `|` (335-367) over the jump_idxs state (241-248 subset) + +// whole-expression fixup (470-473), postfix `?` (403-420) / `*` +// (421-443) / `+` (444-459). The remaining metacharacter arms (`[` +// bracket, `(`/`)` groups, `{` repetition) are one loud +// not-yet-ported error — the explicit fold boundary; falling to +// literal would be a silent semantic lie. group_level (ha:255) is +// kept verbatim but only ever 0 until the group fold — the anchors' +// group_level arms and the postfix inst_groupend/groupstart arms are +// dead-but-verbatim. capture_idx and the bracket quad stay dropped +// with their arms; the loop-head `done` Unmatched-'(' check +// (ha:278-280) rides the '(' arm. // // Hare's `defer if (!ok) free(...)` cleanup (ha:231-237) is omitted: // ww has no `defer if` (cf lib/strings/strings.ww:85), and the free() @@ -155,7 +207,16 @@ export fn compile(expr: str) (regex | error | nomem) = { let charsets: []charset; // stays empty until the '[' fold let iter: strings.iterator = strings.iter(expr); let r_idx: size = 0; + // jump_idxs tracks the pending alternation jumps per group level; + // only level 0 can populate until the group fold. Hare's + // `append(jump_idxs, [])` (ha:242) spells through a typed empty + // let (the #25/#31 ruling; cf the inst_skip let below). + let jump_idxs: [][]size; + let lvl0: []size; + append(jump_idxs, lvl0); + let was_prev_rune_pipe: bool = false; let n_reps: size = 0; + let group_level: size = 0; for (true) { let next: (rune | utf8.done) = strings.next(&iter); @@ -172,14 +233,184 @@ export fn compile(expr: str) (regex | error | nomem) = { append(insts, v); }; - // regex.ha:277-284 minus the group_level check (it rides - // the deferred '(' arm's state). let r: rune = match (next) { case utf8.done => break; case let x: rune => yield x; }; switch (r) { + case '\\': { // regex.ha:286-293 + let peek1: (rune | utf8.done) = strings.next(&iter); + if (peek1 is utf8.done) { + return "Trailing backslash '\\'": error; + }; + append(insts, ((peek1 as rune): inst_lit)); + r_idx += 1; + }; + case '^': { // regex.ha:294-300 + if (group_level > 0) { + return "Anchor '^' in capture groups is unsupported": error; + }; + if (!(r_idx == 0 || was_prev_rune_pipe)) { + return "Anchor '^' not at start of whole pattern or alternation": error; + }; + }; + case '$': { // regex.ha:301-312 + if (group_level > 0) { + return "Anchor '$' in capture groups is unsupported": error; + }; + let peek1: (rune | utf8.done) = strings.next(&iter); + if (peek1 is rune) { + if ((peek1 as rune) != '|') { + return "Anchor '$' not at end of whole pattern or alternation": error; + }; + strings.prev(&iter); + }; + append(insts, (true: inst_match)); + }; + case '|': { // regex.ha:335-367 + append(insts, (types.SIZE_MAX: inst_jump)); + // origin = the instruction after the innermost + // groupstart, or 0 for whole-expression alternation + // (the error arm — always taken until the group + // fold). Hare binds via a match EXPRESSION + // (ha:337-342); statement-match into a declared + // local, the search/scanrune precedent (#51). + let origin: size = 0; + match (find_last_groupstart(insts)) { + case let e: error => { origin = 0; }; + case let sz: size => { origin = sz + 1; }; + }; + let newinst: inst = (((insts.len: size) + 1): inst_split); + // add split after last jump (if any) or at origin. + // Hare's if-EXPRESSION (ha:345-346) spells as a + // statement; the tail read is the PE2 double-index. + let split_idx: size = origin; + if (jump_idxs[group_level].len > 0) { + split_idx = jump_idxs[group_level][jump_idxs[group_level].len - 1] + 1; + }; + insert(insts[split_idx], newinst); + shift(insts[split_idx + 1:]); + // our insertion of our split_idx should never + // interfere with an existing jump_idx; if this check + // ends up being hit in the future, it is a sign that + // jump_idx should be incremented. Hare asserts + // (ha:351-356); the assert builtin is wwstage-broken + // (ww-core #58) and the by-value range over an + // indexed element segfaults (ww-core #57) — if+abort + // over a D9-class index loop. + for (let k: size = 0; k < (jump_idxs[group_level].len: size); k += 1) { + if (!(jump_idxs[group_level][k] < split_idx)) { + abort("Found jump_idx interference. Please report this as a bug"); + }; + }; + append(jump_idxs[group_level], (insts.len: size) - 1); + // add skip if it's a whole-expression alternation + if (origin == 0) { + let peek1: (rune | utf8.done) = strings.next(&iter); + if (peek1 is rune) { + if ((peek1 as rune) != '^') { + let sk: inst_skip; + let v: inst = sk; + append(insts, v); + }; + strings.prev(&iter); + }; + }; + }; + case '?': { // regex.ha:403-420 + if (r_idx == 0 || insts.len == 0) { + return "Unused '?'": error; + }; + let term_start_idx: size = (insts.len: size) - 1; + // Hare's multi-type arm `case (inst_lit | + // inst_charset | inst_any)` (ha:407) is loud-rejected + // by design BOTH stages (PE5; Hare-parity task + // ww-core #13) — three void arms. The scrutinee binds + // through a typed let (the run_thread spelling). + let cur: inst = insts[term_start_idx]; + match (cur) { + case inst_lit => void; + case inst_charset => void; + case inst_any => void; + case inst_groupend => { + // dead until the group fold ('(' is loud). + // Hare propagates with `?` (ha:410-411); + // `?` into compile's >32B tagged return is + // loud-stopped (#38b) — the explicit D13 + // match, harec's own desugaring. + match (find_last_groupstart( + insts[0:term_start_idx])) { + case let e: error => return e; + case let sz: size => { term_start_idx = sz; }; + }; + }; + case inst_groupstart => + return "Unused '?'": error; + case => + return "Misused '?'": error; + }; + let after_idx: size = (insts.len: size) + 1; + insert(insts[term_start_idx], (after_idx: inst_split)); + shift(insts[term_start_idx + 1:]); + }; + case '*': { // regex.ha:421-443 + if (r_idx == 0 || insts.len == 0) { + return "Unused '*'": error; + }; + let new_inst_offset: size = 1; + let jump_idx: size = (insts.len: size) + new_inst_offset; + let after_idx: size = jump_idx + 1; + let term_start_idx: size = (insts.len: size) - 1; + let cur: inst = insts[term_start_idx]; + match (cur) { + case inst_lit => void; + case inst_charset => void; + case inst_any => void; + case inst_groupend => { + // dead until the group fold; D13 match, cf '?' + match (find_last_groupstart( + insts[0:term_start_idx])) { + case let e: error => return e; + case let sz: size => { term_start_idx = sz; }; + }; + }; + case inst_groupstart => + return "Unused '*'": error; + case => + return "Misused '*'": error; + }; + let split_idx: size = term_start_idx; + term_start_idx += new_inst_offset; + insert(insts[split_idx], (after_idx: inst_split)); + shift(insts[split_idx + 1:]); + append(insts, (split_idx: inst_jump)); + }; + case '+': { // regex.ha:444-459 + if (r_idx == 0 || insts.len == 0) { + return "Unused '+'": error; + }; + let term_start_idx: size = (insts.len: size) - 1; + let cur: inst = insts[term_start_idx]; + match (cur) { + case inst_lit => void; + case inst_charset => void; + case inst_any => void; + case inst_groupend => { + // dead until the group fold; D13 match, cf '?' + match (find_last_groupstart( + insts[0:term_start_idx])) { + case let e: error => return e; + case let sz: size => { term_start_idx = sz; }; + }; + }; + case inst_groupstart => + return "Unused '+'": error; + case => + return "Misused '+'": error; + }; + append(insts, (term_start_idx: inst_split)); + }; case '.': { // regex.ha:460-461 let av: inst_any; let v: inst = av; @@ -187,22 +418,36 @@ export fn compile(expr: str) (regex | error | nomem) = { }; case ']': // regex.ha:315-316 — literal outside a bracket append(insts, (r: inst_lit)); - case '\\', '^', '$', '[', '(', ')', '|', '{', '?', '*', '+': - // fold-2a boundary: regex.ha:286-459 arms deferred. + case '[', '(', ')', '{': + // fold-3 boundary: the bracket (ha:313-334 + 268-275), + // group (317-334) and repetition (368-401) arms ride + // later folds. return "regex: metacharacter not yet ported": error; case: // regex.ha:462-463 append(insts, (r: inst_lit)); }; + was_prev_rune_pipe = (r == '|'); // regex.ha:465 r_idx += 1; }; - // regex.ha:475-477. `$` appends true: inst_match — deferred, so - // the guard can only see no-match today; kept verbatim. + // handle whole expression alternation (regex.ha:470-473). Index + // loop + if+abort: the by-value range over an indexed element + // segfaults (#57) and the assert builtin is wwstage-broken (#58). + for (let k: size = 0; k < (jump_idxs[0].len: size); k += 1) { + let jump_idx: size = jump_idxs[0][k]; + let cur: inst = insts[jump_idx]; + if (!(cur is inst_jump)) { + abort("regex: alternation fixup: not a jump"); + }; + insts[jump_idx] = ((insts.len: size): inst_jump); + }; + + // regex.ha:475-477. `$` appends true: inst_match, so the guard + // sees real anchored programs now. if (insts.len == 0 || !(insts[insts.len - 1] is inst_match)) { append(insts, (false: inst_match)); }; - // regex.ha:479-484. The alternation fixup (ha:470-473) drops with - // jump_idxs. + // regex.ha:479-484. return regex { insts = insts, charsets = charsets, @@ -277,8 +522,9 @@ fn add_thread(threads: *[]thread, parent_idx: size, new_pc: size) (void | nomem) return; }; -// ref/hare/regex/regex.ha:589-742. Only the arms fold-2a's compile() -// can emit execute (skip + match non-consuming; lit + any consuming); +// ref/hare/regex/regex.ha:589-742. Only the arms compile() can emit +// execute (skip + split + jump + match non-consuming; lit + any +// consuming — split/jump went live with fold 3's `|`/`?`/`*` arms); // every other inst arm is one loud not-yet-ported abort — the fold // boundary, the compile() metachar discipline. The "unreachable" // aborts on lit/any inside the non-consuming loop (ha:604-605) and @@ -307,10 +553,13 @@ fn run_thread( match (re.insts[(*threads)[i].pc]) { case inst_lit => abort("regex: unreachable"); case inst_any => abort("regex: unreachable"); - case inst_split => - abort("regex: inst_split not yet ported"); - case inst_jump => - abort("regex: inst_jump not yet ported"); + case let z: inst_split => { // regex.ha:606-609 + let new_pc: size = (z: size); + add_thread(threads, i, new_pc)?; + (*threads)[i].pc += 1; + }; + case let z: inst_jump => // regex.ha:610-611 + (*threads)[i].pc = (z: size); case inst_skip => { let new_pc: size = (*threads)[i].pc + 1; (*threads)[i].start_idx = str_idx; diff --git a/lib/regex/regex_test.ww b/lib/regex/regex_test.ww index 4f864c96..22b0b6b9 100644 --- a/lib/regex/regex_test.ww +++ b/lib/regex/regex_test.ww @@ -260,13 +260,12 @@ fn fail() void = { os.exit(signalled + 10); }; // fold-boundary text — falling through to the literal default would // silently compile a wrong program, and any OTHER error text would // mean an arm was half-ported. One pattern per deferred arm so the -// fold that ports an arm consciously deletes its row. '^' leads its -// pattern: the r_idx==0 skip gate (regex.ha:261) must compose with -// the loud arm, not bypass it. +// fold that ports an arm consciously deletes its row. Fold 3 flipped +// \ ^ $ | ? * + positive (fold3_* below); the bracket/group/ +// repetition four remain. @test fn compile_metachar_loud() void = { - let pats: [11]str = [ - "a\\", "^a", "a$", "a[", "a(", "a)", - "a|", "a{", "a?", "a*", "a+", + let pats: [4]str = [ + "a[", "a(", "a)", "a{", ]; let i: i32 = 0; for (i < len(pats)) { @@ -420,7 +419,7 @@ type nmexp = struct { // through a REAL compile() error, completing the exported error // surface end to end. @test fn strerror_identity() void = { - match (regex.compile("a*")) { + match (regex.compile("a[")) { case let e: regex.error => { if (strings.compare(regex.strerror(e), "regex: metacharacter not yet ported") != 0) { @@ -981,7 +980,7 @@ type fdexp = struct { }; @test fn findall_fields() void = { - let exp: [10]fdexp = [ + 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" }, @@ -1002,8 +1001,16 @@ type fdexp = struct { // ("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: [7]fdcase = [ + 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 }, @@ -1012,6 +1019,7 @@ type fdexp = struct { 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)) { @@ -1051,6 +1059,298 @@ type fdexp = struct { }; }; + +// ---- 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 => return 9999; + }; +}; + +// Emitted-program pins for the fold-3 arms — deterministic, engine- +// independent: the exact inst sequence (kinds + jump/split targets) +// each metachar must compile to. Derived by hand-executing +// regex.ha:286-473 (insert-before + shift + the SIZE_MAX-sentinel +// jump fixup); the a|b row pins the whole jump_idxs pipeline +// including the sentinel overwrite at ha:470-473. +type pgmcase = struct { + expr: str, + soff: i32, + scnt: i32, +}; + +@test fn fold3_programs() void = { + let sigs: [25]i64 = [ + // "^a": anchored — no leading skip + 1097, 6000, + // "a$": skip, lit a, match(TRUE) + 2000, 1097, 6001, + // "a?": split jumps OVER the lit to the match + 2000, 4003, 1097, 6000, + // "a*": split to match; jump back to the split + 2000, 4004, 1097, 5001, 6000, + // "a+": split back to the lit + 2000, 1097, 4001, 6000, + // "a|b": leading split to the second branch's skip; the + // first branch's jump lands on the epilogue match (the + // fixed-up SIZE_MAX sentinel) + 4004, 2000, 1097, 5006, 2000, 1098, 6000, + ]; + let rows: [6]pgmcase = [ + pgmcase { expr = "^a", soff = 0, scnt = 2 }, + pgmcase { expr = "a$", soff = 2, scnt = 3 }, + pgmcase { expr = "a?", soff = 5, scnt = 4 }, + pgmcase { expr = "a*", soff = 9, scnt = 5 }, + pgmcase { expr = "a+", soff = 14, scnt = 4 }, + pgmcase { expr = "a|b", soff = 18, scnt = 7 }, + ]; + let i: i32 = 0; + for (i < len(rows)) { + let ex: str = rows[i].expr; + let c: (regex.regex | regex.error | nomem) = regex.compile(ex); + match (c) { + case let re: regex.regex => { + if (re.insts.len != rows[i].scnt) { fail(); }; + let k: i32 = 0; + for (k < rows[i].scnt) { + if (instsig(re.insts[k]) + != sigs[rows[i].soff + k]) { + fail(); + }; + k += 1; + }; + regex.finish(&re); + }; + case => fail(); + }; + i += 1; + }; +}; + +// The fold-3 compile-error surface, exact texts (regex.ha:289 / 296-299 +// / 303-308 / 405-417 / 423-435 / 446-455). The "ab\|^cd" row is +// Hare's own ERROR fixture (+test.ha:634) — the escaped '|' must NOT +// set was_prev_rune_pipe, so the following '^' misplaces. +type cerow = struct { + pat: str, + want: str, +}; + +@test fn fold3_compile_errors() void = { + let rows: [12]cerow = [ + cerow { pat = "\\", want = "Trailing backslash '\\'" }, + cerow { pat = "a\\", want = "Trailing backslash '\\'" }, + cerow { pat = "a^", + want = "Anchor '^' not at start of whole pattern or alternation" }, + cerow { pat = "$a", + want = "Anchor '$' not at end of whole pattern or alternation" }, + cerow { pat = "ab\\|^cd", + want = "Anchor '^' not at start of whole pattern or alternation" }, + cerow { pat = "?", want = "Unused '?'" }, + cerow { pat = "*", want = "Unused '*'" }, + cerow { pat = "+", want = "Unused '+'" }, + // '^' appends nothing, so insts is still empty (ha:404's + // len check, not the r_idx one) + cerow { pat = "^*", want = "Unused '*'" }, + cerow { pat = "a*?", want = "Misused '?'" }, + cerow { pat = "a**", want = "Misused '*'" }, + cerow { pat = "a*+", want = "Misused '+'" }, + ]; + let i: i32 = 0; + for (i < len(rows)) { + let p: str = rows[i].pat; + match (regex.compile(p)) { + case let e: regex.error => { + let w: str = rows[i].want; + if (strings.compare((e: str), w) != 0) { fail(); }; + }; + case => fail(); + }; + i += 1; + }; +}; + +// find_last_groupstart (regex.ha:104-119) — driven directly (private +// fn): no inst_groupstart exists in any fold-3 program, so the error +// arm is the live one; pin its exact text. A hand-built groupstart +// row pins the success arm the group fold will rely on. +@test fn find_last_groupstart_cases() void = { + let insts: []regex.inst = []; + append(insts, ('a': regex.inst_lit)); + match (find_last_groupstart(insts)) { + case let e: regex.error => { + if (strings.compare((e: str), "Unmatched ')'") != 0) { + fail(); + }; + }; + case => fail(); + }; + append(insts, ((1: size): regex.inst_groupstart)); + append(insts, ('b': regex.inst_lit)); + match (find_last_groupstart(insts)) { + case let sz: size => { if (sz != 1) { fail(); }; }; + case => fail(); + }; +}; + +// shift (regex.ha:123-133) — driven directly over a sub-slice view: +// jump/split payloads in the view bump by one, the element before the +// view and non-jump kinds are untouched (the PE3/PE4 shapes). +@test fn shift_direct() void = { + let insts: []regex.inst = []; + append(insts, ((3: size): regex.inst_jump)); + append(insts, ('a': regex.inst_lit)); + append(insts, ((5: size): regex.inst_split)); + append(insts, ((7: size): regex.inst_jump)); + shift(insts[1:]); + if (instsig(insts[0]) != 5003) { fail(); }; + if (instsig(insts[1]) != 1097) { fail(); }; + if (instsig(insts[2]) != 4006) { fail(); }; + if (instsig(insts[3]) != 5008) { fail(); }; +}; + +// fold-3 find/test rows — the group-free subset of Hare's own table +// (+test.ha:221-256 anchors/postfix, :622-650 whole-expression and +// multiple alternation; end == -1 resolved to rune-length per +// +test.ha:693-697) plus rob's dedup/leftmost/longest riders: +// `a*` over "aaaa" must yield ONE (0,4) (split spawns same-pc threads +// every step — the ha:872-889 dedup pin gone observable), `b+` over +// "abab" pins the leftmost trim (1,2 not 3,4), `b*`/`^b*` over +// "aaaabbbb" pin longest-pick vs anchored zero-length. The multibyte +// `b+` row keeps every idx != bytesize (B4). Reuses the fcase shape; +// every row also cross-pins test() == (find() matched). +@test fn fold3_find_cases() void = { + let rows: [42]fcase = [ + fcase { expr = "^abc$", input = "abc", matches = true, + start = 0, sb = 0, end = 3, eb = 3, content = "abc" }, + fcase { expr = "^abc$", input = "axc", matches = false, ... }, + fcase { expr = "^.$", input = "x", matches = true, + start = 0, sb = 0, end = 1, eb = 1, content = "x" }, + fcase { expr = "^.$", input = "", matches = false, ... }, + fcase { expr = "^a+$", input = "a", matches = true, + start = 0, sb = 0, end = 1, eb = 1, content = "a" }, + fcase { expr = "^a+$", input = "aaa", matches = true, + start = 0, sb = 0, end = 3, eb = 3, content = "aaa" }, + fcase { expr = "^a+$", input = "", matches = false, ... }, + fcase { expr = "^a*$", input = "", matches = true, + start = 0, sb = 0, end = 0, eb = 0, content = "" }, + fcase { expr = "^a*$", input = "aaaa", matches = true, + start = 0, sb = 0, end = 4, eb = 4, content = "aaaa" }, + fcase { expr = "^a*$", input = "b", matches = false, ... }, + fcase { expr = "^a?$", input = "", matches = true, + start = 0, sb = 0, end = 0, eb = 0, content = "" }, + fcase { expr = "^a?$", input = "a", matches = true, + start = 0, sb = 0, end = 1, eb = 1, content = "a" }, + fcase { expr = "^a?$", input = "b", matches = false, ... }, + fcase { expr = "^a*", input = "aaaa", matches = true, + start = 0, sb = 0, end = 4, eb = 4, content = "aaaa" }, + fcase { expr = "a*$", input = "aaaa", matches = true, + start = 0, sb = 0, end = 4, eb = 4, content = "aaaa" }, + fcase { expr = "a*", input = "aaaa", matches = true, + start = 0, sb = 0, end = 4, eb = 4, content = "aaaa" }, + fcase { expr = "b*", input = "aaaabbbb", matches = true, + start = 4, sb = 4, end = 8, eb = 8, content = "bbbb" }, + fcase { expr = "^b*", input = "aaaabbbb", matches = true, + start = 0, sb = 0, end = 0, eb = 0, content = "" }, + fcase { expr = "b*$", input = "aaaabbbb", matches = true, + start = 4, sb = 4, end = 8, eb = 8, content = "bbbb" }, + fcase { expr = "b+", input = "abab", matches = true, + start = 1, sb = 1, end = 2, eb = 2, content = "b" }, + // multibyte rider: 2-byte ß before the b's splits every + // idx from its bytesize + fcase { expr = "b+", input = "aßbb", matches = true, + start = 2, sb = 3, end = 4, eb = 5, content = "bb" }, + fcase { expr = "ab|cd", input = "cd", matches = true, + start = 0, sb = 0, end = 2, eb = 2, content = "cd" }, + fcase { expr = "ab|cd", input = "abc", matches = true, + start = 0, sb = 0, end = 2, eb = 2, content = "ab" }, + fcase { expr = "ab|cd", input = "abcd", matches = true, + start = 0, sb = 0, end = 2, eb = 2, content = "ab" }, + fcase { expr = "ab|cd", input = "bcd", matches = true, + start = 1, sb = 1, end = 3, eb = 3, content = "cd" }, + fcase { expr = "^ab|cd", input = "bcd", matches = true, + start = 1, sb = 1, end = 3, eb = 3, content = "cd" }, + fcase { expr = "^ab|cd", input = "zab", matches = false, ... }, + fcase { expr = "ab$|cd", input = "ab", matches = true, + start = 0, sb = 0, end = 2, eb = 2, content = "ab" }, + fcase { expr = "ab$|cd", input = "abc", matches = false, ... }, + fcase { expr = "ab|cd$", input = "cde", matches = false, ... }, + fcase { expr = "ab|^cd", input = "bcd", matches = false, ... }, + fcase { expr = "ab|^cd", input = "cde", matches = true, + start = 0, sb = 0, end = 2, eb = 2, content = "cd" }, + fcase { expr = "a|b|c|d|e", input = "e", matches = true, + start = 0, sb = 0, end = 1, eb = 1, content = "e" }, + fcase { expr = "a|b|c|d|e", input = "xe", matches = true, + start = 1, sb = 1, end = 2, eb = 2, content = "e" }, + fcase { expr = "a|b$|c$|d$|e", input = "cd", matches = true, + start = 1, sb = 1, end = 2, eb = 2, content = "d" }, + fcase { expr = "a|b$|c$|d$|e", input = "ax", matches = true, + start = 0, sb = 0, end = 1, eb = 1, content = "a" }, + fcase { expr = "a|b$|c$|d$|e", input = "cx", matches = false, ... }, + fcase { expr = "a|b$|c$|d$|e", input = "ex", matches = true, + start = 0, sb = 0, end = 1, eb = 1, content = "e" }, + fcase { expr = "a|^b|^c|^d|e", input = "cd", matches = true, + start = 0, sb = 0, end = 1, eb = 1, content = "c" }, + fcase { expr = "a|^b|^c|^d|e", input = "xa", matches = true, + start = 1, sb = 1, end = 2, eb = 2, content = "a" }, + fcase { expr = "a|^b|^c|^d|e", input = "xc", matches = false, ... }, + fcase { expr = "a|^b|^c|^d|e", input = "xe", matches = true, + start = 1, sb = 1, end = 2, eb = 2, content = "e" }, + ]; + let i: i32 = 0; + for (i < len(rows)) { + let ex: str = rows[i].expr; + let inp: str = rows[i].input; + let c: (regex.regex | regex.error | nomem) = regex.compile(ex); + match (c) { + case let re: regex.regex => { + let fr: (regex.result | nomem) = regex.find(&re, inp); + if (!(fr is regex.result)) { fail(); }; + let res: regex.result = fr as regex.result; + if (rows[i].matches) { + if (len(res) != 1) { fail(); }; + if (res[0].start != rows[i].start) { fail(); }; + if (res[0].start_bytesize != rows[i].sb) { + fail(); + }; + if (res[0].end != rows[i].end) { fail(); }; + if (res[0].end_bytesize != rows[i].eb) { + fail(); + }; + let wc: str = rows[i].content; + if (strings.compare(res[0].content, wc) != 0) { + fail(); + }; + } else { + if (len(res) != 0) { fail(); }; + }; + let tr: (bool | nomem) = regex.test(&re, inp); + if (!(tr is bool)) { fail(); }; + if ((tr as bool) != (len(res) != 0)) { fail(); }; + regex.result_free(res); + regex.finish(&re); + }; + case => fail(); + }; + i += 1; + }; +}; + export fn main() i32 = { signalled = 1; lit_and_match(); signalled = 2; size_aliases_distinct(); @@ -1078,5 +1378,10 @@ export fn main() i32 = { signalled = 24; find_cases(); signalled = 25; findall_content(); signalled = 26; findall_fields(); + signalled = 27; fold3_programs(); + signalled = 28; fold3_compile_errors(); + signalled = 29; find_last_groupstart_cases(); + signalled = 30; shift_direct(); + signalled = 31; fold3_find_cases(); return 0; };