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
7 changes: 4 additions & 3 deletions crates/perry-codegen/src/lower_call/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ mod native;
mod native_module_dispatch;
mod native_table;
mod new;
mod new_ctor_args;
mod new_helpers;
mod omitted_native_params;
mod options;
Expand Down Expand Up @@ -109,9 +110,9 @@ pub(crate) use native::lower_native_method_call;
// (codegen.rs / expr.rs / stmt.rs) so `crate::lower_call::lower_new`
// etc. keep resolving after the split.
pub(crate) use field_init::{apply_field_initializers_recursive, FieldInitMode};
pub(crate) use new::{
bind_inline_constructor_params, emit_class_capture_writeback, lower_new,
lower_new_member_captured, restore_inline_constructor_scope, CaptureFill,
pub(crate) use new::{emit_class_capture_writeback, lower_new, lower_new_member_captured};
pub(crate) use new_ctor_args::{
bind_inline_constructor_params, restore_inline_constructor_scope, CaptureFill,
};
// The derived-ctor no-super static-throw predicates (shared with the
// standalone-ctor-symbol path in `codegen/method.rs`, which is the default
Expand Down
401 changes: 21 additions & 380 deletions crates/perry-codegen/src/lower_call/new.rs

Large diffs are not rendered by default.

438 changes: 438 additions & 0 deletions crates/perry-codegen/src/lower_call/new_ctor_args.rs

Large diffs are not rendered by default.

21 changes: 11 additions & 10 deletions crates/perry-runtime/src/object/class_registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,19 +53,20 @@ pub(crate) use state::{
class_decl_prototype_value, class_decl_prototype_value_for_instance_class,
class_delete_own_dynamic_prop, class_dynamic_prop_root_store, class_has_own_dynamic_prop,
class_id_for_decl_prototype_object, class_is_key_deleted, class_mark_key_deleted,
class_own_enumerable_field_names, class_own_static_field_value, class_parent_closure,
class_parent_closure_root_store, class_prototype_method_is_enumerable,
class_prototype_method_set_enumerable, class_prototype_method_value_cache_root_store,
class_prototype_object_root_store, global_object_prototype_bits,
is_bound_native_method_closure_value, is_non_constructable_builtin_function_value,
parent_closure_in_chain, throw_non_constructable_builtin_function,
class_object_value_for_cid, class_object_value_root_store, class_own_enumerable_field_names,
class_own_static_field_value, class_parent_closure, class_parent_closure_root_store,
class_prototype_method_is_enumerable, class_prototype_method_set_enumerable,
class_prototype_method_value_cache_root_store, class_prototype_object_root_store,
global_object_prototype_bits, is_bound_native_method_closure_value,
is_non_constructable_builtin_function_value, parent_closure_in_chain,
throw_non_constructable_builtin_function,
};
pub use state::{
ClassVTable, VTableMethodEntry, CLASS_DECL_PROTOTYPE_OBJECTS, CLASS_DYNAMIC_PARENT_VALUE,
CLASS_METHOD_BIND_LENGTHS, CLASS_PARENT_CLOSURES, CLASS_PROTOTYPE_METHOD_NONENUM,
CLASS_PROTOTYPE_OBJECTS, CLASS_STATIC_ACCESSORS, CLASS_STATIC_METHODS,
CLASS_STATIC_METHOD_BIND_LENGTHS, CLASS_SYMBOL_ACCESSORS, CLASS_SYMBOL_METHODS,
CLASS_VTABLE_REGISTRY, FUNCTION_CLASS_IDS, REGISTERED_CLASS_IDS,
CLASS_METHOD_BIND_LENGTHS, CLASS_OBJECT_VALUES, CLASS_PARENT_CLOSURES,
CLASS_PROTOTYPE_METHOD_NONENUM, CLASS_PROTOTYPE_OBJECTS, CLASS_STATIC_ACCESSORS,
CLASS_STATIC_METHODS, CLASS_STATIC_METHOD_BIND_LENGTHS, CLASS_SYMBOL_ACCESSORS,
CLASS_SYMBOL_METHODS, CLASS_VTABLE_REGISTRY, FUNCTION_CLASS_IDS, REGISTERED_CLASS_IDS,
};

// ── prototype_objects.rs ────────────────────────────────────────────────────
Expand Down
32 changes: 32 additions & 0 deletions crates/perry-runtime/src/object/class_registry/gc_roots.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ enum ClassSideTableRootSlot {
DynamicParentValue {
class_id: u32,
},
ClassObjectValue {
class_id: u32,
},
ClassSymbolMethod {
class_id: u32,
sym_key: usize,
Expand Down Expand Up @@ -134,6 +137,18 @@ pub fn scan_class_side_table_roots_mut(visitor: &mut crate::gc::RuntimeRootVisit
}
}

// #6530: cid → per-evaluation class OBJECT (`instance.constructor`
// identity for capture-carrying classes). Same liveness/forwarding needs
// as the dynamic-parent stash above: the entries are heap objects a
// moving GC must visit + forward.
if let Ok(mut guard) = CLASS_OBJECT_VALUES.write() {
if let Some(map) = guard.as_mut() {
for value_bits in map.values_mut() {
visitor.visit_nanbox_u64_slot(value_bits);
}
}
}

scan_class_symbol_member_keys_mut(visitor);
scan_function_class_id_keys_mut(visitor);
}
Expand Down Expand Up @@ -242,6 +257,16 @@ fn class_side_table_root_snapshot() -> Vec<ClassSideTableRootSlot> {
}
}

