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
9 changes: 9 additions & 0 deletions changelog.d/11777-class-super-instanceof-relink.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
`super.m()`, `typeof super.m`, `super.m` as a value and `super.x` now read the
parent prototype as it is when they run, so a patched, deleted or getter-backed
parent method applies, and so does `Object.setPrototypeOf` on the class
prototype or, in a static method, on the class itself. A static `super.s()` no
longer calls an instance method of the same name. After a class prototype is
relinked, `instanceof` follows the new chain, including `instanceof Object` and
an instance whose own prototype was replaced. `C.prototype.__proto__ = X` now
relinks the prototype like `Object.setPrototypeOf` instead of creating an own
property named `__proto__`.
97 changes: 70 additions & 27 deletions crates/perry-codegen/src/expr/super_method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,16 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
}
return Ok(double_literal(0.0));
};
// Walk parent chain starting from extends_name.
let mut parent = ctx
.classes
.get(&current_class_name)
.and_then(|c| c.extends_name.clone());
// Walk parent chain starting from extends_name. The method tables
// hold INSTANCE methods only: in a static member `super` is the
// parent constructor, which the runtime path resolves.
let mut parent = if ctx.in_static_member {
None
} else {
ctx.classes
.get(&current_class_name)
.and_then(|c| c.extends_name.clone())
};
let mut resolved_fn: Option<String> = None;
// #8040: the class the body actually lives on, so the trailing
// parameter shape below is read off the callee we are calling.
Expand Down Expand Up @@ -344,11 +349,15 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
let Some(current_class_name) = ctx.class_stack.last().cloned() else {
return Ok(undef);
};
let immediate_parent = ctx
.classes
.get(&current_class_name)
.and_then(|c| c.extends_name.clone());
let mut parent = immediate_parent.clone();
// As for the call form: in a static member the instance method
// tables do not describe `super`.
let mut parent = if ctx.in_static_member {
None
} else {
ctx.classes
.get(&current_class_name)
.and_then(|c| c.extends_name.clone())
};
let mut resolved_fn: Option<String> = None;
while let Some(p) = parent {
let key = (p.clone(), property.clone());
Expand All @@ -358,18 +367,13 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
}
parent = ctx.classes.get(&p).and_then(|c| c.extends_name.clone());
}
let Some(fn_name) = resolved_fn else {
// Not a method on the parent chain — `super.prop` then reads the
// property off the parent prototype with `this` as receiver:
// an accessor (getter) is INVOKED, a data property
// (`B.prototype.x = 42`) is returned. Route to the runtime,
// which walks the parent class chain. Refs
// class/super/in-{constructor,getter,methods,setter}.
let parent_cid = immediate_parent
.as_ref()
.and_then(|p| ctx.class_ids.get(p))
.copied()
.unwrap_or(0);
// `super.prop` reads the property off the home object's current
// `[[Prototype]]` with `this` as receiver: an accessor (getter) is
// INVOKED, a data property (`B.prototype.x = 42`) is returned. The
// runtime reads that chain from the home class id. Refs
// class/super/in-{constructor,getter,methods,setter}.
let home_cid = ctx.class_ids.get(&current_class_name).copied().unwrap_or(0);
let runtime_get = |ctx: &mut FnCtx<'_>| -> String {
let recv_v = if let Some(this_slot) = ctx.this_stack.last().cloned() {
ctx.block().load(DOUBLE, &this_slot)
} else {
Expand All @@ -378,12 +382,41 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
let key_idx = ctx.strings.intern(property);
let key_handle_global = format!("@{}", ctx.strings.entry(key_idx).handle_global);
let key_box = ctx.block().load(DOUBLE, &key_handle_global);
let parent_cid_s = parent_cid.to_string();
return Ok(ctx.block().call(
let home_cid_s = home_cid.to_string();
ctx.block().call(
DOUBLE,
"js_super_accessor_get",
&[(I32, &parent_cid_s), (DOUBLE, &key_box), (DOUBLE, &recv_v)],
));
&[(I32, &home_cid_s), (DOUBLE, &key_box), (DOUBLE, &recv_v)],
)
};
let Some(fn_name) = resolved_fn else {
return Ok(runtime_get(ctx));
};
// The method above was resolved along the declared `extends`
// chain, which holds while no prototype surgery touched this
// name (the guard bytes `super.m()` reads too).
let guarded = if home_cid != 0 {
let key_idx = ctx.strings.intern(property);
let slot = (ctx.strings.entry(key_idx).dispatch_hash & 0xffff).to_string();
let direct_idx = ctx.new_block("super_get.direct");
let dynamic_idx = ctx.new_block("super_get.dynamic");
let merge_idx = ctx.new_block("super_get.merge");
let direct_label = ctx.block_label(direct_idx);
let dynamic_label = ctx.block_label(dynamic_idx);
let merge_label = ctx.block_label(merge_idx);
let ok = crate::lower_call::method_override::emit_prototype_method_guard_ok(
ctx.block(),
&slot,
);
ctx.block().cond_br(&ok, &direct_label, &dynamic_label);
ctx.current_block = dynamic_idx;
let dynamic_value = runtime_get(ctx);
let dynamic_end = ctx.block().label.clone();
ctx.block().br(&merge_label);
ctx.current_block = direct_idx;
Some((merge_idx, merge_label, dynamic_value, dynamic_end))
} else {
None
};
// Mirror Expr::FuncRef: route through the singleton wrapper
// so callers can invoke via the closure-call ABI. The
Expand Down Expand Up @@ -413,7 +446,17 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
let blk = ctx.block();
let wrap_info = blk.fn_info_ref(&wrap_name);
let closure_handle = blk.call(I64, "js_closure_alloc_singleton", &[(PTR, &wrap_info)]);
Ok(nanbox_pointer_inline(blk, &closure_handle))
let direct_value = nanbox_pointer_inline(blk, &closure_handle);
let Some((merge_idx, merge_label, dynamic_value, dynamic_end)) = guarded else {
return Ok(direct_value);
};
let direct_end = ctx.block().label.clone();
ctx.block().br(&merge_label);
ctx.current_block = merge_idx;
Ok(ctx.block().phi(
DOUBLE,
&[(&direct_value, &direct_end), (&dynamic_value, &dynamic_end)],
))
}

