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