Skip to content

fix(runtime): the shape is the one record of [[Prototype]] (null links, function links) - #13666

Merged
proggeramlug merged 5 commits into
mainfrom
gpp3/prototype-shape-authority
Nov 1, 2026
Merged

proggeramlug merged 5 commits into
mainfrom
gpp3/prototype-shape-authority

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

The shape becomes the one record of [[Prototype]]: for a null link (part 1) and for a function's link (part 2). This follows #13648, which made a callable's inherited read one OrdinaryGet.

Part 1: a null [[Prototype]] is the shape's identity, not a header bit

A null prototype was recorded twice. The shape's identity carried it, and so did OBJ_FLAG_NULL_PROTO, a header bit set at birth. The identity was derived from that bit, and about 20 readers tested the bit directly. The bit stuck across setPrototypeOf, a later null link never set it, and the declared-keys family require.cache (tagged null) was registered with its class's identity.

Now:

  • A born-null birth publishes its first shape with the null identity.
  • A family tagged null gets a null-kind layout word from the codegen and a null-kind identity from registration.
  • A module namespace links to null through the prototype funnel.
  • Every reader asks object_prototype_is_null / cell_prototype_is_null.
  • The bit, its store-lane gates and its realm-image header bit are gone; 0x40 is free.

A null-chain store answers "nothing intercepts" without walking the chain. The JSON paths ask object_static_prototype_is_linked, so a null chain reads like the default chain. A property bag's holder slot, which holds its owner's [[Prototype]], is never given the bag's own null.

Bugs fixed on main (test test_gap_null_prototype_shape_authority, test_gap_null_prototype_require_cache):

  • a thenable on a relinked Object.create(null) object was not followed;
  • util.inspect called an object relinked to Object.prototype "null prototype", and missed setPrototypeOf(x, null);
  • require.cache inherited Object.prototype's methods.

Part 2: a function's [[Prototype]] is its shape's identity

Object.setPrototypeOf(fn, p) moved the function to the shared FunctionDictionary shape and stored the link as a key in its state record. Now the function moves to the Function shape of its own keys, with the linked identity. That is the class-less rule an ordinary object takes; the prototype gets its stable serial first. The record holds the link only for a FunctionDictionary function.

The readers follow the one link:

  • closure_static_prototype / object_static_prototype: a function answers from its own link, never from its bag's record;
  • the layout probe;
  • instanceof C, instanceof Function and @@hasInstance on the linked chain;
  • constructor and the members of a null link.

Stores onto a bound function take the function store lane, which checks the chain. A function's base identity comes from its body kind, without a record read.

Bugs fixed on main (tests test_gap_function_link_on_shape, test_gap_function_first_inherited_read, test_gap_prototype_link_fast_paths):

  • fn instanceof C and fn instanceof Function ignored the link;
  • a function linked to null read toString / constructor from Function.prototype;
  • @@hasInstance on the linked chain was ignored;
  • a program's first fn.toString read answered undefined.

Verification (qb9)

  • Gap subset (268 tests): part 1 = base + its 2 new tests; parts 1+2 = part 1 + its 3 new tests; identical after merging main.
  • Unit tests:
    • cargo test -p perry-runtime --lib (RUST_TEST_THREADS=1): 6393/0, same as main.
    • perry-codegen realm 35/35, layout 119/119.
    • perry-abi: layouts_and_native_identity_are_pinned fails, as it does on main.
  • Startup, irdiff with link pad (Ir):
witness main → part 1 part 1 → part 2
hello 0 0
commander --help −152,702 −29,646
commander −152,732 −29,997
fastify inject −244,884 −191,231
AWFY richards −290,750 −78,270
binary_trees −148,498 −24,761
  • Synthetic, Ir per op on a callable class instance (part 1 → part 2):
    • inherited read: 1,933 → 1,638;
    • getter: 2,842 → 2,548;
    • call: 3,695 → 3,222;
    • store: 40,456 → 38,482;
    • ark.js: 4.04 G → 3.94 G.
  • Sabotage: each new fast path broken on purpose must fail a gap test. 7 of 8 are caught. The eighth, the null-chain store answer matched to every link, is unreachable: every receiver form tried answers through its layout proof first.
  • Supabase CLI --version/--help: still running; I'll post the results in a comment.

