regex: fold 4 — bracket expressions (handle_bracket + run_thread charset arm)

Port of ref/hare/regex/regex.ha:135-225 (handle_bracket, whole),
265-275 (in_bracket dispatch), 313-314 (the `[` flip), 249-252 (the
bracket state quad) and 704-737 (the consuming charset arm). `[`
graduates from the fold-2a loud set; `(` `)` `{` are the last
three loud metachars. The POSIX-class arm keeps its DETECTION
verbatim but loud-aborts its BODY (charclass_map stays #25-blocked;
falling through to the literal arm would silently compile
[[:alpha:]] as a 9-literal charset). is_consuming_inst already
covered inst_charset.

Spelling divergences, all site-documented: the dispatch propagates
via the explicit D13 match, not `?` (compile's 64B sret return is
the #38b loud-stop; the fold-3 find_last_groupstart precedent);
charset's declaration moves BELOW its member types (cstage sizes a
tagged alias with forward-declared members at a degenerate 8B —
ww-core #69, wwstage is correct); run_thread binds the charset
structurally, not via the alias (alias-typed slice locals mis-scale
their index reads in wwstage — ww-core #68).

Tests: Hare's own bracket block (+test.ha:278-345, the group and
POSIX rows excluded with their loud arms) as the 72-row find/test
table incl. multibyte literal+range brackets and an unanchored
[ab]+ composition row; charsets-table content pins (lit/range
discrimination, first-char ]/[ literals, literal dashes, multibyte
codepoints); program-shape pins ([abc] / ^[abc]$ / [^ab] /
[ab][cd] / [abc]*); exact-text error rows (Unmatched '[' ×3 incl
the escape interaction, descending [z-a]); findall composition.
The [[:alpha:]] abort text is unpinnable in-process (it kills the
runner) — source-audited until the POSIX fold.
This commit is contained in:
2026-06-04 20:38:42 +09:00
parent 0055ac2cd3
commit 95fea97868
2 changed files with 576 additions and 36 deletions

View File

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