-
-
Notifications
You must be signed in to change notification settings - Fork 161
fix(gc): tombstone shape publish arms old_carrier — deleted receivers no longer lose their keys array under evacuating GC (#9200) #9317
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
proggeramlug
merged 2 commits into
PerryTS:main
from
proggeramlug:fix/9200-tombstone-evac
Aug 31, 2026
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| ### Fixed | ||
|
|
||
| - **A tombstone delete on a promoted receiver no longer loses the object's | ||
| keys array under an evacuating collection** (#9200). On | ||
| `test_gap_repsel_pshape_tower_delete.ts` with `PERRY_OBJECT_TOMBSTONES=1 | ||
| PERRY_GC_HEAP_LIMIT=8 PERRY_GC_FORCE_EVACUATE=1`, a deleted receiver came | ||
| out of the churn with `Object.keys()` empty and a previously-live field | ||
| reading `NaN` — silently, exit 0, stderr empty. This is the corruption that | ||
| forced #9038's default-on tombstone deletes back to opt-in in #9212; the | ||
| default flip is deliberately NOT part of this change. | ||
|
|
||
| The mechanism, confirmed by tracing the descriptor lifecycle on the fixture | ||
| (not by code reading — one earlier confident hypothesis had already failed | ||
| against it): | ||
|
|
||
| 1. The delete's clone fork published an intermediate descriptor for the | ||
| receiver's freshly cloned, nursery-young keys array and correctly armed | ||
| its `old_carrier` gate (`set_object_keys_array_with_live` has done that | ||
| since #8256 — the receiver had been promoted by the pre-delete churn, so | ||
| no minor would ever enumerate it again). | ||
| 2. `publish_object_shape_holes` then minted the hole successor with | ||
| `old_carrier: false`, stamped it, and **retired the armed intermediate** | ||
| in its keys-address sweep (the #9064 descriptor-pile-up fix removes | ||
| every other id under the owned keys address). Net effect: an old | ||
| receiver stamped with an unarmed descriptor naming a young keys array. | ||
| 3. The next minor walks a non-carrier record metadata-only | ||
| (`scan_shape_table_rekey_mut`) and never enumerates the old receiver, | ||
| so the keys array had **no root at all**: it was swept while live, | ||
| `prune_dead_shape_keys` dropped the descriptor as dead, and the | ||
| receiver's stamp dangled. `object_keys_array()` resolves through the | ||
| descriptor (#8047), so the receiver was shapeless from then on. | ||
| `PERRY_GC_VERIFY_EVACUATION` cannot see any of this — the only edge | ||
| lives in table metadata, and it is gone by sweep. | ||
|
|
||
| The fix is structural, not a patched call site: | ||
| `shapes::stamp_object_shape_id_with_carrier_note` is now the one post-birth | ||
| publication point for a ShapeId into a receiver's header word — it stamps | ||
| and, for any receiver outside the nursery, arms the descriptor's | ||
| old-carrier gate in the same breath, mirroring the note | ||
| `visit_gc_layout_slot_descriptors` makes at trace time. Every post-birth | ||
| publish routes through it: `publish_object_shape_holes` (the bug), | ||
| `try_update_stable_tombstone_shape`, `publish_object_shape_from`, | ||
| `stamp_object_shape`, `birth_stamp_object_shape`, | ||
| `transition_object_shape_semantics`, `transition_object_shape_to_class`, | ||
| the reserved-floor stamp, and the plain cached-shape install (which | ||
| hand-rolled the same note; the cache-carried install keeps its documented | ||
| skip — `cache_carrier` is the stronger registration). The arming | ||
| `set_object_keys_array_with_live` carried at its tail moved into the | ||
| funnel. Over-arming costs one rooted record for at most one full trace — | ||
| the epoch contract `old_carrier` already lives by (#8112). | ||
|
|
||
| Affected files: | ||
|
|
||
| - `crates/perry-runtime/src/object/shapes.rs` — the funnel, and the | ||
| stamp sites in it. | ||
| - `crates/perry-runtime/src/object/shapes_slot_list.rs` — | ||
| `publish_object_shape_holes` / `try_update_stable_tombstone_shape` stamp | ||
| through the funnel. | ||
| - `crates/perry-runtime/src/object/mod.rs` — tail arming folded into the | ||
| funnel. | ||
| - `crates/perry-runtime/src/object/reserved_floor.rs` — floor stamp through | ||
| the funnel. | ||
|
|
||
| Validation: the tower fixture flag-on is byte-identical to node 26.5.1 | ||
| 5/5 runs on `HEAP_LIMIT=8 + FORCE_EVACUATE`, 5/5 on the tighter | ||
| `HEAP_LIMIT=4`, and 5/5 with `VERIFY_EVACUATION` added; the trace shows the | ||
| hole descriptors as carriers with their keys arrays rewritten (evacuated | ||
| live) instead of pruned. Flag-off is byte-identical to node before and | ||
| after. `gc_repsel_matrix.sh --arms force_verify --filter tower_delete` with | ||
| the flag exported: PASS with the arm live (moved-objects 1/1, copy-minor | ||
| 1/1). New witnesses: `test_gap_repsel_pshape_tombstone_oldgen_delete.ts` | ||
| (the minimized non-tower trigger, registered in the corpus) and a unit pin | ||
| (`tombstone_publish_on_untraced_receiver_arms_old_carrier`) that fails on | ||
| the unfixed runtime and passes with the funnel. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| // #9200 discriminator. The original fixture reads rows only through the | ||
| // cross-module dispatch tower (`pickAll`), so a wrong answer cannot be | ||
| // attributed to the DATA vs the TOWER. This reads every row BOTH ways at every | ||
| // stage: directly (`r.a`, `r.c`, Object.keys) and through the tower. | ||
| // | ||
| // If the direct reads stay correct while the tower reads go NaN, the object is | ||
| // intact and the tower's inline keys-pointer guard is wrongly passing. | ||
| // If the direct reads ALSO break, the object itself lost its keys array. | ||
| import { Row, pickAll } from "./_helpers/repsel_pshape_tower_rows.ts"; | ||
|
|
||
| function churn(n: number): number { | ||
| const sink: { x: number }[] = []; | ||
| for (let i = 0; i < n; i++) sink.push({ x: i }); | ||
| return sink.length; | ||
| } | ||
|
|
||
| const rows: Row[] = []; | ||
| for (let i = 0; i < 4; i++) rows.push(new Row(i + 1, (i + 1) * 10, (i + 1) * 3)); | ||
|
|
||
| function report(tag: string): void { | ||
| const direct: string[] = []; | ||
| for (let i = 0; i < rows.length; i++) { | ||
| const r: any = rows[i]; | ||
| direct.push(r.a + "/" + r.c + "[" + Object.keys(r).join("") + "]"); | ||
| } | ||
| console.log(tag + " direct: " + direct.join(" ")); | ||
| console.log(tag + " tower : " + pickAll(rows).join(",")); | ||
| } | ||
|
|
||
| report("S0"); | ||
| delete (rows[1] as any).b; | ||
| report("S1-after-delete-row1"); | ||
| churn(200_000); | ||
| report("S2-after-churn"); | ||
| delete (rows[3] as any).a; | ||
| report("S3-after-delete-row3"); | ||
| churn(200_000); | ||
| report("S4-after-churn2"); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Correct the publication-path list.
try_update_stable_tombstone_shapeupdates the existing descriptor in place and can return the existing ShapeId without callingstamp_object_shape_id_with_carrier_note. Remove it from this list, or describe it as an in-place update. This keeps the changelog aligned withcrates/perry-runtime/src/object/shapes_slot_list.rsLines 260-313.🤖 Prompt for AI Agents