Skip to content

runtime: complete Object.create descriptor and prototype validation semantics #2816

Closed

Description

@andrewtdiz

Summary

Object.create currently only models Object.create(proto). Node also supports Object.create(proto, propertiesObject) and requires the prototype argument to be either an object or null; primitives and undefined throw TypeError.

Perry's current runtime/lowering ignores the descriptor argument and treats non-object prototypes like null/prototype-less objects, which hides invalid input and drops own properties from descriptor-form clones.

Expected Node behavior

Local Node v25.9.0 probe:

Object.getPrototypeOf(Object.create(null)) === null; // true

const p = { x: 1 };
const o = Object.create(p);
console.log(Object.getPrototypeOf(o) === p, o.x, Object.keys(o)); // true, 1, []

Object.create(1);         // TypeError: Object prototype may only be an Object or null
Object.create("x");       // TypeError
Object.create(undefined); // TypeError

const d = Object.create(
  { p: 1 },
  {
    a: { value: 2, enumerable: true },
    b: { value: 3, enumerable: false },
  },
);
console.log(d.p, d.a, d.b, Object.keys(d)); // 1, 2, 3, ["a"]
console.log(Object.getOwnPropertyDescriptor(d, "b").enumerable); // false

Current Perry implementation

Source evidence from current tree:

  • crates/perry-hir/src/lower/expr_call/object_static.rs lowers "create" to Expr::ObjectCreate(Box::new(iter.next().unwrap_or(Expr::Undefined))), discarding every argument after the first.
  • crates/perry-hir/src/lower/expr_call/native_module.rs does the same for direct Object.create(...) calls.
  • crates/perry-hir/src/ir/expr.rs only has ObjectCreate(Box<Expr>), so there is no HIR slot for the optional descriptor object.
  • crates/perry-codegen/src/expr/array_methods.rs lowers Expr::ObjectCreate to one runtime call: js_object_create(proto).
  • crates/perry-codegen/src/runtime_decls/strings.rs declares js_object_create with one DOUBLE argument.
  • crates/perry-runtime/src/object/object_ops.rs::js_object_create() comments that Object.create(null) or a non-object proto falls back to a prototype-less object. Node only permits null; non-object, non-null prototypes must throw.
  • docs/typescript-parity-gaps.md already notes Object.create() is limited and lacks the property descriptors argument, but there is no open issue tracking the concrete runtime/API work.

Suggested test surface

Add parity tests for:

const proto = { p: 1 };
const obj = Object.create(proto, {
  a: { value: 2, enumerable: true },
  b: { value: 3, enumerable: false },
});
assert.equal(obj.p, 1);
assert.equal(obj.a, 2);
assert.equal(obj.b, 3);
assert.deepEqual(Object.keys(obj), ["a"]);
assert.equal(Object.getOwnPropertyDescriptor(obj, "b")!.enumerable, false);

assert.throws(() => Object.create(1 as any), TypeError);
assert.throws(() => Object.create(undefined as any), TypeError);
assert.equal(Object.getPrototypeOf(Object.create(null)), null);

Scope / non-goals

This is not about the already-fixed Object.create(proto) prototype-chain wiring or instanceof behavior (#1805 / PR #2244, #809 / PR #815). It tracks the remaining descriptor-form Object.create(proto, propertiesObject) behavior and prototype-argument validation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions