Skip to content

runtime: route rebound global built-in constructors through real semantics #2889

Closed

Description

@andrewtdiz

Summary

globalThis exposes many built-in constructors as function-shaped values, but most of those values are no-op sentinels when they are called or rebound through a local. Node treats these as the real constructors/functions, so common patterns like const C = globalThis.Number; C("42") or const E = globalThis.Error; new E("x") should preserve the corresponding built-in semantics.

Node behavior

On local Node v25.9.0:

for (const [name,args] of Object.entries({
  Array:[3], Object:[null], String:[123], Number:["42"], Boolean:[0],
  Date:[0], RegExp:["a","g"], Error:["x"], Uint8Array:[3], ArrayBuffer:[4]
})) {
  const F = globalThis[name];
  const called = F(...args);
  const constructed = new F(...args);
  console.log(name, typeof F, Object.prototype.toString.call(called), Object.prototype.toString.call(constructed));
}

Observed examples:

  • Array is a function; globalThis.Array(3) and new globalThis.Array(3) both produce arrays.
  • Number is a function; calling it coerces to a number and constructing it produces a Number object.
  • Boolean, String, Date, RegExp, and Error all preserve their Node call/construct behavior when read from globalThis.
  • constructors such as Map, Set, typed arrays, and ArrayBuffer throw on bare call but construct correctly with new.

Perry evidence

The current runtime intentionally installs most globalThis built-in constructors as no-op closure sentinels:

  • crates/perry-runtime/src/object/global_this.rs::GLOBAL_THIS_BUILTIN_CONSTRUCTORS lists core constructors including Array, Object, String, Number, Boolean, RegExp, Date, Error, Map, Set, typed arrays, ArrayBuffer, DataView, etc.
  • global_this_builtin_noop_thunk() documents that these values only make typeof globalThis.Array === "function" and Array.prototype reads work, while direct calls like globalThis.Array(3) return undefined.
  • populate_global_this_builtins() uses global_this_builtin_noop_thunk for every constructor except String and Buffer.
  • crates/perry-codegen/src/expr/new_dynamic.rs routes new <LocalGet>(...) / new <PropertyGet>(...) through js_new_function_construct(), so rebound constructors depend on runtime recovery.
  • crates/perry-runtime/src/object/class_registry.rs::js_new_function_construct() recognizes the global no-op constructor closure, but only dispatches real construction for Date, Array, Object, and TextEncoderStream / TextDecoderStream; other recognized built-in names fall through to class-id-0 object allocation and then invoke the no-op thunk.

That means examples such as these cannot match Node today:

const NumberCtor = globalThis.Number;
const ErrorCtor = globalThis.Error;
const Uint8Ctor = globalThis.Uint8Array;

NumberCtor("42");        // should be 42, not undefined
new NumberCtor("42");    // should be a Number object, not an empty object
new ErrorCtor("boom");   // should be an Error instance with message/name/stack shape
new Uint8Ctor(3);         // should be a length-3 Uint8Array view

Expected fix shape

Make the globalThis.<Builtin> values dispatch through the same call and construct paths as the direct syntax where possible:

  • call-form constructors: Array, Object, String, Number, Boolean, Date, RegExp, Error subclasses, Symbol, BigInt, etc. should match Node call semantics, including TypeErrors for non-callable constructors such as Map, Set, typed arrays, and ArrayBuffer.
  • construct-form constructors reached through new globalThis.X(...), const X = globalThis.X; new X(...), and .constructor should reuse the real built-in constructor implementations and validation.
  • Keep direct new X(...) lowering as-is, but avoid the no-op sentinel behavior for first-class global constructor values.

Related but not duplicate: #2586 tracks making URL / URLSearchParams global values real constructors, #2608 tracks Fetch API constructor globals, #2882 tracks the missing AggregateError global, and #2883 tracks direct new Symbol / new BigInt TypeErrors.

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