lib/regex: fold-3 — anchors, escape, postfix ?/*/+, alternation
Ports compile()'s fold-3 arms (ref/hare/regex/regex.ha): \ escape
(286-293), ^ (294-300), $ (301-312, goes live with run_thread's
anchored ha:621-624 pin), | (335-367) over the restored jump_idxs
prologue (241-256 subset: jump_idxs + was_prev_rune_pipe +
group_level verbatim-but-0) + the whole-expression fixup (470-473,
the SIZE_MAX-sentinel overwrite 2a dropped), ? (403-420), * (421-443),
+ (444-459); find_last_groupstart (104-119, whole — error arm is the
live one until the group fold) and shift (123-133). run_thread's
inst_split/inst_jump aborts FLIP live (606-611, all 2b-proven
shapes). Still loud: [ ( ) { in compile; groupstart/groupend/repeat/
charset arms; add_thread's capture-dup bound.
Spelling divergences, each cited at site:
- multi-type case (inst_lit | inst_charset | inst_any) (ha:407) is
loud-rejected both stages (PE5; Hare-parity #13) -> three void arms.
- find_last_groupstart(...)? in the dead groupend arms hits the #38b
>32B-tagged-return propagate loud-stop -> explicit D13 match
(harec's own ? desugaring).
- by-value range over an indexed element (for (let x .. jump_idxs[g]))
SEGFAULTS both stages (NEW, filed ww-core #57) -> D9 index loops at
ha:351-356 + 470-473.
- assert(cond, msg) is wwstage-broken (CALL assert(SB), undefined at
link; NEW, filed ww-core #58) -> if+abort at the ha:355 site.
- Hare's match/if EXPRESSIONS (ha:337-346) -> statement spellings
(the #51 search/scanrune precedent).
Tests: the 11-pattern loud row shrinks to the 4 remaining metachars;
strerror reroutes through a[. New: fold3_programs (exact inst
sequences incl. the a|b sentinel fixup), fold3_compile_errors (12
exact texts incl. Hare's own ab\|^cd ERROR fixture), direct
find_last_groupstart/shift rows, fold3_find_cases (42 rows from
Hare's +test.ha anchors/postfix/alternation blocks, group-free
subset, end=-1 resolved to rune-length; rob's dedup/leftmost/longest
riders now observable: a* over aaaa -> single (0,4), b+ over abab ->
(1,2), b* / ^b* over aaaabbbb -> (4,8)/(0,0); multibyte b+ row keeps
idx != bytesize) + the a*-over-baa findall rider (longest-pick beats
the zero-length candidate; trailing zero-length match takes the
ha:942-945 break). Both drivers green; regex_test.combined.ww cs==ww
byte-identical.
This commit is contained in:
@@ -7,10 +7,13 @@
|
||||
// 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. Every other
|
||||
// metacharacter arm (and replace) is DEFERRED behind compiler fixes —
|
||||
// probed pre-port, pA*/pB*/PB*/PC*/PD* probes. They land with those
|
||||
// fixes.
|
||||
// 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. Remaining
|
||||
// metacharacter arms (`[` bracket, `(`/`)` groups, `{` repetition —
|
||||
// and replace) are DEFERRED — probed pre-port, pA*/pB*/PB*/PC*/PD*/
|
||||
// PE* probes. They land with their folds.
|
||||
//
|
||||
// One fold-1 construct is held back behind a filed compiler/fidelity
|
||||
// gap (see the charclass_map site below):
|
||||
@@ -132,16 +135,65 @@ export fn finish(re: *regex) void = {
|
||||
free(re.charsets);
|
||||
};
|
||||
|
||||
// ref/hare/regex/regex.ha:104-119, verbatim. The error arm is ALWAYS
|
||||
// taken until the group fold ('(' is loud, no inst_groupstart can
|
||||
// exist); the '|' arm consumes it as origin = 0 (whole-expression
|
||||
// alternation). 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;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
// Compiles a regular expression string into a [[regex]].
|
||||
//
|
||||
// ref/hare/regex/regex.ha:227-263. Fold 2a ports the literal core:
|
||||
// inst_lit / inst_any / inst_match plus the leading inst_skip
|
||||
// (regex.ha:261-263 — unanchored exec depends on it). Every other
|
||||
// metacharacter arm is one loud not-yet-ported error — the explicit
|
||||
// fold boundary; falling to literal would be a silent semantic lie.
|
||||
// State serving only the deferred arms is dropped with them:
|
||||
// jump_idxs (ha:241-248), the bracket quad (ha:249-252),
|
||||
// was_prev_rune_pipe / group_level / capture_idx (ha:253-256).
|
||||
// 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 remaining metacharacter arms (`[`
|
||||
// bracket, `(`/`)` groups, `{` repetition) are one loud
|
||||
// not-yet-ported error — the explicit fold boundary; falling to
|
||||
// literal would be a silent semantic lie. group_level (ha:255) is
|
||||
// kept verbatim but only ever 0 until the group fold — the anchors'
|
||||
// group_level arms and the postfix inst_groupend/groupstart arms are
|
||||
// dead-but-verbatim. capture_idx and the bracket quad stay dropped
|
||||
// with their arms; the loop-head `done` Unmatched-'(' check
|
||||
// (ha:278-280) rides the '(' arm.
|
||||
//
|
||||
// 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()
|
||||
@@ -155,7 +207,16 @@ export fn compile(expr: str) (regex | error | nomem) = {
|
||||
let charsets: []charset; // stays empty until the '[' fold
|
||||
let iter: strings.iterator = strings.iter(expr);
|
||||
let r_idx: size = 0;
|
||||
// jump_idxs tracks the pending alternation jumps per group level;
|
||||
// only level 0 can populate until the group fold. 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);
|
||||
let was_prev_rune_pipe: bool = false;
|
||||
let n_reps: size = 0;
|
||||
let group_level: size = 0;
|
||||
|
||||
for (true) {
|
||||
let next: (rune | utf8.done) = strings.next(&iter);
|
||||
@@ -172,14 +233,184 @@ export fn compile(expr: str) (regex | error | nomem) = {
|
||||
append(insts, v);
|
||||
};
|
||||
|
||||
// regex.ha:277-284 minus the group_level check (it rides
|
||||
// the deferred '(' arm's state).
|
||||
let r: rune = match (next) {
|
||||
case utf8.done => break;
|
||||
case let x: rune => yield x;
|
||||
};
|
||||
|
||||
switch (r) {
|
||||
case '\\': { // regex.ha: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 — always taken until the group
|
||||
// fold). 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 => {
|
||||
// dead until the group fold ('(' is loud).
|
||||
// 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 => {
|
||||
// dead until the group fold; 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 => {
|
||||
// dead until the group fold; 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;
|
||||
@@ -187,22 +418,36 @@ export fn compile(expr: str) (regex | error | nomem) = {
|
||||
};
|
||||
case ']': // regex.ha:315-316 — literal outside a bracket
|
||||
append(insts, (r: inst_lit));
|
||||
case '\\', '^', '$', '[', '(', ')', '|', '{', '?', '*', '+':
|
||||
// fold-2a boundary: regex.ha:286-459 arms deferred.
|
||||
case '[', '(', ')', '{':
|
||||
// fold-3 boundary: the bracket (ha:313-334 + 268-275),
|
||||
// group (317-334) and repetition (368-401) arms ride
|
||||
// later folds.
|
||||
return "regex: metacharacter not yet ported": error;
|
||||
case: // regex.ha:462-463
|
||||
append(insts, (r: inst_lit));
|
||||
};
|
||||
was_prev_rune_pipe = (r == '|'); // regex.ha:465
|
||||
r_idx += 1;
|
||||
};
|
||||
|
||||
// regex.ha:475-477. `$` appends true: inst_match — deferred, so
|
||||
// the guard can only see no-match today; kept verbatim.
|
||||
// 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. The alternation fixup (ha:470-473) drops with
|
||||
// jump_idxs.
|
||||
// regex.ha:479-484.
|
||||
return regex {
|
||||
insts = insts,
|
||||
charsets = charsets,
|
||||
@@ -277,8 +522,9 @@ fn add_thread(threads: *[]thread, parent_idx: size, new_pc: size) (void | nomem)
|
||||
return;
|
||||
};
|
||||
|
||||
// ref/hare/regex/regex.ha:589-742. Only the arms fold-2a's compile()
|
||||
// can emit execute (skip + match non-consuming; lit + any consuming);
|
||||
// ref/hare/regex/regex.ha:589-742. Only the arms compile() can emit
|
||||
// execute (skip + split + jump + match non-consuming; lit + any
|
||||
// consuming — split/jump went live with fold 3's `|`/`?`/`*` arms);
|
||||
// every other inst arm is one loud not-yet-ported abort — the fold
|
||||
// boundary, the compile() metachar discipline. The "unreachable"
|
||||
// aborts on lit/any inside the non-consuming loop (ha:604-605) and
|
||||
@@ -307,10 +553,13 @@ fn run_thread(
|
||||
match (re.insts[(*threads)[i].pc]) {
|
||||
case inst_lit => abort("regex: unreachable");
|
||||
case inst_any => abort("regex: unreachable");
|
||||
case inst_split =>
|
||||
abort("regex: inst_split not yet ported");
|
||||
case inst_jump =>
|
||||
abort("regex: inst_jump not yet ported");
|
||||
case 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;
|
||||
|
||||
@@ -260,13 +260,12 @@ fn fail() void = { os.exit(signalled + 10); };
|
||||
// fold-boundary text — falling through to the literal default would
|
||||
// silently compile a wrong program, and any OTHER error text would
|
||||
// mean an arm was half-ported. One pattern per deferred arm so the
|
||||
// fold that ports an arm consciously deletes its row. '^' leads its
|
||||
// pattern: the r_idx==0 skip gate (regex.ha:261) must compose with
|
||||
// the loud arm, not bypass it.
|
||||
// fold that ports an arm consciously deletes its row. Fold 3 flipped
|
||||
// \ ^ $ | ? * + positive (fold3_* below); the bracket/group/
|
||||
// repetition four remain.
|
||||
@test fn compile_metachar_loud() void = {
|
||||
let pats: [11]str = [
|
||||
"a\\", "^a", "a$", "a[", "a(", "a)",
|
||||
"a|", "a{", "a?", "a*", "a+",
|
||||
let pats: [4]str = [
|
||||
"a[", "a(", "a)", "a{",
|
||||
];
|
||||
let i: i32 = 0;
|
||||
for (i < len(pats)) {
|
||||
@@ -420,7 +419,7 @@ type nmexp = struct {
|
||||
// through a REAL compile() error, completing the exported error
|
||||
// surface end to end.
|
||||
@test fn strerror_identity() void = {
|
||||
match (regex.compile("a*")) {
|
||||
match (regex.compile("a[")) {
|
||||
case let e: regex.error => {
|
||||
if (strings.compare(regex.strerror(e),
|
||||
"regex: metacharacter not yet ported") != 0) {
|
||||
@@ -981,7 +980,7 @@ type fdexp = struct {
|
||||
};
|
||||
|
||||
@test fn findall_fields() void = {
|
||||
let exp: [10]fdexp = [
|
||||
let exp: [12]fdexp = [
|
||||
// ("ab", "abxab")
|
||||
fdexp { start = 0, sb = 0, end = 2, eb = 2, content = "ab" },
|
||||
fdexp { start = 3, sb = 3, end = 5, eb = 5, content = "ab" },
|
||||
@@ -1002,8 +1001,16 @@ type fdexp = struct {
|
||||
// ("ab", "xab") — tail match: the post-match seek lands at
|
||||
// end-of-string and the next search returns void
|
||||
fdexp { start = 1, sb = 1, end = 3, eb = 3, content = "ab" },
|
||||
// ("a*", "baa") — fold-3 rider: search's longest-pick beats
|
||||
// the zero-length candidate at 0 (the b*-over-"aaaabbbb"
|
||||
// semantics), so the greedy (1,3) "aa" leads; the trailing
|
||||
// end-of-string zero-length match takes the ha:942-945
|
||||
// break, pinning a real splitting pattern through the 2c
|
||||
// zero-length machinery
|
||||
fdexp { start = 1, sb = 1, end = 3, eb = 3, content = "aa" },
|
||||
fdexp { start = 3, sb = 3, end = 3, eb = 3, content = "" },
|
||||
];
|
||||
let rows: [7]fdcase = [
|
||||
let rows: [8]fdcase = [
|
||||
fdcase { expr = "ab", input = "abxab", eoff = 0, ecnt = 2 },
|
||||
fdcase { expr = "ab", input = "abab", eoff = 2, ecnt = 2 },
|
||||
fdcase { expr = "aa", input = "aaa", eoff = 4, ecnt = 1 },
|
||||
@@ -1012,6 +1019,7 @@ type fdexp = struct {
|
||||
fdcase { expr = "ab", input = "xab", eoff = 9, ecnt = 1 },
|
||||
// no match → empty slice the caller still result_freealls
|
||||
fdcase { expr = "ab", input = "xyz", eoff = 10, ecnt = 0 },
|
||||
fdcase { expr = "a*", input = "baa", eoff = 10, ecnt = 2 },
|
||||
];
|
||||
let i: i32 = 0;
|
||||
for (i < len(rows)) {
|
||||
@@ -1051,6 +1059,298 @@ type fdexp = struct {
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
// ---- fold 3: anchors / escape / postfix / alternation ----------------
|
||||
|
||||
// instsig — flatten an inst for the table-driven program pins below:
|
||||
// kind base + payload. Takes the 56B inst by value (the #19-landed
|
||||
// is_consuming_inst shape).
|
||||
fn instsig(v: regex.inst) i64 = {
|
||||
match (v) {
|
||||
case let l: regex.inst_lit => return 1000 + ((l: rune): i64);
|
||||
case regex.inst_skip => return 2000;
|
||||
case regex.inst_any => return 3000;
|
||||
case let s: regex.inst_split => return 4000 + ((s: size): i64);
|
||||
case let j: regex.inst_jump => return 5000 + ((j: size): i64);
|
||||
case let m: regex.inst_match => {
|
||||
if ((m: bool)) { return 6001; };
|
||||
return 6000;
|
||||
};
|
||||
case let g: regex.inst_groupstart => return 7000 + ((g: size): i64);
|
||||
case regex.inst_groupend => return 8000;
|
||||
case => return 9999;
|
||||
};
|
||||
};
|
||||
|
||||
// Emitted-program pins for the fold-3 arms — deterministic, engine-
|
||||
// independent: the exact inst sequence (kinds + jump/split targets)
|
||||
// each metachar must compile to. Derived by hand-executing
|
||||
// regex.ha:286-473 (insert-before + shift + the SIZE_MAX-sentinel
|
||||
// jump fixup); the a|b row pins the whole jump_idxs pipeline
|
||||
// including the sentinel overwrite at ha:470-473.
|
||||
type pgmcase = struct {
|
||||
expr: str,
|
||||
soff: i32,
|
||||
scnt: i32,
|
||||
};
|
||||
|
||||
@test fn fold3_programs() void = {
|
||||
let sigs: [25]i64 = [
|
||||
// "^a": anchored — no leading skip
|
||||
1097, 6000,
|
||||
// "a$": skip, lit a, match(TRUE)
|
||||
2000, 1097, 6001,
|
||||
// "a?": split jumps OVER the lit to the match
|
||||
2000, 4003, 1097, 6000,
|
||||
// "a*": split to match; jump back to the split
|
||||
2000, 4004, 1097, 5001, 6000,
|
||||
// "a+": split back to the lit
|
||||
2000, 1097, 4001, 6000,
|
||||
// "a|b": leading split to the second branch's skip; the
|
||||
// first branch's jump lands on the epilogue match (the
|
||||
// fixed-up SIZE_MAX sentinel)
|
||||
4004, 2000, 1097, 5006, 2000, 1098, 6000,
|
||||
];
|
||||
let rows: [6]pgmcase = [
|
||||
pgmcase { expr = "^a", soff = 0, scnt = 2 },
|
||||
pgmcase { expr = "a$", soff = 2, scnt = 3 },
|
||||
pgmcase { expr = "a?", soff = 5, scnt = 4 },
|
||||
pgmcase { expr = "a*", soff = 9, scnt = 5 },
|
||||
pgmcase { expr = "a+", soff = 14, scnt = 4 },
|
||||
pgmcase { expr = "a|b", soff = 18, scnt = 7 },
|
||||
];
|
||||
let i: i32 = 0;
|
||||
for (i < len(rows)) {
|
||||
let ex: str = rows[i].expr;
|
||||
let c: (regex.regex | regex.error | nomem) = regex.compile(ex);
|
||||
match (c) {
|
||||
case let re: regex.regex => {
|
||||
if (re.insts.len != rows[i].scnt) { fail(); };
|
||||
let k: i32 = 0;
|
||||
for (k < rows[i].scnt) {
|
||||
if (instsig(re.insts[k])
|
||||
!= sigs[rows[i].soff + k]) {
|
||||
fail();
|
||||
};
|
||||
k += 1;
|
||||
};
|
||||
regex.finish(&re);
|
||||
};
|
||||
case => fail();
|
||||
};
|
||||
i += 1;
|
||||
};
|
||||
};
|
||||
|
||||
// The fold-3 compile-error surface, exact texts (regex.ha:289 / 296-299
|
||||
// / 303-308 / 405-417 / 423-435 / 446-455). The "ab\|^cd" row is
|
||||
// Hare's own ERROR fixture (+test.ha:634) — the escaped '|' must NOT
|
||||
// set was_prev_rune_pipe, so the following '^' misplaces.
|
||||
type cerow = struct {
|
||||
pat: str,
|
||||
want: str,
|
||||
};
|
||||
|
||||
@test fn fold3_compile_errors() void = {
|
||||
let rows: [12]cerow = [
|
||||
cerow { pat = "\\", want = "Trailing backslash '\\'" },
|
||||
cerow { pat = "a\\", want = "Trailing backslash '\\'" },
|
||||
cerow { pat = "a^",
|
||||
want = "Anchor '^' not at start of whole pattern or alternation" },
|
||||
cerow { pat = "$a",
|
||||
want = "Anchor '$' not at end of whole pattern or alternation" },
|
||||
cerow { pat = "ab\\|^cd",
|
||||
want = "Anchor '^' not at start of whole pattern or alternation" },
|
||||
cerow { pat = "?", want = "Unused '?'" },
|
||||
cerow { pat = "*", want = "Unused '*'" },
|
||||
cerow { pat = "+", want = "Unused '+'" },
|
||||
// '^' appends nothing, so insts is still empty (ha:404's
|
||||
// len check, not the r_idx one)
|
||||
cerow { pat = "^*", want = "Unused '*'" },
|
||||
cerow { pat = "a*?", want = "Misused '?'" },
|
||||
cerow { pat = "a**", want = "Misused '*'" },
|
||||
cerow { pat = "a*+", want = "Misused '+'" },
|
||||
];
|
||||
let i: i32 = 0;
|
||||
for (i < len(rows)) {
|
||||
let p: str = rows[i].pat;
|
||||
match (regex.compile(p)) {
|
||||
case let e: regex.error => {
|
||||
let w: str = rows[i].want;
|
||||
if (strings.compare((e: str), w) != 0) { fail(); };
|
||||
};
|
||||
case => fail();
|
||||
};
|
||||
i += 1;
|
||||
};
|
||||
};
|
||||
|
||||
// find_last_groupstart (regex.ha:104-119) — driven directly (private
|
||||
// fn): no inst_groupstart exists in any fold-3 program, so the error
|
||||
// arm is the live one; pin its exact text. A hand-built groupstart
|
||||
// row pins the success arm the group fold will rely on.
|
||||
@test fn find_last_groupstart_cases() void = {
|
||||
let insts: []regex.inst = [];
|
||||
append(insts, ('a': regex.inst_lit));
|
||||
match (find_last_groupstart(insts)) {
|
||||
case let e: regex.error => {
|
||||
if (strings.compare((e: str), "Unmatched ')'") != 0) {
|
||||
fail();
|
||||
};
|
||||
};
|
||||
case => fail();
|
||||
};
|
||||
append(insts, ((1: size): regex.inst_groupstart));
|
||||
append(insts, ('b': regex.inst_lit));
|
||||
match (find_last_groupstart(insts)) {
|
||||
case let sz: size => { if (sz != 1) { fail(); }; };
|
||||
case => fail();
|
||||
};
|
||||
};
|
||||
|
||||
// shift (regex.ha:123-133) — driven directly over a sub-slice view:
|
||||
// jump/split payloads in the view bump by one, the element before the
|
||||
// view and non-jump kinds are untouched (the PE3/PE4 shapes).
|
||||
@test fn shift_direct() void = {
|
||||
let insts: []regex.inst = [];
|
||||
append(insts, ((3: size): regex.inst_jump));
|
||||
append(insts, ('a': regex.inst_lit));
|
||||
append(insts, ((5: size): regex.inst_split));
|
||||
append(insts, ((7: size): regex.inst_jump));
|
||||
shift(insts[1:]);
|
||||
if (instsig(insts[0]) != 5003) { fail(); };
|
||||
if (instsig(insts[1]) != 1097) { fail(); };
|
||||
if (instsig(insts[2]) != 4006) { fail(); };
|
||||
if (instsig(insts[3]) != 5008) { fail(); };
|
||||
};
|
||||
|
||||
// fold-3 find/test rows — the group-free subset of Hare's own table
|
||||
// (+test.ha:221-256 anchors/postfix, :622-650 whole-expression and
|
||||
// multiple alternation; end == -1 resolved to rune-length per
|
||||
// +test.ha:693-697) plus rob's dedup/leftmost/longest riders:
|
||||
// `a*` over "aaaa" must yield ONE (0,4) (split spawns same-pc threads
|
||||
// every step — the ha:872-889 dedup pin gone observable), `b+` over
|
||||
// "abab" pins the leftmost trim (1,2 not 3,4), `b*`/`^b*` over
|
||||
// "aaaabbbb" pin longest-pick vs anchored zero-length. The multibyte
|
||||
// `b+` row keeps every idx != bytesize (B4). Reuses the fcase shape;
|
||||
// every row also cross-pins test() == (find() matched).
|
||||
@test fn fold3_find_cases() void = {
|
||||
let rows: [42]fcase = [
|
||||
fcase { expr = "^abc$", input = "abc", matches = true,
|
||||
start = 0, sb = 0, end = 3, eb = 3, content = "abc" },
|
||||
fcase { expr = "^abc$", input = "axc", matches = false, ... },
|
||||
fcase { expr = "^.$", input = "x", matches = true,
|
||||
start = 0, sb = 0, end = 1, eb = 1, content = "x" },
|
||||
fcase { expr = "^.$", input = "", matches = false, ... },
|
||||
fcase { expr = "^a+$", input = "a", matches = true,
|
||||
start = 0, sb = 0, end = 1, eb = 1, content = "a" },
|
||||
fcase { expr = "^a+$", input = "aaa", matches = true,
|
||||
start = 0, sb = 0, end = 3, eb = 3, content = "aaa" },
|
||||
fcase { expr = "^a+$", input = "", matches = false, ... },
|
||||
fcase { expr = "^a*$", input = "", matches = true,
|
||||
start = 0, sb = 0, end = 0, eb = 0, content = "" },
|
||||
fcase { expr = "^a*$", input = "aaaa", matches = true,
|
||||
start = 0, sb = 0, end = 4, eb = 4, content = "aaaa" },
|
||||
fcase { expr = "^a*$", input = "b", matches = false, ... },
|
||||
fcase { expr = "^a?$", input = "", matches = true,
|
||||
start = 0, sb = 0, end = 0, eb = 0, content = "" },
|
||||
fcase { expr = "^a?$", input = "a", matches = true,
|
||||
start = 0, sb = 0, end = 1, eb = 1, content = "a" },
|
||||
fcase { expr = "^a?$", input = "b", matches = false, ... },
|
||||
fcase { expr = "^a*", input = "aaaa", matches = true,
|
||||
start = 0, sb = 0, end = 4, eb = 4, content = "aaaa" },
|
||||
fcase { expr = "a*$", input = "aaaa", matches = true,
|
||||
start = 0, sb = 0, end = 4, eb = 4, content = "aaaa" },
|
||||
fcase { expr = "a*", input = "aaaa", matches = true,
|
||||
start = 0, sb = 0, end = 4, eb = 4, content = "aaaa" },
|
||||
fcase { expr = "b*", input = "aaaabbbb", matches = true,
|
||||
start = 4, sb = 4, end = 8, eb = 8, content = "bbbb" },
|
||||
fcase { expr = "^b*", input = "aaaabbbb", matches = true,
|
||||
start = 0, sb = 0, end = 0, eb = 0, content = "" },
|
||||
fcase { expr = "b*$", input = "aaaabbbb", matches = true,
|
||||
start = 4, sb = 4, end = 8, eb = 8, content = "bbbb" },
|
||||
fcase { expr = "b+", input = "abab", matches = true,
|
||||
start = 1, sb = 1, end = 2, eb = 2, content = "b" },
|
||||
// multibyte rider: 2-byte ß before the b's splits every
|
||||
// idx from its bytesize
|
||||
fcase { expr = "b+", input = "aßbb", matches = true,
|
||||
start = 2, sb = 3, end = 4, eb = 5, content = "bb" },
|
||||
fcase { expr = "ab|cd", input = "cd", matches = true,
|
||||
start = 0, sb = 0, end = 2, eb = 2, content = "cd" },
|
||||
fcase { expr = "ab|cd", input = "abc", matches = true,
|
||||
start = 0, sb = 0, end = 2, eb = 2, content = "ab" },
|
||||
fcase { expr = "ab|cd", input = "abcd", matches = true,
|
||||
start = 0, sb = 0, end = 2, eb = 2, content = "ab" },
|
||||
fcase { expr = "ab|cd", input = "bcd", matches = true,
|
||||
start = 1, sb = 1, end = 3, eb = 3, content = "cd" },
|
||||
fcase { expr = "^ab|cd", input = "bcd", matches = true,
|
||||
start = 1, sb = 1, end = 3, eb = 3, content = "cd" },
|
||||
fcase { expr = "^ab|cd", input = "zab", matches = false, ... },
|
||||
fcase { expr = "ab$|cd", input = "ab", matches = true,
|
||||
start = 0, sb = 0, end = 2, eb = 2, content = "ab" },
|
||||
fcase { expr = "ab$|cd", input = "abc", matches = false, ... },
|
||||
fcase { expr = "ab|cd$", input = "cde", matches = false, ... },
|
||||
fcase { expr = "ab|^cd", input = "bcd", matches = false, ... },
|
||||
fcase { expr = "ab|^cd", input = "cde", matches = true,
|
||||
start = 0, sb = 0, end = 2, eb = 2, content = "cd" },
|
||||
fcase { expr = "a|b|c|d|e", input = "e", matches = true,
|
||||
start = 0, sb = 0, end = 1, eb = 1, content = "e" },
|
||||
fcase { expr = "a|b|c|d|e", input = "xe", matches = true,
|
||||
start = 1, sb = 1, end = 2, eb = 2, content = "e" },
|
||||
fcase { expr = "a|b$|c$|d$|e", input = "cd", matches = true,
|
||||
start = 1, sb = 1, end = 2, eb = 2, content = "d" },
|
||||
fcase { expr = "a|b$|c$|d$|e", input = "ax", matches = true,
|
||||
start = 0, sb = 0, end = 1, eb = 1, content = "a" },
|
||||
fcase { expr = "a|b$|c$|d$|e", input = "cx", matches = false, ... },
|
||||
fcase { expr = "a|b$|c$|d$|e", input = "ex", matches = true,
|
||||
start = 0, sb = 0, end = 1, eb = 1, content = "e" },
|
||||
fcase { expr = "a|^b|^c|^d|e", input = "cd", matches = true,
|
||||
start = 0, sb = 0, end = 1, eb = 1, content = "c" },
|
||||
fcase { expr = "a|^b|^c|^d|e", input = "xa", matches = true,
|
||||
start = 1, sb = 1, end = 2, eb = 2, content = "a" },
|
||||
fcase { expr = "a|^b|^c|^d|e", input = "xc", matches = false, ... },
|
||||
fcase { expr = "a|^b|^c|^d|e", input = "xe", matches = true,
|
||||
start = 1, sb = 1, end = 2, eb = 2, content = "e" },
|
||||
];
|
||||
let i: i32 = 0;
|
||||
for (i < len(rows)) {
|
||||
let ex: str = rows[i].expr;
|
||||
let inp: str = rows[i].input;
|
||||
let c: (regex.regex | regex.error | nomem) = regex.compile(ex);
|
||||
match (c) {
|
||||
case let re: regex.regex => {
|
||||
let fr: (regex.result | nomem) = regex.find(&re, inp);
|
||||
if (!(fr is regex.result)) { fail(); };
|
||||
let res: regex.result = fr as regex.result;
|
||||
if (rows[i].matches) {
|
||||
if (len(res) != 1) { fail(); };
|
||||
if (res[0].start != rows[i].start) { fail(); };
|
||||
if (res[0].start_bytesize != rows[i].sb) {
|
||||
fail();
|
||||
};
|
||||
if (res[0].end != rows[i].end) { fail(); };
|
||||
if (res[0].end_bytesize != rows[i].eb) {
|
||||
fail();
|
||||
};
|
||||
let wc: str = rows[i].content;
|
||||
if (strings.compare(res[0].content, wc) != 0) {
|
||||
fail();
|
||||
};
|
||||
} else {
|
||||
if (len(res) != 0) { fail(); };
|
||||
};
|
||||
let tr: (bool | nomem) = regex.test(&re, inp);
|
||||
if (!(tr is bool)) { fail(); };
|
||||
if ((tr as bool) != (len(res) != 0)) { fail(); };
|
||||
regex.result_free(res);
|
||||
regex.finish(&re);
|
||||
};
|
||||
case => fail();
|
||||
};
|
||||
i += 1;
|
||||
};
|
||||
};
|
||||
|
||||
export fn main() i32 = {
|
||||
signalled = 1; lit_and_match();
|
||||
signalled = 2; size_aliases_distinct();
|
||||
@@ -1078,5 +1378,10 @@ export fn main() i32 = {
|
||||
signalled = 24; find_cases();
|
||||
signalled = 25; findall_content();
|
||||
signalled = 26; findall_fields();
|
||||
signalled = 27; fold3_programs();
|
||||
signalled = 28; fold3_compile_errors();
|
||||
signalled = 29; find_last_groupstart_cases();
|
||||
signalled = 30; shift_direct();
|
||||
signalled = 31; fold3_find_cases();
|
||||
return 0;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user