Port of search (ref/hare/regex/regex.ha:746-898) per the drew §9c
map: the thread-machine driver over bufio.scanrune — per-rune
dispatch, all_matched best-pick (leftmost-longest), need_captures
early-exit, first-match leftmost trim, same-pc dedup, failed sweep.
compile()'s literal programs now match end to end; test/find (the
exec surface) ride tranche D behind the C6 multi-success `?` gate.
Spelling divergences, each documented at site with its ha cite:
io::handle param → io.stream; alloc([thread{...}])? → decl + append;
defer-block cleanup omitted (single-expr defer, no-op frees, #27);
rep_counters prefill → ratified loud n_reps>0 abort (no 2b program
can set it); newscanner default maxread → types.I32_MAX; scanrune
nomem arm dropped (no such member) and multi-type arms split (#13);
`return [];` → bind-first zero header (#25/#31 ruling); `result`
internals spelled []capture (#20/#38 alias family — reverts with
#47); ha:820's indexed capture spread loud-bounded provably-empty
(#35); `&..` by-ref ranges → index loops; the ha:821 sized
fill-append → count loop (self-activates with the group fold).
Two checker findings surfaced mid-port, probe-isolated, dodged at
site and FILED: #51 (cs≠ww — the cstage checker types a
match-EXPRESSION by its first arm's yield and rejects the io.eof
arm against rune; w6c_ww accepts the expression form and emits
runtime-correct code, review-verified on scratch/r51.ww), so the
scanrune receive is a statement match assigning into a pre-declared
(rune | io.eof); #52 (cs≠ww, wwstage only) — a same-name let in a
CLOSED sibling scope poisons a later for-init rhs (`let j: i64 =
i + 1` resolves i against the dead `let i: size`), so the ha:872
dedup counters are di/dj at site (scratch probes p51/p51b/c/d
isolate the trigger and prove the rename byte-identical).
add_thread's dedup bound reverts to len(*threads) — the FB1/#41
dodge, fix landed at 796d41b. Imports grow bufio + types.
regex_test: +3 @test fns (signalled 20-22) driving private search
directly over memio.fixed streams. search_matches = 6 struct-row
table rows: full match mid-string, mismatch-restart bcd/abcd,
leftmost-longest aa/aaa, zero-length ""/"" (the all_matched path
with matchlen 0 must NOT take the need_captures=false early-exit),
multibyte b.d over "aßbxd" (root 2/3..5/6 — every idx differs from
its bytesize), dedup-heavy aa/aaaa (stable across >=3 same-pc
passes). search_early_exit pins ha:845-847 (empty result, len 0);
search_no_match pins thread-drain void + EOF-mid-pattern void.
Every match row checks all four root indices plus content and
result_frees its result (including the early-exit empty one).
Coverage limit, mutation-verified and documented at the dedup row:
in 2a's fixed-length program space every match ties on match_len,
so the leftmost trim (ha:860-866) and the dedup sweep (ha:872-889)
are result-invisible — disabling either still passes the table;
disabling the failed sweep hangs (caught). Both turn result- and
termination-visible with the split/star fold; result stability is
the only external pin available today (threads is search-local).
Both drivers run the fixture exit 0; w6c vs w6c_ww on the
regenerated combined are byte-identical (FC0). PC1-PC4 + P12 probed
at base; PC2's blocker fix is the separate #49 commit (c34a48a).
615 lines
21 KiB
Plaintext
615 lines
21 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. Every other
|
|
// metacharacter arm 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*/PC* 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 bufio;
|
|
import io;
|
|
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);
|
|
};
|
|
|
|
// 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). 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 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;
|
|
};
|
|
|
|
// 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;
|
|
};
|
|
};
|
|
};
|
|
};
|
|
|
|
// 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;
|
|
};
|