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
32 changes: 32 additions & 0 deletions crates/perry-codegen/src/expr/this_super_call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -664,6 +664,38 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
)?;
return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)));
}
// `class X extends Promise` — `super(executor)` runs the
// ECMA-262 27.2.3.1 Promise constructor against a hidden
// backing `Promise` cell stashed on `this`. Inherited
// `then`/`catch`/`finally` unwrap that cell (see
// `promise::subclass::subclass_backing_promise`), so a
// subclass instance behaves as a promise while keeping its
// own `constructor`/`instanceof` identity.
if parent_name.as_str() == "Promise" {
let undef = double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED));
let mut lowered: Vec<String> = Vec::with_capacity(super_args.len());
for a in super_args {
lowered.push(lower_expr(ctx, a)?);
}
let executor = lowered.first().cloned().unwrap_or_else(|| undef.clone());
let this_box = match ctx.this_stack.last().cloned() {
Some(slot) => ctx.block().load(DOUBLE, &slot),
None => undef.clone(),
};
ctx.block().call(
DOUBLE,
"js_promise_subclass_init",
&[(DOUBLE, &this_box), (DOUBLE, &executor)],
);
let current_class_name =
ctx.class_stack.last().cloned().unwrap_or_default();
crate::lower_call::apply_field_initializers_recursive(
ctx,
&current_class_name,
crate::lower_call::FieldInitMode::SelfOnly,
)?;
return Ok(double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)));
}
let fetch_subclass_fn = match parent_name.as_str() {
"Request" => Some("js_request_subclass_init"),
"Response" => Some("js_response_subclass_init"),
Expand Down
14 changes: 12 additions & 2 deletions crates/perry-codegen/src/lower_call/new.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ use super::lower_builtin_new;
use super::new_helpers::{
collect_decl_local_ids, ctor_body_calls_super, ctor_body_closure_calls_super,
ctor_body_has_value_return, ctor_body_uses_this, ctor_chain_uses_new_target,
effective_constructor_param_count, local_constructor_symbol_exists, map_set_default_super_kind,
node_stream_parent_kind, restore_imported_ctor_new_target, set_imported_ctor_new_target,
effective_constructor_param_count, emit_promise_subclass_init, local_constructor_symbol_exists,
map_set_default_super_kind, node_stream_parent_kind, restore_imported_ctor_new_target,
set_imported_ctor_new_target,
};
use crate::expr::{lower_expr, lower_js_args_array, nanbox_pointer_inline, FnCtx};
use crate::nanbox::{double_literal, POINTER_MASK_I64};
Expand Down Expand Up @@ -1313,6 +1314,10 @@ fn lower_new_impl(
} else {
None
};
// `class X extends Promise {}` with no own ctor — `new X(executor)` runs the
// Promise constructor against a hidden backing cell (see new_helpers).
let promise_parent_runtime =
!has_own_ctor && !has_imported_ctor && class.extends_name.as_deref() == Some("Promise");
Comment on lines +1317 to +1320

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== new.rs around promise_parent_runtime =="
sed -n '1290,1345p' crates/perry-codegen/src/lower_call/new.rs
echo
sed -n '1645,1675p' crates/perry-codegen/src/lower_call/new.rs
echo
sed -n '1900,1935p' crates/perry-codegen/src/lower_call/new.rs

echo "== new_helpers.rs relevant helpers =="
sed -n '300,430p' crates/perry-codegen/src/lower_call/new_helpers.rs

echo "== search for promise_parent_in_chain =="
rg -n "promise_parent_in_chain|promise_parent_runtime|map_set_default_super_kind|node_stream_parent_kind" crates/perry-codegen/src -S

Repository: PerryTS/perry

Length of output: 11503


Walk Promise ancestors before setting promise_parent_runtime. class Leaf extends Mid with Mid extends Promise and no constructors is treated as non-Promise here, so emit_promise_subclass_init never runs and new Leaf(executor) misses the hidden backing promise. Match the ancestor walk used by the Map/Set and stream helpers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-codegen/src/lower_call/new.rs` around lines 1317 - 1320, The
`promise_parent_runtime` check in `new.rs` only looks at the direct superclass,
so `Leaf extends Mid` where `Mid extends Promise` is missed and
`emit_promise_subclass_init` never runs. Update the logic around
`promise_parent_runtime` to walk the class ancestry the same way the Map/Set and
stream helper paths do, so any Promise-descended class without its own/imported
constructor is recognized. Keep the existing `has_own_ctor` and
`has_imported_ctor` gating, but determine the Promise parent status from the
full inheritance chain before deciding whether to emit the hidden backing
promise init.

let map_set_parent_kind = if !has_own_ctor && !has_imported_ctor {
map_set_default_super_kind(ctx.classes, class.extends_name.as_deref())
} else {
Expand Down Expand Up @@ -1653,6 +1658,10 @@ fn lower_new_impl(
);
found_inherited_ctor = true;
}
if promise_parent_runtime {
emit_promise_subclass_init(ctx, &lowered_args);
found_inherited_ctor = true;
}
// If no parent constructor was found (imported class with no
// inlineable constructor body), call the cross-module constructor.
// Refs #420: walk past empty-bodied ancestors with param_count==0
Expand Down Expand Up @@ -1909,6 +1918,7 @@ fn lower_new_impl(
if !has_own_ctor && (has_extends || class.extends_expr.is_some()) && !has_imported_ctor {
if builtin_parent_runtime.is_some()
|| fetch_parent_runtime.is_some()
|| promise_parent_runtime
|| (class.extends_expr.is_some() && !has_extends)
{
apply_field_initializers_recursive(ctx, class_name, FieldInitMode::SelfOnly)?;
Expand Down
25 changes: 25 additions & 0 deletions crates/perry-codegen/src/lower_call/new_helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,31 @@
use perry_hir::Expr;

use crate::expr::FnCtx;
use crate::types::DOUBLE;

/// Emit `js_promise_subclass_init(this, executor)` for a no-own-ctor
/// `class X extends Promise {}` on the runtime `new X(executor)` path. Runs the
/// ECMA-262 Promise constructor against a hidden backing cell stashed on the
/// freshly-allocated instance. `lowered_args` are the already-lowered `new`
/// arguments; the first is the executor.
pub(crate) fn emit_promise_subclass_init(ctx: &mut FnCtx<'_>, lowered_args: &[String]) {
let undef = crate::nanbox::double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED));
let executor = lowered_args
.first()
.cloned()
.unwrap_or_else(|| undef.clone());
let this_box = ctx
.this_stack
.last()
.cloned()
.map(|slot| ctx.block().load(DOUBLE, &slot))
.unwrap_or(undef);
ctx.block().call(
DOUBLE,
"js_promise_subclass_init",
&[(DOUBLE, &this_box), (DOUBLE, &executor)],
);
}

/// Generic "does any statement in this ctor body satisfy `stmt_pred` or
/// contain an expression satisfying `expr_pred`" walker, shared by the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub(crate) fn declare_streams_events(module: &mut LlModule) {
module.declare_function("js_event_emitter_subclass_init", DOUBLE, &[DOUBLE]); // #5137 EE subclass init
module.declare_function("js_array_subclass_init", DOUBLE, &[DOUBLE, DOUBLE]); // class extends Array
module.declare_function("js_map_set_subclass_init", DOUBLE, &[DOUBLE, I32, DOUBLE]); // class extends Map/Set
module.declare_function("js_promise_subclass_init", DOUBLE, &[DOUBLE, DOUBLE]); // class extends Promise
module.declare_function("js_node_stream_readable_new", DOUBLE, &[DOUBLE]);
module.declare_function(
"js_node_stream_readable_subclass_init",
Expand Down
2 changes: 1 addition & 1 deletion crates/perry-runtime/src/object/class_registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ pub(crate) use construct::{
extends_target_must_throw, function_would_have_own_prototype, is_callable_function_value,
js_value_is_constructor, lookup_prototype_method, nm_ctor_fs, nm_ctor_readline, nm_ctor_repl,
nm_ctor_stream, nm_ctor_tls, nm_ctor_tty, nm_ctor_vm, nm_ctor_wasi,
ordinary_function_prototype_value_for_read,
ordinary_function_prototype_value_for_read, promise_parent_in_chain,
};
pub use construct::{
js_ctor_return_override, js_function_prototype_value_for_read, js_new_function_construct,
Expand Down
5 changes: 5 additions & 0 deletions crates/perry-runtime/src/object/class_registry/class_meta.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,11 @@ pub(crate) fn identify_global_builtin_constructor(func_value: f64) -> Option<&'s
|| func_ptr == weak_map_constructor_call_thunk as *const u8 as usize
|| func_ptr == weak_set_constructor_call_thunk as *const u8 as usize
|| func_ptr == weak_ref_constructor_call_thunk as *const u8 as usize
// `class X extends Promise` needs its parent VALUE recognized as the
// Promise constructor (for the runtime `new Subclass` /
// `NewPromiseCapability(Subclass)` path). The Promise ctor value
// carries `promise_constructor_call_thunk`.
|| func_ptr == promise_constructor_call_thunk as *const u8 as usize
|| func_ptr
== crate::messaging::js_message_channel_constructor_call_error as *const u8
as usize
Expand Down
42 changes: 42 additions & 0 deletions crates/perry-runtime/src/object/class_registry/construct.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1374,6 +1374,34 @@ fn new_target_class_id(new_target: f64) -> Option<u32> {
constructor_class_ref_id(new_target).or_else(|| class_object_class_id(new_target))
}

/// True when class `cid` (or an ancestor) `extends Promise` — its registered
/// dynamic-parent value resolves to the intrinsic `Promise` constructor. Used to
/// run `js_promise_subclass_init` on the dynamic (runtime) `new Subclass(exec)`
/// path, where codegen's `super()` Promise branch never emitted the init (e.g.
/// `NewPromiseCapability(Subclass)` inside a combinator, which calls the runtime
/// `js_new_function_construct` directly rather than a compiled `new`).
pub(crate) fn promise_parent_in_chain(class_id: u32) -> bool {
let mut cid = class_id;
let mut depth = 0u32;
while depth < 32 && cid != 0 {
let parent_val = js_get_dynamic_parent_value(cid);
if matches!(
identify_global_builtin_constructor(parent_val),
Some("Promise")
) {
return true;
}
match get_parent_class_id(cid) {
Some(p) if p != 0 && p != cid => {
cid = p;
depth += 1;
}
_ => break,
}
}
false
}

unsafe fn construct_registered_class_ref(
target_cid: u32,
instance_cid: u32,
Expand Down Expand Up @@ -1419,6 +1447,20 @@ unsafe fn construct_registered_class_ref(
super::super::attach_fetch_handle_for_construction(inst, kind, args_ptr, args_len);
}
}
// ClassRef `new` of a Promise subclass — run the Promise constructor against
// a hidden backing cell (only when the compiled ctor's `super()` didn't
// already attach one). `NewPromiseCapability(Subclass)` reaches here.
if promise_parent_in_chain(target_cid) {
let inst_val = crate::value::js_nanbox_pointer(inst as i64);
if crate::promise::subclass_backing_promise(inst_val).is_none() {
let executor = if args_len >= 1 && !args_ptr.is_null() {
*args_ptr
} else {
f64::from_bits(crate::value::TAG_UNDEFINED)
};
crate::promise::js_promise_subclass_init(inst_val, executor);
}
}
crate::value::js_nanbox_pointer(inst as i64)
}

Expand Down
20 changes: 20 additions & 0 deletions crates/perry-runtime/src/object/class_registry/parent_static.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1197,6 +1197,26 @@ pub unsafe extern "C" fn js_class_static_method_call(
{
return result;
}
// `class X extends Promise` — inherited builtin static (`X.all(...)`,
// `X.resolve(...)`, …). Dispatch the spec static with `this` = the subclass
// receiver so `NewPromiseCapability(X)` constructs the subclass. Resolves the
// reified static value and calls it (its thunk reads `this` from the
// implicit-this slot, already bound to `receiver` by the caller above).
if super::promise_parent_in_chain(class_id)
&& crate::object::promise_static_function_spec(name).is_some()
{
let static_val = crate::object::js_promise_static_function_value(name.as_ptr(), name.len());
if static_val.to_bits() != crate::value::TAG_UNDEFINED {
// The reified static thunk reads its `this` constructor from the
// implicit-this slot, so bind it to the subclass receiver for the
// duration of the call — `NewPromiseCapability(receiver)` then
// constructs the subclass.
let prev_this = crate::object::js_implicit_this_set(receiver);
let result = crate::closure::js_native_call_value(static_val, args_ptr, args_len);
crate::object::js_implicit_this_set(prev_this);
return result;
}
}
// True miss: no static method and no callable static field resolved on the
// class chain. We hand back the receiver (load-bearing for effect's
// `.pipe()`-during-init chains, #687) — but that silent class-ref is exactly
Expand Down
40 changes: 40 additions & 0 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 @@ -118,6 +118,34 @@ pub extern "C" fn js_object_get_field_by_name(
}
}
}
// `class X extends Promise` instance — a value read of `then`/`catch`/
// `finally` (`p.then` / `typeof p.finally`, and codegen's `p.finally(cb)`
// which reads the property first) must resolve the reified Promise prototype
// method. The generic prototype walk does not surface these builtin
// `Promise.prototype` methods for a subclass instance, so hook them here when
// no own key shadows them. The method thunks unwrap the backing cell from the
// implicit-this receiver (see `promise_prototype_receiver`).
if !key.is_null()
&& ((obj as u64) >> 48) == 0
&& crate::value::addr_class::is_above_handle_band(obj as usize)
{
unsafe {
let name_ptr = (key as *const u8).add(std::mem::size_of::<crate::StringHeader>());
let name_len = (*key).byte_len as usize;
let name =
std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len)).unwrap_or("");
if matches!(name, "then" | "catch" | "finally")
&& !super::super::own_key_present(obj as *mut ObjectHeader, key)
{
let boxed = f64::from_bits(JSValue::pointer(obj as *const u8).bits());
if crate::promise::subclass_backing_promise(boxed).is_some() {
if let Some(m) = crate::promise::promise_proto_method(name) {
return JSValue::from_bits(m.to_bits());
}
}
}
}
}
// A per-evaluation class object (`ClassExprFresh`, #1772/#1787) reaches
// here as a RAW heap pointer (a real ObjectHeader, so its top 16 address
// bits are 0 — distinguishing it from a `0x7FFE` class-ref value or any
Expand Down Expand Up @@ -753,6 +781,18 @@ pub extern "C" fn js_object_get_field_by_name(
let result = js_class_method_bind(class_value, heap_name, name_len);
return JSValue::from_bits(result.to_bits());
}
// `class X extends Promise` — a value read of an inherited
// builtin static (`X.resolve`, `X.all`, …) resolves to the
// reified Promise static (so `X.resolve.bind(X)` works). Only
// fires when no user static shadowed it above.
if super::super::promise_parent_in_chain(class_id)
&& super::super::promise_static_function_spec(name).is_some()
{
let v = super::super::js_promise_static_function_value(name_ptr, name_len);
if v.to_bits() != crate::value::TAG_UNDEFINED {
return JSValue::from_bits(v.to_bits());
}
}
if let Some(v) =
super::super::class_registry::class_static_accessor_getter_value(
class_id,
Expand Down
19 changes: 19 additions & 0 deletions crates/perry-runtime/src/object/native_call_method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1505,6 +1505,25 @@ pub unsafe extern "C" fn js_native_call_method(
}
}

// `class X extends Promise`: inherited `then`/`catch`/`finally` dispatch
// against the hidden backing Promise cell. A subclass override (own field /
// vtable / prototype method) has already been consulted above, so only a
// genuinely inherited builtin reaches here. Bind `this` to the instance so
// the reified thunk unwraps the backing cell and species-chains via
// `receiver.constructor`. (Covers the `X.resolve().finally().then()` chains
// that codegen dispatches straight through `js_native_call_method`.)
if jsval.is_pointer() && matches!(method_name, "then" | "catch" | "finally") {
if crate::promise::subclass_backing_promise(object).is_some() {
if let Some(m) = crate::promise::promise_proto_method(method_name) {
let args = refreshed_args();
let prev_this = crate::object::js_implicit_this_set(object);
let result = crate::closure::js_native_call_value(m, args.as_ptr(), args.len());
crate::object::js_implicit_this_set(prev_this);
return result;
}
}
}

// `class X extends Temporal.<Type>`: the prototype methods (`add`/`abs`/
// `toString`/…) dispatch via the Temporal brand on the underlying cell, not
// the JS prototype chain. All user-defined dispatch (own fields, vtable,
Expand Down
17 changes: 14 additions & 3 deletions crates/perry-runtime/src/promise/checked_dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,12 @@ pub extern "C" fn js_promise_then_checked(
crate::object::js_implicit_this_set(prev);
return result;
}
if promise_has_own_constructor(promise_addr) {
if promise_has_own_constructor(promise_addr)
|| subclass::subclass_backing_promise(promise_val).is_some()
{
// Own `constructor` override OR a `class X extends Promise` instance:
// route through the SpeciesConstructor-aware thunk, which unwraps the
// backing cell and reads `this.constructor` for species chaining.
let prev = crate::object::js_implicit_this_set(promise_val);
let result = promise_prototype_then_thunk(std::ptr::null(), on_fulfilled, on_rejected);
crate::object::js_implicit_this_set(prev);
Expand All @@ -73,7 +78,10 @@ pub extern "C" fn js_promise_then_checked(
#[no_mangle]
pub extern "C" fn js_promise_catch_checked(promise_val: f64, on_rejected: f64) -> f64 {
let promise_addr = (promise_val.to_bits() & crate::value::POINTER_MASK) as usize;
if promise_has_own_property(promise_addr, "then") || promise_has_own_constructor(promise_addr) {
if promise_has_own_property(promise_addr, "then")
|| promise_has_own_constructor(promise_addr)
|| subclass::subclass_backing_promise(promise_val).is_some()
{
let prev = crate::object::js_implicit_this_set(promise_val);
let result = promise_prototype_catch_thunk(std::ptr::null(), on_rejected);
crate::object::js_implicit_this_set(prev);
Expand All @@ -86,7 +94,10 @@ pub extern "C" fn js_promise_catch_checked(promise_val: f64, on_rejected: f64) -
#[no_mangle]
pub extern "C" fn js_promise_finally_checked(promise_val: f64, on_finally: f64) -> f64 {
let promise_addr = (promise_val.to_bits() & crate::value::POINTER_MASK) as usize;
if promise_has_own_property(promise_addr, "then") || promise_has_own_constructor(promise_addr) {
if promise_has_own_property(promise_addr, "then")
|| promise_has_own_constructor(promise_addr)
|| subclass::subclass_backing_promise(promise_val).is_some()
{
let prev = crate::object::js_implicit_this_set(promise_val);
let result = promise_prototype_finally_thunk(std::ptr::null(), on_finally);
crate::object::js_implicit_this_set(prev);
Expand Down
3 changes: 3 additions & 0 deletions crates/perry-runtime/src/promise/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ pub mod microtasks;
pub mod native_async;
pub mod scanners;
pub mod spec_combinators;
pub mod subclass;
pub mod then;

// ─── Explicit named re-exports ────────────────────────────────────
Expand Down Expand Up @@ -67,6 +68,8 @@ pub use spec_combinators::{
js_promise_reject_spec, js_promise_resolve_spec, js_promise_try_spec,
js_promise_with_resolvers_spec,
};
pub use subclass::js_promise_subclass_init;
pub(crate) use subclass::subclass_backing_promise;
pub(crate) use then::{
box_promise_ptr, js_promise_attach_handlers, js_promise_attach_settle_listener,
mark_rejection_handled, promise_has_own_constructor, promise_has_own_property,
Expand Down
Loading
Loading