Summary
layout_transfer (crates/perry-runtime/src/gc/layout.rs:1119) still resolves typed layouts only through the per-object TYPED_LAYOUTS map. #6893 moved the canonical TypedLayoutDescriptor for objects carrying a keys_array into the shape-keyed SHAPE_LAYOUTS map and deletes the per-object entry — so for every class instance, layout_transfer's lookup misses:
let new_has_typed = TYPED_LAYOUTS.with(|m| {
let mut typed = m.borrow_mut();
typed.remove(&(new_user as usize));
if let Some(layout) = typed.remove(&(old_user as usize)) {
typed.insert(new_user as usize, layout);
true
} else {
false // <-- always taken for shape-keyed objects
}
});
...
if new_has_typed {
header_set_typed_layout_intact(new_header);
} else {
header_clear_typed_layout_intact(new_header); // <-- clears a bit that is still valid
}
The relocated copy loses GC_OBJ_TYPED_LAYOUT_INTACT even though its shape-keyed descriptor is still live and still correct.
This is the same omission class as #6963 (which fixed the four query helpers). layout_transfer was deliberately left out of that PR's scope because changing relocation behaviour warrants a gap sweep.
Severity: latent, not live — but primed, and it lands on the critical path
Not a correctness bug. Clearing INTACT is the conservative direction: it means "don't trust the typed layout", which deopts rather than corrupts.
It is also not currently reachable, and the reason matters. The three callers:
| caller |
reachable today? |
gc/copying.rs:544 |
evacuation — no (see #6950) |
gc/oldgen.rs:1804 |
nursery→old-gen evacuation — no (#6950) |
gc/oldgen.rs:1908 |
old-page compaction — no (#6950) |
array/push_pop.rs:99 |
array growth — yes, but GC_TYPE_ARRAY, which does not carry a shape-keyed typed layout |
So every path that would expose this is exactly the evacuation machinery that #6950 measured inert (45 cycles, all manual full mark-sweeps, total_moved_objects: 0 under PERRY_GC_FORCE_EVACUATE=1).
That is the point of filing this now. The moment #6950 makes a precise-rooted evacuating minor reachable, this fires on every tenured class instance: each one silently loses its typed layout, and its typed guards deopt permanently. The symptom would be a diffuse performance regression appearing at the same time as a GC change, with no correctness signal to point at it — the hardest possible thing to attribute after the fact.
Fix sketch
Mirror the #6963 pattern: consult the shape-keyed half as well, and set INTACT when either half resolves a descriptor. The per-object half must stay ungated so the forged-intact-bit check survives (as #6963 established).
Acceptance
Found while root-causing #6957; re-scoped out of #6963.
Summary
layout_transfer(crates/perry-runtime/src/gc/layout.rs:1119) still resolves typed layouts only through the per-objectTYPED_LAYOUTSmap. #6893 moved the canonicalTypedLayoutDescriptorfor objects carrying akeys_arrayinto the shape-keyedSHAPE_LAYOUTSmap and deletes the per-object entry — so for every class instance,layout_transfer's lookup misses:The relocated copy loses
GC_OBJ_TYPED_LAYOUT_INTACTeven though its shape-keyed descriptor is still live and still correct.This is the same omission class as #6963 (which fixed the four query helpers).
layout_transferwas deliberately left out of that PR's scope because changing relocation behaviour warrants a gap sweep.Severity: latent, not live — but primed, and it lands on the critical path
Not a correctness bug. Clearing
INTACTis the conservative direction: it means "don't trust the typed layout", which deopts rather than corrupts.It is also not currently reachable, and the reason matters. The three callers:
gc/copying.rs:544gc/oldgen.rs:1804gc/oldgen.rs:1908array/push_pop.rs:99GC_TYPE_ARRAY, which does not carry a shape-keyed typed layoutSo every path that would expose this is exactly the evacuation machinery that #6950 measured inert (45 cycles, all
manualfull mark-sweeps,total_moved_objects: 0underPERRY_GC_FORCE_EVACUATE=1).That is the point of filing this now. The moment #6950 makes a precise-rooted evacuating minor reachable, this fires on every tenured class instance: each one silently loses its typed layout, and its typed guards deopt permanently. The symptom would be a diffuse performance regression appearing at the same time as a GC change, with no correctness signal to point at it — the hardest possible thing to attribute after the fact.
Fix sketch
Mirror the #6963 pattern: consult the shape-keyed half as well, and set
INTACTwhen either half resolves a descriptor. The per-object half must stay ungated so the forged-intact-bit check survives (as #6963 established).Acceptance
gc/tests/layout_trace.rsthat relocates a shape-keyed object (one with akeys_array, i.e. a class instance — notjs_object_alloc, which is class 0 and takes the surviving per-object path) and assertsINTACTis preserved. The absence of exactly this coverage is why TYPED_LAYOUTS stores per-class-constant layout masks per OBJECT — O(objects) memory (272MB on churn bench) + hashmap insert on everynew#6893 merged green.Found while root-causing #6957; re-scoped out of #6963.