lib/regex: fold-2b tranche B1-B3 — thread-machine leaf fns (run_thread held on #40)

Port is_consuming_inst (regex.ha:553-555), delete_thread (ha:547-551)
and add_thread (ha:557-587) per the tranche-B scope (drew §9b). D12
chained-|| spells the multi-type is (parity task #13); D9 index loops
replace the &.. ranges (#11); the dedup-scan bound reads .len, not
the len() builtin (FB1, #41 — len(*p) loads the data pointer as the
length). add_thread's capture/rep_counter dup is loud-bounded per the
rob-ratified ruling: every ww route into the dup is blocked at HEAD
(#35 spread source, #34 element source, #7 element let-copy), and
fold-2a compile() cannot emit inst_groupstart/inst_repeat, so both
parent slices are provably empty in every reachable program; the
verbatim dup lands with the group/repeat fold (#3). run_thread (B4)
stays out: its loop condition passes the 56B inst by value from a
slice-element source, gated on the #38b extension (#40, in flight).

Fixture cases 15-17 drive the three fns directly (package regex):
all-10-kind consuming table, delete at middle/last/0-to-empty,
dedup suppress/strict-</matched-guard + inheritance + zeroed headers.
Byte-cmp on the regenerated combined holds the FC0-only baseline;
both drivers run the fixture green.
This commit is contained in:
2026-06-04 12:12:27 +09:00
parent 074e68f05d
commit f3f74c9b21
2 changed files with 211 additions and 21 deletions

View File

@@ -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]].
//

View File

@@ -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;
};