Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions changelog.d/13666-prototype-shape-authority.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
An object's and a function's [[Prototype]] now have one record: the shape.

A null prototype was recorded twice, in the shape's identity and in a header
bit (`OBJ_FLAG_NULL_PROTO`) set at birth, and readers disagreed. Fixed: 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 `Object.setPrototypeOf(x, null)`; `require.cache`
inherited `Object.prototype`'s methods. The bit is gone (0x40 is free).

`Object.setPrototypeOf(fn, p)` moved a function to the shared
FunctionDictionary shape and kept the link as a key in its state record; the
function now stays on a described shape that names the link, as an ordinary
object does. Fixed along the way: `fn instanceof C` and `fn instanceof
Function` ignored a function's link; a function linked to `null` still read
`toString` and `constructor` from `Function.prototype`; an `@@hasInstance` on
a function's linked chain was ignored; a program's first inherited read of a
function (`fn.toString`) answered `undefined`. Stores onto a bound function
take the function store lane. An inherited read on a callable class instance
drops from 1.93k to 1.64k Ir, a call from 3.70k to 3.22k; startup Ir drops on
every witness (fastify inject -0.27%, commander -0.89%).
4 changes: 2 additions & 2 deletions crates/perry-abi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -328,8 +328,8 @@ pub const GC_OBJECT_CALLABLE: u16 = 0x0020;
/// Heap-birth facts shared by runtime allocation and realm-cell emission.
/// These are existing meanings in GcHeader::_reserved, never image markers.
pub const OBJ_FLAG_PLAIN_ORDINARY: u16 = 0x0200;
/// An object has a null [[Prototype]].
pub const OBJ_FLAG_NULL_PROTO: u16 = 0x40;
// 0x40 was `OBJ_FLAG_NULL_PROTO`: a null [[Prototype]] is the shape's
// identity (`PROTO_ID_NULL`), not a header bit. The bit is free.
/// At least one own property carries non-default descriptor attributes.
pub const OBJ_FLAG_HAS_DESCRIPTORS: u16 = 0x800;
/// An array has one front-reserve pointer to its key-attribute array.
Expand Down
12 changes: 4 additions & 8 deletions crates/perry-abi/src/realm_header.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
use crate::{
GC_CLOSURE_NON_CONSTRUCTABLE, GC_LAYOUT_ALL_POINTERS, GC_LAYOUT_POINTER_FREE,
GC_LAYOUT_SIDE_MASK, GC_OBJECT_CALLABLE, NATIVE_MODULE_CLASS_ID, OBJ_FLAG_FROZEN,
OBJ_FLAG_HAS_DESCRIPTORS, OBJ_FLAG_NEVER_MOVE, OBJ_FLAG_NO_EXTEND, OBJ_FLAG_NULL_PROTO,
OBJ_FLAG_PLAIN_ORDINARY, OBJ_FLAG_SEALED, OBJ_FLAG_TYPED_ARRAY_PROTO,
OBJ_FLAG_HAS_DESCRIPTORS, OBJ_FLAG_NEVER_MOVE, OBJ_FLAG_NO_EXTEND, OBJ_FLAG_PLAIN_ORDINARY,
OBJ_FLAG_SEALED, OBJ_FLAG_TYPED_ARRAY_PROTO,
};

