Summary
Object.defineProperty(SomeClass, "name", { value: "X" }) is silently ignored for classes. The same call on a plain function works. Even Object.getOwnPropertyDescriptor(D, "name") reports the original name afterwards, so defineProperty never stored anything — .name keeps resolving through the class registry.
Deterministic, GC-independent, reproduces at d8be5f4a2 in the default configuration.
Minimal repro
class D {}
Object.defineProperty(D, "name", { value: "Renamed" });
console.log("class-name:", D.name);
function f() {}
Object.defineProperty(f, "name", { value: "RenamedFn" });
console.log("fn-name:", f.name);
class Base {}
class Sub extends Base {}
Object.defineProperty(Sub, "name", { value: "SubRenamed" });
console.log("sub-name:", Sub.name);
console.log("ctor-name:", (new (Sub as any)() as any).constructor.name);
const E = class {};
Object.defineProperty(E, "name", { value: "ExprRenamed" });
console.log("expr-name:", (E as any).name);
console.log("desc:", JSON.stringify(Object.getOwnPropertyDescriptor(D, "name")));
| line |
node 26.5.1 |
perry d8be5f4a2 |
class-name |
Renamed |
D |
fn-name |
RenamedFn |
RenamedFn ✅ |
sub-name |
SubRenamed |
Sub |
ctor-name |
SubRenamed |
Sub |
expr-name |
ExprRenamed |
E |
desc |
{"value":"Renamed","writable":false,"enumerable":false,"configurable":true} |
{"value":"D",…} |
class_object_name_value (crates/perry-runtime/src/object/field_get_set/class_object_props.rs:24) does check own_data_field_by_name before falling back to class_name_for_id, so the miss looks like it is on the store side: defineProperty with key "name" on a class value never installs the own data field. Note Function.prototype.name redefinition works, so the divergence is specific to the class path (INT32 ClassRef vs heap ClassExprFresh).
Why it matters: zod
$constructor (zod/src/v4/core/core.ts) renames both the synthetic base class and the constructor function:
class Definition extends Parent {}
Object.defineProperty(Definition, "name", { value: name });
…
Object.defineProperty(_, "name", { value: name });
so under perry a caught zod error reports the wrong constructor name:
try { z.string().parse(5); } catch (e: any) { console.log(e.constructor.name); }
// node : ZodError
// perry: Definition
Any user code branching on err.constructor.name (or logging it) sees Definition for every zod error type.
Environment
Node 26.5.1 (.node-version pin), perry @ d8be5f4a2, macOS arm64. Found while investigating #7154.
Refs #7154.
Summary
Object.defineProperty(SomeClass, "name", { value: "X" })is silently ignored for classes. The same call on a plain function works. EvenObject.getOwnPropertyDescriptor(D, "name")reports the original name afterwards, sodefinePropertynever stored anything —.namekeeps resolving through the class registry.Deterministic, GC-independent, reproduces at
d8be5f4a2in the default configuration.Minimal repro
d8be5f4a2class-nameRenamedDfn-nameRenamedFnRenamedFn✅sub-nameSubRenamedSubctor-nameSubRenamedSubexpr-nameExprRenamedEdesc{"value":"Renamed","writable":false,"enumerable":false,"configurable":true}{"value":"D",…}class_object_name_value(crates/perry-runtime/src/object/field_get_set/class_object_props.rs:24) does checkown_data_field_by_namebefore falling back toclass_name_for_id, so the miss looks like it is on the store side:definePropertywith key"name"on a class value never installs the own data field. NoteFunction.prototype.nameredefinition works, so the divergence is specific to the class path (INT32ClassRefvs heapClassExprFresh).Why it matters: zod
$constructor(zod/src/v4/core/core.ts) renames both the synthetic base class and the constructor function:so under perry a caught zod error reports the wrong constructor name:
Any user code branching on
err.constructor.name(or logging it) seesDefinitionfor every zod error type.Environment
Node 26.5.1 (
.node-versionpin), perry @d8be5f4a2, macOS arm64. Found while investigating #7154.Refs #7154.