You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
PERRY_DELETE_SHAPE_TRANSITION=0 is unsound now that the property ICs no longer test slots for TAG_HOLE — retire the kill switch (#10826 own note asked for it; the store tower now depends on it too) #10879
Where.crates/perry-runtime/src/object/delete_rest.rs:1567-1590 still reads PERRY_DELETE_SHAPE_TRANSITION (object_delete_shape_transition_enabled); =0|off|false restores #9064's id-preserving delete publish, with a #[cfg(test)] override cell (DELETE_TRANSITION_TEST_OVERRIDE) beside it.
Why it is no longer a knob.#10826 made every successful delete a shape transition, and on that argument the read tower dropped its per-read TAG_HOLE compare (crates/perry-codegen/src/expr/property_get/generic_dispatch.rs, the comment above val_hit). That comment says, in so many words:
#10826 keeps PERRY_DELETE_SHAPE_TRANSITION=0 as a kill switch that restores the id-preserving publish. With this compare gone that switch is no longer a performance knob: under it a shape-gated hit CAN address a deleted slot and return the raw hole word. It must be retired with that PR, not kept.
It was not retired. On main today (train 250, v0.5.1629), under the switch a compact-word read hit primed before a delete still matches after it and returns the raw hole word.
The store tower now depends on it as well. The property-store lane (branch perf/property-store, PR to follow) removes the static write PIC's OBJ_FLAG_STABLE_TOMBSTONES bit test and the TAG_HOLE slot validation behind it from the hit path, on the same argument: a ShapeId hit (compact word or way token) proves the slot it names is live. Under the switch, a shape-gated STORE hit then writes into a deleted slot — the key reads back through both ICs while Object.keys / JSON.stringify omit it. Fixture: delete o.a; put(o, 7) through a primed static store site, then Object.keys(o); the output under the switch against node will be attached when that stage is measured.
Ask. Delete the env read and the test override cell. PERRY_OBJECT_TOMBSTONES is a different decision (O(1) delete vs compaction) and stays. Any A/B of the transition must now be two binaries, not one switch — the "A/B in ONE binary" rationale in the doc comment is exactly what became unsound.
Not this lane's file (delete_rest.rs belongs to the delete-transition lane), so filed rather than fixed.
Demonstrated on main (train 250, v0.5.1629), read side, one binary two switches.
Fixture (du.ts; the read must be called INDIRECTLY, const FN=[rd]; FN[0](o), because a direct call is inlined at every call site and each copy gets its own IC, so the site primed in the loop is never the one that reads after the delete — that cost three fixture iterations):
constO={a:1,b:2,c:3,d:4};constSINK=[O];functionrd(o){returno.a;}constFN=[rd];functionrun(n){leth=0;for(letk=0;k<n;k++){h+=FN[0](SINK[0])&1;}returnh;}constout=[];out.push(run(N));deleteO.d;// shared small keys array is forked (owned) and takes the tombstone laneout.push(JSON.stringify(Object.keys(O)));out.push(run(N));// re-prime the site on the owned shapedeleteO.a;// owned: tombstone lane; id-preserving under the switchconstv=FN[0](O);out.push(typeofv,String(v),('a'inO)?'in':'out',JSON.stringify(Object.keys(O)));O.a=5;out.push(FN[0](O),JSON.stringify(Object.keys(O)));console.log(out.join('|'));
String(v) is NaN (the raw TAG_HOLE word read as a double) and the read after the re-add prints empty: the compact-word read hit primed before the delete still matches after it and addresses the deleted slot. typeof happens to say undefined because the hole word's tag classifies that way; the value is not undefined.
The store twin (dt.ts, same shape with o.a=k through the indirect site) is still correct on main under the switch because the static write PIC's OBJ_FLAG_STABLE_TOMBSTONES slot validation is intact there; the store lane removes it on the same argument as the read tower, at which point this fixture goes wrong under the switch too (result to follow from that lane's measurement).
Evidence from PR #10968, which takes the same compare off the polymorphic way path and therefore shares this premise: I tried to make the switch produce a wrong answer from compiled code and could not.
test-files/test_parity_delete_shape_transition.ts compiled by both arms and run with PERRY_DELETE_SHAPE_TRANSITION=0: every line identical to node, on both.
A purpose-built fixture — five shapes resident at ONE site (the MRU entry plus the four ways), delete the read key off two of the receivers, read back through the SAME site, re-add — run under the switch on the arm that has no hole compare left on either the MRU or the way path: still identical to node.
So the body's "under the switch a compact-word read hit primed before a delete still matches after it and returns the raw hole word" is argued, not reproduced, and I filed it that way. A plausible reason it does not bite: under the switch, publish_object_shape_holes (crates/perry-runtime/src/object/shapes_slot_list.rs:634) first tries try_update_stable_tombstone_shape, and when that declines it falls through to SHAPE_SEMANTIC_NEXT.fetch_add(1) and mints a fresh generation anyway — so the id moves even with the switch off for every receiver that fails the stable-tombstone admission.
The ask is unchanged, and if anything the reason is sharper: the switch's own tests (tombstone_tests.rs:842) assert it restores #9064, no compiled-code test drives it at all, and what it actually restores today is "id-preserving, but only for receivers that pass an admission check three files away". Retire it — but it should not be sold on a hazard nobody has exhibited. If the store lane's delete o.a; put(o, 7) fixture does exhibit one, that output belongs in this issue.
Activity
proggeramlug commentedon Sep 21, 2026
Demonstrated on main (train 250, v0.5.1629), read side, one binary two switches.
Fixture (
du.ts; the read must be called INDIRECTLY,const FN=[rd]; FN[0](o), because a direct call is inlined at every call site and each copy gets its own IC, so the site primed in the loop is never the one that reads after the delete — that cost three fixture iterations):3000|["a","b","c"]|3000|undefined|undefined|out|["b","c"]|5|["b","c","a"]PERRY_DELETE_SHAPE_TRANSITION=03000|["a","b","c"]|3000|undefined|NaN|out|["b","c"]||["b","c","a"]String(v)isNaN(the rawTAG_HOLEword read as a double) and the read after the re-add prints empty: the compact-word read hit primed before the delete still matches after it and addresses the deleted slot.typeofhappens to sayundefinedbecause the hole word's tag classifies that way; the value is notundefined.The store twin (
dt.ts, same shape witho.a=kthrough the indirect site) is still correct on main under the switch because the static write PIC'sOBJ_FLAG_STABLE_TOMBSTONESslot validation is intact there; the store lane removes it on the same argument as the read tower, at which point this fixture goes wrong under the switch too (result to follow from that lane's measurement).proggeramlug commentedon Sep 22, 2026
Evidence from PR #10968, which takes the same compare off the polymorphic way path and therefore shares this premise: I tried to make the switch produce a wrong answer from compiled code and could not.
test-files/test_parity_delete_shape_transition.tscompiled by both arms and run withPERRY_DELETE_SHAPE_TRANSITION=0: every line identical to node, on both.deletethe read key off two of the receivers, read back through the SAME site, re-add — run under the switch on the arm that has no hole compare left on either the MRU or the way path: still identical to node.So the body's "under the switch a compact-word read hit primed before a
deletestill matches after it and returns the raw hole word" is argued, not reproduced, and I filed it that way. A plausible reason it does not bite: under the switch,publish_object_shape_holes(crates/perry-runtime/src/object/shapes_slot_list.rs:634) first triestry_update_stable_tombstone_shape, and when that declines it falls through toSHAPE_SEMANTIC_NEXT.fetch_add(1)and mints a fresh generation anyway — so the id moves even with the switch off for every receiver that fails the stable-tombstone admission.The ask is unchanged, and if anything the reason is sharper: the switch's own tests (
tombstone_tests.rs:842) assert it restores #9064, no compiled-code test drives it at all, and what it actually restores today is "id-preserving, but only for receivers that pass an admission check three files away". Retire it — but it should not be sold on a hazard nobody has exhibited. If the store lane'sdelete o.a; put(o, 7)fixture does exhibit one, that output belongs in this issue.