// 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). Every other metacharacter // arm (and replace/findall) is DEFERRED behind compiler fixes — // probed pre-port, pA*/pB*/PB*/PC*/PD* 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 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); }; // 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; }; }; }; }; // 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; }; }; // 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; };