/// The integrity level a birth leaves, as `Object.preventExtensions`,
Expand Down Expand Up @@ -127,7 +127,8 @@ pub enum PrototypeMark {
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ObjectBirth {
pub exotic: ObjectExotic,
/// The `[[Prototype]]` is null.
/// The `[[Prototype]]` is null: the cell is born on a layout whose
/// identity is null. It sets no header bit (the shape is the authority).
pub null_prototype: bool,
pub layout: ObjectLayout,
pub prototype_mark: PrototypeMark,
Expand Down Expand Up @@ -189,11 +190,6 @@ pub const fn object_reserved(birth: ObjectBirth) -> u16 {
ObjectExotic::Ordinary => {
integrity
| never_move
| if birth.null_prototype {
OBJ_FLAG_NULL_PROTO
} else {
0
}
| if birth.typed_array_prototype {
OBJ_FLAG_TYPED_ARRAY_PROTO
} else {
Expand Down
2 changes: 1 addition & 1 deletion crates/perry-codegen/src/codegen/realm_image/functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ pub(crate) fn emit_function(
exported: false,
class_id: 0,
layout_word: word,
reserved: perry_abi::OBJ_FLAG_NULL_PROTO | perry_abi::OBJ_FLAG_PLAIN_ORDINARY,
reserved: perry_abi::OBJ_FLAG_PLAIN_ORDINARY,
meta: None,
slots: &[prototype],
},
Expand Down
4 changes: 3 additions & 1 deletion crates/perry-codegen/src/layout_desc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -810,7 +810,9 @@ fn emit_desc(
let word = crate::layout_ordinals::descriptor_word(
desc_name,
format!("{keys:?}|{family}|{slot_count}|{class:?}"),
0,
// A family the descriptor links to null (`require.cache`) carries
// the null identity, which only a null-kind word names.
if proto == 1 { 2 } else { 0 },
true,
// Both nominal and admitted classless families carry their DPA word.
true,
Expand Down
8 changes: 2 additions & 6 deletions crates/perry-runtime/src/builtins/formatting.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1144,12 +1144,8 @@ unsafe fn format_object_as_json(
};
// Node prefixes a null-prototype object with `[Object: null prototype]`
// (e.g. `Object.create(null)`), since it has no constructor to name.
// The flag is set at allocation by `js_object_alloc_null_proto`.
let is_null_proto = {
let gc =
(obj_ptr as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader;
(*gc)._reserved & crate::gc::OBJ_FLAG_NULL_PROTO != 0
};
// The object's shape (or recorded prototype) says whether it has one.
let is_null_proto = crate::object::shapes::cell_prototype_is_null(obj_ptr as usize);
// The display prefix before the `{ … }` body: a real class/constructor
// name when present, otherwise `[Object: null prototype]` for a
// null-proto plain object, otherwise nothing. (Distinct from
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,7 @@ pub(super) fn prototype_token(value: f64) -> Option<u64> {
}

unsafe {
let gc_header =
(addr as *const u8).sub(crate::gc::GC_HEADER_SIZE) as *const crate::gc::GcHeader;
if (*gc_header)._reserved & crate::gc::OBJ_FLAG_NULL_PROTO != 0 {
if crate::object::shapes::cell_prototype_is_null(addr) {
return Some(TAG_NULL);
}
let obj = addr as *const crate::object::ObjectHeader;
Expand Down
60 changes: 58 additions & 2 deletions crates/perry-runtime/src/closure/dynamic_props.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,21 +63,41 @@ pub fn closure_set_static_prototype(closure_ptr: usize, proto_bits: u64) {
}
return;
}
unsafe { super::props::state_set_prototype(closure_ptr, proto_bits) };
// A described function names the link on its shape; a dictionary one
// (or a link with no stable identity) keeps it in its own record.
if unsafe { super::shape::closure_link_on_shape(closure_ptr, proto_bits) } {
return;
}
// Into FunctionDictionary first: it moves a link the described shape
// named into the record, which this link then replaces.
super::shape::note_function_own_state_changed(closure_ptr);
unsafe { super::props::state_set_prototype(closure_ptr, proto_bits) };
}

/// Look up the static prototype object bits recorded for a closure, if any.
pub fn closure_static_prototype(closure_ptr: usize) -> Option<u64> {
if closure_ptr == 0 || !is_closure_ptr(closure_ptr) {
return None;
}
closure_static_prototype_proven(closure_ptr)
}

/// [`closure_static_prototype`] of a proven, live closure cell.
pub(crate) fn closure_static_prototype_proven(closure_ptr: usize) -> Option<u64> {
if unsafe { super::shape::is_class_info((*(closure_ptr as *const ClosureHeader)).info) } {
return Some(unsafe {
crate::object::class_value::constructor_parent_bits(closure_ptr as *mut ClosureHeader)
});
}
unsafe { super::props::state_prototype(closure_ptr) }
// A described shape is the record of the link; FunctionDictionary keeps
// it in the function's own record ("ask the object").
let closure = closure_ptr as *const ClosureHeader;
unsafe {
if (*closure).shape_id != super::shape::function_dictionary_shape() {
return super::shape::described_link_bits(closure);
}
super::props::state_prototype(closure_ptr)
}
}

/// Is `ptr` a live function object (`GC_TYPE_CLOSURE` cell)? Safe for an
Expand Down Expand Up @@ -366,6 +386,11 @@ fn closure_proto_chain_get(
if depth > 0 {
proto_bits = closure_static_prototype(cur)?;
}
// A null link ends the chain: the read's answer is `undefined`, not
// `%Function.prototype%`'s.
if proto_bits == crate::value::TAG_NULL {
return Some(f64::from_bits(crate::value::TAG_UNDEFINED));
}
let proto_f64 = f64::from_bits(proto_bits);
let proto_ptr = crate::value::js_nanbox_get_pointer(proto_f64) as usize;
if proto_ptr == 0 || proto_ptr == cur {
Expand Down Expand Up @@ -467,6 +492,25 @@ fn closure_proto_chain_get(
None
}

/// Build the realm global that holds %Function.prototype% (its first
/// inherited read), keeping the reading function and receiver alive across
/// the allocation. Returns both, re-read.
#[cold]
#[inline(never)]
fn cold_materialize_function_prototype(self_ptr: usize, receiver: f64) -> (usize, f64) {
let scope = crate::gc::RuntimeHandleScope::new();
let receiver_root = scope.root_nanbox_f64(receiver);
let self_root = (self_ptr != 0)
.then(|| scope.root_nanbox_f64(crate::value::js_nanbox_pointer(self_ptr as i64)));
let _ = super::shape::function_prototype_ptr_materialized();
(
self_root.map_or(0, |r| {
(r.get_nanbox_u64() & crate::value::POINTER_MASK) as usize
}),
receiver_root.get_nanbox_f64(),
)
}

/// `[[Get]]` of `prop` on the real `%Function.prototype%` object, for a
/// function-object receiver whose own/static lookup already missed — a
/// user method or expando (`Function.prototype.myHelper = fn`) or a
Expand All @@ -485,6 +529,18 @@ pub(crate) fn function_prototype_inherited_get(
if crate::object::reified_function_method_name(prop).is_some() {
return None;
}
// A read needs %Function.prototype% itself: build the realm global that
// holds it when this is the program's first inherited read (the set
// side's descriptor probe must not, so the shared target answers 0).
let (self_ptr, receiver) = if crate::array::function_prototype_addr() == 0
&& !matches!(
prop,
"prototype" | "name" | "length" | "caller" | "arguments"
) {
cold_materialize_function_prototype(self_ptr, receiver)
} else {
(self_ptr, receiver)
};
let proto_ptr = function_prototype_fallback_target(self_ptr, prop)?;
// A defineProperty accessor on Function.prototype
// (`{ get: () => 12 }`) is invoked with the reading
Expand Down
4 changes: 2 additions & 2 deletions crates/perry-runtime/src/closure/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,8 @@ pub use unbox::{js_closure_unbox_callee_checked, js_closure_unbox_callee_checked

pub(crate) use dynamic_props::{
clone_closure_rebind_this, closure_get_dynamic_prop_keyed, closure_reads_this_from_capture,
closure_set_via_function_prototype_descriptor, function_prototype_fallback_target,
function_prototype_inherited_get,
closure_set_via_function_prototype_descriptor, closure_static_prototype_proven,
function_prototype_fallback_target, function_prototype_inherited_get,
};
pub(crate) use dynamic_props::{
closure_define_data_with_attrs, closure_define_data_with_attrs_bytes,
Expand Down
Loading
Loading