diff --git a/lib/regex/regex.ww b/lib/regex/regex.ww index b21cc6a1..8b4c692c 100644 --- a/lib/regex/regex.ww +++ b/lib/regex/regex.ww @@ -2,14 +2,13 @@ // ref/hare/regex/regex.ha. Fold 1 = the data model; fold 2a = the // compile() literal core (lit/any/match + the leading skip); fold 2b // tranche A = the thread-machine scaffolding (thread/newmatch types, -// result_free, strerror); fold 2b tranche B = the engine leaf fns -// (delete_thread/is_consuming_inst/add_thread — dead until search -// lands, fold-1 precedent). Every other metacharacter arm, the rest -// of the engine (run_thread, see its deferral note below; search on -// F5 element-copy + F2 len-builtin, tranche C) and the exec surface -// (test/find on the C6 multi-success `?` gate, tranche D; replace) -// are DEFERRED behind compiler fixes — probed pre-port, pA*/pB*/PB* -// probes. They land with those fixes. +// result_free, strerror); fold 2b tranche B = the engine +// (delete_thread/is_consuming_inst/add_thread/run_thread — dead +// until search lands, fold-1 precedent). Every other metacharacter +// arm, search (F5 element-copy + F2 len-builtin, tranche C) and the +// exec surface (test/find on the C6 multi-success `?` gate, tranche +// D; replace) are DEFERRED behind compiler fixes — probed pre-port, +// pA*/pB*/PB* probes. They land with those fixes. // // One fold-1 construct is held back behind a filed compiler/fidelity // gap (see the charclass_map site below): @@ -19,6 +18,7 @@ // uses exact type_eq, no element decay). package regex; +import io; import strings; import encoding.utf8; @@ -275,16 +275,107 @@ fn add_thread(threads: *[]thread, parent_idx: size, new_pc: size) (void | nomem) return; }; -// run_thread (regex.ha:589-742) is deferred behind ONE remaining -// compiler boundary: its loop condition -// `is_consuming_inst(re.insts[threads[i].pc])` passes the 56B inst -// BY VALUE from a slice-element source, which the #38b arg wiring -// loud-rejects ("unsupported source kind 12" — ident sources landed -// with #19; the slice-element extension is task #40, in flight). -// Probed pre-port (PB6c); ruled: run_thread lands Hare-VERBATIM once -// #40 wires the source. Every other run_thread shape is probe-proven -// at HEAD (PB6b match-arm store + composed frombytes bound, P7 -// composed scrutinee + compound pc step, P8 newmatch return). +// ref/hare/regex/regex.ha:589-742. Only the arms fold-2a's compile() +// can emit execute (skip + match non-consuming; lit + any consuming); +// every other inst arm is one loud not-yet-ported abort — the fold +// boundary, the compile() metachar discipline. The "unreachable" +// aborts on lit/any inside the non-consuming loop (ha:604-605) and +// the trailing default (ha:738) are Hare's own unreachable arms; +// Hare spells them bare `abort()`, ww carries a message — the +// zero-arg builtin form is shadowed in any combined unit that +// declares its own abort fn (os.ww:16 is private yet shadows +// cross-module; filed, ww-core #45). Hare's loop match omits inst_charset +// (consuming — the loop condition excludes it); ww has no +// match-exhaustiveness analysis, so the omission becomes the loud +// default arm. +fn run_thread( + i: size, + re: *regex, + string: str, + threads: *[]thread, + r_or_end: (rune | io.eof), + str_idx: size, + str_bytesize: size, +) (void | newmatch | nomem) = { + let str_bytes: []u8 = strings.toutf8(string); + if ((*threads)[i].matched) { + return; + }; + for (!is_consuming_inst(re.insts[(*threads)[i].pc])) { + match (re.insts[(*threads)[i].pc]) { + case inst_lit => abort("regex: unreachable"); + case inst_any => abort("regex: unreachable"); + case inst_split => + abort("regex: inst_split not yet ported"); + case inst_jump => + abort("regex: inst_jump not yet ported"); + case inst_skip => { + let new_pc: size = (*threads)[i].pc + 1; + (*threads)[i].start_idx = str_idx; + (*threads)[i].start_bytesize = str_bytesize; + add_thread(threads, i, new_pc)?; + break; + }; + case let anchored: inst_match => { + // Do not match if we need an end-anchored match, but we + // have not exhausted our string + // + // Hare spells `anchored` bare (ha:621) — alias + // transparency; ww named aliases are nominal in bool / + // comparison position, so this cast and the consuming + // arm's (lit: rune) are checker-required. + if ((anchored: bool) && !(r_or_end is io.eof)) { + (*threads)[i].failed = true; + return; + }; + let content: str = strings.frombytes(str_bytes[ + (*threads)[i].start_bytesize:str_bytesize]); + (*threads)[i].root_capture = capture { + start = (*threads)[i].start_idx, + start_bytesize = (*threads)[i].start_bytesize, + end = str_idx, + end_bytesize = str_bytesize, + content = content, + }; + (*threads)[i].matched = true; + let nm: newmatch; + return nm; + }; + case inst_groupstart => + abort("regex: inst_groupstart not yet ported"); + case inst_groupend => + abort("regex: inst_groupend not yet ported"); + case inst_repeat => + abort("regex: inst_repeat not yet ported"); + case => abort("regex: unreachable"); + }; + }; + + // From now on, we're only matching consuming instructions, and these + // can't do anything without another rune. + if (r_or_end is io.eof) { + (*threads)[i].failed = true; + return; + }; + + let r: rune = r_or_end as rune; + + match (re.insts[(*threads)[i].pc]) { + case inst_skip => return; + case let lit: inst_lit => { + if (r != (lit: rune)) { + (*threads)[i].failed = true; + }; + }; + case inst_any => void; + case inst_charset => + abort("regex: inst_charset not yet ported"); + case => abort("regex: unreachable"); // unreachable (ha:738) + }; + + (*threads)[i].pc += 1; + return; +}; // Frees a [[result]]. // diff --git a/lib/regex/regex_test.ww b/lib/regex/regex_test.ww index 7d1833f3..a7e1753d 100644 --- a/lib/regex/regex_test.ww +++ b/lib/regex/regex_test.ww @@ -1,8 +1,8 @@ // regex_test — exercises the lib/regex fold-1 data model (the type // model + finish()), the fold-2a compile() literal core, and the // fold-2b tranche-A/B thread machine (thread/newmatch + result_free -// + strerror; delete_thread/is_consuming_inst/add_thread; run_thread -// and the exec surface are deferred — see regex.ww). Run with +// + strerror; delete_thread/is_consuming_inst/add_thread/run_thread; +// search and the exec surface are deferred — see regex.ww). Run with // `out/bin/ww run lib/regex/regex_test.ww`. // // Private symbols (thread, newmatch) are reached unqualified: this @@ -30,6 +30,7 @@ package regex; import regex; +import io; import os; import strings; @@ -544,6 +545,125 @@ fn ic_one(v: regex.inst, want: bool) void = { if (ts3[2].start_idx != (5: size)) { fail(); }; }; +// run_thread (regex.ha:589-742) driven directly over compile("ab")'s +// real program [skip, lit 'a', lit 'b', match(false)] — the arms +// fold-2a can emit. Phases: parked-skip spawn (len 1→2, parent pc +// unmoved — the unanchored-restart engine), lit advance, lit +// mismatch (failed=true AND pc still steps — ha:741 runs regardless +// of the arm's verdict), EOF on a consuming pc (failed, pc frozen), +// match arm (root_capture spans start_bytesize..str_bytesize + +// matched + `is newmatch`), and the matched-thread early return +// (ha:599-601). +@test fn run_thread_literal_program() void = { + // typed-let + match receive, the compile_literal_program shape + let c: (regex.regex | regex.error | nomem) = regex.compile("ab"); + match (c) { + case let re: regex.regex => { + // skip spawn: thread 0 parks on the skip, child enters at pc 1 + let ra: (rune | io.eof) = 'a'; + let ts: []thread = []; + append(ts, thread { pc = 0, ... }); + let r1: (void | newmatch | nomem) = run_thread(0, &re, "ab", &ts, ra, 0, 0); + if (!(r1 is void)) { fail(); }; + if (len(ts) != 2) { fail(); }; + if (ts[0].pc != (0: size)) { fail(); }; + if (ts[1].pc != (1: size)) { fail(); }; + if (ts[1].failed) { fail(); }; + + // lit match advances pc past 'a' + let r2: (void | newmatch | nomem) = run_thread(1, &re, "ab", &ts, ra, 0, 0); + if (!(r2 is void)) { fail(); }; + if (ts[1].pc != (2: size)) { fail(); }; + if (ts[1].failed) { fail(); }; + + // lit mismatch fails the thread; pc steps anyway (ha:741) + let rx: (rune | io.eof) = 'x'; + let r3: (void | newmatch | nomem) = run_thread(1, &re, "ab", &ts, rx, 1, 1); + if (!(r3 is void)) { fail(); }; + if (!ts[1].failed) { fail(); }; + if (ts[1].pc != (3: size)) { fail(); }; + + // EOF on a consuming pc fails the thread before pc steps + let ev: io.eof; + let reof: (rune | io.eof) = ev; + let ts2: []thread = []; + append(ts2, thread { pc = 1, ... }); + let r4: (void | newmatch | nomem) = run_thread(0, &re, "ab", &ts2, reof, 2, 2); + if (!(r4 is void)) { fail(); }; + if (!ts2[0].failed) { fail(); }; + if (ts2[0].pc != (1: size)) { fail(); }; + + // match arm: root_capture spans start_bytesize..str_bytesize, + // matched set, newmatch returned + let ts3: []thread = []; + append(ts3, thread { pc = 3, ... }); + let r5: (void | newmatch | nomem) = run_thread(0, &re, "ab", &ts3, reof, 2, 2); + if (!(r5 is newmatch)) { fail(); }; + if (!ts3[0].matched) { fail(); }; + if (ts3[0].failed) { fail(); }; + if (ts3[0].root_capture.start != (0: size)) { fail(); }; + if (ts3[0].root_capture.start_bytesize != (0: size)) { fail(); }; + if (ts3[0].root_capture.end != (2: size)) { fail(); }; + if (ts3[0].root_capture.end_bytesize != (2: size)) { fail(); }; + if (strings.compare(ts3[0].root_capture.content, "ab") != 0) { fail(); }; + + // an already-matched thread is inert (ha:599-601): void + // return, state untouched + let r6: (void | newmatch | nomem) = run_thread(0, &re, "ab", &ts3, ra, 3, 3); + if (!(r6 is void)) { fail(); }; + if (ts3[0].root_capture.end != (2: size)) { fail(); }; + + // idx/bytesize split: every all-ASCII row has idx == + // bytesize, so a port swapping start/start_bytesize (or + // end/end_bytesize) in root_capture passes them. One 2-byte + // rune ('ß') consumed before the match start makes all four + // values distinct: start=1 start_bytesize=2 end=3 + // end_bytesize=4; content = bytes[2:4] = "ab". + let ts4: []thread = []; + append(ts4, thread { pc = 3, start_idx = 1, start_bytesize = 2, ... }); + let r7: (void | newmatch | nomem) = run_thread(0, &re, "ßab", &ts4, reof, 3, 4); + if (!(r7 is newmatch)) { fail(); }; + if (ts4[0].root_capture.start != (1: size)) { fail(); }; + if (ts4[0].root_capture.start_bytesize != (2: size)) { fail(); }; + if (ts4[0].root_capture.end != (3: size)) { fail(); }; + if (ts4[0].root_capture.end_bytesize != (4: size)) { fail(); }; + if (strings.compare(ts4[0].root_capture.content, "ab") != 0) { fail(); }; + regex.finish(&re); + }; + case => fail(); + }; +}; + +// The anchored route (ha:621-624) needs a (true: inst_match) program +// — compile() can't emit `$` yet, so it is HAND-BUILT — pinned from +// both sides: anchored + string-not-exhausted fails the thread; +// anchored + EOF falls through to the match (empty content). +@test fn run_thread_anchored_route() void = { + let insts: []regex.inst = []; + append(insts, (true: regex.inst_match)); + let re: regex.regex; + re.insts = insts; + re.n_reps = 0; + + let ra: (rune | io.eof) = 'a'; + let ts: []thread = []; + append(ts, thread { pc = 0, ... }); + let r1: (void | newmatch | nomem) = run_thread(0, &re, "ab", &ts, ra, 0, 0); + if (!(r1 is void)) { fail(); }; + if (!ts[0].failed) { fail(); }; + if (ts[0].matched) { fail(); }; + + let ev: io.eof; + let reof: (rune | io.eof) = ev; + let ts2: []thread = []; + append(ts2, thread { pc = 0, ... }); + let r2: (void | newmatch | nomem) = run_thread(0, &re, "", &ts2, reof, 0, 0); + if (!(r2 is newmatch)) { fail(); }; + if (!ts2[0].matched) { fail(); }; + if (ts2[0].root_capture.content.len != 0) { fail(); }; + if (ts2[0].root_capture.end != (0: size)) { fail(); }; +}; + export fn main() i32 = { signalled = 1; lit_and_match(); signalled = 2; size_aliases_distinct(); @@ -562,5 +682,7 @@ export fn main() i32 = { signalled = 15; is_consuming_kinds(); signalled = 16; delete_thread_middle(); signalled = 17; add_thread_dedup_inherit(); + signalled = 18; run_thread_literal_program(); + signalled = 19; run_thread_anchored_route(); return 0; };