1426 lines
48 KiB
Plaintext
1426 lines
48 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; 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 errors;
|
|
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/scanner.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 errors.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;
|
|
};
|