Repository navigation
Conversation
|
Reproduction (any time zone for the UTC lines): // repro.js: TZ=UTC bun repro.js / node repro.js / jsc repro.js
const p = typeof print === "function" ? print : console.log;
p(new Date(0).setUTCFullYear(275761, -12, 1), Date.UTC(275761, -12, 1)); // bun 1.4.3: NaN 8639977881600000 this branch, node: 8639977881600000 twice
p(new Date(0).setFullYear(300000, -300000, 1)); // bun 1.4.3: NaN this branch, node: 8615994624000000 (TZ=UTC)
p(new Date(Date.UTC(-1, 0, 1)).setUTCMonth(12 * 275761)); // bun 1.4.3: NaN this branch, node: 8639977881600000
p(new Date(NaN).setUTCFullYear(-271821, 3600000)); // bun 1.4.3: NaN this branch, node: 827076182400000
const d = new Date(0);
d.setUTCFullYear(-187631, 8, 27);
p(d.setUTCMonth(3472364)); // bun 1.4.3: NaN this branch, node: 3148207948800000 (+101732-09-27, oven-sh/bun#46342)
p(new Date(0).setUTCFullYear(275760, -12, 1), new Date(0).setUTCFullYear(275761, 0, 1)); // controls, unchanged: 8639946345600000 NaNHow I verified, with
Bun side: oven-sh/bun#42193 pins the preview build of this PR and adds |
There was a problem hiding this comment.
I reviewed this PR and didn't find any bugs. Because it changes spec-conformance numerics for all Date setters and tightens the shared makeDay() helper (also used by Date.UTC, the Date constructor, and Temporal), and Source/JavaScriptCore / JSTests are CODEOWNER-covered, a human look is still worthwhile.
What was reviewed:
BrokenDownDatefield widening todoubleand the newtoTimeValue()path — recomposition now goes throughmakeDay/makeTime/makeDate→localTimeToMS→timeClip, matching the constructor path inDateConstructor.cpp.- Per-argument
std::isfinitechecks infillStructuresUsingTimeArgs/DateArgs— each argument is still individually gated, soInfinity, -Infinitycombinations still yield NaN as the spec requires. - The added
monthInt32 < 0 || monthInt32 > 11guard inmakeDay()— for |month| < 2^53 the remainder is exact and in [0,11], so no regression for existing in-range callers; Temporal call sites pass a normalized month. - The new stress test throws on failure, does no logging, and covers int32-wrap, 2^53 day-count precision, and non-finite arguments.
Extended reasoning...
Overview
This PR reworks how Date.prototype.set{FullYear,Month,Date,Hours,Minutes,Seconds,Milliseconds,Year} (and UTC variants) rebuild the time value in Source/JavaScriptCore/runtime/DatePrototype.cpp. BrokenDownDate fields become double, the two fillStructuresUsing*Args helpers now just store arguments and check finiteness, and a new BrokenDownDate::toTimeValue() runs the spec's MakeDate(MakeDay(...), MakeTime(...)) → localTimeToMS → timeClip sequence. DateConstructor.h's makeDay() gains a guard rejecting a month remainder outside [0, 11], which only occurs when |month| ≥ 2^53. A new JSTests/stress/date-setters-year-month-range.js exercises boundary years, huge months/days, int32-wrap cases, and non-finite arguments.
Security risks
None identified. This is pure numeric/spec-conformance work in Date runtime host functions. The int32 truncations that were removed were previously guarded by ad-hoc range checks; the replacement path keeps everything in double until makeDay()'s existing yearInt32 != ym check, and timeClip() still bounds the final result. No memory, auth, or injection surface is touched.
Level of scrutiny
Moderate-to-high. The change is small in line count and the recomposition now literally reuses the same helpers the Date constructor and Date.UTC already use, which is a strong correctness argument. However, it is a behavioral change to a widely used built-in, the makeDay() tweak is shared with Date.UTC/new Date()/Temporal callers, and floating-point edge-case reasoning (2^53 splits, day×msPerDay precision) is subtle enough that a JSC reviewer should sign off. .github/CODEOWNERS routes both Source/JavaScriptCore and JSTests to WebKit/jsc-reviewers.
Other factors
The PR description reports no new test262 failures, differential fuzzing against V8 across several time zones, and that the new stress test fails unpatched and passes patched under all JIT tiers in ~10 ms — consistent with JSTests conventions. DateCache::gregorianDateTimeToMS is intentionally left in place for the external Bun binding. Nothing in the timeline indicates outstanding third-party objections.
|
No actionable comments were generated in the recent review. ℹ️ Recent review info
WalkthroughChangesDate setters now retain numeric fields through shared Date setter range handling
Possibly related issues
Priority: Merge Risk:
|
|
Preview build of 66ac271: |
5e00ef5 to
189b3ff
Compare
There was a problem hiding this comment.
Beyond the inline findings, I also checked the int32 wraparound path the old per-argument guards protected against: with makeDay now forcing the month remainder into [0, 11], the only int arithmetic in dateToDaysFrom1970 (year += month / 12, --year) is a no-op and daysFrom1970ToYear is pure double math, so an in-int32 normalised year cannot wrap. localTimeToMS is guarded by canNarrowToInt64Milliseconds and passes NaN/huge values through to timeClip unchanged, and the ms default for setters called without a milliseconds argument is still seeded from this before fillStructuresUsingTimeArgs runs.
Extended reasoning...
The change reworks the Date.prototype setters in Source/JavaScriptCore/runtime/DatePrototype.cpp to route through makeDay/makeTime/makeDate/timeClip (BrokenDownDate fields become double, per-argument range guards removed), adds a month-remainder check to makeDay in Source/JavaScriptCore/runtime/DateConstructor.h, and adds a stress test. It touches no security-sensitive surface. Two confirmed findings (a timezone-dependent test failure and a non-NaN wrong result for |month| >= 2^53) are posted inline; this note only records the additional overflow and NaN-propagation paths examined and ruled out.
Findings marked 🟡 are optional suggestions and need no follow-up push.
189b3ff to
e43886a
Compare
There was a problem hiding this comment.
I re-reviewed the amended push and didn't find any bugs; the local-time case in the test now starts from new Date(2000, 0, 1), which addresses the US/Pacific concern from my earlier comment. Since this changes the arithmetic behind every Date setter and touches makeDay, which Date.UTC, the constructor and the Temporal code share, a human look from a JSC reviewer would still be worthwhile.
What was reviewed:
- In-range equivalence with the old path: makeDay(y, m, d) is dateToDaysFrom1970(y, m, 1) + d - 1 and makeTime matches timeToMS on exact integers, and localTimeToMS does the same conversion gregorianDateTimeToMS did, so previously-accepted inputs keep their results.
- fillStructuresUsingTimeArgs still keeps the sub-second part of
thiswhen no milliseconds argument is given (the oldmilliseconds += *mselse-branch), so setHours/setMinutes/setSeconds semantics are unchanged. - The new [0, 11] guard in makeDay: all Temporal callers pass month - 1 in [0, 11] and the remainder is exact for |month| < 2^53, so Date.UTC, the constructor and Temporal see no change for normal inputs.
- Every expectation in the existing JSTests/stress/date-timeClip-large-values.js remains consistent with the new makeDay/makeTime/timeClip path.
Extended reasoning...
The change rewrites how the Date setters in Source/JavaScriptCore/runtime/DatePrototype.cpp build their result (BrokenDownDate fields become double, a new toTimeValue runs makeDay/makeTime/makeDate then localTimeToMS and timeClip), adds a remainder guard to makeDay in Source/JavaScriptCore/runtime/DateConstructor.h, and adds one stress test. It touches no security-sensitive surface (no auth, injection or data exposure), only Date arithmetic. The amended commit addressed the blocking test issue from the prior review, and the bug hunt ran dry with no findings. It is not approved outright because it alters core engine builtin semantics with subtle double arithmetic shared by Date.UTC and Temporal, the paths fall under the /Source/JavaScriptCore and /JSTests CODEOWNERS entries, and the test could not be executed in this environment.
Still open from earlier reviews (1):
- Unresolved: 1 minor or pre-existing.
…k year + floor(month / 12)) The preview is 79618395ee (the fork's main, which contains the current pin d6375c72af) plus the two commits of oven-sh/WebKit#616. The test gains the case from #46342: a setMonth() whose month count spans more years than the whole time value range.
e43886a to
0b77b50
Compare
…t64 year/month split Date.prototype.setFullYear, setMonth and their UTC forms return NaN for a valid date when the year argument alone is outside +-275760, or the month count alone is outside +-3309120. new Date(0).setUTCFullYear(275761, -12, 1) is NaN, and Date.UTC(275761, -12, 1) is 8639977881600000 (275760-01-01). oven-sh/bun#46342 hits it through date-fns differenceInMonths, which calls setMonth(3472364) on a date in -187631. MakeDay range-checks year + floor(month / 12) only. fillStructuresUsingDateArgs() bounded years and months / 12 each (295562@main and 296644@main, so that toInt32() of a huge argument could not wrap into range) and narrowed both to int. setYear had its own copy of the bound. makeDayForIntegers() is MakeDay for an int64_t year and month count. It splits the month count by 12 in integer arithmetic, which is exact, and answers NaN when the combined year does not fit the int32_t the day arithmetic takes. makeDay(), which Date.UTC, the Date constructor and Temporal call, is now a range check in front of it. The date setters keep the year and the month count they were given as int64_t and the day count as a double, and call the same function. So a date setter gives the time value Date.UTC gives for the same fields, and there is one normalisation with one domain: a year and a month count of at most 2^53 - 1 in magnitude, and a combined year that fits int32_t. Outside it the result is NaN. Besides the NaN above, these results change: * The day argument is no longer multiplied by msPerDay and added to the milliseconds first. When that sum is beyond 2^53 it lost a millisecond: new Date(8.64e15 - 999).setUTCDate(-199000000) was -8553601036801000 and is -8553601036800999, as Date.UTC(275760, 8, -199000000, 23, 59, 59, 1) is. * Date.UTC and new Date(y, m, ...) with a year or a month count beyond 2^53 - 1 are NaN. A double cannot split such a month count exactly, and it gave a wrong date or an int overflow in dateToDaysFrom1970(): Date.UTC(-6004799503158661, 2 ** 56, 1) was 2000-01-01, where MakeDay says 2000-05-01. V8 answers NaN for these as well. * makeDay() truncates a year or a month count that has a fraction, as MakeDay says, where it answered NaN. No caller passes one. The time setters are not touched. setNewValueFromTimeArgs() and DateCache::gregorianDateTimeToMS() compile to the same instructions as before. * JSTests/stress/date-setters-year-month-range.js: Added. * Source/JavaScriptCore/runtime/DateConstructor.h: (JSC::isYearOrMonthForMakeDay): (JSC::makeDayForIntegers): (JSC::makeDay): * Source/JavaScriptCore/runtime/DatePrototype.cpp: (JSC::fillStructuresUsingDateArgs): (JSC::dateSetterTimeValue): (JSC::setNewValueFromDateArgs): (JSC::JSC_DEFINE_HOST_FUNCTION):
0b77b50 to
5af60b6
Compare
… setter test The WebKit change now gives the date setters and Date.UTC one MakeDay with an exact int64 year/month split, and leaves the time setters as they are. The preview is the fork's 79618395ee (the commit main pins, 5e3554face, plus #807 and the CI files of #881) plus that change. The test gains the domain of the split (exact up to 2^53 - 1, NaN beyond it, combined year in int32), each setter with each argument count, and the local setters in four time zones.
… fails shouldBe() took NaN for NaN. Date.UTC and the Date constructor share MakeDay with the date setters, so a date that both sides lose passed. The local setDate(-199000000) case was such a line: it started from the year 2000 and both sides were NaN. It starts from 275760 now, where the setter is 1 ms off without the MakeDay change. A NaN is expected through shouldBeNaN() only. Shorten the comments in the test, DateConstructor.h and DatePrototype.cpp. No code change in Source/.
There was a problem hiding this comment.
I reviewed the two new commits and found no bugs; both earlier inline findings (the local-time test cases that depended on the US/Pacific zone, and the inexact split for |month| >= 2^53) are addressed in the current code. Because this also changes the observable results of Date.UTC and the Date constructor (NaN for |year| or |month| above 2^53-1, where the spec's exact MakeDay would still yield a number) and the paths are under CODEOWNERS, a human look would still be worthwhile.
What was reviewed:
- makeDayForIntegers: the int64 floor-division split cannot overflow for |month| <= 2^53-1 (year + additionalYears stays far inside int64), the int32 identity check rejects wrap-around, and days + date - 1 keeps the previous makeDay order.
- fillStructuresUsingDateArgs / dateSetterTimeValue: argument conversion still runs in order on the NaN path, the +0 start for a NaN this in setFullYear/setYear is unchanged, and the setter now computes exactly what millisecondsFromComponents in DateConstructor.cpp computes (makeDate, makeTime, localTimeToMS, timeClip).
- Other makeDay callers (Temporal ISOArithmetic, TimeZoneICUBridge, CalendarICUBridge, DurationArithmetic) all pass integral values, so truncating instead of returning NaN for a fractional year/month does not reach them.
- The stress test's local-time assertions start from local-midnight constructor dates or compare against the same local path, so they hold under the TZ forced by run-jsc-stress-tests.
Extended reasoning...
The change touches Source/JavaScriptCore/runtime/DateConstructor.h (makeDay split into isYearOrMonthForMakeDay and makeDayForIntegers), Source/JavaScriptCore/runtime/DatePrototype.cpp (date setters and setYear now build their time value through makeDayForIntegers/makeDate/makeTime/localTimeToMS/timeClip instead of gregorianDateTimeToMS), and adds a 240-line stress test; it touches no security-sensitive surface (no auth, injection, or data exposure). The bug hunt ran dry with no findings, the two commits since the last review resolve both prior inline comments, and the code paths match the existing Date.UTC path. Defer rather than approve because the change alters observable Date.UTC / new Date results beyond 2^53 (a spec-vs-V8 design choice a maintainer should weigh), and /Source/JavaScriptCore and /JSTests are listed in .github/CODEOWNERS.
makeDay() and the date setters answered NaN for a year or a month count above 2^53 - 1. ECMA-262's MakeDay has no such limit. Up to 12 * 2^53 floor(month / 12) is an exact Number, so the 64-bit integer split and sum are the ones MakeDay specifies. Date.UTC(-6004799503158661, 2 ** 56, 1) is 2000-05-01 now, not NaN. Beyond 12 * 2^53 MakeDay rounds the quotient to a Number, and the answer stays NaN. The generated code differs in the compared constant only.
There was a problem hiding this comment.
I reviewed the three new commits and didn't find any bugs; both points raised in earlier reviews are now addressed in the code (the exact int64 month split with the 12 * 2^53 bound, and the local-time test cases starting from a local date instead of new Date(0)). Because this changes observable results of Date.UTC and the Date constructor for |year| or |month| beyond 2^53 (number to NaN) and the files are under the jsc-reviewers entry in CODEOWNERS, a human look would still be worthwhile.
What was reviewed:
- makeDayForIntegers: the truncating / and % with the negative-remainder fix-up give floor division for every month count; year + additionalYears cannot overflow int64 within the 12 * 2^53 bound; the int32 check on the combined year matches what the spec's Number addition yields whenever the result is a valid year.
- dateSetterTimeValue vs the old DateCache::gregorianDateTimeToMS path: same day * msPerDay + time, local offset and timeClip sequence; the NaN-start paths of setFullYear/setYear and the argument-conversion order are unchanged.
- The remaining makeDay callers (DateConstructor.cpp, Temporal core) pass integral values inside the new range, so only the >2^53 behaviour changes for them.
Extended reasoning...
The change touches Source/JavaScriptCore/runtime/DateConstructor.h (makeDay split into isYearOrMonthForMakeDay and makeDayForIntegers), Source/JavaScriptCore/runtime/DatePrototype.cpp (the seven date setters now build their time value through MakeDay instead of DateCache::gregorianDateTimeToMS), and a new 248-line stress test. It touches no security-sensitive surface: no auth, injection or data exposure, only Date arithmetic on already-converted doubles. The three commits since the last review resolve both earlier inline findings, and I found no new defect. Approval is withheld because the runtime files and JSTests are covered by the jsc-reviewers CODEOWNERS entry and the change alters observable Date.UTC/constructor results at the 2^53 edge, which a maintainer should weigh.
…joins the month's days first A month argument that carried into the year cost a chained setter 3% to 8% more cycles than before, although it ran fewer instructions. The month went through a double, a range check and the 64-bit split before the year could be formed, and the day sum had three additions after the year's day count. - An int32 year or month argument is stored as it is. - A year or month that is a double is stored without a read of the field it replaces. - The setters add the date to the month's days before the year's days are known. makeDay() keeps MakeDay's order, which Date.UTC can see. A setter cannot see it: its time of day is below msPerDay, so a time value needs a day sum of at most 1e8, where every partial sum is exact in either order. - normalizeYearAndMonthForMakeDay() is the one split and the one int32 check, for makeDay() and for the setters. The stress test gains arguments that are not int32, the order of MakeDay's sum as Date.UTC sees it, and Date.UTC and the Date constructor beyond int32.
…re not int32 The WebKit change now stores an int32 year or month argument directly and gives the setters their own day sum, so a month argument that carries into the year is no longer slower than before. The test gains the other argument path: fractions, -0, a string.
There was a problem hiding this comment.
Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.
Beyond the inline finding, I checked a few other things in this version and found them in order: the local-time assertions in JSTests/stress/date-setters-year-month-range.js now start from locally constructed dates (e.g. new Date(2000, 0, 1)) rather than new Date(0), so the US/Pacific time-of-day mismatch from an earlier review no longer applies; the int64 split in normalizeYearAndMonthForMakeDay makes the 2^56 month case exact, which closes the inexact-split concern raised on an earlier version; and makeDay's switch from NaN to truncation for fractional year/month does not affect any caller, since Date.UTC/new Date trunc their arguments first and the Temporal callers pass integers.
Extended reasoning...
The change rewrites the year/month range handling in makeDay (Source/JavaScriptCore/runtime/DateConstructor.h) and the date setters in Source/JavaScriptCore/runtime/DatePrototype.cpp, replacing per-argument int bounds with a 12 * 2^53 magnitude check and an exact int64 floor-division split, and adds a stress test. It touches no security-sensitive surface. One confirmed spec-divergence finding is posted inline; the note above only records the additional checks that were ruled out this run, including that feedback from earlier versions is reflected in the current code.
…ent month / 12 MakeDay step 5 is floor(month / 12) on the mathematical value of the month, so the quotient is the real one. A double quotient has no fraction from 2^52: for a month count of 12k + 8 with k >= 2^52 it is k + 1, and the date would be 12 months late. The 64-bit split gives k. The stress test did not tell the two apart. It does now, for a positive and a negative month count, through Date.UTC and through a setter: Date.UTC(2000 - 2 ** 52, 12 * 2 ** 52 + 8, 1) is 2000-09-01. The two comments in DateConstructor.h named floor(month / 12) without a word on which quotient it is, and one of them named 2^53 for the double. No code changes.
There was a problem hiding this comment.
I reviewed the latest revision (through 0141eac) and found no bugs; the points raised on earlier pushes are addressed in the code (the local-time cases in the stress test now start from new Date(2000, 0, 1), and the year/month split is exact in int64). Because this changes Date.UTC and new Date as well as the setters, and deliberately picks a fork-level limit (NaN above |month| = 12 * 2^53, replacing three upstream guards), a human maintainer should still weigh that choice.
What was reviewed:
- normalizeYearAndMonthForMakeDay / makeDay: with both inputs bounded by 12 * 2^53 the int64 sums cannot overflow, the int32 fit check precedes every narrowing, and dayInYear only ever sees a month in 0..11; the Temporal callers pass int32 years and months 0..11, so they are unaffected.
- dateSetterTimeValue: daysFrom1970ToYear is double arithmetic for any int32 year (at most ~7.8e11 days), so any in-range result needs |monthDay| well below 2^53 and the partial sums are exact as the comment states; a huge monthDay reaches timeClip and gives NaN.
- fillStructuresUsingDateArgs: the int32 fast path matches ToIntegerOrInfinity, -0 takes the double path to 0, and later arguments are still converted after an earlier one fails (pinned by the test's valueOf log).
- Ruled out: a JSTests/BUN-TEST-DIFFERENCES.md entry is not needed, since this is a new fork-only test rather than a changed upstream test.
Extended reasoning...
The change touches Source/JavaScriptCore/runtime/DateConstructor.h (makeDay, shared by Date.UTC, the Date constructor and Temporal) and DatePrototype.cpp (all seven date setters), replacing per-argument year/month bounds with an int64 MakeDay split and an int32 fit check, plus a 270-line stress test. It touches no security-sensitive surface (no auth, injection or data exposure), only numeric date semantics. Deciding against approve: the paths are covered by .github/CODEOWNERS (jsc-reviewers), the PR itself says a maintainer must choose between this design and a wider per-argument bound, and Date.UTC / new Date now return NaN above |month| = 12 * 2^53 where ECMA-262 defines a value, which is a deliberate fork-level limit rather than an obviously mechanical change. No third-party objections are open in the timeline metadata, and the bug hunt ran dry with no findings on this revision.
main moved WEBKIT_VERSION to 680ef47cec (#46690). The pin here is now the preview of oven-sh/WebKit#616 merged with that commit, autobuild-preview-pr-616-66ac271a.
…re filed oven-sh/bun#46865: Bun 1.4.4's JavaScriptCore rejects EST5EDT, CST6CDT, MST7MDT and PST8PDT on macOS. fullcalendar/temporal-polyfill#118: the polyfill identifies zones by Intl's resolvedOptions().timeZone, which JavaScriptCore no longer canonicalizes. Both rows in docs/upstream.md, sections 8 and 9 of docs/bun-compatibility.md and the known-failures comment link them; section 8 no longer cites supportedValuesOf, which lists only primary zones and so says nothing about links. The sweep of the other items: temporal_rs merged the ZonedDateTime cache (boa-dev/temporal#757, closing #756) on 2026-10-21, after v0.2.6, so V8 does not have it yet; the fix for oven-sh/bun#46342 is in review (oven-sh/WebKit#616).
Related to oven-sh/bun#46342
Problem
NaNwhen the year alone is outside ±275760 or the month count outside ±3309120:new Date(0).setUTCFullYear(275761, -12, 1)isNaN,Date.UTC(275761, -12, 1)is8639977881600000. One report, for dates over 275,760 years apart.year + floor(month/12).fillStructuresUsingDateArgs()(runtime/DatePrototype.cpp) bounds each argument and narrows it toint.Fix
normalizeYearAndMonthForMakeDay()(runtime/DateConstructor.h) splits anint64_tmonth count by 12 exactly and checks that the year fitsint32_t.makeDay()(Date.UTC,new Date, Temporal) and the date setters call it.JSTests/stress/date-setters-year-month-range.js(fails on the base). 7813 fuzzed calls equal a BigInt MakeDay. test262 Date: 0 new failures.Background
Date.prototype.setYearRespects TimeClip Range by Guarding GregorianDateTime::years Overflow WebKit/WebKit#46001 and [JSC] EnsureDate.prototype.setMonthandDate.prototype.setUTCMonthRespects TimeClip Range by Guarding months of GregorianDateTime Overflow WebKit/WebKit#47036, and is not filed upstream. A maintainer has to choose: this, or a wider per-argument bound that fixes the report and keeps two normalisations.Downsides
Date.UTCandnew Date(y, m)answerNaNwhere ECMA-262 defines a date (this fork's limit): 3,431 of 300,000 fuzzed calls, 859 were right. 882 wrong dates become exact.Date.UTC31..text: +224 bytes (-2,176 with ThinLTO).Notes
Measurements (base =
680ef47cec, the tip ofmain, which bun pins since oven-sh/bun#46690. Shells: Release-march=nehalem, and a ThinLTO + frame pointer build with the flags of the shippedltolane, both clang 23.1.2, both built with and without this change. An earlier round on79618395eegave the same figures within the noise, exceptsetMonthon one Date: -13.7% there, -4.7% here.)setNewValueFromTimeArgsand 0 of 107 inDateCache::gregorianDateTimeToMS(non-LTO), 0 of 749 and 0 of 108 (ThinLTO) (objdump, base vs PR, x64)toInt32flavour: time setters +0.0, date setters -123.8 to -30.0, with double arguments -87.8 and -66.8, with a carrying month -74.0, Date.UTC(3 args) -39.8.fjcvtzsflavour (-mcpu=apple-m1): time setters +0.0, date setters -86.0 to -10.0, with double arguments -50.0 and -46.0, with a carrying month -52.0, Date.UTC(3 args) -2.0. These are from the previous base,79618395ee, and were not run again: the Date functions have the same x64 instruction counts on both bases, with and without the change..text: +224 bytes non-LTO, -2,176 bytes ThinLTO (llvm-size -A, base vs PR)Results that change
new Date(0).setUTCFullYear(275761, -12, 1)8639977881600000 (was NaN).new Date(Date.UTC(-1, 0, 1)).setUTCMonth(12 * 275761)8639977881600000 (was NaN).new Date(NaN).setUTCFullYear(-271821, 3600000)827076182400000 (was NaN). The lines of Date#setMonth returns Invalid Date when the result lies more than 8.64e15 ms from the date bun#46342:d.setFullYear(-187631, 8, 27); d.setMonth(3472364)is +101732-09-27 (was Invalid Date). On released bun, dayjs and momentdiff()return 0 for two such dates, and date-fnsdifferenceInMonthsis off by one.msPerDayand added to the milliseconds first. Beyond 2^53 that sum lost a millisecond:new Date(8.64e15 - 999).setUTCDate(-199000000)was -8553601036801000 and is -8553601036800999, asDate.UTC(275760, 8, -199000000, 23, 59, 59, 1)is. This is not a second fix: without the per-argument bound, a day count can be that large and still give a date.new Date(0).setUTCFullYear(275761, 0, -200)(a day count that pulls the year back) equalsDate.UTC(275761, 0, -200)now, as in V8. A strict reading of MakeDay step 8 does not require this one: no time value has the year 275761.Date.UTC(-6004799503158661, 2 ** 56, 1)was 2000-01-01 where MakeDay gives 2000-05-01: the double splitfloor(month / 12)andmonth - years * 12rounds beyond 2^53. It is 2000-05-01 now.Date.UTC(2000 - 2 ** 52, 12 * 2 ** 52 + 8, 1)was 2000-05-01 and is 2000-09-01. MakeDay floors the real quotientmonth / 12. A double quotient has no fraction from 2^52, so its floor is one more for that month count.floor(month / 12)is not always a Number and MakeDay adds the rounded value: forDate.UTC(2000 - 2 ** 53, 12 * 2 ** 53 + 16, 1)MakeDay gives 2000-05-01, exact arithmetic gives 2001-05-01, and this change gives NaN. V8 gives NaN from |month| = 1e7.makeDay()truncates a year or month count that has a fraction, as MakeDay's ToIntegerOrInfinity does, where it answered NaN. No caller passes one.Tests
run-jsc-stress-tests(which forces TZ=US/Pacific), and 8 time zones. An expected value that is NaN fails it, becauseDate.UTCand the Date constructor share MakeDay with the setters. A NaN is expected throughshouldBeNaN()only. 24 of its lines are outside V8's own caps (|year| above 1e6 or |month| above 1e7), where V8 answers NaN. V8 agrees with all the others.Date.UTCbeyond int32. It does now. The double quotient came from a review: the test passed with it for every month count, and now fails from 12 × 2^52.setYearcalls in America/Los_Angeles on a date 1 ms after the minimum time value, where every local setter of the base answers NaN too ([JSC] Date: convert a local time to UTC with the zone's own offset when the local time is outside the time value range #615 is about that).run-jsc-stress-testson the date and Temporal tests ofJSTests/stressandLayoutTests/jsc-layout-tests.yaml, and test262built-ins/Temporalandintl402, ran on the first commit of this branch, on the previous base: 2,781 stress runs pass (the 16 failing runs aretemporal-invalid-calendar-identifier-huge-string.js, killed by the memory limit of the machine, on the base as well), 15,048 test262 runs pass with 0 new failures.main,66ac271a67, against680ef47cec: the stress test fails on the base and passes in the 17 configurations and 8 time zones. The fuzz outputs of the base and of the change are byte for byte the ones of the previous base. test262built-ins/Date,annexB/built-ins/Date,intl402/Dateandstaging/sm/Date(1,316 runs), andbuilt-ins/Temporal,intl402/Temporalandintl402/DateTimeFormat(13,752 runs): 0 new failures, and every run has the same result with and without the change.run-jsc-stress-testson the 199 date and Temporal tests ofJSTests/stress: 3,221 of 3,237 runs pass. The 16 others aretemporal-invalid-calendar-identifier-huge-string.js, killed by the memory limit of the machine, on the base as well.Design
79618395ee.makeDay()keeps MakeDay's order for the day sum,Day(t) + dt - 1.Date.UTCcan see it when its time arguments cancel a day count beyond 2^53 (the stress test has two such calls). The setters add the date to the month's days first, which does not wait for the year's day count. A setter cannot see the order: its time of day is belowmsPerDay, so a time value needs a day sum of at most 1e8, where every partial sum is exact.dateSetterTimeValue()isALWAYS_INLINE: out of line,setMonth(m)andsetUTCMonth(m)in a chain cost 2.9% to 4.1% more cycles than the base under ThinLTO.isSafeInteger(), which also truncates,Date.UTCwas +3 instructions on thefjcvtzsflavour.month / 12(MakeDay step 5 divides the mathematical value) is an exact Number, so the integer sum is the sum MakeDay specifies. A bound of 2^53 - 1 (the first version of this rework) answered NaN for 6,932 of the 300,000 fuzzedDate.UTCcalls and made none of the wrong dates exact. With a bound of 2^62 the integer sum is exact arithmetic, but not MakeDay's, which adds the rounded quotient.makeDay()'s double split, leavesDate.UTCwith its wrong dates beyond 2^53, and a setter andDate.UTCstill differ between the two bounds. That is the smaller change if themakeDay()contract of this fork must stay upstream's.mainmerged in at680ef47cec, the commit bun pins since Upgrade WebKit to f59345e04a8d bun#46690. So the preview build of this PR differs from bun's WebKit by this change only. The earlier commits were based on79618395ee.DatePrototype.cpp,DateConstructor.h,DateConstructor.cpp,JSDateMath.*,wtf/DateMath.*and the Temporal callers ofmakeDay()did not change between the two.Self-review
makeDay()is not left as upstream has it.DatePrototype.cppat the base is the file upstream has today.Not in this PR
new Date(1875619980085361).setUTCMinutes(-150119987617)is -7131579276994640 and MakeTime gives -7131579276994639 (V8 too). Two arguments that cancel are up to a day off:new Date(82800000).setUTCSeconds(2 ** 70, -(2 ** 70) * 1000)is 82800000 where V8 andDate.UTC(1970, 0, 1, 23, 0, 2 ** 70, -(2 ** 70) * 1000)give 134217728. A form with all 15 setters on one path was built. It is not here because it moves those results, which is a decision of its own, and because it gives up the proof that the time setters compile to the same instructions. No issue tracks it yet.intmonth:WTF::dateToDaysFrom1970,WTF::daysFromYearMonth,v8::MakeDaybehindDate.parse.TemporalCore::balanceISOYearMonthcould share the int64 split.int32_tand letstimeClip()decide. V8 caps |year| at 1e6 and |month| at 1e7 first.