// Step twin of the CLASS_OBJECT_VALUES block in
// `scan_class_side_table_roots_mut` (#6530).
if let Ok(guard) = CLASS_OBJECT_VALUES.read() {
if let Some(map) = guard.as_ref() {
for &class_id in map.keys() {
slots.push(ClassSideTableRootSlot::ClassObjectValue { class_id });
}
}
}

if let Ok(guard) = CLASS_SYMBOL_METHODS.read() {
if let Some(map) = guard.as_ref() {
for &(class_id, sym_key, is_static) in map.keys() {
Expand Down Expand Up @@ -332,6 +357,13 @@ fn scan_class_side_table_root_slot(
}
}
}
ClassSideTableRootSlot::ClassObjectValue { class_id } => {
if let Ok(mut guard) = CLASS_OBJECT_VALUES.write() {
if let Some(value_bits) = guard.as_mut().and_then(|map| map.get_mut(class_id)) {
visitor.visit_nanbox_u64_slot(value_bits);
}
}
}
ClassSideTableRootSlot::ClassSymbolMethod {
class_id,
sym_key,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,12 @@ pub extern "C" fn js_object_mark_class(obj: i64) {
if obj != 0 {
unsafe {
(*(obj as *mut ObjectHeader)).object_type = crate::error::OBJECT_TYPE_CLASS;
// #6530: record cid → class object so `instance.constructor`
// resolves to the SAME value the module scope/exports hold (see
// `CLASS_OBJECT_VALUES`). The template cid was stamped by the
// `js_object_alloc(cid, …)` call directly preceding this mark.
let cid = (*(obj as *const ObjectHeader)).class_id;
super::class_object_value_root_store(cid, obj as *mut ObjectHeader);
}
}
}
Expand Down
41 changes: 41 additions & 0 deletions crates/perry-runtime/src/object/class_registry/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,47 @@ pub static CLASS_PARENT_CLOSURES: RwLock<Option<HashMap<u32, usize>>> = RwLock::
/// (INT32-tagged) and object/closure (POINTER-tagged) parents.
pub static CLASS_DYNAMIC_PARENT_VALUE: RwLock<Option<HashMap<u32, u64>>> = RwLock::new(None);

/// #6530: maps a template class_id to the raw NaN-boxed POINTER bits of the
/// per-evaluation CLASS OBJECT the class statement materialized as (marked by
/// `js_object_mark_class`). A capture-carrying class has no INT32 ClassRef
/// value at runtime — the class VALUE is this heap object — so
/// `instance.constructor` must hand back the same object the module scope /
/// exports hold, or identity checks (`x.constructor === Sub`, bundled zod's
/// `describe()` re-construction via `this.constructor`) break. Last-wins
/// across evaluations of the same class statement, matching the template-cid
/// compromise used by the sibling tables above.
pub static CLASS_OBJECT_VALUES: RwLock<Option<HashMap<u32, u64>>> = RwLock::new(None);

/// Store the marked class object for its template class id (see
/// `CLASS_OBJECT_VALUES`).
pub(crate) fn class_object_value_root_store(class_id: u32, obj_ptr: *mut ObjectHeader) {
if class_id == 0 || obj_ptr.is_null() {
return;
}
let bits = crate::value::js_nanbox_pointer(obj_ptr as i64).to_bits();
let mut guard = CLASS_OBJECT_VALUES.write().unwrap();
if guard.is_none() {
*guard = Some(HashMap::new());
}
guard.as_mut().unwrap().insert(class_id, bits);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
crate::gc::runtime_write_barrier_root_raw_ptr(obj_ptr);
}

/// Read back the class object registered for `class_id`, or `None` when the
/// class never materialized as a per-evaluation object (ordinary
/// ClassRef-valued classes).
pub(crate) fn class_object_value_for_cid(class_id: u32) -> Option<f64> {
if class_id == 0 {
return None;
}
CLASS_OBJECT_VALUES.read().ok().and_then(|guard| {
guard
.as_ref()
.and_then(|map| map.get(&class_id).copied())
.map(f64::from_bits)
})
}

pub(crate) fn class_prototype_object_root_store(class_id: u32, proto_ptr: *mut ObjectHeader) {
if class_id == 0 || proto_ptr.is_null() {
return;
Expand Down
106 changes: 106 additions & 0 deletions crates/perry-runtime/src/object/field_get_set/class_object_props.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,109 @@ pub(super) unsafe fn class_object_name_value(
crate::js_nanbox_string(s as i64).to_bits(),
))
}

/// #6530 (size-gate split from `get_field_by_name_tail.rs` — pure
/// relocation): resolve the `constructor` special key for an instance
/// receiver. Own `constructor` data field wins; then WeakMap/WeakSet,
/// the per-evaluation class-object registry (capture-carrying classes),
/// vtable `constructor` methods, boxed primitives, anon shapes, the
/// function-class table, and the INT32 ClassRef synthesis. `None` means
/// "not resolved here" — the caller falls through to the generic walk.
pub(super) unsafe fn instance_constructor_value(
obj: *const ObjectHeader,
key: *const crate::StringHeader,
) -> Option<JSValue> {
if let Some(v) = own_data_field_by_name(obj, key) {
return Some(v);
}
let class_id = (*obj).class_id;
// #6530: a capture-carrying class has no ClassRef value — the
// class VALUE is the per-evaluation class OBJECT registered at
// `js_object_mark_class` time. Return that same object so
// identity holds (`x.constructor === Sub`, bundled zod's
// `describe()` re-construction via `this.constructor`
// yielding the SUBCLASS instead of collapsing to the base).
// The arms below would otherwise hand back a bound
// constructor-method closure (whose underlying func is the
// nearest ancestor ctor — reporting the BASE class's name for
// every subclass) or the bare INT32 ClassRef.
if let Some(v) = super::super::class_registry::class_object_value_for_cid(class_id) {
return Some(JSValue::from_bits(v.to_bits()));
}
// #5834: WeakMap/WeakSet instances carry a reserved class_id
// (not a registered declared-class one), so none of the
// arms below resolve them and `(new WeakMap()).constructor`
// fell through to `undefined`.
if class_id == crate::weakref::CLASS_ID_WEAKMAP || class_id == crate::weakref::CLASS_ID_WEAKSET
{
let name: &[u8] = if class_id == crate::weakref::CLASS_ID_WEAKMAP {
b"WeakMap"
} else {
b"WeakSet"
};
let v = js_get_global_this_builtin_value(name.as_ptr(), name.len());
return Some(JSValue::from_bits(v.to_bits()));
}
if class_id != 0 && class_has_own_method(class_id, "constructor") {
let value = class_prototype_method_value_for_name(class_id, "constructor");
return Some(JSValue::from_bits(value.to_bits()));
}
if matches!(
class_id,
CLASS_ID_BOXED_NUMBER
| CLASS_ID_BOXED_STRING
| CLASS_ID_BOXED_BOOLEAN
| CLASS_ID_BOXED_BIGINT
| CLASS_ID_BOXED_SYMBOL
) {
let name = match class_id {
CLASS_ID_BOXED_NUMBER => b"Number".as_slice(),
CLASS_ID_BOXED_STRING => b"String".as_slice(),
CLASS_ID_BOXED_BOOLEAN => b"Boolean".as_slice(),
CLASS_ID_BOXED_BIGINT => b"BigInt".as_slice(),
CLASS_ID_BOXED_SYMBOL => b"Symbol".as_slice(),
_ => unreachable!(),
};
let v = js_get_global_this_builtin_value(name.as_ptr(), name.len());
return Some(JSValue::from_bits(v.to_bits()));
}
// Object-literal instances (`{ x: 1 }`) carry a synthetic
// `__AnonShape_*` class id. Spec says their `.constructor`
// is the global `Object`, not the synthetic class — so
// resolve through the globalThis singleton so the value
// matches the bare `Object` identifier (`x.constructor
// === Object`, date-fns `constructFrom`, drizzle's
// `isPlainObject` duck check).
if class_id != 0 && is_anon_shape_class_id(class_id) {
let v = js_get_global_this_builtin_value(b"Object".as_ptr(), 6);
return Some(JSValue::from_bits(v.to_bits()));
}
if let Some(func_value) = super::super::class_registry::function_value_for_class_id(class_id) {
return Some(JSValue::from_bits(func_value.to_bits()));
}
if class_id != 0 && is_class_id_registered(class_id) {
let bits = 0x7FFE_0000_0000_0000u64 | (class_id as u64);
return Some(JSValue::from_bits(bits));
}
// class_id == 0 fallback: plain ObjectHeader allocated
// without an HIR shape (Object.create(null) hybrids, raw
// empty `{}` produced by JSON.parse, etc.). Report
// `Object` so duck-type tests don't trip undefined.
if class_id == 0 {
// #6537 review: an EXPLICIT null-prototype object
// (`Object.create(null)` / `js_object_alloc_null_proto`, marked with
// `OBJ_FLAG_NULL_PROTO` on the GC header) has NO `constructor` —
// fall through (→ undefined) instead of reporting `Object`. The
// `Object` report stays for ordinary shapeless objects (raw `{}`
// from JSON.parse etc.), which spec-correctly inherit
// `Object.prototype.constructor`.
if let Some(gc_header) = crate::value::addr_class::try_read_gc_header(obj as usize) {
if gc_header._reserved & crate::gc::OBJ_FLAG_NULL_PROTO != 0 {
return None;
}
}
let v = js_get_global_this_builtin_value(b"Object".as_ptr(), 6);
return Some(JSValue::from_bits(v.to_bits()));
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
None
}
59 changes: 41 additions & 18 deletions crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -243,18 +243,28 @@ pub extern "C" fn js_object_get_field_by_name(
{
return JSValue::from_bits(v.to_bits());
}
if let Some(parent) = crate::object::class_registry::class_object_pinned_parent(obj)
{
let pbits = parent.to_bits();
if (pbits >> 48) == 0x7FFD {
let praw = (pbits & 0x0000_FFFF_FFFF_FFFF) as *mut ObjectHeader;
if praw as usize != obj as usize
&& crate::value::addr_class::is_above_handle_band(praw as usize)
&& crate::object::is_valid_obj_ptr(praw as *const u8)
{
let v = js_object_get_field_by_name(praw, key);
if !v.is_undefined() {
return v;
// #6530: `name` and `prototype` are OWN properties of every
// constructor — never inherited through the parent edge. Skip
// the pinned-parent recursion for them so the generic tail
// below synthesizes both from THIS object's class_id (the
// recursion otherwise answered with the BASE class's `.name`
// for every capture-carrying subclass — bundled zod's
// identity collapse to "ZodType").
if want != b"name" && want != b"prototype" {
if let Some(parent) =
crate::object::class_registry::class_object_pinned_parent(obj)
{
let pbits = parent.to_bits();
if (pbits >> 48) == 0x7FFD {
let praw = (pbits & 0x0000_FFFF_FFFF_FFFF) as *mut ObjectHeader;
if praw as usize != obj as usize
&& crate::value::addr_class::is_above_handle_band(praw as usize)
&& crate::object::is_valid_obj_ptr(praw as *const u8)
{
let v = js_object_get_field_by_name(praw, key);
if !v.is_undefined() {
return v;
}
}
}
}
Expand Down Expand Up @@ -909,7 +919,13 @@ pub extern "C" fn js_object_get_field_by_name(
// this covers the data-field case that was returning `undefined`
// (Auth.js sets `SignInError.kind = "signIn"` and reads it off a
// `CredentialsSignin` subclass to pick the sign-in vs error page).
{
//
// #6530: `name` is an OWN property of every constructor — a
// subclass never inherits its parent's `.name` (spec:
// ClassDefinitionEvaluation installs it per class). Skip the
// chain walk so the #2059 own-name synthesis below answers
// with THIS class's registered name instead of an ancestor's.
if name != "name" {
let mut cid = class_id;
let mut depth = 0usize;
while depth < 32 {
Expand Down Expand Up @@ -979,11 +995,18 @@ pub extern "C" fn js_object_get_field_by_name(
// parent object was recorded as `class_id`'s static
// prototype at `extends` time; walk that chain (also
// covering multi-level `class Leaf extends Mid {}`).
if let Some(v) =
super::super::class_registry::resolve_proto_chain_field(class_id, key)
{
if !v.is_undefined() && !v.is_null() {
return v;
// #6530: except `name` — an own property of every
// constructor, never inherited; without the guard a
// subclass of a per-evaluation class object reported its
// BASE's synthesized `.name` (bundled zod:
// `z.string().constructor.name` gave "ZodType").
if name != "name" {
if let Some(v) =
super::super::class_registry::resolve_proto_chain_field(class_id, key)
{
if !v.is_undefined() && !v.is_null() {
return v;
}
}
}
// #36 / #321: the subclass extends a FUNCTION value
Expand Down
Loading
Loading