// 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 leaf fns // (delete_thread/is_consuming_inst/add_thread — dead until search // lands, fold-1 precedent). Every other metacharacter arm, the rest // of the engine (run_thread, see its deferral note below; search on // 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 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; }; // run_thread (regex.ha:589-742) is deferred behind ONE remaining // compiler boundary: its loop condition // `is_consuming_inst(re.insts[threads[i].pc])` passes the 56B inst // BY VALUE from a slice-element source, which the #38b arg wiring // loud-rejects ("unsupported source kind 12" — ident sources landed // with #19; the slice-element extension is task #40, in flight). // Probed pre-port (PB6c); ruled: run_thread lands Hare-VERBATIM once // #40 wires the source. Every other run_thread shape is probe-proven // at HEAD (PB6b match-arm store + composed frombytes bound, P7 // composed scrutinee + compound pc step, P8 newmatch 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; };