Skip to content

Enable Temporal by default - #32978

Merged
dylan-conway merged 3 commits into
mainfrom
farm/551f7451/enable-temporal
Aug 5, 2026
Merged

dylan-conway merged 3 commits into
mainfrom
farm/551f7451/enable-temporal

Conversation

@robobun

@robobun robobun commented Jun 28, 2026 •

Copy link
Copy Markdown
Collaborator

What does this PR do?

Enables JavaScriptCore's Temporal implementation by default by setting JSC::Options::useTemporal() = true alongside the other JSC option defaults in ZigGlobalObject.cpp. This exposes the Temporal global and Date.prototype.toTemporalInstant. BUN_JSC_useTemporal=0 still turns it back off, because the BUN_JSC_* environment overrides are applied after the defaults block.

Fixes #15853.

Why now

JSC's Temporal implementation has improved a lot since the 40% test262 pass rate reported on #15853 in early 2025. Measured with temporal-test262-runner 0.4.0 (the same tool that produced that 40% figure) against test262 de8e621c, with Node v26.3.0 (which ships Temporal on by default) for comparison:

Suite Tests Bun Node 26.3
built-ins/Temporal (core spec) 4603 100.0% 98.7%
built-ins/Date/prototype/toTemporalInstant 8 100% 100%
intl402/DateTimeFormat + DurationFormat on Temporal objects 259 99.2% 91.9%
intl402/Temporal (non-ISO calendars) 2029 56.2% 38.3%

Every one of the 136 tests where Bun is behind Node is in intl402/Temporal, which is non-ISO calendar behavior. Outside of that there is not a single test262 test where Bun is behind Node. I also byte-diffed the output of 20,500 deterministic Temporal operations (seeded identically in both engines): the iso8601, gregory, and roc calendars, 2000 DST-disambiguation cases, all offset-option handling, Instant rounding, and formatting options produced zero divergences.

The full test262 run was repeated on a debug + ASAN build of this branch's parent: zero crashes, zero ASAN reports, zero assertion failures, and a pass/fail set identical to release across all 6634 tests.

Chrome 144, Firefox, and Node 26 ship Temporal on by default. TypeScript 6.0 ships the type declarations and its lib.esnext.d.ts references esnext.temporal, so nothing is needed in bun-types.

The Windows blocker is resolved

This PR was converted to a draft when CI showed Temporal.PlainDate.prototype.since throwing RangeError: Duration is outside the representable range on trivial input, on Windows x64 and Windows aarch64 only (the two platforms where WTF's Int128 is the emulated Int128Impl rather than native __int128).

The WebKit upgrade in #34373 (2603e9eb41f0) brought in upstream's Temporal duration-arithmetic and spec-alignment fixes, and the bug no longer reproduces. Verified on current main's canary (1.4.0-canary.1+6b6fb1a33) on both Windows 2019 x64 and Windows 11 aarch64 with BUN_JSC_useTemporal=1: the previously failing expression returns P17M14D, and a 10-expression smoke covering since/until, Duration.round/total/compare, Instant arithmetic, epoch nanoseconds, and ZonedDateTime DST transitions produces byte-identical output to Linux on both.

Cost

The Temporal namespace object is installed the same way Intl is (one line above it in JSGlobalObject::init): a single small object whose nine sub-constructors are PropertyCallback lookup-table entries created on first access, with their structures registered via initLater. Measured over 420 cold starts of bun -e '': 1887 us/start with useTemporal off, 1875 us/start with it on (within measurement noise). RSS is 31.5 MB vs 31.4 MB. The implementation was already compiled into the binary behind the runtime flag, so there is no size change.

Known gaps, all non-ISO calendars and all upstream JavaScriptCore

  • dayOfYear ignores the calendar and returns the ISO day of year. Every other calendar-dependent field getter has a non-ISO branch through TemporalCore; this one does not, and TemporalZonedDateTimePrototype.cpp already has a source comment acknowledging it.
  • The buddhist calendar's year returns the ISO year instead of BE, because TemporalCore::calendarYear() uses ICU's UCAL_EXTENDED_YEAR, which is the Gregorian year for the GregorianCalendar-derived calendars. The same function already special-cases roc for this exact quirk.
  • Chinese/dangi leap-month placement disagrees with the reference on 2-3% of dates, and the ad/bc era aliases are not accepted (ce/bce are).
  • Legacy IANA time zone IDs such as CET and EST5EDT are rejected. This is not Temporal-specific: new Date() and Intl.DateTimeFormat().resolvedOptions().timeZone already fall back to UTC for them today.

The first two are being filed upstream on bugs.webkit.org with one-line reproductions. None of these are regressions this PR can introduce; they are pre-existing in the JSC implementation, which is only reachable behind BUN_JSC_useTemporal=1 today.

Nothing else needed to change

  • test/js/node/test/common/globals.js already handles the new global conditionally (if (global.Temporal) intrinsics.add('Temporal')).
  • test/js/bun/jsc/temporal-global.test.ts (added by Upgrade WebKit to 2603e9eb41f0 #34373 to pin the off-by-default state) is updated to assert the new default and the BUN_JSC_useTemporal=0 opt-out; the subprocess flag tests live there, next to the other JSC option coverage, and the in-process functional coverage lives in test/js/web/temporal/temporal.test.ts.
  • structuredClone and postMessage correctly reject Temporal objects with a DataCloneError, matching Node.
  • Temporal string parsing uses its own strict ISO 8601 grammar in ISO8601.cpp. It never touches the date parser selected by useV8DateParser, which has exactly one call site (DateCache::parseDate), so there is no interaction with that override.

LeakSanitizer

The x64-asan lane flags JSC's Temporal time zone bridge on any test that touches a named time zone. JSC::TemporalCore::withTimeZone keeps a process-lifetime LazyNeverDestroyed LRU of at most 8 open UCalendars (evicted entries are ucal_closed), and the TimeZoneCacheEntry that owns each one lives in bmalloc memory, which LeakSanitizer does not scan as a root region, so the libc-allocated UCalendar is misreported as a direct leak. This PR adds the one test/leaksan.supp entry for it with a comment. It is bounded and intentional, not a real leak.

How did you verify your code works?

test/js/bun/jsc/temporal-global.test.ts covers the global being present by default and the BUN_JSC_useTemporal=0 escape hatch. test/js/web/temporal/temporal.test.ts covers the spec property attributes, all nine namespaces, Temporal.Now, parsing/arithmetic/DST-disambiguation round-trips, Date.prototype.toTemporalInstant, Intl.DateTimeFormat formatting of Temporal objects, and structuredClone rejection.

$ USE_SYSTEM_BUN=1 bun test test/js/bun/jsc/temporal-global.test.ts test/js/web/temporal/temporal.test.ts
 1 pass
 9 fail

$ bun bd test test/js/bun/jsc/temporal-global.test.ts test/js/web/temporal/temporal.test.ts
 10 pass
 0 fail

test/js/web/workers/structured-clone.test.ts (231 tests) also passes with the flag on.


[review] gate passed · iteration 4 · 4 files touched

fails on main (without fix)
ASAN without fix: 9 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/bun/jsc/temporal-global.test.ts test/js/web/temporal/temporal.test.ts
bun test v1.4.0 (f2e0cd5a1)

test/js/bun/jsc/temporal-global.test.ts:
 9 |     cmd: [bunExe(), "-e", `process.stdout.write(typeof Temporal + " " + typeof Temporal.Now.instant)`],
10 |     env: { ...bunEnv, BUN_JSC_useTemporal: undefined },
11 |     stderr: "pipe",
12 |   });
13 |   const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
14 |   expect({ stdout, stderr, exitCode }).toEqual({ stdout: "object function", stderr: expect.any(String), exitCode: 0 });
                                            ^
