Skip to content

runtime: make Reflect.construct honor newTarget, array-like args, and constructor errors #2768

@andrewtdiz

Description

@andrewtdiz

Summary

Reflect.construct(target, argumentsList, newTarget?) currently loses required Reflect semantics in lowering and runtime:

  • the HIR node stores only target and args, so newTarget is ignored,
  • a special-case fold turns known-class Reflect.construct(ClassName, [args...]) into direct new ClassName(...), also ignoring newTarget,
  • generic codegen calls js_proxy_construct(target, args, undefined), even for non-proxy targets,
  • argumentsList is treated like Perry's array helper input rather than Node's array-like CreateListFromArrayLike,
  • constructor/non-constructor validation and proxy construct trap result validation are missing.

Node behavior

Checked with Node v25.9.0:

function Target(a) { this.a = a; }
function NewTarget() {}
NewTarget.prototype.kind = 'custom';

const obj = Reflect.construct(Target, ['x'], NewTarget);
obj.a; // 'x'
Object.getPrototypeOf(obj) === NewTarget.prototype; // true
obj.kind; // 'custom'

const arrayLikeArgs = Reflect.construct(
  function (a, b) { this.sum = a + b; },
  { 0: 2, 1: 3, length: 2 },
);
arrayLikeArgs.sum; // 5

const proxy = new Proxy(function (a) { this.a = a; }, {
  construct(target, args, newTarget) {
    return { arg: args[0], isNewTarget: newTarget === proxy };
  },
});

Reflect.construct(proxy, ['p']);
// { arg: 'p', isNewTarget: true }

const badReturn = new Proxy(function () {}, {
  construct() { return 1; },
});
Reflect.construct(badReturn, []);
// TypeError: 'construct' on proxy: trap returned non-object ('1')

Reflect.construct(1, []);                 // TypeError: 1 is not a constructor
Reflect.construct(function () {}, null);   // TypeError: CreateListFromArrayLike called on non-object
Reflect.construct(function () {}, 1);      // TypeError: CreateListFromArrayLike called on non-object
Reflect.construct(function () {}, [], 1);  // TypeError: 1 is not a constructor

Perry implementation

Lowering loses newTarget and has a direct-new shortcut:

  • crates/perry-hir/src/lower/expr_call/native_module.rs:969 handles Reflect.construct.
  • crates/perry-hir/src/lower/expr_call/native_module.rs:970 special-cases Reflect.construct(ClassName, [args...]) and lowers it directly to Expr::New.
  • crates/perry-hir/src/lower/expr_call/native_module.rs:995 otherwise reads only target and args_arr.
  • crates/perry-hir/src/lower/expr_call/native_module.rs:998 returns Expr::ReflectConstruct { target, args }; there is no newTarget field.

Generic codegen hard-codes undefined for newTarget:

  • crates/perry-codegen/src/expr/proxy_reflect.rs:177 lowers Expr::ReflectConstruct.
  • crates/perry-codegen/src/expr/proxy_reflect.rs:180 creates an undefined value.
  • crates/perry-codegen/src/expr/proxy_reflect.rs:183 calls js_proxy_construct(target, args, undefined).

The runtime helper is proxy-specific and not a full Reflect.construct implementation:

  • crates/perry-runtime/src/proxy.rs:499 implements js_proxy_construct(proxy_boxed, args_array, _new_target).
  • crates/perry-runtime/src/proxy.rs:500 returns undefined when lookup(proxy_boxed) fails, so generic non-proxy Reflect.construct does not construct or throw through this path.
  • crates/perry-runtime/src/proxy.rs:518 invokes a construct trap when present.
  • crates/perry-runtime/src/proxy.rs:520 returns the trap result directly, without checking that the trap returned an object.
  • crates/perry-runtime/src/proxy.rs:525 falls back to call_with_args_array(target, args_array), which shares the array-only / no validation behavior noted in runtime: make Reflect.apply use thisArg, array-like args, and TypeErrors #2767 for Reflect.apply.

Expected fix direction

Reflect.construct should have a real Reflect path that:

  • carries optional newTarget through HIR and codegen,
  • defaults newTarget to target when omitted,
  • validates that target and newTarget are constructors,
  • creates arguments from any array-like object and throws for non-object argumentsList,
  • constructs non-proxy targets instead of routing them through the proxy-only helper,
  • invokes proxy construct traps with (target, args, newTarget),
  • throws when a proxy construct trap returns a non-object,
  • preserves direct-known-class optimizations only when they are semantics-preserving.

Duplicate search

Searched existing issues/PRs for:

  • Reflect.construct
  • Reflect.construct newTarget
  • Reflect.construct array-like
  • proxy construct trap
  • construct trap returned non-object
  • PR search for Reflect.construct OR proxy construct

No existing issue covered this Reflect.construct behavior.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions