regex: fold 5b — repetition {m,n} (the { arm + run_thread inst_repeat + rep prefill)

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.
This commit is contained in:
2026-06-05 04:50:27 +09:00
parent c0c15945be
commit 7545bf7dcd
2 changed files with 340 additions and 47 deletions

View File

@@ -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;
};