// 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; fold 4 = bracket // expressions `[..]` (handle_bracket + the run_thread charset arm); // fold 5a = capture groups `(`/`)` (compile arms + run_thread // groupstart/groupend + the add_thread capture dup + the search // capture spread); fold 5b = repetition `{m,n}` (parse_repetition + // the `{` arm + the run_thread inst_repeat arm + the search // rep_counters prefill); fold 6 = POSIX character classes // `[[:class:]]` (charclass_map + compile/exec arms). Remaining: // replace. It lands with its fold. package regex; import ascii; import bufio; import io; import memio; import strconv; 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. Declaration ORDER diverges from Hare // (which declares charset first): a cstage type-table bug leaves a // tagged alias whose member types are forward-declared with a // degenerate 8-byte tinfo — silent payload truncation (filed, ww-core // #69; wwstage resolves the forward refs correctly). Members first // until #69 lands. export type charset_lit_item = rune; export type charset_range_item = (u32, u32); export type charset_class_item = (str, *fn(c: rune) bool); export type charset = [](charset_lit_item | charset_range_item | charset_class_item); // ref/hare/regex/regex.ha:74-87 — POSIX class token → ascii predicate. // Inline tuple type (not charset_class_item alias) matches the Hare // decl form; #124 cgen closed the cross-module &fn-in-const gap. const charclass_map: [](str, *fn(c: rune) bool) = [ (":alnum:]", &ascii.isalnum), (":alpha:]", &ascii.isalpha), (":blank:]", &ascii.isblank), (":cntrl:]", &ascii.iscntrl), (":digit:]", &ascii.isdigit), (":graph:]", &ascii.isgraph), (":lower:]", &ascii.islower), (":print:]", &ascii.isprint), (":punct:]", &ascii.ispunct), (":space:]", &ascii.isspace), (":upper:]", &ascii.isupper), (":xdigit:]", &ascii.isxdigit), ]; // 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 '|' arm consumes the // error arm as origin = 0 (whole-expression alternation); the postfix // `?`/`*`/`+` groupend arms propagate it. 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; }; }; }; // Handles a rune inside a bracket expression, mutating the in-flight // charset / bracket state through the pointer params. // // ref/hare/regex/regex.ha:135-225. Hare's `append(charsets, [])?` / // `append(...)?` nomem propagation drops as usual (#36, ww append // returns void); the discarded `strings::next` advances (ha:214-215) // bind to throwaway lets — a bare call statement of a tagged-returning // fn is an unprobed shape. fn handle_bracket( insts: *[]inst, r: rune, r_idx: *size, bracket_idx: *int, iter: *strings.iterator, charsets: *[]charset, skip_charclass_rest: *bool, is_charset_positive: *bool, in_bracket: *bool, ) (void | error | nomem) = { let peek1: (rune | utf8.done) = strings.next(iter); let peek2: (rune | utf8.done) = strings.next(iter); let peek3: (rune | utf8.done) = strings.next(iter); if (!(peek1 is utf8.done)) { strings.prev(iter); }; if (!(peek2 is utf8.done)) { strings.prev(iter); }; if (!(peek3 is utf8.done)) { strings.prev(iter); }; if (*bracket_idx == -1) { // Hare `append(charsets, [])?` (ha:160) — the empty slice // literal spells through a bare typed let (#25/#31 ruling). let empty: charset; append(*charsets, empty); }; *bracket_idx += 1; if (*skip_charclass_rest) { if (r == ']') { *skip_charclass_rest = false; }; *r_idx += 1; return; }; let is_range: bool = peek1 is rune && (peek1 as rune) == '-' && !(peek2 is utf8.done) && !(peek3 is utf8.done) && !((peek2 as rune) == ']'); let range_end: (rune | utf8.done) = peek2; let is_first_char: bool = *bracket_idx == 0 || *bracket_idx == 1 && !*is_charset_positive; if (r == ']' && !is_first_char) { // regex.ha:179-187 let newinst: inst = inst_charset { idx = (len(*charsets): size) - 1, is_positive = *is_charset_positive, }; append(*insts, newinst); *in_bracket = false; *bracket_idx = -1; *is_charset_positive = true; } else if (r == '^' && *bracket_idx == 0) { // regex.ha:188-189 *is_charset_positive = false; } else if (r == '[' && !(peek1 is utf8.done) && (peek1 as rune) == ':') { // regex.ha:190-204 let rest: str = strings.iterstr(iter); for (let cc_idx: size = 0; cc_idx < (len(charclass_map): size); cc_idx += 1) { if (strings.hasprefix(rest, charclass_map[cc_idx].0)) { let n: size = (len(*charsets): size); append((*charsets)[n - 1], (charclass_map[cc_idx]: charset_class_item)); *skip_charclass_rest = true; break; }; }; if (!*skip_charclass_rest) { return "No character class after '[:'": error; }; } else if (is_range) { // regex.ha:205-217 let start_b: u32 = (r: u32); let end_b: u32 = ((range_end as rune): u32); if (end_b < start_b) { return "Descending bracket expression range '[z-a]'": error; }; append((*charsets)[len(*charsets) - 1], ((start_b, end_b): charset_range_item)); let skip1: (rune | utf8.done) = strings.next(iter); let skip2: (rune | utf8.done) = strings.next(iter); *r_idx += 2; } else { // regex.ha:218-221 append((*charsets)[len(*charsets) - 1], (r: charset_lit_item)); }; *r_idx += 1; return; }; // 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 fold-4 bracket surface: the // in_bracket dispatch (265-275), the `[` flip (313-314) and the // handle_bracket state quad (249-252) — the fold-5a group arms: // `(` (317-323), `)` (324-334), capture_idx (256) and the loop-exit // `done` Unmatched-'(' check (277-282); the anchors' group_level // arms and the postfix inst_groupend/groupstart arms went live with // them — and the fold-5b repetition arm `{` (368-402) over // parse_repetition + n_reps (255). Every metacharacter is ported; // the only remaining loud surface is the POSIX class BODY inside // handle_bracket. // // 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; let iter: strings.iterator = strings.iter(expr); let r_idx: size = 0; // jump_idxs tracks the pending alternation jumps per group level; // the '(' arm grows it past level 0. 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); // bracket-expression state (regex.ha:249-252). let in_bracket: bool = false; let skip_charclass_rest: bool = false; let bracket_idx: int = -1; let is_charset_positive: bool = true; let was_prev_rune_pipe: bool = false; let n_reps: size = 0; let group_level: size = 0; let capture_idx: 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); }; if (in_bracket) { // regex.ha:265-275 if (next is utf8.done) { return "Unmatched '['": error; }; // Hare propagates with `?` (ha:271-274); `?` into // compile's >32B tagged return is loud-stopped (#38b) // — the explicit D13 match, harec's own desugaring // (ref/harec/src/check.c:2780), the fold-3 // find_last_groupstart-arm precedent. The continue // skips the loop tail — handle_bracket owns r_idx // inside a bracket (the #138-fixed shape). match (handle_bracket(&insts, next as rune, &r_idx, &bracket_idx, &iter, &charsets, &skip_charclass_rest, &is_charset_positive, &in_bracket)) { case let e: error => return e; case let n: nomem => return n; case void => void; }; continue; }; let r: rune = match (next) { case utf8.done => { // regex.ha:277-282 if (group_level > 0) { return "Unmatched '('": error; }; 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). 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 => { // 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 => { // 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 => { // 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:313-314 in_bracket = true; case ']': // regex.ha:315-316 — literal outside a bracket append(insts, (r: inst_lit)); case '(': { // regex.ha:317-323 append(insts, (capture_idx: inst_groupstart)); group_level += 1; capture_idx += 1; // Hare grows with `append(jump_idxs, [])?` // (ha:321-323); the empty-slice literal spells // through a typed let (the #25/#31 ruling, cf the // lvl0 prologue). for ((jump_idxs.len: size) < group_level + 1) { let lvl: []size; append(jump_idxs, lvl); }; }; case ')': { // regex.ha:324-334 if (group_level == 0) { // VERBATIM duplicate of find_last_groupstart's // text — Hare keeps two copies (ha:118/326). return "Unmatched ')'": error; }; // payload-less void variant through a typed let (the // inst_skip spelling above). let ge: inst_groupend; let v: inst = ge; append(insts, v); // jump fixup (ha:329-332): by-value range over an // INDEXED base is the #70-fixed shape; assert is the // #58 builtin. for (let jump_idx .. jump_idxs[group_level]) { assert(insts[jump_idx] is inst_jump); insts[jump_idx] = (((insts.len: size) - 1): inst_jump); }; delete(jump_idxs[group_level][:]); group_level -= 1; }; case '{': { // regex.ha:368-402 let origin: size = (insts.len: size) - 1; if (insts[origin] is inst_groupend) { // D13 match, cf '?' — `?` into compile's >32B // tagged return is loud-stopped (#38b). match (find_last_groupstart(insts[0:origin])) { case let e: error => return e; case let sz: size => { origin = sz; }; }; }; let rest: str = strings.iterstr(&iter); // Hare's `parse_repetition(rest)?` (ha:374) — the D13 // match; the binding unwraps FIELD-WISE: a whole-struct // assign from a match binding with tagged fields // silently corrupts them (filed, ww-core #49). The // rp_* locals are Hare's rep_parts.0/.1/.2 through the // repparts respell (#47; see parse_repetition). let rp_min: (void | size) = void; let rp_max: (void | size) = void; let rp_replen: size = 0; match (parse_repetition(rest)) { case let e: error => return e; case let rp: repparts => { rp_min = rp.min; rp_max = rp.max; rp_replen = rp.replen; }; }; // ha:375 compares the tagged elem to 0 directly // (`rep_parts.0 == 0`); no tagged==int compare in ww — // the is/as respell (ruled, scope §9b). let can_skip: bool = rp_min is size && rp_min as size == 0; // ha:376-380's if-EXPRESSION min selection spells as a // widen-assign into the tagged let. let min: (void | size) = rp_min; if (can_skip) { min = (1: size); }; if (can_skip) { // len(insts) - 1 is the current last instruction // len(insts) is the next instruction // advance to len(insts) + 1 to make space for the `inst_split` // advance to len(insts) + 2 to make space for the `inst_repeat` // // Hare evaluates the payload BEFORE the insert // (ha:386-387); ww's insert() grows the dst // before evaluating the value arg (filed, // ww-core #50) — payload pre-bound, the // '?'/'|' arm convention. let split_target: size = (insts.len: size) + 2; insert(insts[origin], (split_target: inst_split)); shift(insts[origin + 1:]); origin += 1; }; let newinst: inst = inst_repeat { id = n_reps, origin = origin, min = min, max = rp_max, }; // ha:397-400 — the bound is INCLUSIVE (`<=`), exactly // as Hare; the discarded strings.next binds to a // throwaway let (the handle_bracket skip precedent). for (let i: size = 0; i <= rp_replen; i += 1) { let skip: (rune | utf8.done) = strings.next(&iter); r_idx += 1; }; append(insts, newinst); n_reps += 1; }; 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, }; }; // parse_repetition's Hare-verbatim return is the 3-tuple // ((void | size), (void | size), size) (regex.ha:488); a tuple with // tagged elements can't yet cross a union boundary in ww — cstage's // return store into (tuple | error) is cgen-unwired // (cg_widen_tagged_store) and wwstage's variant-match rejects the // tuple case arm (filed, ww-core #47) — so the tuple respells as this // private struct, positionally: .0 → min, .1 → max, .2 → replen. // GRADUATION: when #47 closes, repparts reverts to the verbatim tuple // with the test rows unchanged. type repparts = struct { min: (void | size), max: (void | size), replen: size, }; // ref/hare/regex/regex.ha:486-545, whole. strings.index returns // (i32 | void) in ww — the standing lib-wide str-index-i32 convention // (lib/strings/strings.ww:54, #8) vs Hare's (size | void) // (ref/hare/strings/index.ha:10) — so the index results stay i32 // INTERNALLY (the ha:499 comparison is same-domain) and each value // widens explicitly at the boundary Hare types as size (the repparts // field stores). ha:494 re-binds `first_endbrace` over itself; ww // rejects a same-scope re-decl ("let redeclared"), so the unwrapped // value is `feb`. fn parse_repetition(s: str) (repparts | error) = { let first_comma: (i32 | void) = strings.index(s, ","); let first_endbrace: (i32 | void) = strings.index(s, "}"); if (first_endbrace is void) { return "Repetition expression syntax error '{n}'": error; }; let feb: i32 = first_endbrace as i32; let min_str: str = ""; let max_str: str = ""; let is_single_arg: bool = false; if (first_comma is void || feb < first_comma as i32) { let cut: (str, str) = strings.cut(s, "}"); min_str = cut.0; max_str = cut.0; is_single_arg = true; } else { let cut: (str, str) = strings.cut(s, ","); min_str = cut.0; // Hare reads .0 straight off the call (ha:507); a tuple-elem // read on a CALL result is loud-unsupported (filed, ww-core // #48) — bound local first. let cut2: (str, str) = strings.cut(cut.1, "}"); max_str = cut2.0; }; let min: (void | size) = void; let max: (void | size) = void; if (min_str.len > 0) { // Hare's match-EXPRESSION with if-yields (ha:514-525) // spells as a statement match (no yield-expr; the search // scanrune precedent, #51). stoi's base is explicit — ww // has no default args (the newscanner precedent; cf // lib/encoding/hex/hex.ww:131). match (strconv.stoi(min_str, strconv.base.DEC)) { case let res: int => { if (res < 0) { return "Negative repetition count '{-n}'": error; }; min = (res: size); }; case => return "Repetition expression syntax error '{n}'": error; }; } else { // `min = 0;` (ha:524): an untyped-int widen-store into // (void | size) mis-tags (#33) — cast form pinned. min = (0: size); }; if (max_str.len > 0) { match (strconv.stoi(max_str, strconv.base.DEC)) { case let res: int => { if (res < 0) { return "Negative repetition count '{-n}'": error; }; max = (res: size); }; case => return "Repetition expression syntax error '{n}'": error; }; }; // Hare's if-EXPRESSION (ha:539-544) spells as a statement. let rep_len: size = (min_str.len: size); if (!is_single_arg) { rep_len = (min_str.len: size) + 1 + (max_str.len: size); }; return repparts { min = min, max = max, replen = rep_len }; }; // 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 via `alloc(threads[parent_idx].captures...)?` // (ha:568-573); the alloc-dup form has no ww spelling (D3) — // fresh slice header + spread-append, the #35-landed deref-spine // source. The ok/defer-if frees (ha:570/573) drop: free() is the // documented no-op (#27; see finish()). let captures: []capture; append(captures, (*threads)[parent_idx].captures...); let rep_counters: []size; append(rep_counters, (*threads)[parent_idx].rep_counters...); 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. Every arm compile() can emit // executes (skip + split + jump + match + groupstart + groupend + // repeat non-consuming; lit + any + charset consuming — split/jump // went live with fold 3's `|`/`?`/`*` arms, charset with fold 4's // brackets, groupstart/groupend with fold 5a's `(`/`)`, repeat with // fold 5b's `{`). 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 let gs: inst_groupstart => { // regex.ha:636-652 let idx: size = (gs: size); // ha:637-641: Hare's 3-arg fill-append `append(..., // [capture { ... }...], idx - len + 1)?` has no ww // spelling — the ratified count-loop fill (cf the // search epilogue's pad loop). for (((*threads)[i].captures.len: size) < idx + 1) { append((*threads)[i].captures, capture { ... }); }; assert((*threads)[i].captures[idx].end != types.SIZE_MAX); (*threads)[i].captures[idx] = capture { content = "", start = str_idx, start_bytesize = str_bytesize, // end=types.SIZE_MAX indicates that the // capture group hasn't ended yet end = types.SIZE_MAX, end_bytesize = types.SIZE_MAX, }; (*threads)[i].pc += 1; }; case inst_groupend => { // regex.ha:653-668 let curr_capture: size = ((*threads)[i].captures.len: size); // ha:655's 2-clause `for (cond; post)` has no ww // parse form (1- and 3-clause only) — the post-step // moves to the body tail. Equivalent: break skips // post in both, and the body has NO continue (which // would run post in Hare but skip the inline tail). for (curr_capture > 0) { // find inner-most unclosed capture group if ((*threads)[i].captures[curr_capture - 1].end == types.SIZE_MAX) { break; }; curr_capture -= 1; }; assert(curr_capture > 0, "Found a groupend token \")\" without having previously seen a groupstart token \"(\". Please report this as a bug"); // Hare's name shadows the capture TYPE — ww splits the // type/value namespaces (#225), so it ports verbatim. let capture = &(*threads)[i].captures[curr_capture - 1]; capture.end = str_idx; capture.end_bytesize = str_bytesize; capture.content = strings.frombytes(str_bytes[ capture.start_bytesize:capture.end_bytesize]); (*threads)[i].pc += 1; }; case let ir: inst_repeat => { // regex.ha:669-684 assert(ir.id < (len((*threads)[i].rep_counters): size)); (*threads)[i].rep_counters[ir.id] += 1; if (ir.max is size && (*threads)[i].rep_counters[ir.id] > ir.max as size) { (*threads)[i].failed = true; return; }; let new_pc: size = (*threads)[i].pc + 1; (*threads)[i].pc = ir.origin; if (ir.min is void || (*threads)[i].rep_counters[ir.id] >= ir.min as size) { add_thread(threads, i, new_pc)?; }; }; 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 let cs: inst_charset => { // regex.ha:704-737 // 24B header bind off the ptr-field slice index. Spelled // structurally, not `charset`: an alias-typed slice local's // INDEX read mis-scales in wwstage (cs≠ww, ww-core #68) — // reverts to the alias with the #47 family. let cset: [](charset_lit_item | charset_range_item | charset_class_item) = re.charsets[cs.idx]; // Disprove the match if we're looking for a negative match // Prove the match if we're looking for a positive match let matched: bool = !cs.is_positive; // Hare loops `for (let i = 0z; ...) match (charset[i])` // (ha:709) — index loop with the typed-let scrutinee bind // (the run_thread spelling); Hare's inner `i` renames to k // (it shadows the thread-index param). for (let k: size = 0; k < (len(cset): size); k += 1) { let cur: (charset_lit_item | charset_range_item | charset_class_item) = cset[k]; match (cur) { case let l: charset_lit_item => { if (r == (l: rune)) { // Succeeded if positive match // Failed if negative match matched = cs.is_positive; break; }; }; case let range: charset_range_item => { let r_b: u32 = (r: u32); if (r_b >= range.0 && r_b <= range.1) { // Succeeded if positive match // Failed if negative match matched = cs.is_positive; break; }; }; case let class_item: charset_class_item => { // regex.ha:726-733 let classfn: *fn(c: rune) bool = class_item.1; if ((*classfn)(r)) { // Succeeded if positive match // Failed if negative match matched = cs.is_positive; break; }; }; }; }; if (!matched) { (*threads)[i].failed = true; }; }; 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)?` — the sized-fill alloc has no ww // spelling (D3 family) and ww has no `z` size-literal suffix // (cf lib/strconv/decimal.ww:13) — count-loop append of (0: size). if (re.n_reps > 0) { for (let k: size = 0; k < re.n_reps; k += 1) { append(threads[0].rep_counters, (0: size)); }; }; 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 — Hare's `static append` (capacity-bounded) // is a plain append (D6, no static form); the indexed // spread source is the #35/#25-landed shape. append(res, threads[best_idx].captures...); // ha:821-824's sized fill-append `[capture { ... }...]` // has no ww spelling — the ratified count-loop fill // pads the unset trailing groups. 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"); // Unreachable: this scanner is newscanner(h, I32_MAX), so the // can't-grow ceiling is never hit; the arm keeps the match // total over scanrune's widened overflow surface (drain F-B). case bufio.overflow => abort("regex: scanrune overflow"); }; 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; };