regex: fold 5a — add_thread dups parent captures/rep_counters (ha:568-573)

The loud bound (capture dup not yet portable, #35/#34/#7) flips to the
real dup now that #35's spread place-chain sources landed: fresh slice
header + spread-append, the D3 spelling of Hare's alloc-dup. The
ok/defer-if frees drop (free() is the documented no-op, #27).
Dup-independence rows drive add_thread directly: values carried,
backing independent both directions, empty parent → empty dup.
This commit is contained in:
2026-06-05 00:36:33 +09:00
parent 06b0fea98b
commit 0e61db1857
2 changed files with 64 additions and 18 deletions

View File

@@ -626,23 +626,15 @@ fn add_thread(threads: *[]thread, parent_idx: size, new_pc: size) (void | nomem)
};
};
// 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)");
};
// Hare dups via `alloc(threads[parent_idx].captures...)?`
// (ha:568-573); the alloc-dup form has no ww spelling (D3) —
// fresh slice header + spread-append, the #35-landed deref-spine
// source. The ok/defer-if frees (ha:570/573) drop: free() is the
// documented no-op (#27; see finish()).
let captures: []capture;
append(captures, (*threads)[parent_idx].captures...);
let rep_counters: []size;
append(rep_counters, (*threads)[parent_idx].rep_counters...);
append(*threads, thread {
pc = new_pc,

View File

@@ -504,9 +504,9 @@ fn ic_one(v: regex.inst, want: bool) void = {
// 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.
// inheriting the parent's start/matched/failed with DUPLICATED
// capture/rep_counter slices (empty parent → empty dup, ha:569/572)
// and a zeroed root_capture.
@test fn add_thread_dedup_inherit() void = {
let ts: []thread = [];
append(ts, thread { pc = 0, start_idx = 5, start_bytesize = 4,
@@ -546,6 +546,59 @@ fn ic_one(v: regex.inst, want: bool) void = {
if (ts3[2].start_idx != (5: size)) { fail(); };
};
// add_thread dup (regex.ha:568-573): the child carries a COPY of the
// parent's captures/rep_counters — values equal, backing independent
// in both directions (mutate parent → child unchanged, mutate child →
// parent unchanged). Empty parent → empty dup (the pre-flip rows above
// stay byte-for-byte). Driven directly, the dedup-test precedent.
@test fn add_thread_dup_independence() void = {
let caps: []capture = [];
append(caps, capture {
content = "ab", start = 1, start_bytesize = 1,
end = 2, end_bytesize = 2,
});
append(caps, capture {
content = "c", start = 3, start_bytesize = 3,
end = 4, end_bytesize = 4,
});
let reps: []size = [];
append(reps, (5: size));
append(reps, (6: size));
let ts: []thread = [];
append(ts, thread { pc = 0, start_idx = 1, captures = caps,
rep_counters = reps, ... });
let r: (void | nomem) = add_thread(&ts, 0, 9);
if (!(r is void)) { fail(); };
if (len(ts) != 2) { fail(); };
// dup carried the parent's values
if (ts[1].captures.len != 2) { fail(); };
if (strings.compare(ts[1].captures[0].content, "ab") != 0) { fail(); };
if (ts[1].captures[0].start != (1: size)) { fail(); };
if (ts[1].captures[1].end != (4: size)) { fail(); };
if (ts[1].rep_counters.len != 2) { fail(); };
if (ts[1].rep_counters[0] != (5: size)) { fail(); };
if (ts[1].rep_counters[1] != (6: size)) { fail(); };
// independence, parent → child: mutate the parent post-add
ts[0].captures[0].start = 100;
ts[0].captures[0].content = "zz";
ts[0].rep_counters[0] = 77;
if (ts[1].captures[0].start != (1: size)) { fail(); };
if (strings.compare(ts[1].captures[0].content, "ab") != 0) { fail(); };
if (ts[1].rep_counters[0] != (5: size)) { fail(); };
// independence, child → parent
ts[1].captures[1].end = 200;
ts[1].rep_counters[1] = 88;
if (ts[0].captures[1].end != (4: size)) { fail(); };
if (ts[0].rep_counters[1] != (6: size)) { fail(); };
// empty parent → empty dup
let ts2: []thread = [];
append(ts2, thread { pc = 0, ... });
let r2: (void | nomem) = add_thread(&ts2, 0, 3);
if (!(r2 is void)) { fail(); };
if (ts2[1].captures.len != 0) { fail(); };
if (ts2[1].rep_counters.len != 0) { 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
@@ -1744,5 +1797,6 @@ export fn main() i32 = {
signalled = 34; fold4_compile_errors();
signalled = 35; fold4_find_cases();
signalled = 36; fold4_findall();
signalled = 37; add_thread_dup_independence();
return 0;
};