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>
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