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
1 change: 1 addition & 0 deletions crates/perry-codegen/src/expr/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1065,5 +1065,6 @@ mod static_method;
mod string_regex_proc;
mod super_method;
mod this_super_call;
pub(crate) use this_super_call::is_other_builtin_constructor_name;
mod unary;
mod url_main;
28 changes: 27 additions & 1 deletion crates/perry-codegen/src/lower_call/new.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1677,7 +1677,33 @@ fn lower_new_impl(
// `expr/this_super_call.rs`: resolve the decl-time-registered parent
// value and dispatch it on `this` via `js_fetch_or_value_super`, which
// binds IMPLICIT_THIS to the instance for the duration of the call.
if !found_inherited_ctor && class.extends_expr.is_some() {
//
// #5657: a native BUILTIN base (`class X extends ArrayBuffer / Map /
// Promise / %TypedArray% / RegExp / Function / …`) is also captured as
// `extends_expr` (a bare `ArrayBuffer` Ident doesn't resolve through
// `lookup_class`), but its parent VALUE is a builtin constructor that
// rejects being *called* as a plain function — `js_fetch_or_value_super`
// would route it through `js_native_call_value`, throwing "X is not a
// function" / "Constructor X requires 'new'". Perry can't give a subclass
// instance the builtin's internal slots, so `super()` to such a base is a
// best-effort no-op (the instance is already allocated with the correct
// dynamic-parent prototype chain, so `instanceof` holds). Skip the
// dispatch for those names — mirroring the identical guard the explicit
// `Expr::SuperCall` arm already applies via `is_other_builtin_constructor_name`
// (`expr/this_super_call.rs`). Request/Response/Error are deliberately NOT
// in that set: they DO need the dispatch (native fetch-handle attach /
// callable error thunk), so they keep running it. This is a fast-path
// skip on the textual name; an ALIASED builtin parent (`const AB =
// ArrayBuffer; class X extends AB {}`) whose `extends_name` isn't a known
// builtin still emits the call, but the runtime backstops it by value —
// `js_fetch_or_value_super` no-ops the same builtin set via
// `is_uncallable_builtin_super_parent` (perry-runtime, kept in lockstep).
let parent_is_uncallable_builtin = class
.extends_name
.as_deref()
.map(crate::expr::is_other_builtin_constructor_name)
.unwrap_or(false);
if !found_inherited_ctor && class.extends_expr.is_some() && !parent_is_uncallable_builtin {
Comment on lines +1701 to +1706

@coderabbitai coderabbitai Bot Jun 25, 2026 •

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

Handle aliased uncallable builtin parents too.

This guard only checks the textual extends_name, so const AB = ArrayBuffer; class X extends AB {} still reaches js_fetch_or_value_super and calls the builtin as a function. Consider adding the same no-op guard in the runtime value path after resolving parent_val, while keeping Request/Response/Error dispatchable.

🤖 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 1696 - 1701, The
uncallable-builtin guard in new.rs only checks the textual extends_name, so
aliased parents can still slip through and get treated as callable. Update the
inheritance handling around class lowering and js_fetch_or_value_super to add
the same no-op check after resolving parent_val, so aliases of ArrayBuffer-like
builtins are also blocked, while preserving the special dispatch behavior for
Request, Response, and Error. Use the existing parent_is_uncallable_builtin
logic and the runtime parent resolution path to keep both name-based and
value-based cases aligned.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in the runtime: js_fetch_or_value_super now no-ops by VALUE via is_uncallable_builtin_super_parent (kept in lockstep with codegen's is_other_builtin_constructor_name), so aliased parents like const AB = ArrayBuffer; class X extends AB {} are caught too — verified now constructs. Request/Response/Error stay dispatchable.

if let Some(cid) = ctx.class_ids.get(class_name).copied().filter(|c| *c != 0) {
let undef_lit = double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED));
let parent_val = ctx.block().call(
Expand Down
58 changes: 58 additions & 0 deletions crates/perry-runtime/src/object/global_this/fetch_globals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,55 @@ static KEEP_JS_REQUEST_SUBCLASS_INIT: extern "C" fn(f64, f64, f64) -> f64 =
static KEEP_JS_RESPONSE_SUBCLASS_INIT: extern "C" fn(f64, f64, f64) -> f64 =
js_response_subclass_init;

/// #5657: native builtin constructors that REJECT being called as a plain
/// function (`X is not a function` / `Constructor X requires 'new'`). A
/// `class D extends <one of these> {}` must NOT run the parent as a function in
/// `super()` — Perry can't give the instance the builtin's internal slots, so
/// `super()` is a best-effort no-op (the instance is already allocated with the
/// correct dynamic-parent prototype chain, so `instanceof` holds). This is the
/// VALUE-based companion to codegen's name-based guard
/// (`crate::expr::is_other_builtin_constructor_name` in
/// `perry-codegen/src/expr/this_super_call.rs`); the two lists must stay in
/// lockstep. Resolving by value (not textual name) also catches aliased parents
/// — `const AB = ArrayBuffer; class X extends AB {}` — which the name guard
/// can't see. `Request`/`Response` (native fetch-handle attach) and the `Error`
/// family (callable error thunk) are deliberately EXCLUDED: they keep their
/// dispatch.
fn is_uncallable_builtin_super_parent(name: &str) -> bool {
matches!(
name,
"Map"
| "Set"
| "WeakMap"
| "WeakSet"
| "Array"
| "ArrayBuffer"
| "SharedArrayBuffer"
| "DataView"
| "Boolean"
| "Number"
| "String"
| "Date"
| "RegExp"
| "Promise"
| "Function"
| "BigInt"
| "Symbol"
| "Object"
| "Int8Array"
| "Uint8Array"
| "Uint8ClampedArray"
| "Int16Array"
| "Uint16Array"
| "Int32Array"
| "Uint32Array"
| "Float32Array"
| "Float64Array"
| "BigInt64Array"
| "BigUint64Array"
)
}

/// `super(...)` for `class X extends <runtime-value constructor>` where the
/// parent expression is an alias of the global `Request`/`Response` constructor
/// — e.g. `@hono/node-server`'s `class Request extends GlobalRequest` with
Expand Down Expand Up @@ -442,6 +491,15 @@ pub unsafe extern "C" fn js_fetch_or_value_super(
_ => None,
}
});
// #5657: a native builtin base that can't be called as a function (incl.
// ALIASED parents — `const AB = ArrayBuffer; class X extends AB {}` — which
// the codegen name guard can't see). No-op rather than throwing
// "X is not a function" / "Constructor X requires 'new'".
if let Some(name) = kind {
if is_uncallable_builtin_super_parent(name) {
return undef;
}
}
match kind {
Some("Request") | Some("Response") => {
let arg0 = if args_len >= 1 && !args_ptr.is_null() {
Expand Down
Loading