Skip to content

feat(runtime): .constructor on Date/Array/Object instances#973

Merged
proggeramlug merged 1 commit into
mainfrom
worktree-agent-a522919bbc04aa601
May 18, 2026
Merged

feat(runtime): .constructor on Date/Array/Object instances#973
proggeramlug merged 1 commit into
mainfrom
worktree-agent-a522919bbc04aa601

Conversation

@proggeramlug
Copy link
Copy Markdown
Contributor

Summary

  • date-fns 4.x threw RangeError: Invalid time value on the very first format(...) call because constructFrom(date, value) does new date.constructor(value) and Perry returned undefined from date.constructor. Now both inst.constructor reads and bare Date/Array/Object identifiers resolve to the same singleton closure pointer.
  • New identify_global_builtin_constructor short-circuit in js_new_function_construct dispatches new <inst.constructor>(...) callsites to the real js_date_new_* / js_array_alloc / js_object_alloc factories (matches by stable ClosureHeader.func_ptr so it survives GC evacuation of the singleton closures).
  • Codegen PropertyGet invalid-receiver fall-through now routes through the runtime helper so raw-f64 Date receivers can reach the new Date .constructor arm. NewDynamic with a PropertyGet callee now reaches js_new_function_construct; statically-typed Date receivers shortcut to Expr::DateNew for the hot path.
  • HIR: bare built-in identifiers (Date, Array, Object, …) now lower to PropertyGet { GlobalGet(0), <name> } so they route through the existing globalThis singleton path. Decorator-arg recogniser extended for the new shape.

Test plan

  • test-files/test_constructor_property.ts — typeof / identity-equality / new <inst.constructor>(ts) round-trip for Date, Array, Object: 7/7 byte-for-byte vs Node.
  • test-files/test_gap_closures.ts — no regression.
  • test-files/test_gap_object_methods.ts — no regression.
  • date-fns end-to-end (format(new Date(2024, 0, 15), 'yyyy-MM-dd') under compilePackages: ["date-fns"]) no longer throws RangeError: Invalid time value. Next downstream blocker is an undefined.map in date-fns's normalizeDates — separate gap, not in scope for this PR.

Date-fns 4.x threw `RangeError: Invalid time value` because
`constructFrom(date, value)` clones a Date via `new
date.constructor(value)` and Perry returned `undefined` from
`date.constructor`. The downstream `new undefined(...)` constructed
an empty placeholder and `cloned.getTime()` read garbage.

Three coordinated changes so `inst.constructor` reads and bare
`Date`/`Array`/`Object` identifiers resolve to the same closure
pointer, and `new <inst.constructor>(...)` dispatches to the real
factory:

- HIR: bare built-in idents lower to `PropertyGet { GlobalGet(0),
  name }` so they route through the existing globalThis singleton
  closure path.
- Runtime: `js_object_get_field_by_name(_f64)` returns the
  appropriate global constructor for Array/Object/Date receivers
  (Date is recognized via `is_registered_date_bits`); anon-shape
  classes (synthetic `__AnonShape_*` for object literals) report
  `Object`.
- Runtime: `js_new_function_construct` identifies the singleton
  built-in closures by their stable `func_ptr` (GC-evac-safe) and
  dispatches to `js_date_new_*` / `js_array_alloc` / `js_object_alloc`.
- Codegen: PropertyGet's invalid-receiver fall-through now calls the
  runtime helper so raw-f64 Date receivers reach the Date arm.
  NewDynamic with `PropertyGet { ... }` callee routes through
  `js_new_function_construct`. Statically-typed Date receivers
  shortcut to `Expr::DateNew` for the hot path.

Validation: `test-files/test_constructor_property.ts` (Date / Array /
Object constructor identity + `new <inst.constructor>(...)` clone)
passes byte-for-byte vs Node. date-fns
`format(new Date(...), 'yyyy-MM-dd')` no longer trips
`RangeError: Invalid time value`; the next blocker is an `undefined.map`
inside date-fns's `normalizeDates` chain — a separate downstream gap.
@proggeramlug proggeramlug merged commit 5ddccbb into main May 18, 2026
5 of 9 checks passed
@proggeramlug proggeramlug deleted the worktree-agent-a522919bbc04aa601 branch May 18, 2026 00:23
proggeramlug added a commit that referenced this pull request May 18, 2026
…1009)

PR #973 lowered bare built-in idents (`Promise`, `Array`, `Date`, ...)
as `PropertyGet { GlobalGet(0), name }` so they route through the
globalThis singleton closure path. Two codegen call sites that
specialize `.then()` dispatch were still pattern-matching the legacy
`Expr::GlobalGet(_)` shape only:

- `type_analysis::is_promise_expr` for `Promise.resolve/reject/all/race/
  allSettled/any(...)` and `Array.fromAsync(...)`.
