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

@@ -11,23 +11,17 @@
// `^`/`$`, the `\` escape, postfix `?`/`*`/`+`, alternation `|`
// (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);
// expressions `[..]` (handle_bracket + the run_thread charset arm);
// fold 5a = capture groups `(`/`)` (compile arms + run_thread
// groupstart/groupend + the add_thread capture dup + the search
// 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):
// - charclass_map (regex.ha:74-87) — a module-level const slice of
// (str, *fn(rune) bool) tuples. Blocked on the array-literal→slice
// element-coercion checker gap (#25; type.c:402-404 #258 borrow
// uses exact type_eq, no element decay).
// rep_counters prefill); fold 6 = POSIX character classes
// `[[:class:]]` (charclass_map + compile/exec arms). Remaining:
// replace. It lands with its fold.
package regex;
import ascii;
import bufio;
import io;
import memio;
@@ -111,18 +105,23 @@ 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
// matching ascii predicate. DEFERRED: the array-literal→slice
// assignability check (type.c:402-404, the #258 borrow) compares
// element types with exact type_eq and applies NO element coercion, so
// the literal `[(":alnum:]", &ascii.isalnum), ...]` (typed
// `[N](untyped_str, *fn(rune) bool)`) is rejected against the declared
// `[](str, *fn(rune) bool)`. Minimal repro: `let xs: [](size, size) =
// [(1, 2)];`. Reshaping to a fixed `[12](...)` array would compile but
// is an unfaithful workaround (CLAUDE.md rule-7), so the table — and
// the `import ascii;` it needs — land with the consuming fold (compile)
// once the checker gap is fixed.
// ref/hare/regex/regex.ha:74-87 — POSIX class token → ascii predicate.
// Inline tuple type (not charset_class_item alias) matches the Hare
// decl form; #124 cgen closed the cross-module &fn-in-const gap.
const charclass_map: [](str, *fn(c: rune) bool) = [
(":alnum:]", &ascii.isalnum),
(":alpha:]", &ascii.isalpha),
(":blank:]", &ascii.isblank),
(":cntrl:]", &ascii.iscntrl),
(":digit:]", &ascii.isdigit),
(":graph:]", &ascii.isgraph),
(":lower:]", &ascii.islower),
(":print:]", &ascii.isprint),
(":punct:]", &ascii.ispunct),
(":space:]", &ascii.isspace),
(":upper:]", &ascii.isupper),
(":xdigit:]", &ascii.isxdigit),
];
// ref/hare/regex/regex.ha:89-93.
export type regex = struct {
@@ -192,18 +191,11 @@ 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.
// ref/hare/regex/regex.ha:135-225. 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,
@@ -264,7 +256,21 @@ fn handle_bracket(
*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");
let rest: str = strings.iterstr(iter);
for (let cc_idx: size = 0;
cc_idx < (len(charclass_map): size);
cc_idx += 1) {
if (strings.hasprefix(rest, charclass_map[cc_idx].0)) {
let n: size = (len(*charsets): size);
append((*charsets)[n - 1],
(charclass_map[cc_idx]: charset_class_item));
*skip_charclass_rest = true;
break;
};
};
if (!*skip_charclass_rest) {
return "No character class after '[:'": error;
};
} else if (is_range) { // regex.ha:205-217
let start_b: u32 = (r: u32);
let end_b: u32 = ((range_end as rune): u32);
@@ -1021,9 +1027,7 @@ fn run_thread(
// 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).
// (it shadows the thread-index param).
for (let k: size = 0; k < (len(cset): size); k += 1) {
let cur: (charset_lit_item | charset_range_item |
charset_class_item) = cset[k];
@@ -1046,8 +1050,15 @@ fn run_thread(
break;
};
};
case charset_class_item =>
abort("regex: POSIX character class not yet ported");
case let class_item: charset_class_item => { // regex.ha:726-733
let classfn: *fn(c: rune) bool = class_item.1;
if ((*classfn)(r)) {
// Succeeded if positive match
// Failed if negative match
matched = cs.is_positive;
break;
};
};
};
};
if (!matched) {