Expr::SuperPropertySet {
Expand Down
159 changes: 62 additions & 97 deletions crates/perry-runtime/src/object/class_constructors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ use std::collections::HashMap;
use std::sync::RwLock;

use super::class_registry::call_vtable_method;
use super::class_super_chain::{
class_super_base, declared_chain_has_relinked_prototype, static_chain_relinked,
super_call_on_live_base, super_call_on_relinked_chain, super_home_owner,
};
use super::ObjectHeader;

/// Replace the capture array carried by one heap class-expression value.
Expand Down Expand Up @@ -881,11 +885,7 @@ pub unsafe extern "C" fn js_super_method_call_dynamic(
// Repeated evaluations can share a template id, so the template parent
// table cannot represent their heritage edge. Resolve the method's own
// evaluation first, then read its pinned parent.
let lexical_owner = super::field_get_set::current_private_lexical_brand_value(child_class_id)
.or_else(|| {
super::field_get_set::private_evaluation_brand_value(this_value)
.and_then(|owner| pinned_class_object_for_ancestor(owner, child_class_id))
});
let lexical_owner = super_home_owner(child_class_id, this_value);
let parent_owner = lexical_owner.and_then(|owner| {
let object = crate::value::JSValue::from_bits(owner.to_bits()).as_pointer::<ObjectHeader>();
super::class_registry::class_object_pinned_parent(object)
Expand All @@ -905,16 +905,67 @@ pub unsafe extern "C" fn js_super_method_call_dynamic(
};
let _parent_brand =
super::field_get_set::PrivateHintBrandScope::new(parent_owner.map(f64::to_bits));
// `super.name` is a property lookup on the home object's CURRENT
// `[[Prototype]]`, with `this` as the receiver: a patched, deleted or
// accessor parent member and a relinked home all apply. Where the runtime
// models that chain end to end, read it; the declared-chain lookups below
// serve the rest (native and builtin bases, per-evaluation classes).
let is_static = super::class_ref_id(this_value).is_some()
|| super::class_registry::is_class_object_value(this_value);
// A static member's declared lookup reads the class function objects (an
// assigned or deleted static ends it), so it is the property lookup unless
// a constructor on the way was relinked. An instance member reaches here
// when the compiler could not resolve the name or a prototype-surgery
// guard byte is set: the declared vtable no longer describes the chain.
let static_entry = if is_static {
super::class_registry::parent_static::lookup_static_method_owner(parent_cid, name)
} else {
None
};
let mut base = None;
// The probes below can allocate (materializing a prototype or a class
// value), so the receiver and the arguments ride across them in handles;
// the declared-chain paths below read the refreshed copies.
let refreshed_args: Vec<f64>;
let (this_value, args_ptr) = if static_entry.is_none()
|| super::prototype_chain::any_user_prototype_override()
{
let live_scope = crate::gc::RuntimeHandleScope::new();
let this_handle = live_scope.root_nanbox_f64(this_value);
let arg_handles =
live_scope.root_nanbox_f64_slice(if args_len > 0 && !args_ptr.is_null() {
std::slice::from_raw_parts(args_ptr, args_len)
} else {
&[]
});
let live = match static_entry {
Some((owner, _)) => static_chain_relinked(child_class_id, owner),
None => true,
};
if live {
base = class_super_base(child_class_id, parent_cid, lexical_owner, is_static);
}
refreshed_args = crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&arg_handles);
(
this_handle.get_nanbox_f64(),
if refreshed_args.is_empty() {
args_ptr
} else {
refreshed_args.as_ptr()
},
)
} else {
(this_value, args_ptr)
};
if let Some(base) = base {
return super_call_on_live_base(name, this_value, args_ptr, args_len, base);
}
// Static-context super call (`super.m()` inside a `static` method): the
// receiver is the class constructor (a ClassRef), so resolve the PARENT's
// STATIC method (not an instance/prototype method) and invoke it with
// `this` bound to the current class. Refs class/super/in-static-methods.
if super::class_ref_id(this_value).is_some()
|| super::class_registry::is_class_object_value(this_value)
{
if let Some((func_ptr, param_count, has_rest)) =
super::class_registry::lookup_static_method_in_chain(parent_cid, name)
{
if is_static {
if let Some((_, (func_ptr, param_count, has_rest))) = static_entry {
crate::object::static_this_arm_if_unarmed(this_value);
let result = if has_rest {
// Mirror `js_class_static_method_call`'s rest bundling: fixed
Expand Down Expand Up @@ -1024,92 +1075,6 @@ pub unsafe extern "C" fn js_super_method_call_dynamic(
call_displaced_native_base_method(this_value, name, args_ptr, args_len, undef)
}

/// Is the prototype of `cid` or of one of its declared ancestors relinked by a
/// user operation? Asked only after the declared-member lookups missed.
fn declared_chain_has_relinked_prototype(cid: u32) -> bool {
let mut cur = cid;
for _ in 0..32 {
if cur == 0 {
return false;
}
if super::class_registry::class_decl_prototype_relinked(cur) {
return true;
}
match crate::object::get_parent_class_id(cur) {
Some(p) if p != cur => cur = p,
_ => return false,
}
}
false
}

/// `super.name(...args)` resolved by `read` on a relinked chain: call the value
/// with `this_value` as receiver, or throw the TypeError a call of a
/// non-callable `super.name` throws.
///
/// # Safety
/// `args_ptr` must point to `args_len` valid `f64`s (or be null when
/// `args_len == 0`).
unsafe fn super_call_on_relinked_chain(
name: &str,
this_value: f64,
args_ptr: *const f64,
args_len: usize,
read: impl FnOnce(*const crate::StringHeader, f64) -> Option<crate::value::JSValue>,
) -> f64 {
// The key allocation and the read (a getter on the new chain) can collect;
// the receiver and the arguments ride across them in handles.
let scope = crate::gc::RuntimeHandleScope::new();
let this_handle = scope.root_nanbox_f64(this_value);
let args: Vec<f64> = if args_len > 0 && !args_ptr.is_null() {
std::slice::from_raw_parts(args_ptr, args_len).to_vec()
} else {
Vec::new()
};
let arg_handles = scope.root_nanbox_f64_slice(&args);
let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32);
let value = if key.is_null() {
None
} else {
read(
key as *const crate::StringHeader,
this_handle.get_nanbox_f64(),
)
};
let callable = value.filter(|v| {
let boxed = f64::from_bits(v.bits());
v.is_pointer()
&& ((crate::proxy::js_proxy_is_proxy(boxed) == 1
&& crate::proxy::proxy_wraps_callable(boxed))
|| crate::closure::is_closure_ptr(
crate::value::js_nanbox_get_pointer(boxed) as usize
))
});
let Some(method) = callable else {
crate::error::js_throw_type_error_not_a_function(
std::ptr::null(),
0,
name.as_ptr(),
name.len(),
)
};
let method_handle = scope.root_nanbox_f64(f64::from_bits(method.bits()));
let args = crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&arg_handles);
if crate::proxy::js_proxy_is_proxy(method_handle.get_nanbox_f64()) == 1 {
return crate::proxy::call_proxy_value_with_this(
method_handle.get_nanbox_f64(),
this_handle.get_nanbox_f64(),
&args,
);
}
crate::closure::native_call_value_this(
method_handle.get_nanbox_f64(),
crate::closure::JsThis::from_f64(this_handle.get_nanbox_f64()),
args.as_ptr(),
args.len(),
)
}

/// Invoke the native base method a subclass override displaced (#6316), with
/// `this` bound to the receiver. Falls back to `undef` when the receiver carries
/// no such method — an ordinary `super.m()` miss stays `undefined`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,27 @@ pub unsafe extern "C" fn js_register_prototype_method(
if class_has_instance_getter(class_id, &name) {
return;
}
// `C.prototype.__proto__ = v` is not a method install: it is a `[[Set]]`
// that reaches `Object.prototype`'s `__proto__` accessor, whose setter
// relinks the prototype exactly like `Object.setPrototypeOf`.
if name == "__proto__" {
let proto = super::class_decl_prototype_value(class_id);
if crate::value::JSValue::from_bits(proto.to_bits()).is_pointer() {
let scope = crate::gc::RuntimeHandleScope::new();
let proto = scope.root_nanbox_f64(proto);
let value = scope.root_nanbox_f64(value);
let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32);
let key = f64::from_bits(crate::value::JSValue::string_ptr(key).bits());
crate::proxy::js_put_value_set(
proto.get_nanbox_f64(),
key,
value.get_nanbox_f64(),
proto.get_nanbox_f64(),
1,
);
return;
}
}
class_prototype_method_root_store(class_id, name, value.to_bits());
// Ensure the receiver class can be `typeof`-detected. Method-less
// classes that only get extended via `Class.prototype.m = fn`
Expand Down
Loading
Loading