- `lower_call.rs`'s fused `Promise.resolve(x).then(cb)` fast path that
  routes to `js_promise_resolved_then`.

When `is_promise_expr` returned false, `.then(cb)` fell through to the
generic native method dispatch which doesn't enqueue the callback —
microtask-02..07 and edge-promises went silent in compile-smoke's
Native no-fallback gate, and on Linux V8 surfaced the same shape as
`TypeError: then is not a function`. The Native gate had been red on
every PR since #973 (admin-bypassed on #997 / #1000 / #1003 / #1004).

Extract `type_analysis::is_global_builtin_named(expr, name)` that
matches both shapes (legacy `GlobalGet(_)` and the post-#973
`PropertyGet { GlobalGet(0), name }`) and route both call sites
through it.

Validation: `scripts/run_native_no_fallback_tests.sh` — 35 passed, 0
failed (was 28/7 pre-fix).
proggeramlug pushed a commit that referenced this pull request May 18, 2026
… (regression from #973)

`Number.parseFloat === parseFloat` (and every built-in
wrapper-constructor static accessed as a member value) regressed to
`false`; Node/spec is `true`. Also regressed the core gap test
test_gap_number_math vs node --experimental-strip-types.

Bisected to 5ddccbb (feat(runtime): .constructor property on
Date/Array/Object instances, #973): its HIR arm rewrites bare built-in
idents used as VALUES to PropertyGet{GlobalGet(0),name} for
`inst.constructor === Date` identity. That fired in member-OBJECT
position too, so `Number.parseFloat` resolved via globalThis.Number
rather than the intrinsic static.

Fix in expr_member.rs: after lowering the member object, revert #973's
reroute to the intrinsic GlobalGet(0) when it fired on a member-object
built-in ident. Bare-ident-as-value (#973's feature) untouched; local
shadowing inherently safe (shadowing local lowers to LocalGet).
proggeramlug added a commit that referenced this pull request May 18, 2026
… (regression from #973) (#1007)

`Number.parseFloat === parseFloat` (and every built-in
wrapper-constructor static accessed as a member value) regressed to
`false`; Node/spec is `true`. Also regressed the core gap test
test_gap_number_math vs node --experimental-strip-types.

Bisected to 5ddccbb (feat(runtime): .constructor property on
Date/Array/Object instances, #973): its HIR arm rewrites bare built-in
idents used as VALUES to PropertyGet{GlobalGet(0),name} for
`inst.constructor === Date` identity. That fired in member-OBJECT
position too, so `Number.parseFloat` resolved via globalThis.Number
rather than the intrinsic static.

Fix in expr_member.rs: after lowering the member object, revert #973's
reroute to the intrinsic GlobalGet(0) when it fired on a member-object
built-in ident. Bare-ident-as-value (#973's feature) untouched; local
shadowing inherently safe (shadowing local lowers to LocalGet).

Co-authored-by: Ralph Kuepper <ralph@skelpo.com>
proggeramlug added a commit that referenced this pull request May 28, 2026
… property values (#2142) (#2176)

Extends #2058/#2077 to the remaining built-in prototypes
(`Array`/`Date`/`RegExp`/`String`/`Promise`/`Map`/`Set`/`Error`/typed
arrays/etc.). Before this change, the typeof-fold made `typeof
Array.prototype.map === "function"` pass at AST level, but any
INDIRECT read — storing into a local, an array literal, or otherwise
hiding the value behind a variable — returned `undefined`. The issue's
repro (`for ([n,v] of [[ ... , Array.prototype.map], ...])`) hit
exactly that gap.

Two coordinated changes:

1. HIR (`expr_member.rs`) — broaden the #2060 typed-array-only
   collapse exemption. The #973 builtin-ident reroute collapsed
   `<Ctor>.prototype` to `globalThis.prototype` (which is undefined,
   not the constructor's real prototype). Typed arrays already
   exempted themselves so the per-kind proto with the `length` /
   `byteLength` / `byteOffset` / `buffer` accessor descriptors stayed
   reachable; the same logic applies to every built-in proto, so the
   exemption now fires unconditionally when the outer member is
   `.prototype`.

2. Runtime (`global_this.rs`) — extend `populate_builtin_prototype_methods`
   to install named callable closures for every well-known method on
   `Array`/`Object`/`Function`/`String`/`Number`/`Boolean`/`Date`/
   `RegExp`/`Promise`/`Map`/`Set`/`WeakMap`/`WeakSet`/`Error*`/typed
   arrays. Each closure is backed by the shared `noop_thunk` so
   `typeof === "function"`, `.name` reads the method name, and
   `.length` reads the spec arity — covering Test262's `verifyProperty`
   / `assert.throws` introspection paths.

   `Array.prototype.slice` and `Object.prototype.toString` keep their
   dedicated thunks (ramda's curry/variadic helpers depend on
   `Array.prototype.slice.call(args,…)` returning a real sliced array
   even via indirection, and ramda's `_isArguments.js` IIFE calls
   `Object.prototype.toString.call(arguments)` at module-init time).
   The hot-path calls — `arr.map(fn)` (codegen NativeMethodCall) and
   `Array.prototype.map.call(arr, fn)` (HIR rewrite via
   `try_builtin_prototype_method_apply_call`) — are unaffected.

Adds `test-files/test_issue_2142_builtin_proto_method_values.ts`,
byte-identical to `node --experimental-strip-types`.

Per #2142, this is the single biggest remaining cascade in Test262's
`built-ins/{Object,Array,TypedArray,Date,RegExp,Function}` categories
after #2058/#2059/#2060/#2063 — the `(no output)` / `reading 'name'` /
`reading 'constructor'` cascades all collapse on the same prototype-
method-value gap.

Co-authored-by: Ralph Küpper <ralph@skelpo.com>
proggeramlug added a commit that referenced this pull request Jun 5, 2026
#4622)

Two bugs made the dotted static reads broken:
1. The #973/#4139 reroute-undo collapsed value reads of Date.now/parse/UTC to
   GlobalGet(0).<name>, for which codegen has no intrinsic handler (unlike
   Object.keys / Math.max), so they mis-folded to a number (typeof 'number').
   Add a Date now/parse/UTC exception to the reroute-undo so the read hits the
   reified Date constructor object (the aliased/computed forms already did).
   Direct calls (Date.now()) stay on the Expr::DateNow fast path (unaffected).
2. date_utc_static collects all args into its rest param, but Date.UTC was
   registered with call-arity 7 (7 fixed params before rest), so Date.UTC(2020,0)
   put nothing in the rest array → js_date_utc([]) → NaN. Register call-arity 0
   (all args → rest) while keeping spec .length = 7.

Now byte-identical to node: typeof Date.now/parse/UTC === 'function', correct
.name/.length, value-calls (const u=Date.UTC; u(2020,0)) work, direct calls
unchanged. Fixes test262 Date/{now,parse,UTC}/{name,length}, Date/now/15.9.4.4-0-1/3.

Co-authored-by: Ralph Küpper <ralph@skelpo.com>
proggeramlug added a commit that referenced this pull request Jun 5, 2026
…nction values (#4631)

Same reroute-undo issue as Date.now (#4622) / Array.isArray (#4626). All six
Number statics (isFinite/isInteger/isNaN/isSafeInteger/parseFloat/parseInt) are
reified with metadata via install_constructor_static, but the #973/#4139
reroute-undo collapsed value reads to GlobalGet(0).<name>, whose intrinsic path
dropped the metadata for isInteger/isSafeInteger (.name/.length undefined).

Add Number to the reified-static reroute-undo exception. The already-named
siblings (isFinite/isNaN/parseFloat/parseInt) are also reified, so routing them
to the reified receiver preserves their names; direct calls keep the intrinsic
fast path via the !member_is_call_callee gate.

Byte-identical to node: Number.isInteger.name==='isInteger'/.length===1,
isSafeInteger likewise; value-calls and direct calls work. Fixes test262
Number/{isInteger,isSafeInteger}/{name,length}. (String.fromCharCode/etc. are not
reified yet — tracked in #4627.)

Co-authored-by: Ralph Küpper <ralph@skelpo.com>
proggeramlug added a commit that referenced this pull request Jun 5, 2026
… function values (#4634)

Completes the static-function-values series (#4622 Date, #4626 Array, #4631
Number). Unlike those, String.fromCharCode/fromCodePoint were not reified at all,
so this both reifies them and wires the reroute-undo exception:

- New js_string_from_code_point_array runtime helper (variadic fromCodePoint with
  per-element RangeError validation; lone surrogates → U+FFFD).
- Reify fromCharCode/fromCodePoint via install_constructor_static_with_call_arity
  (call-arity 0 = all args into rest; spec .length 1) — fromCharCode reuses the
  existing js_string_from_char_code_array.
- Explicit String fromCharCode/fromCodePoint exception in the #973/#4139
  reroute-undo (NOT the whole namespace — String.raw is not reified and stays on
  its intrinsic path, unaffected).

Byte-identical to node: typeof/name/length, value-calls (incl. astral code
points), direct calls, RangeError on invalid code points; String.raw unchanged.
Fixes test262 String/{fromCharCode,fromCodePoint}/{name,length}. (String.raw .name
/.length tracked in #4627.)

Co-authored-by: Ralph Küpper <ralph@skelpo.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant