Files
ww/lib/regex/regex.ww
Hojun-Cho e481cb86bd lib/regex: fold-2b tranche B4 — run_thread (thread-machine core)
First end-to-end engine execution in tree: compile("ab")'s program
runs through skip-spawn / lit advance / match capture under the @test
drivers (search and the exec surface stay tranche C/D). Ported
Hare-verbatim from ref/hare/regex/regex.ha:589-742 — the #40 arg
wiring carries the ha:602 loop condition with no let-bind; arm bodies
stay verbatim so the group/repeat fold pastes straight into this
match. Arms compile() cannot emit are one loud not-yet-ported abort
each (the fold boundary); Hare's bare unreachable abort()s carry a
message because os.ww's private abort(msg) shadows the builtin
cross-module (filed, ww-core #45). The (anchored: bool)/(lit: rune)
casts are checker-required (ww aliases are nominal where Hare relies
on transparency), WHY-cited at site.
2026-06-04 13:16:22 +09:00

395 lines
14 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 — dead
// until search lands, fold-1 precedent). Every other metacharacter
// arm, search (F5 element-copy + F2 len-builtin, tranche C) and the
// exec surface (test/find on the C6 multi-success `?` gate, tranche
// D; replace) are DEFERRED behind compiler fixes — probed pre-port,
// pA*/pB*/PB* probes. They land with those fixes.
//
// 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 io;
import strings;
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);
};
// Compiles a regular expression string into a [[regex]].
//
// ref/hare/regex/regex.ha:227-263. Fold 2a ports the literal core:
// inst_lit / inst_any / inst_match plus the leading inst_skip
// (regex.ha:261-263 — unanchored exec depends on it). Every other
// metacharacter arm is one loud not-yet-ported error — the explicit
// fold boundary; falling to literal would be a silent semantic lie.
// State serving only the deferred arms is dropped with them:
// jump_idxs (ha:241-248), the bracket quad (ha:249-252),
// was_prev_rune_pipe / group_level / capture_idx (ha:253-256).
//
// 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;
let n_reps: 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);
};
// regex.ha:277-284 minus the group_level check (it rides
// the deferred '(' arm's state).
let r: rune = match (next) {
case utf8.done => break;
case let x: rune => yield x;
};
switch (r) {
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-2a boundary: regex.ha:286-459 arms deferred.
return "regex: metacharacter not yet ported": error;
case: // regex.ha:462-463
append(insts, (r: inst_lit));
};
r_idx += 1;
};
// regex.ha:475-477. `$` appends true: inst_match — deferred, so
// the guard can only see no-match today; kept verbatim.
if (insts.len == 0 || !(insts[insts.len - 1] is inst_match)) {
append(insts, (false: inst_match));
};
// regex.ha:479-484. The alternation fixup (ha:470-473) drops with
// jump_idxs.
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). Its bound reads the
// `.len` pseudo-field, not the len() builtin — len(*threads)
// mis-reads the data pointer as the length (FB1, ww-core #41; the
// F2/#10 deref sibling). 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 < ((*threads).len: 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 fold-2a's compile()
// can emit execute (skip + match non-consuming; lit + any consuming);
// 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 inst_split =>
abort("regex: inst_split not yet ported");
case inst_jump =>
abort("regex: inst_jump not yet ported");
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;
};
// 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);
};
// 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;
};