From 7545bf7dcd5e3b4237434900acccd6cb313d9231 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Fri, 5 Jun 2026 04:50:27 +0900 Subject: [PATCH] =?UTF-8?q?regex:=20fold=205b=20=E2=80=94=20repetition=20{?= =?UTF-8?q?m,n}=20(the=20{=20arm=20+=20run=5Fthread=20inst=5Frepeat=20+=20?= =?UTF-8?q?rep=20prefill)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All three loud bounds flip: the { metachar (ha:368-402, inclusive advance bound), the run_thread inst_repeat arm (ha:669-684, is/as verbatim per the #42 fence), and the search rep_counters prefill (ha:763-765, count-loop respell of the sized-fill alloc). The deferred-metachar table EMPTIES — every metacharacter compiles; the POSIX class body is the only loud surface left in lib/regex. Two silent compiler finds surfaced and filed, respells drew-signed: #49 (whole-struct assign from a match binding w/ tagged fields corrupts them context-dependently — the parse_repetition unwrap goes field-wise in-arm) and #50 (insert() grows the dst before evaluating its value arg, +1 split mis-target on Hare's len(insts)+2 payload — pre-bound, the '?'/'|' arm convention). Activation table: the {m,n} matrix (+test.ha:443-460) incl. the open-ended (0,7) and {,0}de rows, the \{ \} escape pair, the :635 5a carve-out, the {0,}/{1,}/{0,1} twins (cross-spelling agreement with their 5a */+/? siblings), the Various composed rows minus the [[:class:]] row (POSIX abort, fold-4 ruling), findall fo{2,}, plus ww-added multibyte {2} and long-input {1,} stress rows. --- lib/regex/regex.ww | 135 +++++++++++++++++---- lib/regex/regex_test.ww | 252 ++++++++++++++++++++++++++++++++++++---- 2 files changed, 340 insertions(+), 47 deletions(-) diff --git a/lib/regex/regex.ww b/lib/regex/regex.ww index cc18818d..6e0e419e 100644 --- a/lib/regex/regex.ww +++ b/lib/regex/regex.ww @@ -15,9 +15,10 @@ // 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. +// capture spread); fold 5b = repetition `{m,n}` (parse_repetition + +// the `{` arm + the run_thread inst_repeat arm + the search +// rep_counters prefill). Remaining: the POSIX `[:class:]` BODY (the +// last loud surface) — and replace. They land with their folds. // // One fold-1 construct is held back behind a filed compiler/fidelity // gap (see the charclass_map site below): @@ -294,13 +295,14 @@ fn handle_bracket( // whole-expression fixup (470-473), postfix `?` (403-420) / `*` // (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) — and the fold-5a group arms: +// handle_bracket state quad (249-252) — 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. +// them — and the fold-5b repetition arm `{` (368-402) over +// parse_repetition + n_reps (255). Every metacharacter is ported; +// the only remaining loud surface is the POSIX class BODY inside +// handle_bracket. // // 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() @@ -593,10 +595,78 @@ export fn compile(expr: str) (regex | error | nomem) = { 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:368-402 + let origin: size = (insts.len: size) - 1; + if (insts[origin] is inst_groupend) { + // D13 match, cf '?' — `?` into compile's >32B + // tagged return is loud-stopped (#38b). + match (find_last_groupstart(insts[0:origin])) { + case let e: error => return e; + case let sz: size => { origin = sz; }; + }; + }; + let rest: str = strings.iterstr(&iter); + // Hare's `parse_repetition(rest)?` (ha:374) — the D13 + // match; the binding unwraps FIELD-WISE: a whole-struct + // assign from a match binding with tagged fields + // silently corrupts them (filed, ww-core #49). The + // rp_* locals are Hare's rep_parts.0/.1/.2 through the + // repparts respell (#47; see parse_repetition). + let rp_min: (void | size) = void; + let rp_max: (void | size) = void; + let rp_replen: size = 0; + match (parse_repetition(rest)) { + case let e: error => return e; + case let rp: repparts => { + rp_min = rp.min; + rp_max = rp.max; + rp_replen = rp.replen; + }; + }; + // ha:375 compares the tagged elem to 0 directly + // (`rep_parts.0 == 0`); no tagged==int compare in ww — + // the is/as respell (ruled, scope §9b). + let can_skip: bool = rp_min is size + && rp_min as size == 0; + // ha:376-380's if-EXPRESSION min selection spells as a + // widen-assign into the tagged let. + let min: (void | size) = rp_min; + if (can_skip) { + min = (1: size); + }; + if (can_skip) { + // len(insts) - 1 is the current last instruction + // len(insts) is the next instruction + // advance to len(insts) + 1 to make space for the `inst_split` + // advance to len(insts) + 2 to make space for the `inst_repeat` + // + // Hare evaluates the payload BEFORE the insert + // (ha:386-387); ww's insert() grows the dst + // before evaluating the value arg (filed, + // ww-core #50) — payload pre-bound, the + // '?'/'|' arm convention. + let split_target: size = (insts.len: size) + 2; + insert(insts[origin], + (split_target: inst_split)); + shift(insts[origin + 1:]); + origin += 1; + }; + let newinst: inst = inst_repeat { + id = n_reps, + origin = origin, + min = min, + max = rp_max, + }; + // ha:397-400 — the bound is INCLUSIVE (`<=`), exactly + // as Hare; the discarded strings.next binds to a + // throwaway let (the handle_bracket skip precedent). + for (let i: size = 0; i <= rp_replen; i += 1) { + let skip: (rune | utf8.done) = strings.next(&iter); + r_idx += 1; + }; + append(insts, newinst); + n_reps += 1; + }; case: // regex.ha:462-463 append(insts, (r: inst_lit)); }; @@ -782,13 +852,12 @@ fn add_thread(threads: *[]thread, parent_idx: size, new_pc: size) (void | nomem) return; }; -// ref/hare/regex/regex.ha:589-742. Only the arms compile() can emit -// 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 +// ref/hare/regex/regex.ha:589-742. Every arm compile() can emit +// executes (skip + split + jump + match + groupstart + groupend + +// repeat 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 `(`/`)`, repeat with +// fold 5b's `{`). 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 @@ -901,8 +970,23 @@ fn run_thread( capture.start_bytesize:capture.end_bytesize]); (*threads)[i].pc += 1; }; - case inst_repeat => - abort("regex: inst_repeat not yet ported"); + case let ir: inst_repeat => { // regex.ha:669-684 + assert(ir.id < (len((*threads)[i].rep_counters): size)); + (*threads)[i].rep_counters[ir.id] += 1; + if (ir.max is size + && (*threads)[i].rep_counters[ir.id] + > ir.max as size) { + (*threads)[i].failed = true; + return; + }; + let new_pc: size = (*threads)[i].pc + 1; + (*threads)[i].pc = ir.origin; + if (ir.min is void + || (*threads)[i].rep_counters[ir.id] + >= ir.min as size) { + add_thread(threads, i, new_pc)?; + }; + }; case => abort("regex: unreachable"); }; }; @@ -1002,12 +1086,13 @@ fn search( // and every free in the block is the documented no-op (#27). // ha:763-765 prefills threads[0].rep_counters via - // `alloc([0...], re.n_reps)?`. No program this fold can compile - // has n_reps > 0 (the inst_repeat arm is loud), and the sized - // fill form has no ww spelling yet — loud, like the inst_repeat - // arm; the real prefill lands with the repeat fold. + // `alloc([0...], re.n_reps)?` — the sized-fill alloc has no ww + // spelling (D3 family) and ww has no `z` size-literal suffix + // (cf lib/strconv/decimal.ww:13) — count-loop append of (0: size). if (re.n_reps > 0) { - abort("regex: repetitions not yet ported"); + for (let k: size = 0; k < re.n_reps; k += 1) { + append(threads[0].rep_counters, (0: size)); + }; }; let str_idx: size = 0; diff --git a/lib/regex/regex_test.ww b/lib/regex/regex_test.ww index c8b029f7..fcfb0b64 100644 --- a/lib/regex/regex_test.ww +++ b/lib/regex/regex_test.ww @@ -257,27 +257,6 @@ fn fail() void = { os.exit(signalled + 10); }; }; }; -// Every deferred metacharacter is a LOUD error carrying the exact -// 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. Fold 3 flipped -// \ ^ $ | ? * + positive (fold3_* below); fold 4 flipped `[` -// (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 = { - match (regex.compile("a{")) { - case let e: regex.error => { - if (strings.compare((e: str), - "regex: metacharacter not yet ported") != 0) { - fail(); - }; - }; - case => fail(); - }; -}; - // The thread struct (regex.ha:55-64) is in tree ahead of its engine // consumers so the pending #15/#17 fix probes exercise the real type. // Pin the field layout via the P6-proven wide-literal append + a @@ -2221,6 +2200,233 @@ type prrow = struct { }; }; +// 5b compile-error rows end-to-end through compile() — the two +// parse_repetition texts surface verbatim (+test.ha:461-463 ERROR +// rows). "a{" is the GRADUATED metachar-loud row: with `{` ported the +// deferred-metachar table empties — every metacharacter compiles, and +// the only loud surface left in lib/regex is the POSIX-class runtime +// abort (a fold-4 pin, not an error row). "a{" has no `}` in the +// rest, so it lands on the ha:491-493 syntax text. +@test fn fold5b_compile_errors() void = { + let rows: [4]cerow = [ + cerow { pat = "^x(abc){-1,2}$", + want = "Negative repetition count '{-n}'" }, + cerow { pat = "^x(abc){x,2}$", + want = "Repetition expression syntax error '{n}'" }, + cerow { pat = "^x(abc){0,-2}$", + want = "Negative repetition count '{-n}'" }, + cerow { pat = "a{", + want = "Repetition expression syntax error '{n}'" }, + ]; + 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-5b find/test rows — Hare's own {m,n} matrix (+test.ha:443-460 +// incl. the open-ended (0,7) row and the `{,0}de` pair), the `\{`/`\}` +// escape pair :437-438 (pins that ESCAPED braces never enter the `{` +// arm), the 5a carve-out :635 `(a|(b?|c*){,1}|d+|e)` come home, the +// `{0,}`/`{1,}`/`{0,1}` nested twins :649-665 (each must agree with +// its 5a `*`/`+`/`?` sibling's result on the same input — the +// cross-spelling invariant), and the "Various" composed rows :462-485 +// minus the [[:class:]] row (POSIX class is a runtime loud abort, the +// fold-4 ruling — excluded until the POSIX fold). end == -1 resolves +// to rune length (+test.ha:693-697). The multibyte `{2}` row and the +// long-input `{1,}` thread/counter-explosion stress row are ww-added +// (B4 lesson; rob's rider). Cross-pins test() == (find() matched); +// the 5a/4/3 tables above re-run untouched in the same binary — the +// dedup/trim/leftmost regression net under the new repeat spawns. +@test fn fold5b_find_cases() void = { + let rows: [30]f5case = [ + // {m,n} matrix (+test.ha:443-460) + f5case { expr = "^x(abc){2}$", input = "xabcabc", + matches = true, ncaps = 2, start = 0, sb = 0, + end = 7, eb = 7, content = "xabcabc" }, + f5case { expr = "^x(abc){3}$", input = "xabcabc", + matches = false, ... }, + f5case { expr = "^x(abc){1,2}$", input = "xabc", + matches = true, ncaps = 2, start = 0, sb = 0, + end = 4, eb = 4, content = "xabc" }, + f5case { expr = "^x(abc){1,2}$", input = "xabcabc", + matches = true, ncaps = 2, start = 0, sb = 0, + end = 7, eb = 7, content = "xabcabc" }, + f5case { expr = "^x(abc){1,2}$", input = "xabcabcabc", + matches = false, ... }, + f5case { expr = "^x(abc){,2}$", input = "xabc", + matches = true, ncaps = 2, start = 0, sb = 0, + end = 4, eb = 4, content = "xabc" }, + f5case { expr = "^x(abc){,2}$", input = "xabcabc", + matches = true, ncaps = 2, start = 0, sb = 0, + end = 7, eb = 7, content = "xabcabc" }, + // the open-ended (0,7) row :450 + f5case { expr = "^x(abc){,2}", input = "xabcabcabc", + matches = true, ncaps = 2, start = 0, sb = 0, + end = 7, eb = 7, content = "xabcabc" }, + f5case { expr = "^x(abc){,0}de", input = "xde", + matches = true, ncaps = 2, start = 0, sb = 0, + end = 3, eb = 3, content = "xde" }, + f5case { expr = "^x(abc){,0}de", input = "xe", + matches = false, ... }, + f5case { expr = "^x(abc){,2}$", input = "xabcabcabc", + matches = false, ... }, + f5case { expr = "^x(abc){1,}$", input = "xabc", + matches = true, ncaps = 2, start = 0, sb = 0, + end = 4, eb = 4, content = "xabc" }, + f5case { expr = "^x(abc){1,}$", input = "xabcabc", + matches = true, ncaps = 2, start = 0, sb = 0, + end = 7, eb = 7, content = "xabcabc" }, + f5case { expr = "^x(abc){3,}$", input = "xabcabc", + matches = false, ... }, + f5case { expr = "^x(abc){3,}$", input = "xabcabcabc", + matches = true, ncaps = 2, start = 0, sb = 0, + end = 10, eb = 10, content = "xabcabcabc" }, + f5case { expr = "^x(abc){2,2}$", input = "xabcabc", + matches = true, ncaps = 2, start = 0, sb = 0, + end = 7, eb = 7, content = "xabcabc" }, + f5case { expr = "^x(abc){2,2}$", input = "xabc", + matches = false, ... }, + f5case { expr = "^x(abc){2,2}$", input = "xabcabcabc", + matches = false, ... }, + // escaped braces stay literal (+test.ha:437-438) + f5case { expr = "^x(abc)\\{,2\\}$", input = "xabc{,2}", + matches = true, ncaps = 2, start = 0, sb = 0, + end = 8, eb = 8, content = "xabc{,2}" }, + f5case { expr = "^x(abc)\\{,2\\}$", input = "xabcabc{,2}", + matches = false, ... }, + // the 5a carve-out comes home (+test.ha:635) + f5case { expr = "(a|(b?|c*){,1}|d+|e)", input = "e", + matches = true, ncaps = 3, start = 0, sb = 0, + end = 1, eb = 1, content = "e" }, + // nested twins: each agrees with its 5a sibling + // (+test.ha:662-666: {0,} vs *, {1,} vs +, {0,1} vs ?) + f5case { expr = "(a+|b){0,}", input = "ab", matches = true, + ncaps = 2, start = 0, sb = 0, end = 2, eb = 2, + content = "ab" }, + f5case { expr = "(a+|b){1,}", input = "ab", matches = true, + ncaps = 2, start = 0, sb = 0, end = 2, eb = 2, + content = "ab" }, + f5case { expr = "(a+|b){0,1}", input = "ab", matches = true, + ncaps = 2, start = 0, sb = 0, end = 1, eb = 1, + content = "a" }, + // "Various" composed rows (+test.ha:462-485; the M15 4QN + // [[:class:]] row is excluded — POSIX class is a runtime + // loud abort until the POSIX fold) + f5case { expr = "^.(1024)?(face)*(1024)*ca*(f+e?cafe)(babe)+$", + input = "X1024facefacecaaaaafffcafebabebabe", + matches = true, ncaps = 6, start = 0, sb = 0, + end = 34, eb = 34, + content = "X1024facefacecaaaaafffcafebabebabe" }, + f5case { expr = ".(1024)?(face)*(1024)*ca*(f+e?cafe)(babe)+", + input = "X1024facefacecaaaaafffcafebabebabe", + matches = true, ncaps = 6, start = 0, sb = 0, + end = 34, eb = 34, + content = "X1024facefacecaaaaafffcafebabebabe" }, + f5case { expr = "^.(1024)?(face)*(1024)*ca*(f+e?cafe)(babe)+$", + input = "1024facefacecaaaaafffcafebabebabe", + matches = false, ... }, + f5case { expr = ".(1024)?(face)*(1024)*ca*(f+e?cafe)(babe)+", + input = "1024facefacecaaaaafffcafebabebabe", + matches = true, ncaps = 6, start = 3, sb = 3, + end = 33, eb = 33, + content = "4facefacecaaaaafffcafebabebabe" }, + // ww-added multibyte repeat row (B4) + f5case { expr = "^(ä|b){2}x$", input = "äbx", matches = true, + ncaps = 2, start = 0, sb = 0, end = 3, eb = 4, + content = "äbx" }, + // ww-added (rob's rider): long-input {1,} — thread/counter + // explosion stress under repeat spawns + f5case { expr = "(a+|b){1,}", input = "aaaabbbbaaaabbbb", + matches = true, ncaps = 2, start = 0, sb = 0, + end = 16, eb = 16, content = "aaaabbbbaaaabbbb" }, + ]; + 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; + }; +}; + +// findall × repeat composition (+test.ha:724 `fo{2,}`) — the greedy +// per-position longest pick plus the non-overlap advance over a +// repeat-counted program. +@test fn fold5b_findall() void = { + let targets: [4]str = ["foo", "fooo", "foo", "foo"]; + let rows: [1]facase = [ + facase { expr = "fo{2,}", input = "fo foo fooofoof oofoo", + toff = 0, tcnt = 4 }, + ]; + 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.findall(&re, inp); + if (!(fr is []regex.result)) { fail(); }; + let results: []regex.result = fr as []regex.result; + if (len(results) != rows[i].tcnt) { fail(); }; + let k: i32 = 0; + for (k < rows[i].tcnt) { + let want: str = targets[rows[i].toff + k]; + if (strings.compare(results[k][0].content, + want) != 0) { + fail(); + }; + k += 1; + }; + regex.result_freeall(results); + regex.finish(&re); + }; + case => fail(); + }; + i += 1; + }; +}; + export fn main() i32 = { signalled = 1; lit_and_match(); signalled = 2; size_aliases_distinct(); @@ -2231,7 +2437,6 @@ export fn main() i32 = { signalled = 7; compile_literal_program(); signalled = 8; compile_any_program(); signalled = 9; compile_empty_program(); - signalled = 10; compile_metachar_loud(); signalled = 11; thread_shape(); signalled = 12; newmatch_discriminates(); signalled = 13; result_free_noop(); @@ -2265,5 +2470,8 @@ export fn main() i32 = { signalled = 41; fold5_optional_group(); signalled = 42; fold5_submatches(); signalled = 43; parse_repetition_cases(); + signalled = 44; fold5b_compile_errors(); + signalled = 45; fold5b_find_cases(); + signalled = 46; fold5b_findall(); return 0; };