Skip to content
Closed
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 changelog.d/8724-captured-arraybuffer-construct.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix a regression where constructing `ArrayBuffer`, `SharedArrayBuffer`, or `DataView` through a captured value — `const D = DataView; new D(buf)`, `Reflect.construct(DataView, …)`, `class X extends DataView {}`, `new globalThis.ArrayBuffer(n)` — threw `TypeError: Constructor requires 'new'`. Their global [[Call]] behavior had moved to the shared construct-only thunk without adding that thunk to `identify_global_builtin_constructor`'s allow-list, so the dynamic-`new` path no longer recognized a captured constructor and fell through to the bare-call thunk. Minified bundles capture these globals into locals pervasively; this broke module init (e.g. the natively-compiled Claude Code cli.js bundle failed at startup on nearly every command). Direct `new DataView(buf)` was unaffected.
13 changes: 13 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 @@ -186,6 +186,19 @@ pub(crate) fn identify_global_builtin_constructor(func_value: f64) -> Option<&'s
let is_global_builtin_func = func_ptr
== global_this_builtin_noop_thunk as *const u8 as usize
|| func_ptr == typed_array_constructor_call_thunk as *const u8 as usize
// ArrayBuffer / SharedArrayBuffer / DataView carry the shared
// construct-only call thunk (populate.rs). When one is captured into
// a variable and constructed — `const D = DataView; new D(buf)`,
// `Reflect.construct(DataView, …)`, `class X extends DataView` — the
// dynamic-`new` path lands here and must recognize the thunk so the
// singleton walk recovers the name and construct.rs's
// "ArrayBuffer"/"SharedArrayBuffer"/"DataView" arms build it, instead
// of falling through to invoke the bare-call thunk (which throws
// "Constructor requires 'new'"). Minified bundles capture these
// globals into locals pervasively (the Claude Code cli.js bundle
// fails at module init without this). Regression from the thunk swap
// in 06e1ab349 — before it these carried the recognized noop thunk.
|| func_ptr == construct_only_builtin_call_thunk as *const u8 as usize
// #4102: `Array`/`Object`/`Date` constructor *values* carry their own
// coercion thunks (not the shared noop thunk), so the dynamic
// `instanceof` / reflective `@@hasInstance` path could not recover
Expand Down
33 changes: 33 additions & 0 deletions tests/issue_8724_captured_arraybuffer_construct.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// #8724: constructing ArrayBuffer / SharedArrayBuffer / DataView through a
// CAPTURED value (a local holding the builtin, Reflect.construct, or a
// subclass) must build the object, not throw "Constructor requires 'new'".
// Regression from routing their [[Call]] to the shared construct-only thunk
// without teaching `identify_global_builtin_constructor` about it, so the
// dynamic-`new` path stopped recognizing a captured constructor. Minified
// bundles capture these globals into locals everywhere.

function run(label: string, fn: () => void): void {
try {
fn();
console.log(label, "ok");
} catch (e: any) {
console.log(label, "THREW:", e.message);
}
}

const ab = () => new ArrayBuffer(8);

run("direct DataView", () => { const dv = new DataView(ab()); if (dv.byteLength !== 8) throw new Error("len"); });
run("captured DataView", () => { const D = DataView; const dv = new D(ab()); if (dv.byteLength !== 8) throw new Error("len"); });
run("Reflect.construct DataView", () => { const dv = Reflect.construct(DataView, [ab()]); if ((dv as DataView).byteLength !== 8) throw new Error("len"); });
run("subclass DataView", () => { class MV extends DataView {} const dv = new MV(ab()); if (dv.byteLength !== 8) throw new Error("len"); });
run("captured ArrayBuffer", () => { const AB = ArrayBuffer; const b = new AB(8); if (b.byteLength !== 8) throw new Error("len"); });
run("captured ArrayBuffer resizable", () => { const AB = ArrayBuffer; const b = new AB(8, { maxByteLength: 16 }); if (b.byteLength !== 8) throw new Error("len"); });
run("captured SharedArrayBuffer", () => { const SAB = SharedArrayBuffer; const s = new SAB(8); if (s.byteLength !== 8) throw new Error("len"); });
run("globalThis ArrayBuffer", () => { const b = new (globalThis as any).ArrayBuffer(8); if (b.byteLength !== 8) throw new Error("len"); });

// The captured DataView must be fully functional, not a branded-but-empty stub.
const D2 = DataView;
const dv2 = new D2(ab());
dv2.setInt32(0, 0x41424344);
console.log("readback", dv2.getInt32(0).toString(16));
Loading