error: expect(received).toEqual(expected)

  {
-   "exitCode": 0,
-   "stderr": Any<String>,
-   "stdout": "object function",
+   "exitCode": 1,
+   "stderr": 
+ "1 | process.stdout.write(typeof Temporal + " " + typeof Temporal.Now.instant)
+                                                         ^
+ ReferenceError: Temporal is not defined
+       at /workspace/bun/[eval]:1:53
+ 
+ Bun v1.4.0-debug+f2e0cd5
... (truncated)

release without fix: all passed
bun test v1.4.0-canary.1 (2f503b5f2)

test/js/bun/jsc/temporal-global.test.ts:
(pass) Temporal is exposed by default [18.29ms]
(pass) BUN_JSC_useTemporal=0 disables Temporal [17.95ms]

test/js/web/temporal/temporal.test.ts:
(pass) Temporal global > is installed with the spec property attributes [0.03ms]
(pass) Temporal global > exposes all nine namespaces [0.05ms]
(pass) Temporal core operations > Temporal.Now [5.26ms]
(pass) Temporal core operations > parsing, arithmetic, and formatting round-trip [0.17ms]
(pass) Temporal core operations > DST disambiguation [0.09ms]
(pass) Temporal core operations > Date.prototype.toTemporalInstant [0.04ms]
(pass) Temporal core operations > Intl.DateTimeFormat formats Temporal objects [2.06ms]
(pass) Temporal core operations > structuredClone rejects Temporal objects [0.12ms]

 10 pass
 0 fail
 27 expect() calls
Ran 10 tests across 2 files. [175.00ms]
__F:0:S:0
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/bun/jsc/temporal-global.test.ts test/js/web/temporal/temporal.test.ts
bun test v1.4.0 (f2e0cd5a1)

test/js/bun/jsc/temporal-global.test.ts:
(pass) Temporal is exposed by default [1284.32ms]
(pass) BUN_JSC_useTemporal=0 disables Temporal [1302.12ms]

test/js/web/temporal/temporal.test.ts:
(pass) Temporal global > is installed with the spec property attributes [2.15ms]
(pass) Temporal global > exposes all nine namespaces [3.39ms]
(pass) Temporal core operations > Temporal.Now [168.15ms]
(pass) Temporal core operations > parsing, arithmetic, and formatting round-trip [9.26ms]
(pass) Temporal core operations > DST disambiguation [6.56ms]
(pass) Temporal core operations > Date.prototype.toTemporalInstant [2.74ms]
(pass) Temporal core operations > Intl.DateTimeFormat formats Temporal objects [27.37ms]
(pass) Temporal core operations > structuredClone rejects Temporal objects [7.59ms]

 10 pass
 0 fail
 27 expect() calls
Ran 10 tests across 2 files. [3.65s]
__F:0:S:0

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 711ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/7] cxx obj/src/jsc/bindings/ZigGlobalObject.cpp.o
[2/7] gen cpp.rs (cppbind)
[2/7] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)

  nightly-2026-07-20-x86_64-unknown-linux-gnu unchanged - rustc 1.99.0-nightly (9f36de775 2026-07-19)

