Skip to content

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

Description

@proggeramlug

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.

Activity

  1. proggeramlug commented on Sep 21, 2026

    @proggeramlug
    ContributorAuthor

    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):

    const O={a:1,b:2,c:3,d:4}; const SINK=[O];
    function rd(o){return o.a;} const FN=[rd];
    function run(n){let h=0;for(let k=0;k<n;k++){h+=FN[0](SINK[0])&1;}return h;}
    const out=[]; out.push(run(N));
    delete O.d;                       // shared small keys array is forked (owned) and takes the tombstone lane
    out.push(JSON.stringify(Object.keys(O)));
    out.push(run(N));                 // re-prime the site on the owned shape
    delete O.a;                       // owned: tombstone lane; id-preserving under the switch
    const v=FN[0](O);
    out.push(typeof v, String(v), ('a' in O)?'in':'out', JSON.stringify(Object.keys(O)));
    O.a=5; out.push(FN[0](O), JSON.stringify(Object.keys(O)));
    console.log(out.join('|'));
    output
    node 3000|["a","b","c"]|3000|undefined|undefined|out|["b","c"]|5|["b","c","a"]
    perry main identical to node
    perry main, PERRY_DELETE_SHAPE_TRANSITION=0 3000|["a","b","c"]|3000|undefined|NaN|out|["b","c"]||["b","c","a"]

    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).

  2. proggeramlug commented on Sep 22, 2026

    @proggeramlug
    ContributorAuthor

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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