diff --git a/lib/regex/regex.ww b/lib/regex/regex.ww index ef26ff68..b21cc6a1 100644 --- a/lib/regex/regex.ww +++ b/lib/regex/regex.ww @@ -1,16 +1,15 @@ // 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 that compiles cleanly -// today (thread/newmatch types, result_free, strerror — -// dead-imported until the engine lands, fold-1 precedent). Every -// other metacharacter arm, the engine itself (delete_thread/ -// add_thread/is_consuming_inst/run_thread/search) and the exec -// surface (test/find/replace) are DEFERRED behind compiler fixes: -// every *[]thread-mediated shape the engine needs miscompiles today -// (append-through-ptr #15, deref-spine element reads #17), and the -// 56B-slot inst by-value arg ABI is unwired both sides (#19) — -// probed pre-port, pA*/pB*/pC* probes. They land with those fixes. +// tranche A = the thread-machine scaffolding (thread/newmatch types, +// result_free, strerror); fold 2b tranche B = the engine leaf fns +// (delete_thread/is_consuming_inst/add_thread — dead until search +// lands, fold-1 precedent). Every other metacharacter arm, the rest +// of the engine (run_thread, see its deferral note below; search on +// F5 element-copy + F2 len-builtin, tranche C) and the exec surface +// (test/find on the C6 multi-success `?` gate, tranche D; replace) +// are DEFERRED behind compiler fixes — probed pre-port, pA*/pB*/PB* +// probes. They land with those fixes. // // One fold-1 construct is held back behind a filed compiler/fidelity // gap (see the charclass_map site below): @@ -206,14 +205,86 @@ export fn compile(expr: str) (regex | error | nomem) = { }; }; -// delete_thread (regex.ha:547-551), is_consuming_inst (regex.ha: -// 553-555) and add_thread (regex.ha:557-587) belong here in Hare -// order but are deferred behind compiler fixes: delete_thread/ -// add_thread on append-through-ptr (#15) / deref-spine reads (#17); -// is_consuming_inst on the >48B tagged by-value ABI (#19 — inst is a -// 56B slot; the call side is a loud unwired boundary and the callee -// receive diverges cs≠ww, so even landing it dead would break rule -// 10). See the module header. +// ref/hare/regex/regex.ha:547-551, verbatim — both free()s are the +// documented no-op (#27; see finish()), kept for source parity. ww +// has no pointer auto-deref, so Hare's `threads[i]` spells +// `(*threads)[i]` (the test-804-pinned delete shape). +fn delete_thread(i: size, threads: *[]thread) void = { + free((*threads)[i].captures); + free((*threads)[i].rep_counters); + delete((*threads)[i]); +}; + +// ref/hare/regex/regex.ha:553-555. Hare's multi-type membership test +// `a is (inst_lit | inst_any | inst_charset)` is loud-rejected by +// design (filed as a Hare-parity task, ww-core #13); the ruled +// spelling is the chained ||. +fn is_consuming_inst(a: inst) bool = { + return a is inst_lit || a is inst_any || a is inst_charset; +}; + +// ref/hare/regex/regex.ha:557-587. The dedup scan (ha:560-566) is an +// index loop: ww has no by-ref `&..` range and a by-value range over +// `*threads` miscompiles (F1, ww-core #11). Its bound reads the +// `.len` pseudo-field, not the len() builtin — len(*threads) +// mis-reads the data pointer as the length (FB1, ww-core #41; the +// F2/#10 deref sibling). Hare's `append(...)?` nomem propagation and the +// ok/defer-if unwind (ha:568/573/586) drop together: ww append +// returns void (#36 filed) and free() reclaims nothing (#27), so +// there is nothing to propagate or unwind. +fn add_thread(threads: *[]thread, parent_idx: size, new_pc: size) (void | nomem) = { + // Do not add this thread if there is already another thread with + // the same PC + for (let k: size = 0; k < ((*threads).len: size); k += 1) { + if ((*threads)[k].pc == new_pc + && !(*threads)[k].matched + && (*threads)[k].start_idx + < (*threads)[parent_idx].start_idx) { + return; + }; + }; + + // Hare dups the parent's captures/rep_counters here + // (`alloc(threads[parent_idx].captures...)?`, ha:569/572). Every + // ww route into that dup is blocked today (re-probed post-C3, + // PB7): the deref-spine spread SOURCE is loud-rejected (#35), + // the per-element append is loud-rejected (#34 struct element + // source), and the whole-element let-copy drops bytes (#7/F5). + // The abort is sound, not a semantic hole: fold-2a's compile() + // cannot emit inst_groupstart/inst_repeat, so both slices are + // provably empty in every program this fold can run; the empty + // case appends honest zeroed headers below. The verbatim dup + // lands with the group/repeat fold (ww-core #3). + if ((*threads)[parent_idx].captures.len != 0 + || (*threads)[parent_idx].rep_counters.len != 0) { + abort("regex: capture dup not yet portable (#35/#34/#7)"); + }; + let captures: []capture; + let rep_counters: []size; + + append(*threads, thread { + pc = new_pc, + start_idx = (*threads)[parent_idx].start_idx, + start_bytesize = (*threads)[parent_idx].start_bytesize, + matched = (*threads)[parent_idx].matched, + failed = (*threads)[parent_idx].failed, + captures = captures, + rep_counters = rep_counters, + ... + }); + return; +}; + +// run_thread (regex.ha:589-742) is deferred behind ONE remaining +// compiler boundary: its loop condition +// `is_consuming_inst(re.insts[threads[i].pc])` passes the 56B inst +// BY VALUE from a slice-element source, which the #38b arg wiring +// loud-rejects ("unsupported source kind 12" — ident sources landed +// with #19; the slice-element extension is task #40, in flight). +// Probed pre-port (PB6c); ruled: run_thread lands Hare-VERBATIM once +// #40 wires the source. Every other run_thread shape is probe-proven +// at HEAD (PB6b match-arm store + composed frombytes bound, P7 +// composed scrutinee + compound pc step, P8 newmatch return). // Frees a [[result]]. // diff --git a/lib/regex/regex_test.ww b/lib/regex/regex_test.ww index ce47637c..7d1833f3 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 thread-machine scaffolding (thread/newmatch + -// result_free + strerror; the engine fns are deferred behind -// compiler fixes #15/#17/#19 — see regex.ww). Run with +// 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 // `out/bin/ww run lib/regex/regex_test.ww`. // // Private symbols (thread, newmatch) are reached unqualified: this @@ -428,6 +428,122 @@ type nmexp = struct { }; }; +// is_consuming_inst must discriminate the three consuming kinds from +// the seven non-consuming ones across all 10 inst variants +// (regex.ha:553-555) — the tranche-A-deferred row, graduated by the +// #19 >48B by-value arg wiring. Sequential typed-let + helper calls, +// not a [10](inst, bool) table: tagged-element array literals +// under-copy (#12), and a cast/literal rvalue arg source is +// #38b-unwired, so each value goes through a typed let (the +// #19-landed ident source). +fn ic_one(v: regex.inst, want: bool) void = { + if (is_consuming_inst(v) != want) { fail(); }; +}; + +@test fn is_consuming_kinds() void = { + let lit: regex.inst = ('a': regex.inst_lit); + ic_one(lit, true); + let av: regex.inst_any; + let any: regex.inst = av; + ic_one(any, true); + let cs: regex.inst = (inst_charset { idx = 0, is_positive = true }); + ic_one(cs, true); + let kv: regex.inst_skip; + let sk: regex.inst = kv; + ic_one(sk, false); + let sp: regex.inst = ((5: size): regex.inst_split); + ic_one(sp, false); + let jm: regex.inst = ((6: size): regex.inst_jump); + ic_one(jm, false); + let mt: regex.inst = (false: regex.inst_match); + ic_one(mt, false); + let gs: regex.inst = ((2: size): regex.inst_groupstart); + ic_one(gs, false); + let gv: regex.inst_groupend; + let ge: regex.inst = gv; + ic_one(ge, false); + let rp: regex.inst = (inst_repeat { + id = 1, origin = 4, min = (2: size), max = void, + }); + ic_one(rp, false); +}; + +// delete_thread (regex.ha:547-551) removes exactly the indexed +// element and preserves order; its frees are no-ops (no-free +// runtime), so the survivors' capture headers stay readable. +@test fn delete_thread_middle() void = { + let caps: []regex.capture = []; + append(caps, capture { + content = "x", start = 0, start_bytesize = 0, + end = 1, end_bytesize = 1, + }); + let ts: []thread = []; + append(ts, thread { pc = 1, start_idx = 11, captures = caps, ... }); + append(ts, thread { pc = 2, start_idx = 22, ... }); + append(ts, thread { pc = 3, start_idx = 33, ... }); + delete_thread(1, &ts); + if (len(ts) != 2) { fail(); }; + if (ts[0].pc != (1: size)) { fail(); }; + if (ts[0].start_idx != (11: size)) { fail(); }; + if (ts[0].captures.len != 1) { fail(); }; + if (ts[1].pc != (3: size)) { fail(); }; + if (ts[1].start_idx != (33: size)) { fail(); }; + if (ts[1].captures.len != 0) { fail(); }; + // boundary rows: delete at the last index, then at index 0 down + // to empty — the failed-sweep loop (regex.ha:891-896) deletes at + // every position including both ends. + delete_thread(1, &ts); + if (len(ts) != 1) { fail(); }; + if (ts[0].pc != (1: size)) { fail(); }; + delete_thread(0, &ts); + if (len(ts) != 0) { fail(); }; +}; + +// add_thread (regex.ha:557-587): same-pc dedup suppression fires only +// when the existing thread is unmatched AND started strictly earlier +// than the parent (ha:561-565); otherwise the child appends, +// inheriting the parent's start/matched/failed with fresh empty +// capture/rep_counter headers and a zeroed root_capture. The +// capture-dup loud bound must NOT fire on these empty-caps parents. +@test fn add_thread_dedup_inherit() void = { + let ts: []thread = []; + append(ts, thread { pc = 0, start_idx = 5, start_bytesize = 4, + matched = false, failed = true, ... }); + // inherit: fresh pc, parent fields copied, rest zeroed + let r: (void | nomem) = add_thread(&ts, 0, 7); + if (!(r is void)) { fail(); }; + if (len(ts) != 2) { fail(); }; + if (ts[1].pc != (7: size)) { fail(); }; + if (ts[1].start_idx != (5: size)) { fail(); }; + if (ts[1].start_bytesize != (4: size)) { fail(); }; + if (ts[1].matched) { fail(); }; + if (!ts[1].failed) { fail(); }; + if (ts[1].captures.len != 0) { fail(); }; + if (ts[1].rep_counters.len != 0) { fail(); }; + if (ts[1].root_capture.content.len != 0) { fail(); }; + if (ts[1].root_capture.end != (0: size)) { fail(); }; + // same-pc same-start does NOT suppress (strict <, ha:563-565) + let r2: (void | nomem) = add_thread(&ts, 0, 7); + if (!(r2 is void)) { fail(); }; + if (len(ts) != 3) { fail(); }; + // an earlier-started unmatched existing thread DOES suppress + let ts2: []thread = []; + append(ts2, thread { pc = 0, start_idx = 5, ... }); + append(ts2, thread { pc = 7, start_idx = 2, ... }); + let r3: (void | nomem) = add_thread(&ts2, 0, 7); + if (!(r3 is void)) { fail(); }; + if (len(ts2) != 2) { fail(); }; + // a MATCHED existing thread never suppresses + let ts3: []thread = []; + append(ts3, thread { pc = 0, start_idx = 5, ... }); + append(ts3, thread { pc = 7, start_idx = 2, matched = true, ... }); + let r4: (void | nomem) = add_thread(&ts3, 0, 7); + if (!(r4 is void)) { fail(); }; + if (len(ts3) != 3) { fail(); }; + if (ts3[2].pc != (7: size)) { fail(); }; + if (ts3[2].start_idx != (5: size)) { fail(); }; +}; + export fn main() i32 = { signalled = 1; lit_and_match(); signalled = 2; size_aliases_distinct(); @@ -443,5 +559,8 @@ export fn main() i32 = { signalled = 12; newmatch_discriminates(); signalled = 13; result_free_noop(); signalled = 14; strerror_identity(); + signalled = 15; is_consuming_kinds(); + signalled = 16; delete_thread_middle(); + signalled = 17; add_thread_dedup_inherit(); return 0; };