diff --git a/lib/regex/regex.ww b/lib/regex/regex.ww index 6ebbc46c..72699613 100644 --- a/lib/regex/regex.ww +++ b/lib/regex/regex.ww @@ -10,10 +10,12 @@ // 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. +// 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. // // One fold-1 construct is held back behind a filed compiler/fidelity // gap (see the charclass_map site below): @@ -93,12 +95,17 @@ type thread = struct { // boundary. ref/hare/regex/regex.ha:66. type newmatch = void; -// ref/hare/regex/regex.ha:68-72. -export type charset = [](charset_lit_item | charset_range_item | - charset_class_item); +// ref/hare/regex/regex.ha:68-72. Declaration ORDER diverges from Hare +// (which declares charset first): a cstage type-table bug leaves a +// tagged alias whose member types are forward-declared with a +// degenerate 8-byte tinfo — silent payload truncation (filed, ww-core +// #69; wwstage resolves the forward refs correctly). Members first +// until #69 lands. export type charset_lit_item = rune; export type charset_range_item = (u32, u32); export type charset_class_item = (str, *fn(c: rune) bool); +export type charset = [](charset_lit_item | charset_range_item | + charset_class_item); // ref/hare/regex/regex.ha:74-87 — charclass_map: the const // [](str, *fn(rune) bool) table mapping POSIX class tokens to the @@ -179,16 +186,116 @@ fn shift(sl: []inst) void = { }; }; +// Handles a rune inside a bracket expression, mutating the in-flight +// charset / bracket state through the pointer params. +// +// ref/hare/regex/regex.ha:135-225, whole except the POSIX-class arm +// BODY: its DETECTION (`[` + `:` peek) is verbatim, but the +// charclass_map scan it guards is loud-aborted — the map itself is +// blocked behind the array→slice element-coercion gap (#25; see the +// charclass_map note above) and falling through to the literal arm +// would silently compile `[[:alpha:]]` as a 9-literal charset. The +// skip_charclass_rest block (ha:164-170) is verbatim-dead until then: +// only the loud arm sets it; it self-activates with the POSIX fold. +// Hare's `append(charsets, [])?` / `append(...)?` nomem propagation +// drops as usual (#36, ww append returns void); the discarded +// `strings::next` advances (ha:214-215) bind to throwaway lets — a +// bare call statement of a tagged-returning fn is an unprobed shape. +fn handle_bracket( + insts: *[]inst, + r: rune, + r_idx: *size, + bracket_idx: *int, + iter: *strings.iterator, + charsets: *[]charset, + skip_charclass_rest: *bool, + is_charset_positive: *bool, + in_bracket: *bool, +) (void | error | nomem) = { + let peek1: (rune | utf8.done) = strings.next(iter); + let peek2: (rune | utf8.done) = strings.next(iter); + let peek3: (rune | utf8.done) = strings.next(iter); + if (!(peek1 is utf8.done)) { + strings.prev(iter); + }; + if (!(peek2 is utf8.done)) { + strings.prev(iter); + }; + if (!(peek3 is utf8.done)) { + strings.prev(iter); + }; + + if (*bracket_idx == -1) { + // Hare `append(charsets, [])?` (ha:160) — the empty slice + // literal spells through a bare typed let (#25/#31 ruling). + let empty: charset; + append(*charsets, empty); + }; + *bracket_idx += 1; + + if (*skip_charclass_rest) { + if (r == ']') { + *skip_charclass_rest = false; + }; + *r_idx += 1; + return; + }; + + let is_range: bool = peek1 is rune && (peek1 as rune) == '-' + && !(peek2 is utf8.done) && !(peek3 is utf8.done) + && !((peek2 as rune) == ']'); + let range_end: (rune | utf8.done) = peek2; + let is_first_char: bool = *bracket_idx == 0 || *bracket_idx == 1 + && !*is_charset_positive; + + if (r == ']' && !is_first_char) { // regex.ha:179-187 + let newinst: inst = inst_charset { + idx = (len(*charsets): size) - 1, + is_positive = *is_charset_positive, + }; + append(*insts, newinst); + *in_bracket = false; + *bracket_idx = -1; + *is_charset_positive = true; + } else if (r == '^' && *bracket_idx == 0) { // regex.ha:188-189 + *is_charset_positive = false; + } else if (r == '[' && !(peek1 is utf8.done) + && (peek1 as rune) == ':') { // regex.ha:190-204 + abort("regex: POSIX character class not yet ported"); + } else if (is_range) { // regex.ha:205-217 + let start_b: u32 = (r: u32); + let end_b: u32 = ((range_end as rune): u32); + + if (end_b < start_b) { + return "Descending bracket expression range '[z-a]'": error; + }; + + append((*charsets)[len(*charsets) - 1], + ((start_b, end_b): charset_range_item)); + let skip1: (rune | utf8.done) = strings.next(iter); + let skip2: (rune | utf8.done) = strings.next(iter); + *r_idx += 2; + } else { // regex.ha:218-221 + append((*charsets)[len(*charsets) - 1], + (r: charset_lit_item)); + }; + + *r_idx += 1; + return; +}; + // Compiles a regular expression string into a [[regex]]. // // 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 +// (421-443) / `+` (444-459) — and 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 @@ -204,7 +311,7 @@ export fn compile(expr: str) (regex | error | nomem) = { // zeroes the header (cgen.c:9836 no-rhs multi-word composite // zero-fill, symmetric in cgenstmt.ww). let insts: []inst; - let charsets: []charset; // stays empty until the '[' fold + let charsets: []charset; let iter: strings.iterator = strings.iter(expr); let r_idx: size = 0; // jump_idxs tracks the pending alternation jumps per group level; @@ -214,6 +321,11 @@ export fn compile(expr: str) (regex | error | nomem) = { let jump_idxs: [][]size; let lvl0: []size; append(jump_idxs, lvl0); + // bracket-expression state (regex.ha:249-252). + let in_bracket: bool = false; + let skip_charclass_rest: bool = false; + let bracket_idx: int = -1; + let is_charset_positive: bool = true; let was_prev_rune_pipe: bool = false; let n_reps: size = 0; let group_level: size = 0; @@ -233,6 +345,28 @@ export fn compile(expr: str) (regex | error | nomem) = { append(insts, v); }; + if (in_bracket) { // regex.ha:265-275 + if (next is utf8.done) { + return "Unmatched '['": error; + }; + // Hare propagates with `?` (ha:271-274); `?` into + // compile's >32B tagged return is loud-stopped (#38b) + // — the explicit D13 match, harec's own desugaring + // (ref/harec/src/check.c:2780), the fold-3 + // find_last_groupstart-arm precedent. The continue + // skips the loop tail — handle_bracket owns r_idx + // inside a bracket (the #138-fixed shape). + match (handle_bracket(&insts, next as rune, &r_idx, + &bracket_idx, &iter, &charsets, + &skip_charclass_rest, + &is_charset_positive, &in_bracket)) { + case let e: error => return e; + case let n: nomem => return n; + case void => void; + }; + continue; + }; + let r: rune = match (next) { case utf8.done => break; case let x: rune => yield x; @@ -416,12 +550,13 @@ export fn compile(expr: str) (regex | error | nomem) = { let v: inst = av; append(insts, v); }; + case '[': // regex.ha:313-314 + in_bracket = true; case ']': // regex.ha:315-316 — literal outside a bracket append(insts, (r: inst_lit)); - case '[', '(', ')', '{': - // fold-3 boundary: the bracket (ha:313-334 + 268-275), - // group (317-334) and repetition (368-401) arms ride - // later folds. + case '(', ')', '{': + // fold-4 boundary: the group (ha: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)); @@ -523,19 +658,19 @@ 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 -// 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 -// the trailing default (ha:738) are Hare's own unreachable arms; -// Hare spells them bare `abort()`, ww carries a message — the -// zero-arg builtin form is shadowed in any combined unit that -// declares its own abort fn (os.ww:16 is private yet shadows -// cross-module; filed, ww-core #45). Hare's loop match omits inst_charset -// (consuming — the loop condition excludes it); ww has no -// match-exhaustiveness analysis, so the omission becomes the loud -// default arm. +// 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() +// 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 +// `abort()`, ww carries a message — the zero-arg builtin form is +// shadowed in any combined unit that declares its own abort fn +// (os.ww:16 is private yet shadows cross-module; filed, ww-core +// #45). Hare's loop match omits inst_charset (consuming — the loop +// condition excludes it); ww has no match-exhaustiveness analysis, +// so the omission becomes the loud default arm. fn run_thread( i: size, re: *regex, @@ -619,8 +754,52 @@ fn run_thread( }; }; case inst_any => void; - case inst_charset => - abort("regex: inst_charset not yet ported"); + case let cs: inst_charset => { // regex.ha:704-737 + // 24B header bind off the ptr-field slice index. Spelled + // structurally, not `charset`: an alias-typed slice local's + // INDEX read mis-scales in wwstage (cs≠ww, ww-core #68) — + // reverts to the alias with the #47 family. + let cset: [](charset_lit_item | charset_range_item | + charset_class_item) = re.charsets[cs.idx]; + // Disprove the match if we're looking for a negative match + // Prove the match if we're looking for a positive match + let matched: bool = !cs.is_positive; + // Hare loops `for (let i = 0z; ...) match (charset[i])` + // (ha:709) — index loop with the typed-let scrutinee bind + // (the run_thread spelling); Hare's inner `i` renames to k + // (it shadows the thread-index param). The class arm is the + // loud fold boundary: compile() cannot emit a class item + // until the POSIX fold (charclass_map is #25-blocked). + for (let k: size = 0; k < (len(cset): size); k += 1) { + let cur: (charset_lit_item | charset_range_item | + charset_class_item) = cset[k]; + match (cur) { + case let l: charset_lit_item => { + if (r == (l: rune)) { + // Succeeded if positive match + // Failed if negative match + matched = cs.is_positive; + break; + }; + }; + case let range: charset_range_item => { + let r_b: u32 = (r: u32); + + if (r_b >= range.0 && r_b <= range.1) { + // Succeeded if positive match + // Failed if negative match + matched = cs.is_positive; + break; + }; + }; + case charset_class_item => + abort("regex: POSIX character class not yet ported"); + }; + }; + if (!matched) { + (*threads)[i].failed = true; + }; + }; case => abort("regex: unreachable"); // unreachable (ha:738) }; diff --git a/lib/regex/regex_test.ww b/lib/regex/regex_test.ww index 22b0b6b9..99557b0d 100644 --- a/lib/regex/regex_test.ww +++ b/lib/regex/regex_test.ww @@ -261,11 +261,11 @@ fn fail() void = { os.exit(signalled + 10); }; // 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. Fold 3 flipped -// \ ^ $ | ? * + positive (fold3_* below); the bracket/group/ -// repetition four remain. +// \ ^ $ | ? * + positive (fold3_* below); fold 4 flipped `[` +// (fold4_* below); the group/repetition three remain. @test fn compile_metachar_loud() void = { - let pats: [4]str = [ - "a[", "a(", "a)", "a{", + let pats: [3]str = [ + "a(", "a)", "a{", ]; let i: i32 = 0; for (i < len(pats)) { @@ -419,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) { @@ -1078,6 +1078,11 @@ fn instsig(v: regex.inst) i64 = { }; case let g: regex.inst_groupstart => return 7000 + ((g: size): i64); case regex.inst_groupend => return 8000; + case let c: regex.inst_charset => { + // fold 4: 10xxx positive / 11xxx negated, + charset index + if (c.is_positive) { return 10000 + (c.idx: i64); }; + return 11000 + (c.idx: i64); + }; case => return 9999; }; }; @@ -1351,6 +1356,357 @@ type cerow = struct { }; }; +// ---- fold 4: bracket expressions -------------------------------------- + +// Emitted-program pins for the `[..]` arm: the charset inst lands where +// a literal would (and composes with fold-3's postfix/anchors), the +// charsets table grows one entry per bracket, negation rides +// is_positive. Derived by hand-executing regex.ha:265-275 + 313-314 + +// handle_bracket. +@test fn fold4_programs() void = { + let sigs: [17]i64 = [ + // "[abc]": skip, charset 0 positive, match(false) + 2000, 10000, 6000, + // "^[abc]$": anchored both ends — charset, match(TRUE) + 10000, 6001, + // "[^ab]": skip, charset 0 NEGATED, match(false) + 2000, 11000, 6000, + // "[ab][cd]": two brackets — charset 0, charset 1 + 2000, 10000, 10001, 6000, + // "[abc]*": the fold-3 a* shape with charset as the term + 2000, 4004, 10000, 5001, 6000, + ]; + let rows: [5]pgmcase = [ + pgmcase { expr = "[abc]", soff = 0, scnt = 3 }, + pgmcase { expr = "^[abc]$", soff = 3, scnt = 2 }, + pgmcase { expr = "[^ab]", soff = 5, scnt = 3 }, + pgmcase { expr = "[ab][cd]", soff = 8, scnt = 4 }, + pgmcase { expr = "[abc]*", soff = 12, scnt = 5 }, + ]; + let i: i32 = 0; + for (i < len(rows)) { + let ex: str = rows[i].expr; + let c: (regex.regex | regex.error | nomem) = regex.compile(ex); + match (c) { + case let re: regex.regex => { + 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; + }; +}; + +// cssig — flatten a charset element: lits as 1_000_000 + codepoint, +// ranges as start*10000 + end (codepoints stay < 10000 in these rows), +// class items can't exist (compile()'s POSIX arm is loud). +// The element binds structurally, not via the `charset` alias — an +// alias-typed slice local's index read mis-scales in wwstage (#68). +fn cssig(cs: [](charset_lit_item | charset_range_item | + charset_class_item), k: size) i64 = { + let cur: (charset_lit_item | charset_range_item | + charset_class_item) = cs[k]; + match (cur) { + case let l: charset_lit_item => + return 1000000 + ((l: rune): i64); + case let range: charset_range_item => + return (range.0: i64) * 10000 + (range.1: i64); + case => return -1; + }; +}; + +type cscase = struct { + expr: str, + eoff: i32, + ecnt: i32, +}; + +// charsets-table content pins: literal vs range element discrimination, +// the first-char `]`/`[` literal rules, literal dashes, and multibyte +// codepoints in both element kinds (regex.ha:172-221 state machine). +@test fn fold4_charsets() void = { + let exp: [15]i64 = [ + // "[abc]" + 1000097, 1000098, 1000099, + // "[]ab]" — first-char ] is a literal + 1000093, 1000097, 1000098, + // "[[ab]" — [ inside a bracket is a literal + 1000091, 1000097, 1000098, + // "[a-c]" + 970099, + // "[-a-c]" — leading literal dash + 1000045, 970099, + // "[a-c-]" — trailing literal dash + 970099, 1000045, + // "[ä-ö]" — multibyte range, codepoints 228..246 + 2280246, + ]; + let rows: [7]cscase = [ + cscase { expr = "[abc]", eoff = 0, ecnt = 3 }, + cscase { expr = "[]ab]", eoff = 3, ecnt = 3 }, + cscase { expr = "[[ab]", eoff = 6, ecnt = 3 }, + cscase { expr = "[a-c]", eoff = 9, ecnt = 1 }, + cscase { expr = "[-a-c]", eoff = 10, ecnt = 2 }, + cscase { expr = "[a-c-]", eoff = 12, ecnt = 2 }, + cscase { expr = "[ä-ö]", eoff = 14, ecnt = 1 }, + ]; + let i: i32 = 0; + for (i < len(rows)) { + let ex: str = rows[i].expr; + let c: (regex.regex | regex.error | nomem) = regex.compile(ex); + match (c) { + case let re: regex.regex => { + if (re.charsets.len != 1) { fail(); }; + let cs0: [](charset_lit_item | charset_range_item | + charset_class_item) = re.charsets[0]; + if ((len(cs0): i32) != rows[i].ecnt) { fail(); }; + let k: i32 = 0; + for (k < rows[i].ecnt) { + if (cssig(cs0, (k: size)) + != exp[rows[i].eoff + k]) { + fail(); + }; + k += 1; + }; + regex.finish(&re); + }; + case => fail(); + }; + i += 1; + }; +}; + +// The fold-4 compile-error surface, exact texts (regex.ha:267 / 211). +// The `[[:alpha:]]` POSIX-class arm is a loud ABORT, not an error — +// unpinnable in-process (it kills the runner); its boundary is +// source-audited (handle_bracket's class arm) until the POSIX fold. +@test fn fold4_compile_errors() void = { + let rows: [4]cerow = [ + cerow { pat = "a[", want = "Unmatched '['" }, + cerow { pat = "[abc", want = "Unmatched '['" }, + cerow { pat = "[z-a]", + want = "Descending bracket expression range '[z-a]'" }, + // the escape arm must not eat `[`: "\[" is a literal, the + // SECOND `[` opens an unterminated bracket + cerow { pat = "\\[[", want = "Unmatched '['" }, + ]; + let i: i32 = 0; + for (i < len(rows)) { + let p: str = rows[i].pat; + match (regex.compile(p)) { + case let e: regex.error => { + let w: str = rows[i].want; + if (strings.compare((e: str), w) != 0) { fail(); }; + }; + case => fail(); + }; + i += 1; + }; +}; + +// fold-4 find/test rows — Hare's own bracket block (+test.ha:278-345) +// minus the group row (`(` is loud) and the POSIX-class rows (loud +// abort), plus multibyte riders (literal and range brackets over +// 2-byte runes, idx != bytesize) and an unanchored composition row. +// MATCH 0 -1 resolves to (0, runelen, input) per +test.ha:693-697; +// every input is ASCII unless noted. Cross-pins test() == find(). +@test fn fold4_find_cases() void = { + let rows: [72]fcase = [ + fcase { expr = "^test[abc]$", input = "testa", matches = true, + start = 0, sb = 0, end = 5, eb = 5, content = "testa" }, + fcase { expr = "^test[abc]$", input = "testb", matches = true, + start = 0, sb = 0, end = 5, eb = 5, content = "testb" }, + fcase { expr = "^test[abc]$", input = "testc", matches = true, + start = 0, sb = 0, end = 5, eb = 5, content = "testc" }, + fcase { expr = "^test[abc]$", input = "testd", matches = false, ... }, + fcase { expr = "^test[abc]*$", input = "test", matches = true, + start = 0, sb = 0, end = 4, eb = 4, content = "test" }, + fcase { expr = "^test[abc]*$", input = "testa", matches = true, + start = 0, sb = 0, end = 5, eb = 5, content = "testa" }, + fcase { expr = "^test[abc]*$", input = "testaaa", matches = true, + start = 0, sb = 0, end = 7, eb = 7, content = "testaaa" }, + fcase { expr = "^test[abc]*$", input = "testabc", matches = true, + start = 0, sb = 0, end = 7, eb = 7, content = "testabc" }, + fcase { expr = "^test[abc]?$", input = "test", matches = true, + start = 0, sb = 0, end = 4, eb = 4, content = "test" }, + fcase { expr = "^test[abc]?$", input = "testa", matches = true, + start = 0, sb = 0, end = 5, eb = 5, content = "testa" }, + fcase { expr = "^test[abc]+$", input = "testa", matches = true, + start = 0, sb = 0, end = 5, eb = 5, content = "testa" }, + fcase { expr = "^test[abc]+$", input = "test", matches = false, ... }, + fcase { expr = "^test[]abc]$", input = "test]", matches = true, + start = 0, sb = 0, end = 5, eb = 5, content = "test]" }, + fcase { expr = "^test[[abc]$", input = "test[", matches = true, + start = 0, sb = 0, end = 5, eb = 5, content = "test[" }, + fcase { expr = "^test[^abc]$", input = "testd", matches = true, + start = 0, sb = 0, end = 5, eb = 5, content = "testd" }, + fcase { expr = "^test[^abc]$", input = "test!", matches = true, + start = 0, sb = 0, end = 5, eb = 5, content = "test!" }, + fcase { expr = "^test[^abc]$", input = "testa", matches = false, ... }, + fcase { expr = "^test[^abc]$", input = "testb", matches = false, ... }, + fcase { expr = "^test[^abc]$", input = "testc", matches = false, ... }, + fcase { expr = "^test[^]abc]$", input = "test]", matches = false, ... }, + fcase { expr = "^test[^abc[]$", input = "test[", matches = false, ... }, + fcase { expr = "^test[^abc]*$", input = "testd", matches = true, + start = 0, sb = 0, end = 5, eb = 5, content = "testd" }, + fcase { expr = "^test[^abc]*$", input = "testqqqqq", matches = true, + start = 0, sb = 0, end = 9, eb = 9, content = "testqqqqq" }, + fcase { expr = "^test[^abc]*$", input = "test", matches = true, + start = 0, sb = 0, end = 4, eb = 4, content = "test" }, + fcase { expr = "^test[^abc]*$", input = "testc", matches = false, ... }, + fcase { expr = "^test[^abc]?$", input = "test", matches = true, + start = 0, sb = 0, end = 4, eb = 4, content = "test" }, + fcase { expr = "^test[^abc]?$", input = "testd", matches = true, + start = 0, sb = 0, end = 5, eb = 5, content = "testd" }, + fcase { expr = "^test[^abc]?$", input = "testc", matches = false, ... }, + fcase { expr = "^test[^abc]+$", input = "testd", matches = true, + start = 0, sb = 0, end = 5, eb = 5, content = "testd" }, + fcase { expr = "^test[^abc]+$", input = "testddd", matches = true, + start = 0, sb = 0, end = 7, eb = 7, content = "testddd" }, + fcase { expr = "^test[^abc]+$", input = "testc", matches = false, ... }, + fcase { expr = "^test[^abc]+$", input = "testcccc", matches = false, ... }, + fcase { expr = "^test[a-c]$", input = "testa", matches = true, + start = 0, sb = 0, end = 5, eb = 5, content = "testa" }, + fcase { expr = "^test[a-c]$", input = "testb", matches = true, + start = 0, sb = 0, end = 5, eb = 5, content = "testb" }, + fcase { expr = "^test[a-c]$", input = "testc", matches = true, + start = 0, sb = 0, end = 5, eb = 5, content = "testc" }, + fcase { expr = "^test[a-c]$", input = "testd", matches = false, ... }, + fcase { expr = "^test[a-c]$", input = "test!", matches = false, ... }, + fcase { expr = "^test[a-c]$", input = "test-", matches = false, ... }, + fcase { expr = "^test[-a-c]$", input = "test-", matches = true, + start = 0, sb = 0, end = 5, eb = 5, content = "test-" }, + fcase { expr = "^test[a-c-]$", input = "test-", matches = true, + start = 0, sb = 0, end = 5, eb = 5, content = "test-" }, + fcase { expr = "^test[a-c]*$", input = "test", matches = true, + start = 0, sb = 0, end = 4, eb = 4, content = "test" }, + fcase { expr = "^test[a-c]*$", input = "testa", matches = true, + start = 0, sb = 0, end = 5, eb = 5, content = "testa" }, + fcase { expr = "^test[a-c]*$", input = "testabb", matches = true, + start = 0, sb = 0, end = 7, eb = 7, content = "testabb" }, + fcase { expr = "^test[a-c]*$", input = "testddd", matches = false, ... }, + fcase { expr = "^test[a-c]?$", input = "test", matches = true, + start = 0, sb = 0, end = 4, eb = 4, content = "test" }, + fcase { expr = "^test[a-c]?$", input = "testb", matches = true, + start = 0, sb = 0, end = 5, eb = 5, content = "testb" }, + fcase { expr = "^test[a-c]?$", input = "testd", matches = false, ... }, + fcase { expr = "^test[a-c]+$", input = "test", matches = false, ... }, + fcase { expr = "^test[a-c]+$", input = "testbcbc", matches = true, + start = 0, sb = 0, end = 8, eb = 8, content = "testbcbc" }, + fcase { expr = "^test[a-c]+$", input = "testd", matches = false, ... }, + fcase { expr = "^test[^a-c]$", input = "testa", matches = false, ... }, + fcase { expr = "^test[^a-c]$", input = "testb", matches = false, ... }, + fcase { expr = "^test[^a-c]$", input = "testc", matches = false, ... }, + fcase { expr = "^test[^a-c]$", input = "testd", matches = true, + start = 0, sb = 0, end = 5, eb = 5, content = "testd" }, + fcase { expr = "^test[^a-c]$", input = "test!", matches = true, + start = 0, sb = 0, end = 5, eb = 5, content = "test!" }, + fcase { expr = "^test[^a-c]$", input = "test-", matches = true, + start = 0, sb = 0, end = 5, eb = 5, content = "test-" }, + fcase { expr = "^test[^-a-c]$", input = "test-", matches = false, ... }, + fcase { expr = "^test[^a-c-]$", input = "test-", matches = false, ... }, + fcase { expr = "^test[^a-c-]*$", input = "test", matches = true, + start = 0, sb = 0, end = 4, eb = 4, content = "test" }, + fcase { expr = "^test[^a-c-]*$", input = "test--", matches = false, ... }, + fcase { expr = "^test[^a-c-]*$", input = "testq", matches = true, + start = 0, sb = 0, end = 5, eb = 5, content = "testq" }, + fcase { expr = "^test[^a-c-]?$", input = "test", matches = true, + start = 0, sb = 0, end = 4, eb = 4, content = "test" }, + fcase { expr = "^test[^a-c-]?$", input = "testq", matches = true, + start = 0, sb = 0, end = 5, eb = 5, content = "testq" }, + fcase { expr = "^test[^a-c-]?$", input = "test-", matches = false, ... }, + fcase { expr = "^test[^a-c-]+$", input = "test", matches = false, ... }, + fcase { expr = "^test[^a-c-]+$", input = "testb", matches = false, ... }, + fcase { expr = "^test[^a-c-]+$", input = "testddd", matches = true, + start = 0, sb = 0, end = 7, eb = 7, content = "testddd" }, + // multibyte riders: 2-byte runes in a literal bracket and a + // codepoint range — idx != bytesize in every field (B4) + fcase { expr = "^x[äö]$", input = "xä", matches = true, + start = 0, sb = 0, end = 2, eb = 3, content = "xä" }, + fcase { expr = "^x[äö]$", input = "xq", matches = false, ... }, + fcase { expr = "^[à-ö]$", input = "á", matches = true, + start = 0, sb = 0, end = 1, eb = 2, content = "á" }, + fcase { expr = "^[à-ö]$", input = "x", matches = false, ... }, + // unanchored leftmost-longest composition with fold-3's `+` + fcase { expr = "[ab]+", input = "xxabyyba", matches = true, + start = 2, sb = 2, end = 4, eb = 4, content = "ab" }, + ]; + let i: i32 = 0; + for (i < len(rows)) { + let ex: str = rows[i].expr; + let inp: str = rows[i].input; + let c: (regex.regex | regex.error | nomem) = regex.compile(ex); + match (c) { + case let re: regex.regex => { + let fr: (regex.result | nomem) = regex.find(&re, inp); + 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; + }; +}; + +// findall composition: charset × split (`+`) × the 2c machinery — +// non-overlapping greedy matches over mixed input. +@test fn fold4_findall() void = { + let c: (regex.regex | regex.error | nomem) = regex.compile("[ab]+"); + match (c) { + case let re: regex.regex => { + let fr: ([]regex.result | nomem) = + regex.findall(&re, "xxabyyba"); + if (!(fr is []regex.result)) { fail(); }; + let results: []regex.result = fr as []regex.result; + if (len(results) != 2) { fail(); }; + if (results[0][0].start != (2: size)) { fail(); }; + if (results[0][0].end != (4: size)) { fail(); }; + if (strings.compare(results[0][0].content, "ab") != 0) { + fail(); + }; + if (results[1][0].start != (6: size)) { fail(); }; + if (results[1][0].end != (8: size)) { fail(); }; + if (strings.compare(results[1][0].content, "ba") != 0) { + fail(); + }; + regex.result_freeall(results); + regex.finish(&re); + }; + case => fail(); + }; +}; + export fn main() i32 = { signalled = 1; lit_and_match(); signalled = 2; size_aliases_distinct(); @@ -1383,5 +1739,10 @@ export fn main() i32 = { signalled = 29; find_last_groupstart_cases(); signalled = 30; shift_direct(); signalled = 31; fold3_find_cases(); + signalled = 32; fold4_programs(); + signalled = 33; fold4_charsets(); + signalled = 34; fold4_compile_errors(); + signalled = 35; fold4_find_cases(); + signalled = 36; fold4_findall(); return 0; };