Files
ww/lib/regex/regex.ww
Hojun-Cho da48b29a7c lib/regex: fold-3 — anchors, escape, postfix ?/*/+, alternation
Ports compile()'s fold-3 arms (ref/hare/regex/regex.ha): \ escape
(286-293), ^ (294-300), $ (301-312, goes live with run_thread's
anchored ha:621-624 pin), | (335-367) over the restored jump_idxs
prologue (241-256 subset: jump_idxs + was_prev_rune_pipe +
group_level verbatim-but-0) + the whole-expression fixup (470-473,
the SIZE_MAX-sentinel overwrite 2a dropped), ? (403-420), * (421-443),
+ (444-459); find_last_groupstart (104-119, whole — error arm is the
live one until the group fold) and shift (123-133). run_thread's
inst_split/inst_jump aborts FLIP live (606-611, all 2b-proven
shapes). Still loud: [ ( ) { in compile; groupstart/groupend/repeat/
charset arms; add_thread's capture-dup bound.

Spelling divergences, each cited at site:
- multi-type case (inst_lit | inst_charset | inst_any) (ha:407) is
  loud-rejected both stages (PE5; Hare-parity #13) -> three void arms.
- find_last_groupstart(...)? in the dead groupend arms hits the #38b
  >32B-tagged-return propagate loud-stop -> explicit D13 match
  (harec's own ? desugaring).
- by-value range over an indexed element (for (let x .. jump_idxs[g]))
  SEGFAULTS both stages (NEW, filed ww-core #57) -> D9 index loops at
  ha:351-356 + 470-473.
- assert(cond, msg) is wwstage-broken (CALL assert(SB), undefined at
  link; NEW, filed ww-core #58) -> if+abort at the ha:355 site.
- Hare's match/if EXPRESSIONS (ha:337-346) -> statement spellings
  (the #51 search/scanrune precedent).

Tests: the 11-pattern loud row shrinks to the 4 remaining metachars;
strerror reroutes through a[. New: fold3_programs (exact inst
sequences incl. the a|b sentinel fixup), fold3_compile_errors (12
exact texts incl. Hare's own ab\|^cd ERROR fixture), direct
find_last_groupstart/shift rows, fold3_find_cases (42 rows from
Hare's +test.ha anchors/postfix/alternation blocks, group-free
subset, end=-1 resolved to rune-length; rob's dedup/leftmost/longest
riders now observable: a* over aaaa -> single (0,4), b+ over abab ->
(1,2), b* / ^b* over aaaabbbb -> (4,8)/(0,0); multibyte b+ row keeps
idx != bytesize) + the a*-over-baa findall rider (longest-pick beats
the zero-length candidate; trailing zero-length match takes the
ha:942-945 break). Both drivers green; regex_test.combined.ww cs==ww
byte-identical.
2026-06-04 17:25:05 +09:00

983 lines
34 KiB
Plaintext

// regex — POSIX extended regular expressions. Port of
// ref/hare/regex/regex.ha. Fold 1 = the data model; fold 2a = the
// compile() literal core (lit/any/match + the leading skip); fold 2b
// tranche A = the thread-machine scaffolding (thread/newmatch types,
// result_free, strerror); fold 2b tranche B = the engine
// (delete_thread/is_consuming_inst/add_thread/run_thread); fold 2b
// tranche C = search, the first end-to-end match; fold 2b tranche D
// = the exec surface (test/find — the D13 explicit-match spelling of
// Hare's multi-success `?`, ww-core #14); fold 2c =
// 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.
//
// 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).
package regex;
import bufio;
import io;
import memio;
import strings;
import types;
import encoding.utf8;
// ref/hare/regex/regex.ha:14 — an error string describing a compilation
// error.
export type error = !str;
// ref/hare/regex/regex.ha:16-30.
export type inst_lit = rune;
export type inst_charset = struct { idx: size, is_positive: bool };
export type inst_any = void;
export type inst_split = size;
export type inst_jump = size;
export type inst_skip = void;
export type inst_match = bool;
export type inst_groupstart = size;
export type inst_groupend = void;
export type inst_repeat = struct {
id: size,
origin: size,
min: (void | size),
max: (void | size),
};
// ref/hare/regex/regex.ha:32-35.
export type inst = (inst_lit | inst_any | inst_split | inst_jump |
inst_skip | inst_match | inst_charset |
inst_groupstart | inst_groupend |
inst_repeat);
// The resulting match of a [[regex]] applied to a string.
//
// The first [[capture]] corresponds to the implicit zeroth capture
// group, i.e. the whole expression.
//
// The rest of the [[capture]]s correspond to the rest of the capture
// groups, i.e. the sub-expressions.
// ref/hare/regex/regex.ha:44.
export type result = []capture;
// A (sub)match corresponding to a regular expression's capture group.
// ref/hare/regex/regex.ha:47-53.
export type capture = struct {
content: str,
start: size,
start_bytesize: size,
end: size,
end_bytesize: size,
};
// ref/hare/regex/regex.ha:55-64.
type thread = struct {
pc: size,
start_idx: size,
start_bytesize: size,
root_capture: capture,
captures: []capture,
rep_counters: []size,
matched: bool,
failed: bool,
};
// Discriminates a fresh match from plain void at run_thread's return
// 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);
export type charset_lit_item = rune;
export type charset_range_item = (u32, u32);
export type charset_class_item = (str, *fn(c: rune) bool);
// 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:89-93.
export type regex = struct {
insts: []inst,
charsets: []charset,
n_reps: size,
};
// Frees resources associated with a [[regex]].
//
// ref/hare/regex/regex.ha:96-102, verbatim. The free() builtin is a
// documented no-op (#27 landed): ww is a no-free runtime (rt/alloc.s:30
// — the bump allocator can't reclaim, process-exit does), so each free
// below evaluates its operand and reclaims nothing. Kept verbatim for
// API + source parity with the Hare surface.
export fn finish(re: *regex) void = {
free(re.insts);
for (let charset .. re.charsets) {
free(charset);
};
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).
fn find_last_groupstart(insts: []inst) (size | error) = {
let nested: uint = 0;
for (let i: size = (len(insts): size); i > 0; i -= 1) {
let cur: inst = insts[i - 1];
match (cur) {
case inst_groupstart => {
if (nested == 0) {
return i - 1;
};
nested -= 1;
};
case inst_groupend => { nested += 1; };
case => void;
};
};
return "Unmatched ')'": error;
};
// Increments all inst_jump and inst_split instructions to account
// for a newly inserted instruction before the given slice.
//
// ref/hare/regex/regex.ha:123-133. Hare's by-ref `&..` range has no
// ww spelling (D9, the add_thread/search precedent) — index loop;
// the write-through is a tagged element store via index (PE3), and
// the sub-slice arg at the call sites (`shift(insts[k:])`) is a
// borrowed VIEW, so the stores land in the caller's backing (PE4).
fn shift(sl: []inst) void = {
for (let i: size = 0; i < (len(sl): size); i += 1) {
let cur: inst = sl[i];
match (cur) {
case let z: inst_jump =>
sl[i] = (((z: size) + 1): inst_jump);
case let z: inst_split =>
sl[i] = (((z: size) + 1): inst_split);
case => void;
};
};
};
// 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
// 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.
//
// 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()
// builtin is a no-op anyway (#27 — ww is a no-free runtime), so the
// cleanup would reclaim nothing.
export fn compile(expr: str) (regex | error | nomem) = {
// Hare `let insts: []inst = [];` — a bare ww slice declaration
// 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 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).
let jump_idxs: [][]size;
let lvl0: []size;
append(jump_idxs, lvl0);
let was_prev_rune_pipe: bool = false;
let n_reps: size = 0;
let group_level: size = 0;
for (true) {
let next: (rune | utf8.done) = strings.next(&iter);
if (r_idx == 0 && next is rune && (next as rune) != '^') {
// Bare append: ww append returns void; Hare's
// `append(...)?` nomem propagation is filed #36.
// Hare appends the bare type name (`inst_skip`) as
// the void-variant value; in ww that resolves as a
// symbol ref (cf the alloc(T) rule), so void
// variants go through a typed let.
let sk: inst_skip;
let v: inst = sk;
append(insts, v);
};
let r: rune = match (next) {
case utf8.done => break;
case let x: rune => yield x;
};
switch (r) {
case '\\': { // regex.ha:286-293
let peek1: (rune | utf8.done) = strings.next(&iter);
if (peek1 is utf8.done) {
return "Trailing backslash '\\'": error;
};
append(insts, ((peek1 as rune): inst_lit));
r_idx += 1;
};
case '^': { // regex.ha:294-300
if (group_level > 0) {
return "Anchor '^' in capture groups is unsupported": error;
};
if (!(r_idx == 0 || was_prev_rune_pipe)) {
return "Anchor '^' not at start of whole pattern or alternation": error;
};
};
case '$': { // regex.ha:301-312
if (group_level > 0) {
return "Anchor '$' in capture groups is unsupported": error;
};
let peek1: (rune | utf8.done) = strings.next(&iter);
if (peek1 is rune) {
if ((peek1 as rune) != '|') {
return "Anchor '$' not at end of whole pattern or alternation": error;
};
strings.prev(&iter);
};
append(insts, (true: inst_match));
};
case '|': { // regex.ha:335-367
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
// (ha:337-342); statement-match into a declared
// local, the search/scanrune precedent (#51).
let origin: size = 0;
match (find_last_groupstart(insts)) {
case let e: error => { origin = 0; };
case let sz: size => { origin = sz + 1; };
};
let newinst: inst = (((insts.len: size) + 1): inst_split);
// add split after last jump (if any) or at origin.
// Hare's if-EXPRESSION (ha:345-346) spells as a
// statement; the tail read is the PE2 double-index.
let split_idx: size = origin;
if (jump_idxs[group_level].len > 0) {
split_idx = jump_idxs[group_level][jump_idxs[group_level].len - 1] + 1;
};
insert(insts[split_idx], newinst);
shift(insts[split_idx + 1:]);
// our insertion of our split_idx should never
// interfere with an existing jump_idx; if this check
// ends up being hit in the future, it is a sign that
// jump_idx should be incremented. Hare asserts
// (ha:351-356); the assert builtin is wwstage-broken
// (ww-core #58) and the by-value range over an
// indexed element segfaults (ww-core #57) — if+abort
// over a D9-class index loop.
for (let k: size = 0; k < (jump_idxs[group_level].len: size); k += 1) {
if (!(jump_idxs[group_level][k] < split_idx)) {
abort("Found jump_idx interference. Please report this as a bug");
};
};
append(jump_idxs[group_level], (insts.len: size) - 1);
// add skip if it's a whole-expression alternation
if (origin == 0) {
let peek1: (rune | utf8.done) = strings.next(&iter);
if (peek1 is rune) {
if ((peek1 as rune) != '^') {
let sk: inst_skip;
let v: inst = sk;
append(insts, v);
};
strings.prev(&iter);
};
};
};
case '?': { // regex.ha:403-420
if (r_idx == 0 || insts.len == 0) {
return "Unused '?'": error;
};
let term_start_idx: size = (insts.len: size) - 1;
// Hare's multi-type arm `case (inst_lit |
// inst_charset | inst_any)` (ha:407) is loud-rejected
// by design BOTH stages (PE5; Hare-parity task
// ww-core #13) — three void arms. The scrutinee binds
// through a typed let (the run_thread spelling).
let cur: inst = insts[term_start_idx];
match (cur) {
case inst_lit => void;
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
// match, harec's own desugaring.
match (find_last_groupstart(
insts[0:term_start_idx])) {
case let e: error => return e;
case let sz: size => { term_start_idx = sz; };
};
};
case inst_groupstart =>
return "Unused '?'": error;
case =>
return "Misused '?'": error;
};
let after_idx: size = (insts.len: size) + 1;
insert(insts[term_start_idx], (after_idx: inst_split));
shift(insts[term_start_idx + 1:]);
};
case '*': { // regex.ha:421-443
if (r_idx == 0 || insts.len == 0) {
return "Unused '*'": error;
};
let new_inst_offset: size = 1;
let jump_idx: size = (insts.len: size) + new_inst_offset;
let after_idx: size = jump_idx + 1;
let term_start_idx: size = (insts.len: size) - 1;
let cur: inst = insts[term_start_idx];
match (cur) {
case inst_lit => void;
case inst_charset => void;
case inst_any => void;
case inst_groupend => {
// dead until the group fold; D13 match, cf '?'
match (find_last_groupstart(
insts[0:term_start_idx])) {
case let e: error => return e;
case let sz: size => { term_start_idx = sz; };
};
};
case inst_groupstart =>
return "Unused '*'": error;
case =>
return "Misused '*'": error;
};
let split_idx: size = term_start_idx;
term_start_idx += new_inst_offset;
insert(insts[split_idx], (after_idx: inst_split));
shift(insts[split_idx + 1:]);
append(insts, (split_idx: inst_jump));
};
case '+': { // regex.ha:444-459
if (r_idx == 0 || insts.len == 0) {
return "Unused '+'": error;
};
let term_start_idx: size = (insts.len: size) - 1;
let cur: inst = insts[term_start_idx];
match (cur) {
case inst_lit => void;
case inst_charset => void;
case inst_any => void;
case inst_groupend => {
// dead until the group fold; D13 match, cf '?'
match (find_last_groupstart(
insts[0:term_start_idx])) {
case let e: error => return e;
case let sz: size => { term_start_idx = sz; };
};
};
case inst_groupstart =>
return "Unused '+'": error;
case =>
return "Misused '+'": error;
};
append(insts, (term_start_idx: inst_split));
};
case '.': { // regex.ha:460-461
let av: inst_any;
let v: inst = av;
append(insts, v);
};
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.
return "regex: metacharacter not yet ported": error;
case: // regex.ha:462-463
append(insts, (r: inst_lit));
};
was_prev_rune_pipe = (r == '|'); // regex.ha:465
r_idx += 1;
};
// handle whole expression alternation (regex.ha:470-473). Index
// loop + if+abort: the by-value range over an indexed element
// segfaults (#57) and the assert builtin is wwstage-broken (#58).
for (let k: size = 0; k < (jump_idxs[0].len: size); k += 1) {
let jump_idx: size = jump_idxs[0][k];
let cur: inst = insts[jump_idx];
if (!(cur is inst_jump)) {
abort("regex: alternation fixup: not a jump");
};
insts[jump_idx] = ((insts.len: size): inst_jump);
};
// regex.ha:475-477. `$` appends true: inst_match, so the guard
// sees real anchored programs now.
if (insts.len == 0 || !(insts[insts.len - 1] is inst_match)) {
append(insts, (false: inst_match));
};
// regex.ha:479-484.
return regex {
insts = insts,
charsets = charsets,
n_reps = n_reps,
};
};
// ref/hare/regex/regex.ha:547-551, verbatim — both free()s are the
// documented no-op (#27; see finish()), kept for source parity. ww
// has no pointer auto-deref, so Hare's `threads[i]` spells
// `(*threads)[i]` (the test-804-pinned delete shape).
fn delete_thread(i: size, threads: *[]thread) void = {
free((*threads)[i].captures);
free((*threads)[i].rep_counters);
delete((*threads)[i]);
};
// ref/hare/regex/regex.ha:553-555. Hare's multi-type membership test
// `a is (inst_lit | inst_any | inst_charset)` is loud-rejected by
// design (filed as a Hare-parity task, ww-core #13); the ruled
// spelling is the chained ||.
fn is_consuming_inst(a: inst) bool = {
return a is inst_lit || a is inst_any || a is inst_charset;
};
// ref/hare/regex/regex.ha:557-587. The dedup scan (ha:560-566) is an
// index loop: ww has no by-ref `&..` range and a by-value range over
// `*threads` miscompiles (F1, ww-core #11). Hare's `append(...)?`
// nomem propagation and the ok/defer-if unwind (ha:568/573/586) drop
// together: ww append returns void (#36 filed) and free() reclaims
// nothing (#27), so there is nothing to propagate or unwind.
fn add_thread(threads: *[]thread, parent_idx: size, new_pc: size) (void | nomem) = {
// Do not add this thread if there is already another thread with
// the same PC
for (let k: size = 0; k < (len(*threads): size); k += 1) {
if ((*threads)[k].pc == new_pc
&& !(*threads)[k].matched
&& (*threads)[k].start_idx
< (*threads)[parent_idx].start_idx) {
return;
};
};
// Hare dups the parent's captures/rep_counters here
// (`alloc(threads[parent_idx].captures...)?`, ha:569/572). Every
// ww route into that dup is blocked today (re-probed post-C3,
// PB7): the deref-spine spread SOURCE is loud-rejected (#35),
// the per-element append is loud-rejected (#34 struct element
// source), and the whole-element let-copy drops bytes (#7/F5).
// The abort is sound, not a semantic hole: fold-2a's compile()
// cannot emit inst_groupstart/inst_repeat, so both slices are
// provably empty in every program this fold can run; the empty
// case appends honest zeroed headers below. The verbatim dup
// lands with the group/repeat fold (ww-core #3).
if ((*threads)[parent_idx].captures.len != 0
|| (*threads)[parent_idx].rep_counters.len != 0) {
abort("regex: capture dup not yet portable (#35/#34/#7)");
};
let captures: []capture;
let rep_counters: []size;
append(*threads, thread {
pc = new_pc,
start_idx = (*threads)[parent_idx].start_idx,
start_bytesize = (*threads)[parent_idx].start_bytesize,
matched = (*threads)[parent_idx].matched,
failed = (*threads)[parent_idx].failed,
captures = captures,
rep_counters = rep_counters,
...
});
return;
};
// 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.
fn run_thread(
i: size,
re: *regex,
string: str,
threads: *[]thread,
r_or_end: (rune | io.eof),
str_idx: size,
str_bytesize: size,
) (void | newmatch | nomem) = {
let str_bytes: []u8 = strings.toutf8(string);
if ((*threads)[i].matched) {
return;
};
for (!is_consuming_inst(re.insts[(*threads)[i].pc])) {
match (re.insts[(*threads)[i].pc]) {
case inst_lit => abort("regex: unreachable");
case inst_any => abort("regex: unreachable");
case let z: inst_split => { // regex.ha:606-609
let new_pc: size = (z: size);
add_thread(threads, i, new_pc)?;
(*threads)[i].pc += 1;
};
case let z: inst_jump => // regex.ha:610-611
(*threads)[i].pc = (z: size);
case inst_skip => {
let new_pc: size = (*threads)[i].pc + 1;
(*threads)[i].start_idx = str_idx;
(*threads)[i].start_bytesize = str_bytesize;
add_thread(threads, i, new_pc)?;
break;
};
case let anchored: inst_match => {
// Do not match if we need an end-anchored match, but we
// have not exhausted our string
//
// Hare spells `anchored` bare (ha:621) — alias
// transparency; ww named aliases are nominal in bool /
// comparison position, so this cast and the consuming
// arm's (lit: rune) are checker-required.
if ((anchored: bool) && !(r_or_end is io.eof)) {
(*threads)[i].failed = true;
return;
};
let content: str = strings.frombytes(str_bytes[
(*threads)[i].start_bytesize:str_bytesize]);
(*threads)[i].root_capture = capture {
start = (*threads)[i].start_idx,
start_bytesize = (*threads)[i].start_bytesize,
end = str_idx,
end_bytesize = str_bytesize,
content = content,
};
(*threads)[i].matched = true;
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 inst_repeat =>
abort("regex: inst_repeat not yet ported");
case => abort("regex: unreachable");
};
};
// From now on, we're only matching consuming instructions, and these
// can't do anything without another rune.
if (r_or_end is io.eof) {
(*threads)[i].failed = true;
return;
};
let r: rune = r_or_end as rune;
match (re.insts[(*threads)[i].pc]) {
case inst_skip => return;
case let lit: inst_lit => {
if (r != (lit: rune)) {
(*threads)[i].failed = true;
};
};
case inst_any => void;
case inst_charset =>
abort("regex: inst_charset not yet ported");
case => abort("regex: unreachable"); // unreachable (ha:738)
};
(*threads)[i].pc += 1;
return;
};
// Attempts to match a regular expression against a string and returns
// either the longest leftmost match or all matches.
//
// ref/hare/regex/regex.ha:746-898. Hare's io::handle param is ww's
// io.stream (callers pass &strm.vt over a memio.fixed). The
// (void | []capture | nomem) signature keeps nomem for Hare API
// parity even though nothing in this fold can produce it: ww append
// returns void (#36 filed) and scanrune has no nomem member.
fn search(
re: *regex,
string: str,
h: io.stream,
need_captures: bool,
) (void | []capture | nomem) = {
// Hare builds the initial thread list with `alloc([thread {
// captures = [], ... }])?` (ha:752-754); ww has per-value alloc
// only, so the list is a bare decl plus one appended
// fully-defaulted thread (the `...` fill zeroes captures too).
let threads: []thread;
append(threads, thread { ... });
// Hare's defer-block cleanup (ha:755-761) is omitted: ww defer
// takes a single expression (cf compile()'s omitted `defer if`)
// 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.
if (re.n_reps > 0) {
abort("regex: repetitions not yet ported");
};
let str_idx: size = 0;
// Hare writes `= void` (ha:768); a ww bare tagged decl zeroes
// the tag, which IS the void member here (PC4-pinned).
let first_match_idx: (void | size);
let str_bytesize: size = 0;
let last_bytesize: size = 0;
// ww has no default arguments: Hare's newscanner(handle) maxread
// default spells types.I32_MAX explicitly (lib/bufio/bufio.ww).
let scan: bufio.scanner = bufio.newscanner(h, types.I32_MAX);
defer bufio.finish(&scan);
for (true) {
str_bytesize += last_bytesize;
if (len(threads) == 0) {
return;
};
let all_matched: bool = true;
for (let i: size = 0; i < (len(threads): size); i += 1) {
if (!threads[i].matched) {
all_matched = false;
break;
};
};
if (all_matched) {
let best_len: size = 0;
let best_n_captures: size = 0;
let best_idx: size = 0;
for (let i: size = 0; i < (len(threads): size); i += 1) {
let match_len: size =
threads[i].root_capture.end
- threads[i].root_capture.start;
let is_better: bool = match_len > best_len
|| match_len == best_len
&& (len(threads[i].captures): size)
> best_n_captures;
if (is_better) {
best_len = match_len;
best_idx = i;
best_n_captures =
(len(threads[i].captures): size);
};
};
// length = number of captures (index of final group +
// 1) + root capture
let length: size = 1;
for (let i: size = (len(re.insts): size); i > 0; i -= 1) {
// match-on-indexed scrutinee binds through a
// typed let (the run_thread spelling).
let cur: inst = re.insts[i - 1];
match (cur) {
case let z: inst_groupstart => {
length = (z: size) + 2;
break;
};
case => void;
};
};
// Hare types this `result` with a capacity hint
// (ha:818). A named-alias local trips the #20/#38
// alias-value family, so the internal spelling is the
// structural twin []capture (reverts with ww-core
// #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: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).
for (length != (len(res): size)) {
append(res, capture { ... });
};
return res;
};
// ww scanrune has no nomem member, so Hare's `case nomem =>
// return nomem;` (ha:831-832) drops; the multi-type arms
// (ha:829/833) split per member (the #13 membership-test
// gap); the aborts carry messages (the #45 builtin-shadow
// dodge, cf run_thread). Statement match assigning into the
// declared local, not Hare's expression match: the cstage
// checker types a match-expr by its first arm's yield and
// rejects the io.eof arm against rune — wwstage accepts the
// expression form, a cs≠ww divergence (filed, ww-core #51;
// repro scratch/r51.ww).
let r_or_end: (rune | io.eof);
match (bufio.scanrune(&scan)) {
case let r: rune => { r_or_end = r; };
case io.eof => {
let e: io.eof;
r_or_end = e;
};
case let e: io.error => abort("regex: scanrune io error");
case utf8.invalid => abort("regex: scanrune invalid utf8");
};
if (r_or_end is rune) {
last_bytesize = (utf8.runesz(r_or_end as rune): size);
};
for (let i: size = 0; i < (len(threads): size); i += 1) {
// Hare itself binds run_thread without `?` (ha:841) —
// its nomem drops on the floor here too.
let res: (void | newmatch | nomem) = run_thread(i, re,
string, &threads, r_or_end, str_idx,
str_bytesize);
let matchlen: size = threads[i].root_capture.end
- threads[i].root_capture.start;
if (res is newmatch && matchlen > 0 && !need_captures) {
// `return [];` (ha:845) — the empty-slice
// literal in return position has no ww spelling
// (#25/#31 ruling); a zero header is a valid
// empty result.
let none: []capture;
return none;
};
let is_better: bool = res is newmatch && matchlen > 0
&& (first_match_idx is void
|| threads[i].start_idx
< first_match_idx as size);
if (is_better) {
first_match_idx = threads[i].start_idx;
};
};
str_idx += 1;
// When we only want the leftmost match, delete all threads
// that start after the earliest non-zero-length matched
// thread. (ww has no by-ref `&..` range — index loop, the
// add_thread divergence.)
if (first_match_idx is size) {
for (let k: size = 0; k < (len(threads): size); k += 1) {
if (threads[k].start_idx
> first_match_idx as size) {
threads[k].failed = true;
};
};
};
// Delete threads that have a PC that has already been
// encountered in previous threads. Prioritise threads that
// have an earlier start_idx, and threads that were added
// earlier. Hare names these counters i/j (ha:872); they are
// di/dj here because the wwstage checker resolves the inner
// init's `i + 1` against a dead same-name `let i: size`
// sibling scope and rejects size → i64 (cs≠ww, filed
// ww-core #52).
for (let di: i64 = 0; di < (len(threads): i64) - 1; di += 1) {
for (let dj: i64 = di + 1; dj < (len(threads): i64); dj += 1) {
let same_pc: bool =
threads[di].pc == threads[dj].pc;
let none_matched: bool = !threads[dj].matched
&& !threads[di].matched;
if (same_pc && none_matched) {
if (threads[di].start_idx
<= threads[dj].start_idx) {
delete_thread((dj: size), &threads);
dj -= 1;
} else {
delete_thread((di: size), &threads);
di -= 1;
break;
};
};
};
};
for (let i: size = 0; i < (len(threads): size); i += 1) {
if (threads[i].failed) {
delete_thread(i, &threads);
i -= 1;
};
};
};
};
// Returns whether or not a [[regex]] matches any part of a given
// string.
//
// ref/hare/regex/regex.ha:900-904. Hare's io::handle arg `&strm` is
// the landed memio→io cast `&strm.vt`. Hare unwraps with
// `search(...)? is []capture` — a |success|=2 union; ww's `?` is
// gated to single-success unions (the C6 interim, ww-core #14), so
// the propagation is spelled as the explicit match harec lowers `?`
// into (ref/harec/src/check.c:2780) — the ratified D13 spelling,
// reverts with #14.
export fn test(re: *regex, string: str) (bool | nomem) = {
let strm: memio.stream = memio.fixed(strings.toutf8(string));
let r: (void | []capture | nomem) =
search(re, string, &strm.vt, false);
match (r) {
case let m: []capture => return true;
case void => return false;
case let n: nomem => return n;
};
};
// Attempts to match a [[regex]] against a string and returns the
// longest leftmost match as a [[result]]. The caller must free the
// return value with [[result_free]].
//
// ref/hare/regex/regex.ha:907-918. Same explicit `?` lowering as
// test() (D13, ww-core #14); the no-match `return [];` (ha:916)
// binds a zero header first (#25/#31 ruling) — a valid empty result
// the caller still result_frees.
export fn find(re: *regex, string: str) (result | nomem) = {
let strm: memio.stream = memio.fixed(strings.toutf8(string));
let r: (void | []capture | nomem) =
search(re, string, &strm.vt, true);
match (r) {
case let m: []capture => return m;
case void => {
let empty: []capture;
return empty;
};
case let n: nomem => return n;
};
};
// Attempts to match a [[regex]] against a string and returns all
// non-overlapping matches as a slice of [[result]]s. The caller must
// free the return value with [[result_freeall]].
//
// ref/hare/regex/regex.ha:920-960. Hare's ok-flag + `defer if (!ok)
// result_freeall(res)` (ha:924-926) is omitted: the flag's only use
// is the defer-if, ww defer takes a single expression, and the frees
// are the documented no-op (#27) — compile()'s omitted `defer if`
// precedent. The `search(...)?` unwrap (ha:933) is the same explicit
// D13 lowering as test()/find() (ww-core #14); the nomem arm
// propagates verbatim.
export fn findall(re: *regex, string: str) ([]result | nomem) = {
let res: []result;
let str_idx: size = 0;
let str_bytesize: size = 0;
let strm: memio.stream = memio.fixed(strings.toutf8(string));
let str_bytes: []u8 = strings.toutf8(string);
for (true) {
let substring: str =
strings.frombytes(str_bytes[str_bytesize:]);
let r: (void | []capture | nomem) =
search(re, substring, &strm.vt, true);
match (r) {
case let m: []capture => {
// Hare appends m and THEN fixes m[0] up from
// substring- to whole-string-relative (ha:935-939):
// the appended header shares m's backing, so the
// mutations below are visible through res. Kept
// verbatim — the aliasing is the subtle bit.
append(res, m);
m[0].start += str_idx;
m[0].end += str_idx;
m[0].start_bytesize += str_bytesize;
m[0].end_bytesize += str_bytesize;
str_idx = m[0].end;
str_bytesize = m[0].end_bytesize;
if (m[0].start_bytesize == (len(str_bytes): size)) {
// end-of-string reached
break;
};
if (m[0].start_bytesize == m[0].end_bytesize) {
// zero-length match: forward rune and byte
// indices (ha:946-952 — the guard against
// the classic findall infinite loop)
str_idx += 1;
str_bytesize += (utf8.utf8sz(
str_bytes[str_bytesize])!: size);
};
// ha:953-954: each search call's scanner buffers
// past what scanrune consumed; the absolute SET
// repositions the underlying stream before the
// next call builds a fresh scanner.
io.seek(&strm.vt, (str_bytesize: io.off),
io.whence.SET)!;
};
case void => break;
case let n: nomem => return n;
};
};
return res;
};
// Frees a [[result]].
//
// ref/hare/regex/regex.ha:1113-1116, verbatim — the free() builtin is
// the documented no-op (#27; see finish()).
export fn result_free(s: result) void = {
free(s);
};
// Frees a slice of [[result]]s.
//
// ref/hare/regex/regex.ha:1119-1124, verbatim — both frees are the
// documented no-op (#27).
export fn result_freeall(s: []result) void = {
for (let r .. s) {
result_free(r);
};
free(s);
};
// Converts an [[error]] into a user-friendly string.
//
// ref/hare/regex/regex.ha:1126-1127 (expression-bodied in Hare; ww
// fns take block bodies).
export fn strerror(err: error) str = {
return err;
};