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
22 changes: 22 additions & 0 deletions crates/perry-runtime/src/object/class_registry/construct.rs
Original file line number Diff line number Diff line change
Expand Up @@ -784,6 +784,28 @@ pub unsafe extern "C" fn js_new_function_construct(
let encoder = crate::text::js_text_encoder_new();
return crate::value::js_nanbox_pointer(encoder);
}
// `new P(executor)` where `P` holds the global Promise constructor
// VALUE (`const P = Promise;` / a polyfill alias). Without this arm
// the match fell through to the closure fallback, which CALLED
// `promise_constructor_call_thunk` as a plain function — throwing
// "Constructor Promise requires 'new'". Route to the same executor
// construction as the static literal lowering.
"Promise" => {
let executor = args
.first()
.copied()
.unwrap_or_else(|| f64::from_bits(crate::value::TAG_UNDEFINED));
let bits = executor.to_bits();
let exec_ptr = if (bits & crate::value::TAG_MASK) == crate::value::POINTER_TAG {
(bits & crate::value::POINTER_MASK) as *const crate::closure::ClosureHeader
} else {
// Non-callable executor: `js_promise_new_with_executor`
// validates and throws the spec TypeError synchronously.
std::ptr::null()
};
let promise = crate::promise::js_promise_new_with_executor(exec_ptr);
return crate::value::js_nanbox_pointer(promise as i64);
}
"TextDecoder" => {
let label = args
.first()
Expand Down
59 changes: 59 additions & 0 deletions test-files/test_gap_dynamic_builtin_construct_dispatch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Dynamically-constructed builtin instances must behave like their literal
// counterparts. `new X()` with a LITERAL name is codegen-recognized; obtaining
// the constructor as a VALUE — `require("util").TextEncoder`, `const P =
// Promise`, a property read off a namespace — routes through the runtime's
// dynamic construct + dynamic method dispatch, where three gaps lived:
//
// 1. TextEncoder instances are a shared small-handle sentinel with no dynamic
// method dispatch — `enc.encode(...)` / `enc.encodeInto(...)` returned
// `undefined`. react-server-dom's flight byte-writer does exactly
// `var T = new (require("util").TextEncoder)` and then
// `T.encodeInto(row, scratch).read`, so the flight flush threw and every
// Next.js App Router dynamic route hung (#5989).
// 2. TextDecoder handles had no dynamic `decode` dispatch.
// 3. `new P(executor)` where P holds the global Promise constructor value fell
// through to the call path and threw "Constructor Promise requires 'new'".
//
// Validated byte-for-byte against `node --experimental-strip-types`.

import { TextEncoder as UtilTextEncoder, TextDecoder as UtilTextDecoder } from "util";

// (1) TextEncoder obtained as a value — the flight byte-writer shape.
const T: any = UtilTextEncoder;
const enc: any = new T();
const scratch = new Uint8Array(8);
const r: any = enc.encodeInto("hi", scratch);
console.log(r.read, r.written, scratch[0], scratch[1]);
const encoded: any = enc.encode("xyz");
console.log(encoded.length, encoded[0], encoded[2]);

// multi-byte + partial destination through the dynamic path
const small = new Uint8Array(3);
const rp: any = enc.encodeInto("héllo", small);
console.log(rp.read <= 3, rp.written <= 3);

// (2) TextDecoder obtained as a value.
const D: any = UtilTextDecoder;
const dec: any = new D();
console.log(dec.decode(new Uint8Array([104, 101, 106])));
console.log(JSON.stringify(dec.decode()));

// (3) Promise constructor obtained as a value (polyfill alias shape).
const P: any = Promise;
const p = new P((resolve: any) => resolve(42));
p.then((v: any) => console.log("resolved", v));

// executor receives working resolve/reject; rejection path too
const q = new P((_res: any, rej: any) => rej(new Error("nope")));
q.catch((e: any) => console.log("rejected", e.message));

// (4) Map/Set/WeakMap constructor values (regression guard for the arms that
// already worked — construction via value + native method dispatch).
const M: any = Map;
const m = new M([["k", 7]]);
m.set("j", 8);
console.log(m.get("k"), m.get("j"), m.size);
const S: any = Set;
const s = new S([1, 2]);
s.add(3);
console.log(s.has(2), s.has(3), s.size);
Loading