From 20e6d5e22608581939c2bb38a760774c98ea1cb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Wed, 23 Sep 2026 06:58:32 +0200 Subject: [PATCH 1/2] fix(runtime): preserve URL state in subclasses --- .../perry-codegen/src/expr/instance_misc1.rs | 2 + .../perry-codegen/src/expr/this_super_call.rs | 11 +++--- crates/perry-codegen/src/lower_call/new.rs | 1 + .../src/object/class_registry/construct.rs | 1 + crates/perry-runtime/src/object/instanceof.rs | 2 + .../src/object/instanceof/static_dispatch.rs | 9 +++++ test-files/test_gap_10639_url_subclass.ts | 38 +++++++++++++++++++ 7 files changed, 59 insertions(+), 5 deletions(-) create mode 100644 test-files/test_gap_10639_url_subclass.ts diff --git a/crates/perry-codegen/src/expr/instance_misc1.rs b/crates/perry-codegen/src/expr/instance_misc1.rs index 1596a9a7c2..3ca828dd0e 100644 --- a/crates/perry-codegen/src/expr/instance_misc1.rs +++ b/crates/perry-codegen/src/expr/instance_misc1.rs @@ -122,6 +122,7 @@ pub(crate) fn builtin_parent_reserved_class_id(name: &str) -> Option { "BigInt64Array" => 0xFFFF0039, "BigUint64Array" => 0xFFFF003A, "Function" => 0xFFFF00F0, + "URL" => 0xFFFF0063, // #10556: `class Sub extends EventEmitter {}` — same shape as the // Array/Map/Set/Error builtins above. Without this edge, // `new Sub() instanceof EventEmitter` never reaches the class-chain @@ -554,6 +555,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { "ReadableStream" => 0xFFFF0060u32, "WritableStream" => 0xFFFF0061u32, "TransformStream" => 0xFFFF0062u32, + "URL" => 0xFFFF0063u32, // node:stream/web codec stream constructors are heap // ObjectHeader instances with runtime-owned class IDs. "TextEncoderStream" => 0x7FFFFF30u32, diff --git a/crates/perry-codegen/src/expr/this_super_call.rs b/crates/perry-codegen/src/expr/this_super_call.rs index f0d5af3d44..6566512d59 100644 --- a/crates/perry-codegen/src/expr/this_super_call.rs +++ b/crates/perry-codegen/src/expr/this_super_call.rs @@ -130,11 +130,10 @@ pub(crate) fn bind_derived_this_after_super(ctx: &mut FnCtx<'_>) { /// SuperCall arms) that can appear as a class heritage. `super(...)` to these /// must NOT be routed through the runtime-value dispatch path /// (`js_fetch_or_value_super`), which would invoke e.g. `Map()` without `new` -/// and throw "Constructor requires 'new'". Perry cannot yet give a subclass -/// instance the built-in's internal slots, so `super()` is a best-effort no-op -/// here — enough that `class M extends Map { constructor(){ super(); } }` -/// constructs without throwing. Refs class/subclass/builtin-objects/*/ -/// super-must-be-called. +/// and throw "Constructor requires 'new'". Dedicated arms below either install +/// the built-in state on the provisional receiver or construct a branded value +/// with the subclass as `newTarget`. Refs class/subclass/builtin-objects/*/ +/// super-must-be-called and #10639. pub(crate) fn is_other_builtin_constructor_name(name: &str) -> bool { matches!( name, @@ -152,6 +151,7 @@ pub(crate) fn is_other_builtin_constructor_name(name: &str) -> bool { | "String" | "Date" | "RegExp" + | "URL" | "Promise" | "Function" | "BigInt" @@ -1183,6 +1183,7 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { | "String" | "Date" | "RegExp" + | "URL" | "Function" | "BigInt" | "Symbol" diff --git a/crates/perry-codegen/src/lower_call/new.rs b/crates/perry-codegen/src/lower_call/new.rs index ab8ed03fdf..57c3bdaf29 100644 --- a/crates/perry-codegen/src/lower_call/new.rs +++ b/crates/perry-codegen/src/lower_call/new.rs @@ -1445,6 +1445,7 @@ fn lower_new_impl_inner<'a>( | "String" | "Date" | "RegExp" + | "URL" | "Function" | "BigInt" | "Symbol" diff --git a/crates/perry-runtime/src/object/class_registry/construct.rs b/crates/perry-runtime/src/object/class_registry/construct.rs index a3065ae5fb..290afe302e 100644 --- a/crates/perry-runtime/src/object/class_registry/construct.rs +++ b/crates/perry-runtime/src/object/class_registry/construct.rs @@ -1771,6 +1771,7 @@ pub unsafe extern "C" fn js_new_function_construct_with_new_target( | "Number" | "String" | "RegExp" + | "URL" | "Function" ) { let scope = crate::gc::RuntimeHandleScope::new(); diff --git a/crates/perry-runtime/src/object/instanceof.rs b/crates/perry-runtime/src/object/instanceof.rs index 6a638a6fe9..3d2aa004a4 100644 --- a/crates/perry-runtime/src/object/instanceof.rs +++ b/crates/perry-runtime/src/object/instanceof.rs @@ -17,6 +17,7 @@ const CLASS_ID_SUBTLE_CRYPTO: u32 = 0xFFFF00C1; const CLASS_ID_CRYPTO_KEY: u32 = 0xFFFF00C2; /// `value instanceof Function` reserved id (see `js_instanceof`). const CLASS_ID_FUNCTION: u32 = 0xFFFF00F0; +const CLASS_ID_URL: u32 = 0xFFFF0063; mod dynamic_dispatch; mod static_dispatch; @@ -212,6 +213,7 @@ pub(crate) fn global_builtin_constructor_class_id(name: &str) -> u32 { "BigInt" => 0xFFFF00D3, "Symbol" => 0xFFFF00D4, "Date" => 0xFFFF0020, + "URL" => CLASS_ID_URL, "Error" => crate::error::CLASS_ID_ERROR, "TypeError" => crate::error::CLASS_ID_TYPE_ERROR, "RangeError" => crate::error::CLASS_ID_RANGE_ERROR, diff --git a/crates/perry-runtime/src/object/instanceof/static_dispatch.rs b/crates/perry-runtime/src/object/instanceof/static_dispatch.rs index 7e512bb302..a2190aa444 100644 --- a/crates/perry-runtime/src/object/instanceof/static_dispatch.rs +++ b/crates/perry-runtime/src/object/instanceof/static_dispatch.rs @@ -108,6 +108,15 @@ pub extern "C" fn js_instanceof(value: f64, class_id: u32) -> f64 { false_val }; } + if class_id == CLASS_ID_URL { + let addr = value_addr(value); + let branded = addr != 0 && crate::url::is_url_object_shape(addr as *mut ObjectHeader); + return if branded || recorded_prototype_instanceof_builtin(value, "URL") == Some(true) { + true_val + } else { + false_val + }; + } // Keep in sync with perry-codegen/src/expr/instance_misc1.rs. let classic_stream_name = match class_id { 0xFFFF0070 => Some("Stream"), diff --git a/test-files/test_gap_10639_url_subclass.ts b/test-files/test_gap_10639_url_subclass.ts new file mode 100644 index 0000000000..7dcf901cc3 --- /dev/null +++ b/test-files/test_gap_10639_url_subclass.ts @@ -0,0 +1,38 @@ +// #10639: a URL subclass must receive URL's internal state while preserving +// its declared prototype and fields. Both the implicit derived constructor and +// a written `super(input, base)` call used to leave hostname/pathname undefined. + +class ImplicitUrl extends URL { + kind = "implicit"; +} + +const implicit = new ImplicitUrl("child?q=1", "https://example.com/base/"); +console.log( + "implicit:", + implicit.hostname, + implicit.pathname, + implicit.search, + implicit.kind, + implicit instanceof ImplicitUrl, + implicit instanceof URL, +); + +class ExplicitUrl extends URL { + kind: string; + + constructor(input: string, base: string) { + super(input, base); + this.kind = "explicit"; + } +} + +const explicit = new ExplicitUrl("../next#hash", "https://perry.dev/a/b/"); +console.log( + "explicit:", + explicit.hostname, + explicit.pathname, + explicit.hash, + explicit.kind, + explicit instanceof ExplicitUrl, + explicit instanceof URL, +); From b921d04de7e5f12f4b9586ca5a3f6fecd317241c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Wed, 23 Sep 2026 06:58:56 +0200 Subject: [PATCH 2/2] docs: add changelog for URL subclass fix --- changelog.d/11090-url-subclass-state.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 changelog.d/11090-url-subclass-state.md diff --git a/changelog.d/11090-url-subclass-state.md b/changelog.d/11090-url-subclass-state.md new file mode 100644 index 0000000000..a2b290f315 --- /dev/null +++ b/changelog.d/11090-url-subclass-state.md @@ -0,0 +1,3 @@ +### Fixed + +- Preserve parsed URL state when constructing classes that extend `URL`, including subclasses with an implicit constructor and constructors that explicitly call `super(...)`. URL subclasses now retain their own prototype and fields while exposing the expected URL properties and `instanceof` behavior.