�[1m�[92m   Compiling�[0m bun_core v0.0.0 (/workspace/bun/src/bun_core)
�[1m�[92m   Compiling�[0m bun_errno v0.0.0 (/workspace/bun/src/errno)
�[1m�[92m   Compiling�[0m bun_ptr v0.0.0 (/workspace/bun/src/ptr)
�[1m�[92m   Compiling�[0m bun_boringssl_sys v0.0.0 (/workspace/bun/src/boringssl_sys)
�[1m�[92m   Compiling�[0m bun_safety v0.0.0 (/workspace/bun/src/safety)
�[1m�[92m   Compiling�[0m bun_zlib_sys v0.0.0 (/workspace/bun/src/zlib_sys)
�[1m�[92m   Compiling�[0m bun_cares_sys v0.0.0 (/workspace/bun/src/cares_sys)
�[1m�[92m   Compiling�[0m bun_zstd v0.0.0 (/workspace/bun/src/zstd)
�[1m�[92m   Compiling�[0m bun_picohttp v0.0.0 (/workspace/bun/src/picohttp)
�[1m�[92m   Compiling�[0m bun_brotli v0.0.0 (/workspace/bun/src/brotli)
�[1m�[92m   Compiling�[0m b
... (truncated)
diff hotspot
src/jsc/bindings/ZigGlobalObject.cpp    |  5 +-
 test/js/bun/jsc/temporal-global.test.ts | 20 ++++----
 test/js/web/temporal/temporal.test.ts   | 88 +++++++++++++++++++++++++++++++++
 test/leaksan.supp                       |  9 +++-
 4 files changed, 107 insertions(+), 15 deletions(-)

gate history · 2 passed · 0 rejected · iteration 4

evidence per changed file
file                                     reads  edits  tests
src/jsc/bindings/ZigGlobalObject.cpp         2      1      8
test/js/bun/jsc/temporal-global.test.ts      0      0      0
test/js/web/temporal/temporal.test.ts        0      2      7
test/leaksan.supp                            0      0     10

@coderabbitai

coderabbitai Bot commented Jun 28, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 3 seconds

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9f216feb-6866-4fd7-a628-a30e1f251d96

📥 Commits

Reviewing files that changed from the base of the PR and between 6b6fb1a and f2e0cd5.

📒 Files selected for processing (4)
  • src/jsc/bindings/ZigGlobalObject.cpp
  • test/js/bun/jsc/temporal-global.test.ts
  • test/js/web/temporal/temporal.test.ts
  • test/leaksan.supp

Comment @coderabbitai help to get the list of available commands.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't find any issues — the one-line flag flip and the test file both look correct — but enabling a new global by default is a user-visible product decision, so a human should sign off on the ship call.

Extended reasoning...

Overview

This PR flips JSC::Options::useTemporal() to true in the JSC defaults block of ZigGlobalObject.cpp (one line, identical in form to useExplicitResourceManagement and useImportDefer immediately above it) and adds test/js/web/temporal/temporal.test.ts with 10 tests covering the global's presence/attributes, the BUN_JSC_useTemporal=0 escape hatch, the nine namespaces, Temporal.Now, parsing/arithmetic/DST round-trips, Date.prototype.toTemporalInstant, Intl formatting, and structuredClone rejection.

Security risks

None. This exposes a standards-track date/time API that is already compiled into the binary and reachable today via BUN_JSC_useTemporal=1. No auth, crypto, filesystem, network, or permission surface is touched.

Level of scrutiny

The code is trivial and mechanically correct — the flag is set in the same block as the other JSC feature defaults, before the BUN_JSC_* env overrides are applied, so the escape hatch works as described. The new test file follows existing harness conventions (bunEnv, bunExe, await using for spawned procs). The bug-hunting system found nothing.

The decision, however, is a release-level call: shipping a new global to every Bun user by default. The PR description makes a strong case (100% test262 core pass rate, ASAN-clean, no startup cost, Chrome/Firefox/Node parity, documented non-ISO calendar gaps), but whether Bun is ready to commit to Temporal-on-by-default — and accept the listed known gaps as acceptable for GA — is a product judgment a maintainer should make explicitly rather than have auto-approved.

Other factors

No CODEOWNERS entry covers the changed paths. There are no prior human reviews or outstanding comments on the thread. The test's hard-coded DST and TZ-offset expectations (America/New_York, Asia/Tokyo) are stable historical transitions and should not flake.

@robobun

robobun commented Jun 28, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 8:09 PM PT - Jun 27th, 2026

@robobun, your commit 6355a7b is building: #66024

@robobun

robobun commented Jun 28, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 8:09 PM PT - Jun 27th, 2026

❌ @robobun, your commit 6355a7b has some failures in Build #66024 (All Failures)


🧪   To try this PR locally:

bunx bun-pr 32978

That installs a local version of the PR into your bun-32978 executable, so you can run:

bun-32978 --bun

@robobun

robobun commented Jun 28, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 7:39 PM PT - Aug 4th, 2026

❌ @robobun, your commit f2e0cd5 has 1 failures in Build #89046 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 32978

That installs a local version of the PR into your bun-32978 executable, so you can run:

bun-32978 --bun

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't find any issues with the code itself — the flag flip, test, and LSan suppression all look correct — but enabling Temporal by default is a user-facing product decision (with documented non-ISO-calendar gaps) that a maintainer should explicitly sign off on.

Extended reasoning...

Overview

This PR flips JSC::Options::useTemporal() to true in the JSC defaults block of ZigGlobalObject.cpp, exposing the Temporal global and Date.prototype.toTemporalInstant to all users by default. It adds test/js/web/temporal/temporal.test.ts covering the global, the BUN_JSC_useTemporal=0 escape hatch, and core Temporal operations, plus an LSan suppression for TemporalCore::withTimeZone's process-lifetime TinyLRUCache of UCalendar handles.

Security risks

None. This toggles an existing upstream JSC feature flag; no new attack surface is introduced beyond what was already reachable via BUN_JSC_useTemporal=1. No auth, crypto, filesystem, or network code is touched.

Level of scrutiny

The mechanical change is a one-liner that follows the exact pattern of useExplicitResourceManagement and useImportDefer directly above it, and the test/suppression additions are straightforward. However, the effect is a user-visible runtime behavior change for every Bun process: a new global is installed, and the PR description itself documents known correctness gaps in non-ISO calendars (buddhist year, dayOfYear, chinese/dangi leap months, legacy TZ IDs). Whether those gaps are acceptable to ship — and whether the test262 data presented justifies flipping the default now — is a product/release judgment call, not a code-correctness question.

Other factors

The CI SIGABRT on x64-asan at 6355a7b was the LSan false positive that the follow-up commit ef49ec7b suppresses (with a well-reasoned comment explaining the bmalloc/LSan root-scanning interaction), so that should be resolved. The PR description is exceptionally thorough. Still, shipping a new standard-library global by default is the kind of decision that warrants an explicit maintainer ack rather than bot approval.

@robobun

robobun commented Jun 28, 2026

Copy link
Copy Markdown
Collaborator Author

CI found a blocker, so I am converting this to a draft.

Temporal.PlainDate.prototype.since throws on Windows. On :windows: 2019 x64-baseline and :windows: 11 aarch64, and on no other platform, the core ISO-calendar expression in the new test fails:

Temporal.PlainDate.from("2024-06-15").since("2023-01-01", { largestUnit: "month" })
// every other platform, and Node v26: "P17M14D"
// Windows:                            RangeError: Duration is outside the representable range

This is entirely inside JavaScriptCore, not Bun. The throw is the IsValidDuration rejection in JSC::TemporalCore::temporalDurationFromInternal (Source/JavaScriptCore/runtime/temporal/core/DurationArithmetic.cpp:326). The set of failing platforms (Windows x64 and Windows aarch64, nothing else) is exactly the set where WTF's Int128 is the emulated Int128Impl class rather than the native __int128 (the MSVC ABI has no __int128; see the HAVE(INT128_T) split in Source/WTF/wtf/Int128.h), and the throwing function is the only code path in this test file that goes through Int128 arithmetic and an Int128 to double round-trip. The nine other tests, which do not, pass on Windows.

Enabling Temporal by default would ship a core API that throws RangeError on trivial input to every Windows user, so this must not merge until the Int128Impl bug is fixed in oven-sh/WebKit and the WebKit version is bumped. The one-line flip and the test here are otherwise correct; they are just blocked on the engine fix. The x64-asan LeakSanitizer failure from the previous run is resolved by the test/leaksan.supp entry and that lane is no longer failing.

The other annotation on build 66137, test/js/bun/spawn/spawn-stdin-readable-stream.test.ts, is a pre-existing flake that CI already retried and is unrelated to this change.

robobun added 2 commits August 5, 2026 01:40
Sets JSC::Options::useTemporal() = true alongside the other JSC option
defaults in ZigGlobalObject.cpp, exposing the Temporal global and
Date.prototype.toTemporalInstant. BUN_JSC_useTemporal=0 still turns it
back off because the BUN_JSC_* environment overrides run after the
defaults block.
JSC::TemporalCore::withTimeZone (TimeZoneICUBridge.cpp) keeps up to 8 open
UCalendars in a process-lifetime LazyNeverDestroyed TinyLRUCache, one per
time zone ID; LRU eviction ucal_closes them. The TimeZoneCacheEntry that
owns each UCalendar lives in bmalloc memory, which LeakSanitizer does not
scan as a root region, so the libc-allocated UCalendar is reported as a
direct leak even though it is reachable and bounded. The x64-asan lane
hits this on any Temporal test that touches a named time zone.
@robobun
robobun force-pushed the farm/551f7451/enable-temporal branch from ef49ec7 to 2f503b5 Compare August 5, 2026 01:42
@robobun
robobun marked this pull request as ready for review August 5, 2026 01:42
@robobun

robobun commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator Author

Un-drafting: the Windows blocker is fixed.

The WebKit upgrade in #34373 (2603e9eb41f0) included upstream's Temporal duration-arithmetic fixes, and the expression that failed on Windows no longer throws. Verified on current main's canary (1.4.0-canary.1+6b6fb1a33) on both Windows 2019 x64 and Windows 11 aarch64 with BUN_JSC_useTemporal=1:

Temporal.PlainDate.from("2024-06-15").since("2023-01-01", { largestUnit: "month" }).toString()
// => "P17M14D" on both, matching Linux and Node

A broader smoke (since/until, Duration.round/total/compare, Instant arithmetic, epoch nanoseconds, ZonedDateTime DST transitions) produces byte-identical output across Windows x64, Windows aarch64, and Linux.

Also rebased onto main: #34373 had pinned useTemporal off explicitly and added test/js/bun/jsc/temporal-global.test.ts asserting that state, so this branch now removes the pin and updates that test to cover the new default plus the BUN_JSC_useTemporal=0 opt-out. Functional coverage stays in test/js/web/temporal/temporal.test.ts.

Comment thread test/leaksan.supp
The rebase picked up a bare leak:JSC::TemporalCore::withTimeZone entry
that landed on main while this PR was drafted. The commented entry added
here matches the same frames by substring, so keep the one with the
explanation.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found — the duplicate LSAN suppression from the earlier pass is resolved (line 88 removed, single commented entry remains). The code change itself is a one-line flag flip with solid test coverage, but flipping a default-on global for all users is a product/API call, so leaving this for a maintainer to sign off.

Checked: BUN_JSC_* env overrides are applied after this defaults block so =0 still works; test/js/node/test/common/globals.js already handles Temporal conditionally; the new test's .since() case exercises the Int128 path that previously broke on Windows; leaksan.supp now has exactly one TemporalCore::withTimeZone entry.

Extended reasoning...

Overview

The PR flips JSC::Options::useTemporal() from false to true in the JSC options defaults block in ZigGlobalObject.cpp, exposing the Temporal global and Date.prototype.toTemporalInstant by default. It updates test/js/bun/jsc/temporal-global.test.ts to assert the new default and the BUN_JSC_useTemporal=0 opt-out, adds test/js/web/temporal/temporal.test.ts with in-process functional coverage (property attributes, all nine namespaces, Now, arithmetic/DST/round-trips, toTemporalInstant, Intl formatting, structuredClone rejection), and consolidates the TemporalCore::withTimeZone LSAN suppression into a single commented entry.

Since my previous inline comment, commit f2e0cd5 removed the pre-existing bare leak:JSC::TemporalCore::withTimeZone at line 88, so the file now has exactly one suppression for that frame with the explanatory comment — the nit is resolved.

Security risks

None identified. The change enables an already-compiled JSC feature via a runtime option; no new parsing of untrusted input is introduced on Bun's side, and Temporal's own ISO 8601 parser is upstream JavaScriptCore code that has been through test262 + ASAN per the PR description.

Level of scrutiny

The mechanical change is trivial and the tests follow harness conventions correctly (bunEnv spread, concurrent pipe drain, test.concurrent for independent subprocesses, exact-value assertions). However, this is a user-facing API surface change: it installs a new global on every Bun process and changes what typeof Temporal returns. Per the repo's guidance on API design, shipping decisions of this kind — even when the implementation is upstream and the case is well-argued — are product calls that a maintainer should ratify, particularly given the documented non-ISO calendar gaps. The PR description makes a thorough, evidence-backed case (test262 parity vs Node, ASAN clean, Windows Int128 fix verified on both arches, startup cost measured), which should make that sign-off quick.

Other factors

  • The Windows Int128Impl blocker that drafted this PR is stated resolved by the WebKit upgrade in #34373; the new test includes the exact .since({ largestUnit: "month" }) expression that previously threw, so CI on the Windows lanes will re-verify it.
  • Grepped for other tests asserting typeof Temporal === "undefined" or Temporal absence — none beyond the file this PR updates; test/js/node/test/common/globals.js already guards with if (global.Temporal).
  • The Intl.DateTimeFormat test hard-codes "6/15/2024" for en-US; this matches how other Intl tests in the repo pin locale output, so it is consistent with local convention.
  • Build #89046 for the current head is still in progress per the timeline; the human reviewer will want green Windows lanes before merging.

@robobun

robobun commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator Author

The red :debian: 13 x64-asan lane on the current build is not Temporal. It is a direct leak in RSA key generation (boringssl pkey_rsa_keygen via src/runtime/node/node_crypto_binding.rs:85) reported on test/js/node/async_hooks/AsyncLocalStorage-tracking.test.ts, a file this PR does not touch. It failed identically on the previous build (89042) and is with main-break triage. No Temporal test fails anywhere in the build, and the TemporalCore::withTimeZone LSan suppression is holding on the upgraded WebKit.

@robobun

robobun commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator Author

Build 89046 finished. Every lane is green except the one described above: the :debian: 13 x64-asan failure on test/js/node/async_hooks/AsyncLocalStorage-tracking.test.ts (RSA keygen leak, unrelated to this PR, reproduces identically on the previous build, with main-break triage).

Notably, all three Windows lanes pass, including the PlainDate.since expression in the new test that previously threw RangeError: Duration is outside the representable range on Windows. That confirms the engine-side fix in CI.

From my side this is ready for sign-off: the diff is a one-line default flip plus tests and one documented LSan suppression, with the supporting data in the PR description.

@dylan-conway
dylan-conway merged commit ce1d5b4 into main Aug 5, 2026
53 of 54 checks passed
@dylan-conway
dylan-conway deleted the farm/551f7451/enable-temporal branch August 5, 2026 04:15
springmin pushed a commit to springmin/bun that referenced this pull request Aug 5, 2026
### What does this PR do?

Enables JavaScriptCore's `Temporal` implementation by default by setting
`JSC::Options::useTemporal() = true` alongside the other JSC option
defaults in `ZigGlobalObject.cpp`. This exposes the `Temporal` global
and `Date.prototype.toTemporalInstant`. `BUN_JSC_useTemporal=0` still
turns it back off, because the `BUN_JSC_*` environment overrides are
applied after the defaults block.

Fixes oven-sh#15853.

### Why now

JSC's Temporal implementation has improved a lot since the 40% test262
pass rate reported on oven-sh#15853 in early 2025. Measured with
`temporal-test262-runner` 0.4.0 (the same tool that produced that 40%
figure) against test262 `de8e621c`, with Node v26.3.0 (which ships
Temporal on by default) for comparison:

| Suite | Tests | Bun | Node 26.3 |
|---|---|---|---|
| `built-ins/Temporal` (core spec) | 4603 | 100.0% | 98.7% |
| `built-ins/Date/prototype/toTemporalInstant` | 8 | 100% | 100% |
| `intl402/DateTimeFormat` + `DurationFormat` on Temporal objects | 259
| 99.2% | 91.9% |
| `intl402/Temporal` (non-ISO calendars) | 2029 | 56.2% | 38.3% |

Every one of the 136 tests where Bun is behind Node is in
`intl402/Temporal`, which is non-ISO calendar behavior. Outside of that
there is not a single test262 test where Bun is behind Node. I also
byte-diffed the output of 20,500 deterministic Temporal operations
(seeded identically in both engines): the iso8601, gregory, and roc
calendars, 2000 DST-disambiguation cases, all offset-option handling,
Instant rounding, and formatting options produced zero divergences.

The full test262 run was repeated on a debug + ASAN build of this
branch's parent: zero crashes, zero ASAN reports, zero assertion
failures, and a pass/fail set identical to release across all 6634
tests.

Chrome 144, Firefox, and Node 26 ship Temporal on by default. TypeScript
6.0 ships the type declarations and its `lib.esnext.d.ts` references
`esnext.temporal`, so nothing is needed in `bun-types`.

### The Windows blocker is resolved

This PR was converted to a draft when CI showed
`Temporal.PlainDate.prototype.since` throwing `RangeError: Duration is
outside the representable range` on trivial input, on Windows x64 and
Windows aarch64 only (the two platforms where WTF's `Int128` is the
emulated `Int128Impl` rather than native `__int128`).

The WebKit upgrade in oven-sh#34373 (`2603e9eb41f0`) brought in upstream's
Temporal duration-arithmetic and spec-alignment fixes, and the bug no
longer reproduces. Verified on current main's canary
(`1.4.0-canary.1+6b6fb1a33`) on both Windows 2019 x64 and Windows 11
aarch64 with `BUN_JSC_useTemporal=1`: the previously failing expression
returns `P17M14D`, and a 10-expression smoke covering `since`/`until`,
`Duration.round`/`total`/`compare`, `Instant` arithmetic, epoch
nanoseconds, and ZonedDateTime DST transitions produces byte-identical
output to Linux on both.

### Cost

The `Temporal` namespace object is installed the same way `Intl` is (one
line above it in `JSGlobalObject::init`): a single small object whose
nine sub-constructors are `PropertyCallback` lookup-table entries
created on first access, with their structures registered via
`initLater`. Measured over 420 cold starts of `bun -e ''`: 1887 us/start
with `useTemporal` off, 1875 us/start with it on (within measurement
noise). RSS is 31.5 MB vs 31.4 MB. The implementation was already
compiled into the binary behind the runtime flag, so there is no size
change.

### Known gaps, all non-ISO calendars and all upstream JavaScriptCore

- `dayOfYear` ignores the calendar and returns the ISO day of year.
Every other calendar-dependent field getter has a non-ISO branch through
`TemporalCore`; this one does not, and
`TemporalZonedDateTimePrototype.cpp` already has a source comment
acknowledging it.
- The buddhist calendar's `year` returns the ISO year instead of BE,
because `TemporalCore::calendarYear()` uses ICU's `UCAL_EXTENDED_YEAR`,
which is the Gregorian year for the `GregorianCalendar`-derived
calendars. The same function already special-cases `roc` for this exact
quirk.
- Chinese/dangi leap-month placement disagrees with the reference on
2-3% of dates, and the `ad`/`bc` era aliases are not accepted
(`ce`/`bce` are).
- Legacy IANA time zone IDs such as `CET` and `EST5EDT` are rejected.
This is not Temporal-specific: `new Date()` and
`Intl.DateTimeFormat().resolvedOptions().timeZone` already fall back to
`UTC` for them today.

The first two are being filed upstream on bugs.webkit.org with one-line
reproductions. None of these are regressions this PR can introduce; they
are pre-existing in the JSC implementation, which is only reachable
behind `BUN_JSC_useTemporal=1` today.

### Nothing else needed to change

- `test/js/node/test/common/globals.js` already handles the new global
conditionally (`if (global.Temporal) intrinsics.add('Temporal')`).
- `test/js/bun/jsc/temporal-global.test.ts` (added by oven-sh#34373 to pin the
off-by-default state) is updated to assert the new default and the
`BUN_JSC_useTemporal=0` opt-out; the subprocess flag tests live there,
next to the other JSC option coverage, and the in-process functional
coverage lives in `test/js/web/temporal/temporal.test.ts`.
- `structuredClone` and `postMessage` correctly reject Temporal objects
with a `DataCloneError`, matching Node.
- Temporal string parsing uses its own strict ISO 8601 grammar in
`ISO8601.cpp`. It never touches the date parser selected by
`useV8DateParser`, which has exactly one call site
(`DateCache::parseDate`), so there is no interaction with that override.

### LeakSanitizer

The x64-asan lane flags JSC's Temporal time zone bridge on any test that
touches a named time zone. `JSC::TemporalCore::withTimeZone` keeps a
process-lifetime `LazyNeverDestroyed` LRU of at most 8 open `UCalendar`s
(evicted entries are `ucal_close`d), and the `TimeZoneCacheEntry` that
owns each one lives in bmalloc memory, which LeakSanitizer does not scan
as a root region, so the libc-allocated `UCalendar` is misreported as a
direct leak. This PR adds the one `test/leaksan.supp` entry for it with
a comment. It is bounded and intentional, not a real leak.

### How did you verify your code works?

`test/js/bun/jsc/temporal-global.test.ts` covers the global being
present by default and the `BUN_JSC_useTemporal=0` escape hatch.
`test/js/web/temporal/temporal.test.ts` covers the spec property
attributes, all nine namespaces, `Temporal.Now`,
parsing/arithmetic/DST-disambiguation round-trips,
`Date.prototype.toTemporalInstant`, `Intl.DateTimeFormat` formatting of
Temporal objects, and `structuredClone` rejection.

```
$ USE_SYSTEM_BUN=1 bun test test/js/bun/jsc/temporal-global.test.ts test/js/web/temporal/temporal.test.ts
 1 pass
 9 fail

$ bun bd test test/js/bun/jsc/temporal-global.test.ts test/js/web/temporal/temporal.test.ts
 10 pass
 0 fail
```

`test/js/web/workers/structured-clone.test.ts` (231 tests) also passes
with the flag on.

<!-- robobun:evidence:begin -->

---

**[review]** gate passed · iteration 4 · 4 files touched

<details><summary>fails on main (without fix)</summary>

```console
ASAN without fix: 9 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/bun/jsc/temporal-global.test.ts test/js/web/temporal/temporal.test.ts
bun test v1.4.0 (f2e0cd5)

test/js/bun/jsc/temporal-global.test.ts:
 9 |     cmd: [bunExe(), "-e", `process.stdout.write(typeof Temporal + " " + typeof Temporal.Now.instant)`],
10 |     env: { ...bunEnv, BUN_JSC_useTemporal: undefined },
11 |     stderr: "pipe",
12 |   });
13 |   const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
14 |   expect({ stdout, stderr, exitCode }).toEqual({ stdout: "object function", stderr: expect.any(String), exitCode: 0 });
                                            ^
error: expect(received).toEqual(expected)

  {
-   "exitCode": 0,
-   "stderr": Any<String>,
-   "stdout": "object function",
+   "exitCode": 1,
+   "stderr": 
+ "1 | process.stdout.write(typeof Temporal + " " + typeof Temporal.Now.instant)
+                                                         ^
+ ReferenceError: Temporal is not defined
+       at /workspace/bun/[eval]:1:53
+ 
+ Bun v1.4.0-debug+f2e0cd5
... (truncated)

release without fix: all passed
bun test v1.4.0-canary.1 (2f503b5)

test/js/bun/jsc/temporal-global.test.ts:
(pass) Temporal is exposed by default [18.29ms]
(pass) BUN_JSC_useTemporal=0 disables Temporal [17.95ms]

test/js/web/temporal/temporal.test.ts:
(pass) Temporal global > is installed with the spec property attributes [0.03ms]
(pass) Temporal global > exposes all nine namespaces [0.05ms]
(pass) Temporal core operations > Temporal.Now [5.26ms]
(pass) Temporal core operations > parsing, arithmetic, and formatting round-trip [0.17ms]
(pass) Temporal core operations > DST disambiguation [0.09ms]
(pass) Temporal core operations > Date.prototype.toTemporalInstant [0.04ms]
(pass) Temporal core operations > Intl.DateTimeFormat formats Temporal objects [2.06ms]
(pass) Temporal core operations > structuredClone rejects Temporal objects [0.12ms]

 10 pass
 0 fail
 27 expect() calls
Ran 10 tests across 2 files. [175.00ms]
__F:0:S:0
```

</details>

<details><summary>passes on PR (with fix)</summary>

```console
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/bun/jsc/temporal-global.test.ts test/js/web/temporal/temporal.test.ts
bun test v1.4.0 (f2e0cd5)

test/js/bun/jsc/temporal-global.test.ts:
(pass) Temporal is exposed by default [1284.32ms]
(pass) BUN_JSC_useTemporal=0 disables Temporal [1302.12ms]

test/js/web/temporal/temporal.test.ts:
(pass) Temporal global > is installed with the spec property attributes [2.15ms]
(pass) Temporal global > exposes all nine namespaces [3.39ms]
(pass) Temporal core operations > Temporal.Now [168.15ms]
(pass) Temporal core operations > parsing, arithmetic, and formatting round-trip [9.26ms]
(pass) Temporal core operations > DST disambiguation [6.56ms]
(pass) Temporal core operations > Date.prototype.toTemporalInstant [2.74ms]
(pass) Temporal core operations > Intl.DateTimeFormat formats Temporal objects [27.37ms]
(pass) Temporal core operations > structuredClone rejects Temporal objects [7.59ms]

 10 pass
 0 fail
 27 expect() calls
Ran 10 tests across 2 files. [3.65s]
__F:0:S:0

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 711ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/7] cxx obj/src/jsc/bindings/ZigGlobalObject.cpp.o
[2/7] gen cpp.rs (cppbind)
[2/7] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)

  nightly-2026-07-20-x86_64-unknown-linux-gnu unchanged - rustc 1.99.0-nightly (9f36de775 2026-07-19)

�[1m�[92m   Compiling�[0m bun_core v0.0.0 (/workspace/bun/src/bun_core)
�[1m�[92m   Compiling�[0m bun_errno v0.0.0 (/workspace/bun/src/errno)
�[1m�[92m   Compiling�[0m bun_ptr v0.0.0 (/workspace/bun/src/ptr)
�[1m�[92m   Compiling�[0m bun_boringssl_sys v0.0.0 (/workspace/bun/src/boringssl_sys)
�[1m�[92m   Compiling�[0m bun_safety v0.0.0 (/workspace/bun/src/safety)
�[1m�[92m   Compiling�[0m bun_zlib_sys v0.0.0 (/workspace/bun/src/zlib_sys)
�[1m�[92m   Compiling�[0m bun_cares_sys v0.0.0 (/workspace/bun/src/cares_sys)
�[1m�[92m   Compiling�[0m bun_zstd v0.0.0 (/workspace/bun/src/zstd)
�[1m�[92m   Compiling�[0m bun_picohttp v0.0.0 (/workspace/bun/src/picohttp)
�[1m�[92m   Compiling�[0m bun_brotli v0.0.0 (/workspace/bun/src/brotli)
�[1m�[92m   Compiling�[0m b
... (truncated)
```

</details>

<details><summary>diff hotspot</summary>

```
src/jsc/bindings/ZigGlobalObject.cpp    |  5 +-
 test/js/bun/jsc/temporal-global.test.ts | 20 ++++----
 test/js/web/temporal/temporal.test.ts   | 88 +++++++++++++++++++++++++++++++++
 test/leaksan.supp                       |  9 +++-
 4 files changed, 107 insertions(+), 15 deletions(-)
```

</details>

**gate history** · 2 passed · 0 rejected · iteration 4

<details><summary>evidence per changed file</summary>

```
file                                     reads  edits  tests
src/jsc/bindings/ZigGlobalObject.cpp         2      1      8
test/js/bun/jsc/temporal-global.test.ts      0      0      0
test/js/web/temporal/temporal.test.ts        0      2      7
test/leaksan.supp                            0      0     10
```

</details>

<!-- robobun:evidence:end -->
m-bobrowicz pushed a commit to bystrek/bystrek that referenced this pull request Aug 24, 2026
…oral

Bun 1.4.0 (released today) ships the Temporal global by default
(oven-sh/bun#32978). zoned-time.ts's Intl.DateTimeFormat-based offset
diffing is exactly the kind of manual timezone arithmetic that produced
#17 - replaced with Temporal.Instant/PlainDateTime/ZonedDateTime.

Bumped typescript to 6.0.3 for its official esnext.temporal lib types
(one /// <reference lib="esnext.temporal" /> line) instead of hand-rolling
ambient declarations. That surfaced two TS 6.0 breaking changes: baseUrl
is now a hard error by default (removed, was unused anyway), and nodenext
module resolution needs "types": ["bun-types"] stated explicitly. Build's
tsconfig.build.json also needs its own explicit rootDir now (TS5011).

Same public API and test suite unchanged in zoned-time.ts; ui's
packageManager pin bumped to match Bun. No Dockerfile/CI change needed
since both already float to latest 1.x / no bun-version pin.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
m-bobrowicz added a commit to bystrek/bystrek that referenced this pull request Aug 24, 2026
…l UTC (#19)

* Fix calendar write timezone: offset-less times were misread as server-local UTC

Closes #17. propose_create_calendar_event/propose_update_calendar_event and
list_calendar_events parsed start/end via bare new Date(...), which
interprets an offset-less ISO string in the server's local timezone (UTC)
rather than the user's configured one. New parseZonedIso() (the inverse of
the existing formatZonedIso) treats offset-less input as wall-clock time in
the caller's timezone instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Replace hand-rolled offset/DST math in zoned-time.ts with native Temporal

Bun 1.4.0 (released today) ships the Temporal global by default
(oven-sh/bun#32978). zoned-time.ts's Intl.DateTimeFormat-based offset
diffing is exactly the kind of manual timezone arithmetic that produced
#17 - replaced with Temporal.Instant/PlainDateTime/ZonedDateTime.

Bumped typescript to 6.0.3 for its official esnext.temporal lib types
(one /// <reference lib="esnext.temporal" /> line) instead of hand-rolling
ambient declarations. That surfaced two TS 6.0 breaking changes: baseUrl
is now a hard error by default (removed, was unused anyway), and nodenext
module resolution needs "types": ["bun-types"] stated explicitly. Build's
tsconfig.build.json also needs its own explicit rootDir now (TS5011).

Same public API and test suite unchanged in zoned-time.ts; ui's
packageManager pin bumped to match Bun. No Dockerfile/CI change needed
since both already float to latest 1.x / no bun-version pin.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Trim zoned-time.ts comments to WHY only, drop issue/devlog references

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Validate timezone before parsing in parseZonedIso, not just inside CalendarService

parseZonedIso is called from calendar.tools.ts before the service layer's
own assertValidTimeZone check runs, so a malformed timezone was wrapped
into a misleading "invalid date-time" error blaming the start/end string.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Temporal support (TC39 stage 3 proposal)

3 participants