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
An array that takes an own named property records nothing in its header that the inline push tier can test — GC_ARRAY_NAMED_PROPS is only set when a reserve is created, OBJ_FLAG_ARRAY_DESCRIPTORS gates the fallback table, so const a = [1]; a.push = fn arms neither #11021
The reusable fact. Every other receiver kind has a cheap absence proof, which is why the #10943 own-override guard costs +8.000 instructions on a hot m.get(k): the exotic kinds share one installed-flag, and zero means "nothing anywhere has ever put a named property on a non-ordinary cell". An array has two flags that mean other things and the common spelling arms neither:
GC_ARRAY_NAMED_PROPS (0x100) is set only when a reserve is created (array/named_props.rs:653);
OBJ_FLAG_ARRAY_DESCRIPTORS (0x400) gates the fallback table, and named_props::fallback_possible requires the bit CLEAR and the flag SET — they are alternatives, not a pair.
For const a = [1]; a.push = fnneither is set. Verified by running it: with a header-bit test in the push lowering the own method is never reached, and with the test removed entirely — routing the whole non-inline path through an own-override helper — the program still prints 2, because the receiver passes the admission mask and takes the inline store. The mask never sees the receiver.
Consequence.push cannot be guarded cheaply by any means available today:
main
with a guard
a.push(i) hot
47.354
141.357 (+94.00)
a.indexOf(x) hot
701.002
708.002 (+7.00)
m.get(k) hot
235.484
243.484 (+8.00)
element read — control
16.048
16.048
indexOf is a call on both arms, so it pays only the guard. push pays +94 because a diamond moves the lowering off the inline tier onto the tier whose value is a js_array_length call — 35.9% of the guarded profile, absent from main's. So #10958 ships with push out of the gate and arr.push(x) with an own push still running the builtin, exactly as on main.
Demonstrated red on main, not asserted (a022cf2e4, one program, both rows):
#10958 turns the second green and leaves the first as it is.
Keep this sentence, because it is what protects the design from a future cleanup:
The diamond is not heavy-handed — it is the only instrument that can see a receiver the flags cannot describe.
Without it someone optimises the diamond away later on cost grounds and silently restores the bug, having never learned that the flags cannot substitute for it.
Two facts that make the work scopeable
1. Five emission tiers.js_array_push_f64_spec is called from five places in crates/perry-codegen/src/expr/array_push.rs (≈630 for a global receiver, ≈1000, ≈1016, ≈1207, and the local tail), each with its own exit. Any routing fix has to cover all five, and the global-receiver tier at ≈630 is the one the differential's rows actually use — wiring only the inline tier leaves them red.
2. The ABI blocks the other route.js_array_push_f64_spec returns *mut ArrayHeader, and the expression's value is computed separately from the array's length afterwards (array_push.rs:344). So a receiver that merely fails the admission mask takes the slow path and still runs the builtin: the bail cannot carry an own method's return value. A working fix needs an exit whose value is the method's return — a phi between the length and the helper's result — at each of the five tiers.
What a fix looks like
Give an array the same shape of absence proof the exotic kinds have: a flag (global, or a header bit armed by every named-property install path, including the fallback table and the descriptor route) that the inline push tier can test in the instructions it already spends on _reserved. Then push re-enters the gate at the cost indexOf pays.
Discriminators, ready to use
These are the rows that pin the arm that does not exist yet. They are kept here rather than in test-files/test_parity_own_override_beats_builtin.ts, because a red row in a file whose contract is byte-identity with node is a broken gate rather than documentation. Node's answers are given; they were run.
constp1=[1];p1.push=(x)=>"own:"+x;p1.push(9)// node: own:9 — the value must be the METHOD's return,p1.length// node: 1 not a recomputed length, and no appendconstp4=[];letseen=0;p4.push=(x)=>{seen=x;return0;};p4.push(7);// value discarded, but the method must still runseen+"/"+p4.length// node: 7/0
Two more, which pass today and are in the parity fixture, because they are what the arm must NOT break:
Activity
docs: point the push-out-of-the-gate notes at #11021
docs: point the push-out-of-the-gate notes at #11021
docs: point the push-out-of-the-gate notes at #11021
docs: point the push-out-of-the-gate notes at #11021
changelog: key the #11021 fragment on PR 11333
perf: keep the own-push entry thin and its exit cold (#11021)
fix: an own `push` beats Array.prototype.push on a proven array (#11021)
changelog: key the #11021 fragment on PR 11333
perf: keep the own-push entry thin and its exit cold (#11021)
fix: an own `push` beats Array.prototype.push on a proven array (#11021)
changelog: key the #11021 fragment on PR 11333
perf: keep the own-push entry thin and its exit cold (#11021)