Repro
// util_inherits_class.ts
const util = require("util");
class Base {}
class Derived {}
util.inherits(Derived, Base);
console.log(typeof Derived.super_, Derived.super_ === Base);
node : function true
perry: Uncaught TypeError: Object.defineProperty called on non-object rc=1
Note the asymmetry — only a class in the first position fails:
| call |
node |
perry |
util.inherits(function(){}, function(){}) |
ok |
ok |
util.inherits(function(){}, SomeClass) |
ok |
ok |
util.inherits(SomeClass, function(){}) |
ok |
TypeError |
util.inherits(SomeClass, OtherClass) |
ok |
TypeError |
Long-standing, not a regression: it reproduces identically on 83754818e (#9242) and on a03be729c (#9336).
Cause
set_super_property in crates/perry-runtime/src/util_inherits.rs handles exactly two receiver shapes — a closure ctor and a heap-object ctor:
fn set_super_property(ctor: f64, super_ctor: f64) {
let ctor_closure = closure_ptr(ctor);
if ctor_closure != 0 { /* closure dynamic prop */ return; }
let obj = object_ptr(ctor);
if obj.is_null() {
crate::object::throw_object_type_error(b"Object.defineProperty called on non-object");
}
…
}
A class constructor arrives as an INT32-tagged class ref (0x7FFE_…), which is neither a closure nor a heap object, so it falls to the throw. Confirmed by instrumenting the site — the ctor value is 0x7ffe000000000008, i.e. class ref, class id 8.
Object.defineProperty(SomeClass, "super_", …) on the same value works, because define_property.rs has the third arm this function is missing:
let target_is_class_ref = super::super::class_ref_id(obj_value).is_some();
if !target_is_class_ref && !value_is_object_like(obj_value) { … throw … }
Suggested fix
Give set_super_property the class-ref arm, mirroring what define_property.rs does for a static own property — for a constructor-half class ref (class_ref_id(ctor).is_some() && class_prototype_ref_id(ctor).is_none()):
class_registry::class_dynamic_prop_root_store(cid, "super_", super_ctor);
class_registry::class_static_set_defined_attrs(cid, "super_", /*writable*/ true, /*enumerable*/ false, /*configurable*/ true);
which matches Node's descriptor ({ value: superCtor, writable: true, configurable: true }) and the attrs the closure arm already uses. ensure_function_prototype / js_object_set_prototype_of on the ctor side should be checked at the same time, since the throw currently masks whatever they do with a class ref.
Why it is worth a fixture
Found while bisecting #9341: this is the other of the only two sites in the tree that emit Object.defineProperty called on non-object, and the two are byte-identical strings, so this shape is a standing source of misattribution when that message shows up in a bundle. A regression test pinning util.inherits across {function, class} x {function, class} would keep them distinguishable.
Found with the fixture above; not filed as a blocker for #9341 — that one is the define_property.rs site.
Repro
Note the asymmetry — only a class in the first position fails:
util.inherits(function(){}, function(){})util.inherits(function(){}, SomeClass)util.inherits(SomeClass, function(){})util.inherits(SomeClass, OtherClass)Long-standing, not a regression: it reproduces identically on
83754818e(#9242) and ona03be729c(#9336).Cause
set_super_propertyincrates/perry-runtime/src/util_inherits.rshandles exactly two receiver shapes — a closure ctor and a heap-object ctor:A class constructor arrives as an INT32-tagged class ref (
0x7FFE_…), which is neither a closure nor a heap object, so it falls to the throw. Confirmed by instrumenting the site — the ctor value is0x7ffe000000000008, i.e. class ref, class id 8.Object.defineProperty(SomeClass, "super_", …)on the same value works, becausedefine_property.rshas the third arm this function is missing:Suggested fix
Give
set_super_propertythe class-ref arm, mirroring whatdefine_property.rsdoes for a static own property — for a constructor-half class ref (class_ref_id(ctor).is_some() && class_prototype_ref_id(ctor).is_none()):which matches Node's descriptor (
{ value: superCtor, writable: true, configurable: true }) and the attrs the closure arm already uses.ensure_function_prototype/js_object_set_prototype_ofon the ctor side should be checked at the same time, since the throw currently masks whatever they do with a class ref.Why it is worth a fixture
Found while bisecting #9341: this is the other of the only two sites in the tree that emit
Object.defineProperty called on non-object, and the two are byte-identical strings, so this shape is a standing source of misattribution when that message shows up in a bundle. A regression test pinningutil.inheritsacross {function, class} x {function, class} would keep them distinguishable.Found with the fixture above; not filed as a blocker for #9341 — that one is the
define_property.rssite.