diff --git a/crates/perry-runtime/src/object/object_ops/prototype.rs b/crates/perry-runtime/src/object/object_ops/prototype.rs index c9c60f6a63..189bab2b6a 100644 --- a/crates/perry-runtime/src/object/object_ops/prototype.rs +++ b/crates/perry-runtime/src/object/object_ops/prototype.rs @@ -115,7 +115,23 @@ pub extern "C" fn js_object_create(proto_value: f64) -> f64 { // instanceof C` was always false even though property / // getter dispatch through the chain worked correctly. let parent_class_id = unsafe { (*proto_ptr).class_id }; - if parent_class_id != 0 && parent_class_id != cid { + // #8343 followup: `NATIVE_MODULE_CLASS_ID` (0xFFFFFFFE) is a + // sentinel tagging native-module namespace objects, NOT a real + // declared class. Registering it as a synthetic class's parent + // makes `get_parent_class_id` return it, and + // `js_object_get_prototype_of`'s class-ref branch then returns + // the raw sentinel as an INT32-tagged class ref (`-2`). + // `Object.create(that)` rejects it with + // `TypeError: Object prototype may only be an Object or null: -2` + // (rolldown's `__toESM` → `Object.create(Object.getPrototypeOf(mod))` + // chain). Skip the registration so the synthetic class is treated + // as a root — its prototype is already stored in + // `CLASS_PROTOTYPE_OBJECTS` by `class_prototype_object_root_store` + // above, which is what `getPrototypeOf` reads. + if parent_class_id != 0 + && parent_class_id != cid + && parent_class_id != super::super::native_module::NATIVE_MODULE_CLASS_ID + { register_class(cid, parent_class_id); } class_id = cid; @@ -324,7 +340,16 @@ pub extern "C" fn js_object_get_prototype_of(obj_value: f64) -> f64 { if top16 == 0x7FFE { let class_id = (bits & 0xFFFF_FFFF) as u32; if let Some(parent_id) = get_parent_class_id(class_id) { - if parent_id != 0 { + // #8343 followup: `NATIVE_MODULE_CLASS_ID` (0xFFFFFFFE) is a + // sentinel, not a real class. A prior `Object.create(proto)` whose + // `proto` was a native-module namespace object registered it as a + // synthetic class's parent. Returning the raw sentinel as an + // INT32-tagged class ref (`-2`) trips `Object.create` with + // `TypeError: Object prototype may only be an Object or null: -2`. + // Treat it as a root: a native-module namespace's [[Prototype]] is + // %Object.prototype%, so the synthetic class whose proto was that + // namespace inherits Object.prototype too. + if parent_id != 0 && parent_id != super::super::native_module::NATIVE_MODULE_CLASS_ID { let parent_bits = 0x7FFE_0000_0000_0000u64 | (parent_id as u64); return f64::from_bits(parent_bits); } diff --git a/crates/perry/tests/cjs_wrap_builtin_require.rs b/crates/perry/tests/cjs_wrap_builtin_require.rs index 122843d641..4b4342c541 100644 --- a/crates/perry/tests/cjs_wrap_builtin_require.rs +++ b/crates/perry/tests/cjs_wrap_builtin_require.rs @@ -162,3 +162,80 @@ console.log("join:", join("a", "b")); format!("platform: {expected_platform}\narch: string\njoin: a/b\n") ); } + +/// #8343 followup: `Object.create(require("process"))` stamps the result with +/// a synthetic class_id and — pre-fix — registered `NATIVE_MODULE_CLASS_ID` +/// (0xFFFFFFFE, the `-2` sentinel) as that synthetic class's parent via +/// `register_class`. Later, `Object.getPrototypeOf` on the synthetic class +/// ref (returned by `instance.constructor`) walked the parent chain and +/// returned the raw sentinel as an INT32-tagged class ref (`-2`). +/// `Object.create(-2)` then threw +/// `TypeError: Object prototype may only be an Object or null: -2` — the +/// blocker for `sdxgen --help` (rolldown's `__toESM` calls +/// `Object.create(Object.getPrototypeOf(mod))`). +/// +/// This witness creates the same `Object.create(builtin_namespace)` shape, +/// reads `.constructor` to obtain the synthetic class ref, and then runs the +/// `Object.create(Object.getPrototypeOf(ctor))` chain. Pre-fix it threw the +/// `-2` TypeError; post-fix it succeeds. +#[test] +fn cjs_wrap_object_create_on_builtin_namespace_get_prototype_of_not_sentinel() { + let dir = tempfile::tempdir().expect("tempdir"); + let stdout = compile_and_run_cjs( + dir.path(), + r#" +const ns = require("process"); +const obj = Object.create(ns); +const ctor = obj.constructor; +const proto = Object.getPrototypeOf(ctor); +const obj2 = Object.create(proto); +console.log("ok"); +"#, + ); + assert_eq!(stdout, "ok\n"); +} + +/// The full rolldown `__toESM` shape (the real one using +/// `Object.create(Object.getPrototypeOf(mod))`) must not throw the `-2` +/// TypeError when `mod` is a built-in module namespace AND a prior +/// `Object.create(builtin_namespace)` has registered the sentinel parent. +/// This mirrors the `external-pack.js` startup sequence: `__toESM` calls on +/// `require("process")` etc. interleave with `isPlainObject`/`deepMerge` +/// prototype-chain walks that hit `getPrototypeOf` on synthetic class refs. +#[test] +fn cjs_wrap_rolldown_toesm_after_object_create_on_builtin_namespace() { + let dir = tempfile::tempdir().expect("tempdir"); + let stdout = compile_and_run_cjs( + dir.path(), + r#" +var __create = Object.create; +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __getProtoOf = Object.getPrototypeOf; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( + isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", { value: mod, enumerable: true }) : target, + mod +)); +// First: Object.create on a built-in namespace registers the sentinel parent. +const ns = require("process"); +const obj = Object.create(ns); +const ctor = obj.constructor; +// Now run __toESM on another built-in — its Object.create(getPrototypeOf(mod)) +// must not trip on the registered sentinel. +let node_os = require("os"); +node_os = __toESM(node_os, 1); +console.log("os.cpus:", typeof node_os.cpus); +"#, + ); + assert_eq!(stdout, "os.cpus: function\n"); +}