regex: fold 5a — capture groups (compile ( ) arms + run_thread groupstart/groupend + search spread)

compile: '(' (ha:317-323) appends inst_groupstart(capture_idx) and
grows jump_idxs per level; ')' (ha:324-334) appends the void
inst_groupend, fixes up the level's pending alternation jumps (#70
range + #58 assert), range-deletes them (#8) and closes the level;
the loop-exit done arm gains the Unmatched-'(' check (ha:277-282).
The anchors' group_level arms, the postfix inst_groupend/groupstart
arms and find_last_groupstart's success path go live unchanged.

run_thread: inst_groupstart (ha:636-652) fill-grows captures to
idx+1 (count-loop spelling of Hare's 3-arg fill-append) and opens
the group with the SIZE_MAX end sentinel via the #20-fixed indexed
struct store; inst_groupend (ha:653-668) closes the innermost
unclosed capture (ha:655's 2-clause for respelled — ww has no
cond;post form) and slices content from the bytesize span through
the addr-of-element pointer.

search: the ha:820 loud bound flips to the real capture spread
(#35/#25); the pad fill self-activates for unset trailing groups.

Tests: ( ) graduate from the metachar-loud table into real-text
error rows (+ ww-added anchor-in-group / Unused-on-groupstart
re-verify rows); hand-built groupstart/groupend arm cases; the 5a
find table (+test.ha:257-275 group/alternation, :499-503/:607 jump
bugs, :610-621 submatch family, :635/:640 alternation-group,
:649-665 nested minus the 5b {m,n} twins, + ww-added multibyte
row) with len(res) pinned per row; submatch content rows
(+test.ha:704-708 + ww-added multibyte) — the 5a acceptance gate.
This commit is contained in:
2026-06-05 00:46:04 +09:00
parent 0e61db1857
commit a2c2bbc6b1
2 changed files with 526 additions and 68 deletions

View File

@@ -12,10 +12,12 @@
// (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).
// 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.
// 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.
//
// One fold-1 construct is held back behind a filed compiler/fidelity
// gap (see the charclass_map site below):
@@ -142,11 +144,10 @@ export fn finish(re: *regex) void = {
free(re.charsets);
};
// ref/hare/regex/regex.ha:104-119, verbatim. The error arm is ALWAYS
// taken until the group fold ('(' is loud, no inst_groupstart can
// exist); the '|' arm consumes it as origin = 0 (whole-expression
// alternation). Match-on-indexed scrutinee binds through a typed let
// (the run_thread spelling).
// ref/hare/regex/regex.ha:104-119, verbatim. The '|' arm consumes the
// error arm as origin = 0 (whole-expression alternation); the postfix
// `?`/`*`/`+` groupend arms propagate it. Match-on-indexed scrutinee
// binds through a typed let (the run_thread spelling).
fn find_last_groupstart(insts: []inst) (size | error) = {
let nested: uint = 0;
for (let i: size = (len(insts): size); i > 0; i -= 1) {
@@ -290,17 +291,15 @@ fn handle_bracket(
// (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) — and the fold-4 bracket surface: the
// (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). 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
// with their arms; the loop-head `done` Unmatched-'(' check
// (ha:278-280) rides the '(' arm.
// handle_bracket state quad (249-252) — and 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.
//
// 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()
@@ -315,9 +314,9 @@ export fn compile(expr: str) (regex | error | nomem) = {
let iter: strings.iterator = strings.iter(expr);
let r_idx: size = 0;
// jump_idxs tracks the pending alternation jumps per group level;
// only level 0 can populate until the group fold. Hare's
// `append(jump_idxs, [])` (ha:242) spells through a typed empty
// let (the #25/#31 ruling; cf the inst_skip let below).
// the '(' arm grows it past level 0. Hare's `append(jump_idxs,
// [])` (ha:242) spells through a typed empty let (the #25/#31
// ruling; cf the inst_skip let below).
let jump_idxs: [][]size;
let lvl0: []size;
append(jump_idxs, lvl0);
@@ -329,6 +328,7 @@ export fn compile(expr: str) (regex | error | nomem) = {
let was_prev_rune_pipe: bool = false;
let n_reps: size = 0;
let group_level: size = 0;
let capture_idx: size = 0;
for (true) {
let next: (rune | utf8.done) = strings.next(&iter);
@@ -368,7 +368,12 @@ export fn compile(expr: str) (regex | error | nomem) = {
};
let r: rune = match (next) {
case utf8.done => break;
case utf8.done => { // regex.ha:277-282
if (group_level > 0) {
return "Unmatched '('": error;
};
break;
};
case let x: rune => yield x;
};
@@ -406,8 +411,7 @@ export fn compile(expr: str) (regex | error | nomem) = {
append(insts, (types.SIZE_MAX: inst_jump));
// origin = the instruction after the innermost
// groupstart, or 0 for whole-expression alternation
// (the error arm — always taken until the group
// fold). Hare binds via a match EXPRESSION
// (the error arm). Hare binds via a match EXPRESSION
// (ha:337-342); statement-match into a declared
// local, the search/scanrune precedent (#51).
let origin: size = 0;
@@ -468,7 +472,6 @@ export fn compile(expr: str) (regex | error | nomem) = {
case inst_charset => void;
case inst_any => void;
case inst_groupend => {
// dead until the group fold ('(' is loud).
// Hare propagates with `?` (ha:410-411);
// `?` into compile's >32B tagged return is
// loud-stopped (#38b) — the explicit D13
@@ -502,7 +505,7 @@ export fn compile(expr: str) (regex | error | nomem) = {
case inst_charset => void;
case inst_any => void;
case inst_groupend => {
// dead until the group fold; D13 match, cf '?'
// D13 match, cf '?'
match (find_last_groupstart(
insts[0:term_start_idx])) {
case let e: error => return e;
@@ -531,7 +534,7 @@ export fn compile(expr: str) (regex | error | nomem) = {
case inst_charset => void;
case inst_any => void;
case inst_groupend => {
// dead until the group fold; D13 match, cf '?'
// D13 match, cf '?'
match (find_last_groupstart(
insts[0:term_start_idx])) {
case let e: error => return e;
@@ -554,9 +557,44 @@ export fn compile(expr: str) (regex | error | nomem) = {
in_bracket = true;
case ']': // regex.ha:315-316 — literal outside a bracket
append(insts, (r: inst_lit));
case '(', ')', '{':
// fold-4 boundary: the group (ha:317-334) and
// repetition (368-401) arms ride later folds.
case '(': { // regex.ha:317-323
append(insts, (capture_idx: inst_groupstart));
group_level += 1;
capture_idx += 1;
// Hare grows with `append(jump_idxs, [])?`
// (ha:321-323); the empty-slice literal spells
// through a typed let (the #25/#31 ruling, cf the
// lvl0 prologue).
for ((jump_idxs.len: size) < group_level + 1) {
let lvl: []size;
append(jump_idxs, lvl);
};
};
case ')': { // regex.ha:324-334
if (group_level == 0) {
// VERBATIM duplicate of find_last_groupstart's
// text — Hare keeps two copies (ha:118/326).
return "Unmatched ')'": error;
};
// payload-less void variant through a typed let (the
// inst_skip spelling above).
let ge: inst_groupend;
let v: inst = ge;
append(insts, v);
// jump fixup (ha:329-332): by-value range over an
// INDEXED base is the #70-fixed shape; assert is the
// #58 builtin.
for (let jump_idx .. jump_idxs[group_level]) {
assert(insts[jump_idx] is inst_jump);
insts[jump_idx] =
(((insts.len: size) - 1): inst_jump);
};
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:462-463
append(insts, (r: inst_lit));
@@ -650,10 +688,11 @@ 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 +
// 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()
// 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
// the non-consuming loop (ha:604-605) and the trailing default
// (ha:738) are Hare's own unreachable arms; Hare spells them bare
@@ -719,10 +758,54 @@ fn run_thread(
let nm: newmatch;
return nm;
};
case inst_groupstart =>
abort("regex: inst_groupstart not yet ported");
case inst_groupend =>
abort("regex: inst_groupend not yet ported");
case let gs: inst_groupstart => { // regex.ha:636-652
let idx: size = (gs: size);
// ha:637-641: Hare's 3-arg fill-append `append(...,
// [capture { ... }...], idx - len + 1)?` has no ww
// spelling — the ratified count-loop fill (cf the
// search epilogue's pad loop).
for (((*threads)[i].captures.len: size) < idx + 1) {
append((*threads)[i].captures, capture { ... });
};
assert((*threads)[i].captures[idx].end
!= types.SIZE_MAX);
(*threads)[i].captures[idx] = capture {
content = "",
start = str_idx,
start_bytesize = str_bytesize,
// end=types.SIZE_MAX indicates that the
// capture group hasn't ended yet
end = types.SIZE_MAX,
end_bytesize = types.SIZE_MAX,
};
(*threads)[i].pc += 1;
};
case inst_groupend => { // regex.ha:653-668
let curr_capture: size =
((*threads)[i].captures.len: size);
// ha:655's 2-clause `for (cond; post)` has no ww
// parse form (1- and 3-clause only) — the post-step
// moves to the body tail. Equivalent: break skips
// post in both, and the body has NO continue (which
// would run post in Hare but skip the inline tail).
for (curr_capture > 0) {
// find inner-most unclosed capture group
if ((*threads)[i].captures[curr_capture - 1].end
== types.SIZE_MAX) {
break;
};
curr_capture -= 1;
};
assert(curr_capture > 0, "Found a groupend token \")\" without having previously seen a groupstart token \"(\". Please report this as a bug");
// Hare's name shadows the capture TYPE — ww splits the
// type/value namespaces (#225), so it ports verbatim.
let capture = &(*threads)[i].captures[curr_capture - 1];
capture.end = str_idx;
capture.end_bytesize = str_bytesize;
capture.content = strings.frombytes(str_bytes[
capture.start_bytesize:capture.end_bytesize]);
(*threads)[i].pc += 1;
};
case inst_repeat =>
abort("regex: inst_repeat not yet ported");
case => abort("regex: unreachable");
@@ -900,20 +983,13 @@ fn search(
// #47); the capacity hint drops with per-value alloc.
let res: []capture;
append(res, threads[best_idx].root_capture);
// ha:820 spreads threads[best_idx].captures into res.
// The indexed spread source is #35-blocked, but
// captures only populate via the loud-aborted
// groupstart arm or add_thread's loud-bounded dup —
// provably empty in every program this fold can run.
// Loud-bound like add_thread's dup; the verbatim
// spread lands with the group fold.
if (threads[best_idx].captures.len != 0) {
abort("regex: capture copy not yet ported (#35)");
};
// ha:820 — Hare's `static append` (capacity-bounded)
// is a plain append (D6, no static form); the indexed
// spread source is the #35/#25-landed shape.
append(res, threads[best_idx].captures...);
// ha:821-824's sized fill-append `[capture { ... }...]`
// has no ww spelling; the count loop is shape-correct
// and self-activates with the group fold (length stays
// 1 until then).
// has no ww spelling the ratified count-loop fill
// pads the unset trailing groups.
for (length != (len(res): size)) {
append(res, capture { ... });
};