lib/regex: #123 POSIX character classes [:class:] — charclass_map + compile/exec arms

12-entry charclass_map (str, *fn(rune) bool) + bracket [:class:] recognition
(compile) + charset_class_item predicate dispatch (exec), porting
ref/hare/regex/regex.ha:74-87/190-204/726-733. Table-driven tests cover all 12
classes (±), negation, composition, and the "No character class after '[:'"
error path.

cstage-only (C-first per the speed pivot); wwstage byte-id twin owed in the
batch-converge phase.
This commit is contained in:
2026-06-07 03:10:55 +09:00
parent 66d69537a5
commit 3ae24c22b8
2 changed files with 204 additions and 46 deletions

View File

@@ -14,10 +14,9 @@
// Fold 2a ports compile()'s lit/any/match arms only; exec lives in
// later folds, so the compile_* cases pin the emitted inst PROGRAM
// (shape + payloads via indexed match-extraction), not matching.
// charclass_map's fn-ptr table is deferred behind the array→slice
// element-coercion checker gap (see regex.ww), so this test does not
// exercise the POSIX-class predicate dispatch yet — it pins variant
// discrimination (including the nominally-distinct same-underlying
// fold-6 tests exercise POSIX character classes (charclass_map +
// compile/exec arms). Earlier folds pin variant discrimination
// (including the nominally-distinct same-underlying
// inst_split/inst_jump/inst_groupstart `size` aliases and the
// inst_any/inst_skip/inst_groupend `void` aliases), payload extraction,
// the regex/capture struct shapes, and finish(). Same
@@ -1436,7 +1435,7 @@ type cerow = struct {
// 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).
// class items as -2 (presence sentinel — fn-ptr address not stable).
// 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 |
@@ -1448,6 +1447,7 @@ fn cssig(cs: [](charset_lit_item | charset_range_item |
return 1000000 + ((l: rune): i64);
case let range: charset_range_item =>
return (range.0: i64) * 10000 + (range.1: i64);
case charset_class_item => return -2; // fn-ptr address not stable
case => return -1;
};
};
@@ -2427,6 +2427,150 @@ type prrow = struct {
};
};
// ---- fold 6: POSIX character classes ---------------------------------
// compile error: `[[:` with no valid class name
// ref/hare/regex/+test.ha — no direct cite; error string from
// regex.ha:203 "No character class after '[:'".
@test fn fold6_compile_errors() void = {
let rows: [1]cerow = [
cerow { pat = "[[:xyz", want = "No character class after '[:'" },
];
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;
};
};
// charsets-table content pin: a POSIX class bracket emits ONE
// charset_class_item element (cssig → -2), not a literal expansion.
// ref/hare/regex/+test.ha:305-308 POSIX class rows.
@test fn fold6_charsets() void = {
// "[[:digit:]]" → 1 charset, 1 class elem
let c: (regex.regex | regex.error | nomem) = regex.compile("[[:digit:]]");
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) != 1) { fail(); };
if (cssig(cs0, 0) != -2) { fail(); };
regex.finish(&re);
};
case => fail();
};
};
// end-to-end find/test rows for all 12 POSIX classes + negation.
// ref/hare/regex/+test.ha:278-345 POSIX subset.
@test fn fold6_find_cases() void = {
let rows: [28]fcase = [
// digit
fcase { expr = "[[:digit:]]", input = "5", matches = true,
start = 0, sb = 0, end = 1, eb = 1, content = "5" },
fcase { expr = "[[:digit:]]", input = "a", matches = false, ... },
// alpha
fcase { expr = "^[[:alpha:]]+$", input = "abc", matches = true,
start = 0, sb = 0, end = 3, eb = 3, content = "abc" },
fcase { expr = "^[[:alpha:]]+$", input = "abc1", matches = false, ... },
// alnum
fcase { expr = "^[[:alnum:]]+$", input = "abc9", matches = true,
start = 0, sb = 0, end = 4, eb = 4, content = "abc9" },
fcase { expr = "^[[:alnum:]]+$", input = "abc!", matches = false, ... },
// space
fcase { expr = "[[:space:]]", input = " ", matches = true,
start = 0, sb = 0, end = 1, eb = 1, content = " " },
fcase { expr = "[[:space:]]", input = "a", matches = false, ... },
// upper
fcase { expr = "^[[:upper:]]+$", input = "ABC", matches = true,
start = 0, sb = 0, end = 3, eb = 3, content = "ABC" },
fcase { expr = "^[[:upper:]]+$", input = "ABc", matches = false, ... },
// lower
fcase { expr = "^[[:lower:]]+$", input = "abc", matches = true,
start = 0, sb = 0, end = 3, eb = 3, content = "abc" },
fcase { expr = "^[[:lower:]]+$", input = "abC", matches = false, ... },
// xdigit
fcase { expr = "^[[:xdigit:]]+$", input = "0aF", matches = true,
start = 0, sb = 0, end = 3, eb = 3, content = "0aF" },
fcase { expr = "^[[:xdigit:]]+$", input = "0g", matches = false, ... },
// blank (space or tab)
fcase { expr = "[[:blank:]]", input = "\t", matches = true,
start = 0, sb = 0, end = 1, eb = 1, content = "\t" },
fcase { expr = "[[:blank:]]", input = "a", matches = false, ... },
// punct
fcase { expr = "[[:punct:]]", input = ".", matches = true,
start = 0, sb = 0, end = 1, eb = 1, content = "." },
fcase { expr = "[[:punct:]]", input = "a", matches = false, ... },
// graph
fcase { expr = "[[:graph:]]", input = "!", matches = true,
start = 0, sb = 0, end = 1, eb = 1, content = "!" },
fcase { expr = "[[:graph:]]", input = " ", matches = false, ... },
// print
fcase { expr = "[[:print:]]", input = " ", matches = true,
start = 0, sb = 0, end = 1, eb = 1, content = " " },
fcase { expr = "[[:print:]]", input = "\x01", matches = false, ... },
// cntrl
fcase { expr = "[[:cntrl:]]", input = "\x01", matches = true,
start = 0, sb = 0, end = 1, eb = 1, content = "\x01" },
fcase { expr = "[[:cntrl:]]", input = "a", matches = false, ... },
// negated class
fcase { expr = "^[^[:digit:]]+$", input = "abc", matches = true,
start = 0, sb = 0, end = 3, eb = 3, content = "abc" },
fcase { expr = "^[^[:digit:]]+$", input = "ab5", matches = false, ... },
// composition: digit+ in surrounding text
fcase { expr = "[[:digit:]]+", input = "abc123def",
matches = true, start = 3, sb = 3, end = 6, eb = 6,
content = "123" },
fcase { expr = "[[:digit:]]+", input = "nodigits",
matches = false, ... },
];
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();
@@ -2473,5 +2617,8 @@ export fn main() i32 = {
signalled = 44; fold5b_compile_errors();
signalled = 45; fold5b_find_cases();
signalled = 46; fold5b_findall();
signalled = 47; fold6_compile_errors();
signalled = 48; fold6_charsets();
signalled = 49; fold6_find_cases();
return 0;
};