diff --git a/lib/regex/regex.ww b/lib/regex/regex.ww index 86c79d0b..e5677f3b 100644 --- a/lib/regex/regex.ww +++ b/lib/regex/regex.ww @@ -12,10 +12,12 @@ // (jump_idxs state + find_last_groupstart/shift + the insert() // builtin) and the run_thread split/jump arms; fold 4 = bracket // expressions `[..]` (handle_bracket + the run_thread charset arm — -// the POSIX `[[:class:]]` BODY stays loud behind charclass_map). -// Remaining metacharacter arms (`(`/`)` groups, `{` repetition — and -// replace) are DEFERRED — probed pre-port, pA*/pB*/PB*/PC*/PD*/PE*/ -// PF* probes. They land with their folds. +// the POSIX `[[:class:]]` BODY stays loud behind charclass_map); +// fold 5a = capture groups `(`/`)` (compile arms + run_thread +// groupstart/groupend + the add_thread capture dup + the search +// capture spread). Remaining arms (`{` repetition — and replace) are +// DEFERRED — probed pre-port, pA*/pB*/PB*/PC*/PD*/PE*/PF*/PG* 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): @@ -142,11 +144,10 @@ 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). +// ref/hare/regex/regex.ha:104-119, verbatim. The '|' arm consumes the +// error arm as origin = 0 (whole-expression alternation); the postfix +// `?`/`*`/`+` groupend arms propagate it. 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) { @@ -290,17 +291,15 @@ fn handle_bracket( // (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) — and the fold-4 bracket surface: the +// (421-443) / `+` (444-459) — the fold-4 bracket surface: the // in_bracket dispatch (265-275), the `[` flip (313-314) and the -// handle_bracket state quad (249-252). The remaining metacharacter -// arms (`(`/`)` 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. +// handle_bracket state quad (249-252) — and the fold-5a group arms: +// `(` (317-323), `)` (324-334), capture_idx (256) and the loop-exit +// `done` Unmatched-'(' check (277-282); the anchors' group_level +// arms and the postfix inst_groupend/groupstart arms went live with +// them. The remaining metacharacter arm (`{` repetition) is one loud +// not-yet-ported error — the explicit fold boundary; falling to +// literal would be a silent semantic lie. // // 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() @@ -315,9 +314,9 @@ export fn compile(expr: str) (regex | error | nomem) = { 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). + // the '(' arm grows it past level 0. 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); @@ -329,6 +328,7 @@ export fn compile(expr: str) (regex | error | nomem) = { let was_prev_rune_pipe: bool = false; let n_reps: size = 0; let group_level: size = 0; + let capture_idx: size = 0; for (true) { let next: (rune | utf8.done) = strings.next(&iter); @@ -368,7 +368,12 @@ export fn compile(expr: str) (regex | error | nomem) = { }; let r: rune = match (next) { - case utf8.done => break; + case utf8.done => { // regex.ha:277-282 + if (group_level > 0) { + return "Unmatched '('": error; + }; + break; + }; case let x: rune => yield x; }; @@ -406,8 +411,7 @@ export fn compile(expr: str) (regex | error | nomem) = { 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 + // (the error arm). Hare binds via a match EXPRESSION // (ha:337-342); statement-match into a declared // local, the search/scanrune precedent (#51). let origin: size = 0; @@ -468,7 +472,6 @@ export fn compile(expr: str) (regex | error | nomem) = { 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 @@ -502,7 +505,7 @@ export fn compile(expr: str) (regex | error | nomem) = { case inst_charset => void; case inst_any => void; case inst_groupend => { - // dead until the group fold; D13 match, cf '?' + // D13 match, cf '?' match (find_last_groupstart( insts[0:term_start_idx])) { case let e: error => return e; @@ -531,7 +534,7 @@ export fn compile(expr: str) (regex | error | nomem) = { case inst_charset => void; case inst_any => void; case inst_groupend => { - // dead until the group fold; D13 match, cf '?' + // D13 match, cf '?' match (find_last_groupstart( insts[0:term_start_idx])) { case let e: error => return e; @@ -554,9 +557,44 @@ export fn compile(expr: str) (regex | error | nomem) = { in_bracket = true; case ']': // regex.ha:315-316 — literal outside a bracket append(insts, (r: inst_lit)); - case '(', ')', '{': - // fold-4 boundary: the group (ha:317-334) and - // repetition (368-401) arms ride later folds. + case '(': { // regex.ha:317-323 + append(insts, (capture_idx: inst_groupstart)); + group_level += 1; + capture_idx += 1; + // Hare grows with `append(jump_idxs, [])?` + // (ha:321-323); the empty-slice literal spells + // through a typed let (the #25/#31 ruling, cf the + // lvl0 prologue). + for ((jump_idxs.len: size) < group_level + 1) { + let lvl: []size; + append(jump_idxs, lvl); + }; + }; + case ')': { // regex.ha:324-334 + if (group_level == 0) { + // VERBATIM duplicate of find_last_groupstart's + // text — Hare keeps two copies (ha:118/326). + return "Unmatched ')'": error; + }; + // payload-less void variant through a typed let (the + // inst_skip spelling above). + let ge: inst_groupend; + let v: inst = ge; + append(insts, v); + // jump fixup (ha:329-332): by-value range over an + // INDEXED base is the #70-fixed shape; assert is the + // #58 builtin. + for (let jump_idx .. jump_idxs[group_level]) { + assert(insts[jump_idx] is inst_jump); + insts[jump_idx] = + (((insts.len: size) - 1): inst_jump); + }; + delete(jump_idxs[group_level][:]); + group_level -= 1; + }; + case '{': + // fold-5a boundary: the repetition arm (ha:368-401) + // rides fold 5b. return "regex: metacharacter not yet ported": error; case: // regex.ha:462-463 append(insts, (r: inst_lit)); @@ -650,10 +688,11 @@ fn add_thread(threads: *[]thread, parent_idx: size, new_pc: size) (void | nomem) }; // ref/hare/regex/regex.ha:589-742. Only the arms compile() can emit -// execute (skip + split + jump + match non-consuming; lit + any + -// charset consuming — split/jump went live with fold 3's `|`/`?`/`*` -// arms, charset with fold 4's brackets); every other inst arm is one -// loud not-yet-ported abort — the fold boundary, the compile() +// execute (skip + split + jump + match + groupstart + groupend +// non-consuming; lit + any + charset consuming — split/jump went live +// with fold 3's `|`/`?`/`*` arms, charset with fold 4's brackets, +// groupstart/groupend with fold 5a's `(`/`)`); the inst_repeat arm is +// one loud not-yet-ported abort — the fold-5b boundary, the compile() // metachar discipline. The "unreachable" aborts on lit/any inside // the non-consuming loop (ha:604-605) and the trailing default // (ha:738) are Hare's own unreachable arms; Hare spells them bare @@ -719,10 +758,54 @@ fn run_thread( let nm: newmatch; return nm; }; - case inst_groupstart => - abort("regex: inst_groupstart not yet ported"); - case inst_groupend => - abort("regex: inst_groupend not yet ported"); + case let gs: inst_groupstart => { // regex.ha:636-652 + let idx: size = (gs: size); + // ha:637-641: Hare's 3-arg fill-append `append(..., + // [capture { ... }...], idx - len + 1)?` has no ww + // spelling — the ratified count-loop fill (cf the + // search epilogue's pad loop). + for (((*threads)[i].captures.len: size) < idx + 1) { + append((*threads)[i].captures, capture { ... }); + }; + assert((*threads)[i].captures[idx].end + != types.SIZE_MAX); + (*threads)[i].captures[idx] = capture { + content = "", + start = str_idx, + start_bytesize = str_bytesize, + // end=types.SIZE_MAX indicates that the + // capture group hasn't ended yet + end = types.SIZE_MAX, + end_bytesize = types.SIZE_MAX, + }; + (*threads)[i].pc += 1; + }; + case inst_groupend => { // regex.ha:653-668 + let curr_capture: size = + ((*threads)[i].captures.len: size); + // ha:655's 2-clause `for (cond; post)` has no ww + // parse form (1- and 3-clause only) — the post-step + // moves to the body tail. Equivalent: break skips + // post in both, and the body has NO continue (which + // would run post in Hare but skip the inline tail). + for (curr_capture > 0) { + // find inner-most unclosed capture group + if ((*threads)[i].captures[curr_capture - 1].end + == types.SIZE_MAX) { + break; + }; + curr_capture -= 1; + }; + assert(curr_capture > 0, "Found a groupend token \")\" without having previously seen a groupstart token \"(\". Please report this as a bug"); + // Hare's name shadows the capture TYPE — ww splits the + // type/value namespaces (#225), so it ports verbatim. + let capture = &(*threads)[i].captures[curr_capture - 1]; + capture.end = str_idx; + capture.end_bytesize = str_bytesize; + capture.content = strings.frombytes(str_bytes[ + capture.start_bytesize:capture.end_bytesize]); + (*threads)[i].pc += 1; + }; case inst_repeat => abort("regex: inst_repeat not yet ported"); case => abort("regex: unreachable"); @@ -900,20 +983,13 @@ fn search( // #47); the capacity hint drops with per-value alloc. let res: []capture; append(res, threads[best_idx].root_capture); - // ha:820 spreads threads[best_idx].captures into res. - // The indexed spread source is #35-blocked, but - // captures only populate via the loud-aborted - // groupstart arm or add_thread's loud-bounded dup — - // provably empty in every program this fold can run. - // Loud-bound like add_thread's dup; the verbatim - // spread lands with the group fold. - if (threads[best_idx].captures.len != 0) { - abort("regex: capture copy not yet ported (#35)"); - }; + // ha:820 — Hare's `static append` (capacity-bounded) + // is a plain append (D6, no static form); the indexed + // spread source is the #35/#25-landed shape. + append(res, threads[best_idx].captures...); // ha:821-824's sized fill-append `[capture { ... }...]` - // has no ww spelling; the count loop is shape-correct - // and self-activates with the group fold (length stays - // 1 until then). + // has no ww spelling — the ratified count-loop fill + // pads the unset trailing groups. for (length != (len(res): size)) { append(res, capture { ... }); }; diff --git a/lib/regex/regex_test.ww b/lib/regex/regex_test.ww index b27b1c5b..187017a8 100644 --- a/lib/regex/regex_test.ww +++ b/lib/regex/regex_test.ww @@ -35,6 +35,7 @@ import io; import memio; import os; import strings; +import types; let signalled: i32 = 0; fn fail() void = { os.exit(signalled + 10); }; @@ -262,23 +263,18 @@ fn fail() void = { os.exit(signalled + 10); }; // mean an arm was half-ported. One pattern per deferred arm so the // fold that ports an arm consciously deletes its row. Fold 3 flipped // \ ^ $ | ? * + positive (fold3_* below); fold 4 flipped `[` -// (fold4_* below); the group/repetition three remain. +// (fold4_* below); fold 5a flipped `(`/`)` (fold5_* below — "a("/"a)" +// graduated into fold5_compile_errors with their REAL texts); +// repetition `{` remains. @test fn compile_metachar_loud() void = { - let pats: [3]str = [ - "a(", "a)", "a{", - ]; - let i: i32 = 0; - for (i < len(pats)) { - match (regex.compile(pats[i])) { - case let e: regex.error => { - if (strings.compare((e: str), - "regex: metacharacter not yet ported") != 0) { - fail(); - }; + match (regex.compile("a{")) { + case let e: regex.error => { + if (strings.compare((e: str), + "regex: metacharacter not yet ported") != 0) { + fail(); }; - case => fail(); - }; - i += 1; + }; + case => fail(); }; }; @@ -422,7 +418,7 @@ type nmexp = struct { match (regex.compile("a(")) { case let e: regex.error => { if (strings.compare(regex.strerror(e), - "regex: metacharacter not yet ported") != 0) { + "Unmatched '('") != 0) { fail(); }; }; @@ -1760,6 +1756,387 @@ type cscase = struct { }; }; +// ---- fold 5a: capture groups ------------------------------------------ + +// The fold-5a compile-error surface, exact texts. ")" and "(" are +// Hare's own ERROR fixtures (+test.ha:276/606); "a("/"a)" graduate +// here from the metachar-loud table with their real texts. The +// anchor-in-group and groupstart-postfix rows are ww-added (no Hare +// table row carries these texts): they pin that the fold-3 +// verbatim-dead arms (regex.ha:295-297/302-304 anchors, +// 413-414/434-435/453-454 Unused-on-groupstart) FIRE now that `(` +// emits inst_groupstart — the §9a A0 re-verify, as live rows. +@test fn fold5_compile_errors() void = { + let rows: [9]cerow = [ + cerow { pat = ")", want = "Unmatched ')'" }, + cerow { pat = "(", want = "Unmatched '('" }, + cerow { pat = "a)", want = "Unmatched ')'" }, + cerow { pat = "a(", want = "Unmatched '('" }, + // ww-added: anchor-in-group arms (regex.ha:295-297/302-304) + cerow { pat = "(^a)", + want = "Anchor '^' in capture groups is unsupported" }, + cerow { pat = "(a$)", + want = "Anchor '$' in capture groups is unsupported" }, + // ww-added: postfix on a bare groupstart + // (regex.ha:413-414/434-435/453-454) + cerow { pat = "(?", want = "Unused '?'" }, + cerow { pat = "(*", want = "Unused '*'" }, + cerow { pat = "(+", want = "Unused '+'" }, + ]; + let i: i32 = 0; + for (i < len(rows)) { + let p: str = rows[i].pat; + match (regex.compile(p)) { + case let e: regex.error => { + let w: str = rows[i].want; + if (strings.compare((e: str), w) != 0) { fail(); }; + }; + case => fail(); + }; + i += 1; + }; +}; + +// run_thread inst_groupstart / inst_groupend driven over HAND-BUILT +// programs (the anchored-route precedent): compile() composition is +// pinned by the find/submatch tables below; these pin the arm +// mechanics — fill-grow to idx+1, the SIZE_MAX open sentinel, +// innermost-unclosed close order, content from the bytesize span, +// and the closed-group re-entry overwrite (the ha:642 assert's +// PASSING direction). +@test fn run_thread_group_arms() void = { + // groupstart (ha:636-652): grows captures to idx+1 (zero-filled + // below idx), stamps start/start_bytesize, opens with + // end = end_bytesize = SIZE_MAX + let insts: []regex.inst = []; + append(insts, ((1: size): regex.inst_groupstart)); + append(insts, ('a': regex.inst_lit)); + append(insts, (false: regex.inst_match)); + let re: regex.regex; + re.insts = insts; + re.n_reps = 0; + let ra: (rune | io.eof) = 'a'; + let ts: []thread = []; + append(ts, thread { pc = 0, ... }); + let r1: (void | newmatch | nomem) = run_thread(0, &re, "ab", &ts, ra, 2, 3); + if (!(r1 is void)) { fail(); }; + if (ts[0].captures.len != 2) { fail(); }; + // the fill element below idx is zeroed + if (ts[0].captures[0].end != (0: size)) { fail(); }; + if (ts[0].captures[0].content.len != 0) { fail(); }; + // the opened group: start stamped, end still the open sentinel + if (ts[0].captures[1].start != (2: size)) { fail(); }; + if (ts[0].captures[1].start_bytesize != (3: size)) { fail(); }; + if (ts[0].captures[1].end != types.SIZE_MAX) { fail(); }; + if (ts[0].captures[1].end_bytesize != types.SIZE_MAX) { fail(); }; + // groupstart is non-consuming: pc stepped through it, then the + // lit consumed + if (ts[0].pc != (2: size)) { fail(); }; + + // 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); + if (!(r2 is void)) { fail(); }; + if (ts2[0].captures[1].end != (3: size)) { fail(); }; + if (ts2[0].captures[1].end_bytesize != (4: size)) { fail(); }; + if (strings.compare(ts2[0].captures[1].content, "cd") != 0) { fail(); }; + if (ts2[0].captures[0].end != (3: size)) { fail(); }; + if (ts2[0].captures[0].end_bytesize != (4: size)) { fail(); }; + if (strings.compare(ts2[0].captures[0].content, "bcd") != 0) { fail(); }; + if (ts2[0].pc != (3: size)) { fail(); }; + + // 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); + if (!(r3 is void)) { fail(); }; + if (ts3[0].captures.len != 1) { fail(); }; + if (ts3[0].captures[0].start != (2: size)) { fail(); }; + if (ts3[0].captures[0].end != types.SIZE_MAX) { fail(); }; + if (ts3[0].captures[0].content.len != 0) { fail(); }; +}; + +// fold-5a find/test rows — the group rows of Hare's own table: +// group/alternation +test.ha:257-275, the jump-bug group rows +// :499-503 minus the `{,1}` form (5b) plus `(x?)?` :607, the +// POSIX-comment submatch-handling family :610-621 (MATCH-only root +// assertions, as Hare; the literally-duplicated (a*) pairs :614-617 +// appear once), `a|(b)` :635, `(a|b|c|d|e)f` :640, and the +// nested-group block :649-665 minus the `{0,}`/`{1,}`/`{0,1}` twins +// (5b) and Hare's own TODO-disabled anchor-in-group rows. end == -1 +// resolves to rune length (+test.ha:693-697). ncaps additionally pins +// len(res) = last-groupstart payload + 2 (the search length scan + +// pad fill — unset groups pad to zeroed captures, so `a|(b)` over "a" +// still yields 2). The multibyte row is ww-added (B4: every idx +// splits from its bytesize). `(a+|b)*` over "ab" is rob's +// thread-explosion stress row (group split × star respawn × +// dedup/trim/leftmost pins). Cross-pins test() == (find() matched). +type f5case = struct { + expr: str, + input: str, + matches: bool, + ncaps: i32, + start: size, + sb: size, + end: size, + eb: size, + content: str, +}; + +@test fn fold5_find_cases() void = { + let rows: [32]f5case = [ + // group/alternation (+test.ha:257-275) + f5case { expr = "^(cafe|b)x$", input = "cafex", matches = true, + ncaps = 2, start = 0, sb = 0, end = 5, eb = 5, + content = "cafex" }, + f5case { expr = "^(cafe|b)x$", input = "bx", matches = true, + ncaps = 2, start = 0, sb = 0, end = 2, eb = 2, + content = "bx" }, + f5case { expr = "^(cafe|b)x$", input = "XXXx", + matches = false, ... }, + f5case { expr = "^(Privat|Jagd)(haftpflicht|schaden)versicherungs(police|betrag)$", + input = "Jagdhaftpflichtversicherungsbetrag", + matches = true, ncaps = 4, start = 0, sb = 0, + end = 34, eb = 34, + content = "Jagdhaftpflichtversicherungsbetrag" }, + f5case { expr = "^(Privat|Jagd)(haftpflicht|schaden)versicherungs(police|betrag)$", + input = "Jagdhaftpflichtversicherungsbetrug", + matches = false, ... }, + f5case { expr = "^(Privat|Jagd)(haftpflicht|schaden)versicherungs(police|betrag)$", + input = "Jagdversicherungspolice", + matches = false, ... }, + // jump bugs (+test.ha:499-503 minus `{,1}`; :607) + f5case { expr = "ab?c", input = "ac", matches = true, + ncaps = 1, start = 0, sb = 0, end = 2, eb = 2, + content = "ac" }, + f5case { expr = "ab?c|z", input = "ac", matches = true, + ncaps = 1, start = 0, sb = 0, end = 2, eb = 2, + content = "ac" }, + f5case { expr = "(ab?c)?", input = "ac", matches = true, + ncaps = 2, start = 0, sb = 0, end = 2, eb = 2, + content = "ac" }, + f5case { expr = "(ab?c)*", input = "ac", matches = true, + ncaps = 2, start = 0, sb = 0, end = 2, eb = 2, + content = "ac" }, + f5case { expr = "(x?)?", input = "x", matches = true, + ncaps = 2, start = 0, sb = 0, end = 1, eb = 1, + content = "x" }, + // submatch handling (+test.ha:610-621) — MATCH-only root + // assertions, as Hare (POSIX submatch semantics diverge) + f5case { expr = "(a|ab)(c|bcd)(d*)", input = "abcd", + matches = true, ncaps = 4, start = 0, sb = 0, + end = 4, eb = 4, content = "abcd" }, + f5case { expr = "(a|ab)(bcd|c)(d*)", input = "abcd", + matches = true, ncaps = 4, start = 0, sb = 0, + end = 4, eb = 4, content = "abcd" }, + f5case { expr = "(ab|a)(c|bcd)(d*)", input = "abcd", + matches = true, ncaps = 4, start = 0, sb = 0, + end = 4, eb = 4, content = "abcd" }, + f5case { expr = "(ab|a)(bcd|c)(d*)", input = "abcd", + matches = true, ncaps = 4, start = 0, sb = 0, + end = 4, eb = 4, content = "abcd" }, + f5case { expr = "(a*)(b|abc)(c*)", input = "abc", + matches = true, ncaps = 4, start = 0, sb = 0, + end = 3, eb = 3, content = "abc" }, + f5case { expr = "(a*)(abc|b)(c*)", input = "abc", + matches = true, ncaps = 4, start = 0, sb = 0, + end = 3, eb = 3, content = "abc" }, + f5case { expr = "(a|ab)(c|bcd)(d|.*)", input = "abcd", + matches = true, ncaps = 4, start = 0, sb = 0, + end = 4, eb = 4, content = "abcd" }, + f5case { expr = "(a|ab)(bcd|c)(d|.*)", input = "abcd", + matches = true, ncaps = 4, start = 0, sb = 0, + end = 4, eb = 4, content = "abcd" }, + f5case { expr = "(ab|a)(c|bcd)(d|.*)", input = "abcd", + matches = true, ncaps = 4, start = 0, sb = 0, + end = 4, eb = 4, content = "abcd" }, + f5case { expr = "(ab|a)(bcd|c)(d|.*)", input = "abcd", + matches = true, ncaps = 4, start = 0, sb = 0, + end = 4, eb = 4, content = "abcd" }, + // alternation with a group (+test.ha:635) — the (b) branch + // never runs on "a"; the pad fill still yields 2 captures + f5case { expr = "a|(b)", input = "a", matches = true, + ncaps = 2, start = 0, sb = 0, end = 1, eb = 1, + content = "a" }, + // multiple alternation inside a group (+test.ha:640) + f5case { expr = "(a|b|c|d|e)f", input = "ef", matches = true, + ncaps = 2, start = 0, sb = 0, end = 2, eb = 2, + content = "ef" }, + // nested groups (+test.ha:649-665 minus the `{…}` twins) + f5case { expr = "((a))", input = "abc", matches = true, + ncaps = 3, start = 0, sb = 0, end = 1, eb = 1, + content = "a" }, + f5case { expr = "((a)(b)c)(d)", input = "abcd", matches = true, + ncaps = 5, start = 0, sb = 0, end = 4, eb = 4, + content = "abcd" }, + f5case { expr = "((((((((((a))))))))))", input = "a", + matches = true, ncaps = 11, start = 0, sb = 0, + end = 1, eb = 1, content = "a" }, + f5case { expr = "(((((((((a)))))))))", input = "a", + matches = true, ncaps = 10, start = 0, sb = 0, + end = 1, eb = 1, content = "a" }, + f5case { expr = "(([a-z]+):)?([a-z]+)$", input = "smil", + matches = true, ncaps = 4, start = 0, sb = 0, + end = 4, eb = 4, content = "smil" }, + f5case { expr = "^((a)c)?(ab)$", input = "ab", matches = true, + ncaps = 4, start = 0, sb = 0, end = 2, eb = 2, + content = "ab" }, + f5case { expr = "(a+|b)*", input = "ab", matches = true, + ncaps = 2, start = 0, sb = 0, end = 2, eb = 2, + content = "ab" }, + f5case { expr = "(a+|b)+", input = "ab", matches = true, + ncaps = 2, start = 0, sb = 0, end = 2, eb = 2, + content = "ab" }, + // ww-added multibyte group row (B4) + f5case { expr = "(ä|b)x", input = "äx", matches = true, + ncaps = 2, start = 0, sb = 0, end = 2, eb = 3, + content = "äx" }, + ]; + let i: i32 = 0; + for (i < len(rows)) { + let ex: str = rows[i].expr; + let inp: str = rows[i].input; + let c: (regex.regex | regex.error | nomem) = regex.compile(ex); + match (c) { + case let re: regex.regex => { + let fr: (regex.result | nomem) = regex.find(&re, inp); + if (!(fr is regex.result)) { fail(); }; + let res: regex.result = fr as regex.result; + if (rows[i].matches) { + if ((len(res): i32) != rows[i].ncaps) { 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; + }; +}; + +// `(a+|b)?` over "ab" (+test.ha:665) matches (0,1) "a" — the +// non-greedy END pins that the optional group stops at the leftmost- +// longest SINGLE term, not the star/plus whole-string sweep above. +@test fn fold5_optional_group() void = { + let c: (regex.regex | regex.error | nomem) = regex.compile("(a+|b)?"); + match (c) { + case let re: regex.regex => { + let fr: (regex.result | nomem) = regex.find(&re, "ab"); + if (!(fr is regex.result)) { fail(); }; + let res: regex.result = fr as regex.result; + if (len(res) != 2) { fail(); }; + if (res[0].start != (0: size)) { fail(); }; + if (res[0].end != (1: size)) { fail(); }; + if (strings.compare(res[0].content, "a") != 0) { fail(); }; + regex.result_free(res); + regex.finish(&re); + }; + case => fail(); + }; +}; + +// submatch rows (+test.ha:704-708 via run_submatch_case :72-100) — +// THE 5a acceptance gate: per-group .content equality through find(), +// root first. The second row is ww-added (B4): a 2-byte ß inside both +// captured groups splits every byte offset the contents are sliced +// from. Targets live in a flat pool indexed by (toff, tcnt), the +// findall_content shape. +type smcase = struct { + expr: str, + input: str, + toff: i32, + tcnt: i32, +}; + +@test fn fold5_submatches() void = { + let targets: [6]str = [ + "aaa bbb ccc", "bbb", "ccc", + "aaa bßb cßc", "bßb", "cßc", + ]; + let rows: [2]smcase = [ + smcase { expr = "aaa ([^ ]*) (...)", input = "aaa bbb ccc", + toff = 0, tcnt = 3 }, + // ww-added multibyte variant + smcase { expr = "aaa ([^ ]*) (...)", input = "aaa bßb cßc", + toff = 3, tcnt = 3 }, + ]; + let i: i32 = 0; + for (i < len(rows)) { + let ex: str = rows[i].expr; + let inp: str = rows[i].input; + let c: (regex.regex | regex.error | nomem) = regex.compile(ex); + match (c) { + case let re: regex.regex => { + let fr: (regex.result | nomem) = regex.find(&re, inp); + if (!(fr is regex.result)) { fail(); }; + let res: regex.result = fr as regex.result; + if ((len(res): i32) != rows[i].tcnt) { fail(); }; + let k: i32 = 0; + for (k < rows[i].tcnt) { + let want: str = targets[rows[i].toff + k]; + if (strings.compare(res[k].content, + want) != 0) { + fail(); + }; + k += 1; + }; + 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(); @@ -1798,5 +2175,10 @@ export fn main() i32 = { signalled = 35; fold4_find_cases(); signalled = 36; fold4_findall(); signalled = 37; add_thread_dup_independence(); + signalled = 38; fold5_compile_errors(); + signalled = 39; run_thread_group_arms(); + signalled = 40; fold5_find_cases(); + signalled = 41; fold5_optional_group(); + signalled = 42; fold5_submatches(); return 0; };