Draft: hold for review.

Ralph Küpper added 3 commits November 1, 2026 14:34
…der bit

The link had two records: the shape's identity and OBJ_FLAG_NULL_PROTO, a
header bit set at birth that the identity was derived from and ~20 readers
tested directly. The bit was sticky across setPrototypeOf, a later null link
never set it, and a declared-keys class family tagged null (require.cache)
was registered with its class's identity while its header said null. So
readers disagreed: a thenable on a relinked born-null object was not
followed, util.inspect called a relinked object "null prototype" and missed
setPrototypeOf(x, null), and require.cache inherited Object.prototype's
methods.

The shape is now the one record. A born-null birth publishes its first shape
with the null identity passed in; a layout tagged null registers the null
identity for class families too; a module namespace takes its null link
through the prototype funnel. Every reader asks object_prototype_is_null /
cell_prototype_is_null (the recorded prototype, else the shape), which answer
TAG_NULL for any null link. The bit, its store-lane gates and its realm-image
header word are gone; 0x40 is free.
Object.setPrototypeOf(fn, p) wrote the link as a "p" key in the function's
internal state record and moved the function to the shared FunctionDictionary
shape. Every read of a relinked function (ArkType's callable class instances)
then searched that record by key bytes and lost every shape fast path, and
the readers that never looked there answered from Function.prototype.

A described function's shape already names its [[Prototype]] (its body
kind's intrinsic, by reserved serial, now derived from the body kind without
a record read). A link now moves it to the Function shape of its own keys
with the linked identity, the class-less rule an ordinary receiver takes (the
prototype gets its stable serial first); a key add carries the identity its
current shape names. The record keeps the link only for a FunctionDictionary
function, which answers nothing about it ("ask the object"), and a described
link moves into the record when the function becomes a dictionary. The bag
stays a null-chain slot store, so no own-property writer changes meaning.

The readers follow the one link: closure_static_prototype and
object_static_prototype (a function answers from its own link, never its
property bag's record), the layout probe, instanceof (a linked function's
chain is walked for `C` and for `Function`, and its @@hasInstance is read
from that chain when a hop may own a symbol key), `constructor` and a null
link's members (undefined, not Function.prototype's). Stores onto a bound
function take the function store lane, which checks the chain.

Also: a program's first inherited read of a function (`fn.toString`)
answered undefined because %Function.prototype% was not built yet; the read
now builds the realm global that holds it.
@coderabbitai

coderabbitai Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Caution

Review failed

The pull request is closed.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

Ralph Küpper added 2 commits November 1, 2026 15:37
…authority

# Conflicts:
#	crates/perry-runtime/src/object/instanceof/static_dispatch.rs
@proggeramlug

Copy link
Copy Markdown
Contributor Author

Head 7766f51 merges main, including #13546. In the instanceof Function conflict I took main's version. Its closure_static_prototype check reads the function's shape link on this branch, and it also walks a relinked class constructor's chain. I also updated the has_recorded_prototype doc for the new home of a function's link.

Verification of 7766f51 (qb9):

  • Startup, irdiff main eea3ac6 → head (link pad, 6/6 converged), Ir:
witness delta
hello 0
commander --help −182,656
commander −182,729
fastify inject −436,087
AWFY richards −369,032
binary_trees −173,259
command base head Δ
--version 7,003.86 M 6,964.62 M −39.2 M (−0.56%)
--help 7,083.86 M 7,044.64 M −39.2 M (−0.55%)

@proggeramlug
proggeramlug marked this pull request as ready for review November 1, 2026 16:04
@proggeramlug
proggeramlug merged commit 69bd0cf into main Nov 1, 2026
97 of 103 checks passed
@proggeramlug
proggeramlug deleted the gpp3/prototype-shape-authority branch November 1, 2026 16:07
proggeramlug pushed a commit that referenced this pull request Nov 2, 2026
Since #13666 a null [[Prototype]] is the shape's identity, not header bit 0x40; the derivation emits no such bit and three producer tests still pinned it. They now assert the birth facts: null_prototype and the absent prototype with the header word of the ordinary birth of the same members, callable as the layout's header bit, the frozen word from the ABI constants. The GcHeader bit table lists bit 6 as free.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant