Skip to content

Commit 3e3bf0d

Browse files
committed
fix(gc): deserializer composite-rebuild rooting hole + TAP pin follow-through
Smell-audited: 2: the gc_torture failure A/B-bisected to the D-563(b) commit, but the ROOT CAUSE is a pre-existing latent hole my change was first to mass-exercise: serialize.readValue's composite arms (list / vector / array_map / hash_set) grow `out` (+ in-flight key / cons buf) as Zig LOCALS across per-element readValue/assoc/conj allocs — an alloc-torture collect in the window sweeps the in-progress structure (recycled ArrayMap count → the "index 16, len 16" trace walk panic). Per-def meta constants (D-563b) made every lazy-ns bytecode replay deserialize composites, exposing it at 6/10 under CLJW_GC_TORTURE_ALLOC=1. Fix = the established fabrication-region bracket (ADR-0150) on all four arms: 0/12 post-fix, full gc_torture suite green. Composite constants are small — deferring the collect to the bracket end is bounded. Also: phase15_clojure_test_tap pins updated for the D-563(b) ` (file:line)` suffix (clj's TAP emits it too — oracle-checked; deftest-var-sourced per the narrowed AD-041).
1 parent 2828448 commit 3e3bf0d

2 files changed

Lines changed: 23 additions & 5 deletions

File tree

‎src/eval/bytecode/serialize.zig‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,13 @@ fn readValue(ctx: *ReadCtx, r: *ByteReader) DeserializeError!Value {
551551
// primitive at runtime.
552552
const buf = std.heap.page_allocator.alloc(Value, n) catch return DeserializeError.OutOfMemory;
553553
defer std.heap.page_allocator.free(buf);
554+
// Fabrication region: `buf` elements + the growing `lst` are Zig
555+
// locals across the nested readValue/cons allocs — a collect in
556+
// the window would sweep the in-progress structure (the D-563b
557+
// json_nested alloc-torture crash class). Composite constants are
558+
// small; deferring the collect to the bracket end is safe.
559+
rt.gc.enterFabrication();
560+
defer rt.gc.exitFabrication();
554561
var i: u32 = 0;
555562
while (i < n) : (i += 1) buf[i] = try readValue(ctx, r);
556563
var lst: Value = .nil_val;
@@ -563,6 +570,10 @@ fn readValue(ctx: *ReadCtx, r: *ByteReader) DeserializeError!Value {
563570
},
564571
.vector => {
565572
const n = try r.readU32();
573+
// Fabrication region: see the .list arm — `out` is unrooted
574+
// across the per-element readValue/conj allocs.
575+
rt.gc.enterFabrication();
576+
defer rt.gc.exitFabrication();
566577
var out = vector_mod.empty();
567578
var i: u32 = 0;
568579
while (i < n) : (i += 1) {
@@ -573,6 +584,10 @@ fn readValue(ctx: *ReadCtx, r: *ByteReader) DeserializeError!Value {
573584
},
574585
.array_map => {
575586
const n = try r.readU32();
587+
// Fabrication region: see the .list arm — `out` + the in-flight
588+
// k are unrooted across the per-entry readValue/assoc allocs.
589+
rt.gc.enterFabrication();
590+
defer rt.gc.exitFabrication();
576591
var out = map_mod.empty();
577592
var i: u32 = 0;
578593
while (i < n) : (i += 1) {
@@ -584,6 +599,9 @@ fn readValue(ctx: *ReadCtx, r: *ByteReader) DeserializeError!Value {
584599
},
585600
.hash_set => {
586601
const n = try r.readU32();
602+
// Fabrication region: see the .list arm.
603+
rt.gc.enterFabrication();
604+
defer rt.gc.exitFabrication();
587605
var out = set_mod.empty();
588606
var i: u32 = 0;
589607
while (i < n) : (i += 1) {

‎test/e2e/phase15_clojure_test_tap.sh‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,18 +32,18 @@ EOF
3232
# run-tests emits the full lifecycle event set — :begin-test-ns, :begin/:end-test-var
3333
# (per test), :end-test-ns — which tap's `:default` renders as `# {…}` comment
3434
# lines, EXACTLY as clj's tap does (clj-verified 2026-06-21). cljw omits only the
35-
# ` (file:line)` source suffix on ok/not-ok and the ns identity-hash in the
36-
# begin/end-test-ns map (no source location / JVM identity — AD-041 / AD-002);
37-
# every other byte matches clj. Names render via testing-vars-str `(test-name)`;
35+
# the ns identity-hash in the begin/end-test-ns map (no JVM identity —
36+
# AD-002); the ok/not-ok ` (file:line)` suffix is PRESENT since D-563(b)
37+
# (deftest-var-sourced; clj's is assertion-stack-sourced — narrowed AD-041). Names render via testing-vars-str `(test-name)`;
3838
# `=` actual is the bare (not (= …)) form (D-463).
3939
want='# {:type :begin-test-ns, :ns user}
4040
# {:type :begin-test-var, :var #'\''user/pass-t}
41-
ok (pass-t)
41+
ok (pass-t) (<stdin>:2)
4242
# expected:(= 1 1)
4343
# actual:(= 1 1)
4444
# {:type :end-test-var, :var #'\''user/pass-t}
4545
# {:type :begin-test-var, :var #'\''user/fail-t}
46-
not ok (fail-t)
46+
not ok (fail-t) (<stdin>:3)
4747
# expected:(= 1 2)
4848
# actual:(not (= 1 2))
4949
# {:type :end-test-var, :var #'\''user/fail-t}

0 commit comments

Comments
 (0)