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.
Summary
Reflect.preventExtensionsandReflect.isExtensibleare lowered to the same Perry HIR/runtime paths as theObject.*APIs, but the Reflect APIs have different observable behavior:Reflect.preventExtensions(obj)returns a boolean, whileObject.preventExtensions(obj)returns the object.Reflect.preventExtensions(nonObject)throwsTypeError, while the current shared helper returns the original value.Reflect.isExtensible(nonObject)throwsTypeError, whileObject.isExtensible(nonObject)returnsfalse.Node behavior
Checked with Node v25.9.0:
Node also enforces proxy invariants for
Reflect.isExtensible; for example, anisExtensibletrap returningfalsefor an extensible target throws aTypeError.Perry implementation
Object.preventExtensionsandObject.isExtensiblelower to the expected Object-specific expressions:crates/perry-hir/src/lower/expr_call/native_module.rs:717lowersObject.preventExtensionstoExpr::ObjectPreventExtensions.crates/perry-hir/src/lower/expr_call/native_module.rs:737lowersObject.isExtensibletoExpr::ObjectIsExtensible.The Reflect lowering reuses those same Object expressions:
crates/perry-hir/src/lower/expr_call/native_module.rs:1082lowersReflect.isExtensible(...)toExpr::ObjectIsExtensible(...).crates/perry-hir/src/lower/expr_call/native_module.rs:1086lowersReflect.preventExtensions(...)toExpr::ObjectPreventExtensions(...).The shared runtime helpers implement Object behavior:
crates/perry-runtime/src/object/object_ops.rs:1367documentsjs_object_prevent_extensionsas returning the object, and the function returnsobj_valueafter settingOBJ_FLAG_NO_EXTEND.crates/perry-runtime/src/object/object_ops.rs:1418documentsjs_object_is_extensible; it returnsfalsefor non-objects instead of throwing.That is correct for
Object.isExtensible(1)andObject.preventExtensions(obj), but it is observably wrong for the Reflect entry points.Expected fix direction
Reflect.preventExtensionsandReflect.isExtensibleshould have Reflect-specific lowering/runtime helpers that:TypeErrorfor non-object targets,Reflect.preventExtensions,Duplicate search
Searched existing issues/PRs for:
Reflect.preventExtensionsReflect.isExtensiblepreventExtensions isExtensibleReflect preventExtensions returns objectReflect isExtensible non-objectNo existing issue covered this specific Reflect/Object semantic split.