Skip to content

gc: layout_transfer still probes only TYPED_LAYOUTS — shape-keyed objects lose GC_OBJ_TYPED_LAYOUT_INTACT on relocation (#6893 follow-up) #6964

Description

@proggeramlug

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions