Skip to content

runtime: give Reflect.preventExtensions and Reflect.isExtensible Reflect-specific results and errors #2762

Description

@andrewtdiz

Summary

Reflect.preventExtensions and Reflect.isExtensible are lowered to the same Perry HIR/runtime paths as the Object.* APIs, but the Reflect APIs have different observable behavior:

  • Reflect.preventExtensions(obj) returns a boolean, while Object.preventExtensions(obj) returns the object.
  • Reflect.preventExtensions(nonObject) throws TypeError, while the current shared helper returns the original value.
  • Reflect.isExtensible(nonObject) throws TypeError, while Object.isExtensible(nonObject) returns false.
  • The Reflect operations need proxy trap/invariant behavior rather than directly using the object flag helper.

Node behavior

Checked with Node v25.9.0:

const out = {};

const o = {};
out.objBefore = Reflect.isExtensible(o); // true
Object.preventExtensions(o);
out.objAfter = Reflect.isExtensible(o); // false

const preventProxyFalse = new Proxy({}, {
  preventExtensions() { return false; },
});
out.preventProxyFalse = Reflect.preventExtensions(preventProxyFalse); // false

const preventTarget = {};
const preventProxyTrue = new Proxy(preventTarget, {
  preventExtensions(target) {
    Object.preventExtensions(target);
    return true;
  },
});
out.preventProxyTrue = Reflect.preventExtensions(preventProxyTrue); // true
out.preventProxyTrueExtensible = Reflect.isExtensible(preventTarget); // false

for (const value of [1, null, undefined]) {
  Reflect.preventExtensions(value); // TypeError: Reflect.preventExtensions called on non-object
  Reflect.isExtensible(value); // TypeError: Reflect.isExtensible called on non-object
}

Node also enforces proxy invariants for Reflect.isExtensible; for example, an isExtensible trap returning false for an extensible target throws a TypeError.

Perry implementation

Object.preventExtensions and Object.isExtensible lower to the expected Object-specific expressions:

  • crates/perry-hir/src/lower/expr_call/native_module.rs:717 lowers Object.preventExtensions to Expr::ObjectPreventExtensions.
  • crates/perry-hir/src/lower/expr_call/native_module.rs:737 lowers Object.isExtensible to Expr::ObjectIsExtensible.

The Reflect lowering reuses those same Object expressions:

  • crates/perry-hir/src/lower/expr_call/native_module.rs:1082 lowers Reflect.isExtensible(...) to Expr::ObjectIsExtensible(...).
  • crates/perry-hir/src/lower/expr_call/native_module.rs:1086 lowers Reflect.preventExtensions(...) to Expr::ObjectPreventExtensions(...).

The shared runtime helpers implement Object behavior:

  • crates/perry-runtime/src/object/object_ops.rs:1367 documents js_object_prevent_extensions as returning the object, and the function returns obj_value after setting OBJ_FLAG_NO_EXTEND.
  • crates/perry-runtime/src/object/object_ops.rs:1418 documents js_object_is_extensible; it returns false for non-objects instead of throwing.

That is correct for Object.isExtensible(1) and Object.preventExtensions(obj), but it is observably wrong for the Reflect entry points.

Expected fix direction

Reflect.preventExtensions and Reflect.isExtensible should have Reflect-specific lowering/runtime helpers that:

  • throw TypeError for non-object targets,
  • return booleans for Reflect.preventExtensions,
  • preserve the existing Object API return values/primitive behavior,
  • invoke proxy traps and enforce proxy invariants for the Reflect operations.

Duplicate search

Searched existing issues/PRs for:

  • Reflect.preventExtensions
  • Reflect.isExtensible
  • preventExtensions isExtensible
  • Reflect preventExtensions returns object
  • Reflect isExtensible non-object

No existing issue covered this specific Reflect/Object semantic split.

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