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

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