Skip to content

A dynamic-heritage class with an explicit constructor loses this.<field> written after super() (static extends Object is fine) #9503

Description

@proggeramlug

Repro (single evaluation — no factory chaining needed)

function mkc(P?: any): any {
  class D extends (P ?? Object) {
    constructor(def: any) { super(def); (this as any).def = def; }
  }
  return D;
}
const A = mkc();
console.log("ok " + (new A({ t: 1 }) as any).def.t);
node : ok 1
perry: TypeError: Cannot read properties of undefined (reading 't')     rc=1

this.def is written immediately after super(def) and reads back
undefined. One evaluation of the factory; no chaining.

What narrows it

variant perry
class D extends Object { constructor(def) { super(def); this.def = def } }static heritage ok, node-identical
class D extends (P ?? Object) { … } inside a factory — dynamic heritage this.def is undefined
dynamic heritage, no explicit constructor ok

So the trigger is a dynamic (runtime-value) heritage that resolves to an
uncallable builtin
, combined with an explicit constructor that calls
super(...) and then assigns to this. The identical class with a static
extends Object works, which is why this is not simply "extends Object is
broken".

Suspected site

js_fetch_or_value_super in
crates/perry-runtime/src/object/global_this/fetch_globals.rs. A dynamic
heritage that resolves to a builtin in is_uncallable_builtin_super_parent's
list (Object is in it) takes this arm:

let new_target = crate::object::class_constructor_ref_value(cid);
return crate::object::js_new_function_construct_with_new_target(
    parent_val, args_ptr, args_len, new_target,
);

which returns a fresh object. The caller
(crates/perry-codegen/src/expr/this_super_call.rs) then feeds that return
through js_ctor_return_override(current_this, parent_result, 0) and stores the
result back into the this slot. The subsequent this.def = def therefore
appears to land somewhere other than the object new hands back. The static
extends Object path does not go through this arm, which matches the table
above.

This is a suspicion from reading the two sites, not a bisected root cause — the
observable is the table.

Relationship to #9406

Found while validating #9406. The chained form of this program used to
SIGSEGV; #9406 fixed the crash, and the program now reaches this pre-existing
wrong value instead:

const A = mkc(); const B = mkc(A);
console.log("A " + (new A({ t: 1 }) as any).def);   // node: [object Object] — perry: undefined

Verified as pre-existing: identical failure text on e284cabc3 (#9406's merge
base) and on 23a211383, so it predates that window entirely.

Environment

perrymaster, Node oracle 26.5.1 (.node-version).

Activity

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