From 3307ba43cf1bc6139f20fc81f93d3976389f8524 Mon Sep 17 00:00:00 2001 From: Philip Chimento Date: Mon, 9 Mar 2026 13:00:35 -0700 Subject: [PATCH 1/5] Polyfill: Reject out-of-range years in PlainMonthDay from fields When constructing a PlainMonthDay from a property bag, it's possible to give a year that is way out of range. The spec text says to bail out early in that case, to avoid performing expensive calculations to validate the month and day. Previously, the polyfill did not do this early bail-out step. See: #2997 --- polyfill/lib/calendar.mjs | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/polyfill/lib/calendar.mjs b/polyfill/lib/calendar.mjs index d50ece6df..2e6326fa1 100644 --- a/polyfill/lib/calendar.mjs +++ b/polyfill/lib/calendar.mjs @@ -319,6 +319,41 @@ const monthCodeInfo = { } }; +const calendarMinYear = { + buddhist: -271278, + chinese: -271821, + coptic: -272099, + dangi: -271821, + ethioaa: -266323, + ethiopic: -271823, + gregory: -271821, + hebrew: -268058, + indian: -271899, + 'islamic-civil': -280804, + 'islamic-tbla': -280804, + 'islamic-umalqura': -280804, + japanese: -271821, + persian: -272442, + roc: -273732 +}; +const calendarMaxYear = { + buddhist: 276303, + chinese: 275760, + coptic: 275471, + dangi: 275760, + ethioaa: 281247, + ethiopic: 275747, + gregory: 275760, + hebrew: 279517, + indian: 275682, + 'islamic-civil': 283583, + 'islamic-tbla': 283583, + 'islamic-umalqura': 283583, + japanese: 275760, + persian: 275139, + roc: 273849 +}; + function IsValidMonthCodeForCalendar(calendar, monthCode) { const { monthNumber, isLeapMonth } = ParseMonthCode(monthCode); if (!isLeapMonth && monthNumber >= 1 && monthNumber <= 12) return true; @@ -917,6 +952,10 @@ const nonIsoHelperBase = { // are all present, converting monthCode and eraYear if needed. date = this.adjustCalendarDate(date, cache, overflow, false); + if (date.year > calendarMaxYear[this.id] || date.year < calendarMinYear[this.id]) { + throw new RangeError(`Year ${date.year} out of range for calendar ${this.id}`); + } + // Fix obviously out-of-bounds values. Values that are valid generally, but // not in this particular year, may not be caught here for some calendars. // If so, these will be handled lower below. From 9a02869f2361c037c544c0cdbd56b1578e9c455e Mon Sep 17 00:00:00 2001 From: Philip Chimento Date: Tue, 7 Apr 2026 17:02:29 -0400 Subject: [PATCH 2/5] =?UTF-8?q?Polyfill:=20Rename=20isTemporalObject=20?= =?UTF-8?q?=E2=86=92=20isFormattableTemporalObject?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is clearer because isTemporalObject doesn't include Temporal.Duration. --- polyfill/lib/intl.mjs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/polyfill/lib/intl.mjs b/polyfill/lib/intl.mjs index 6d4768fb0..2bb27cf2c 100644 --- a/polyfill/lib/intl.mjs +++ b/polyfill/lib/intl.mjs @@ -424,7 +424,7 @@ function formatRange(a, b) { let formatter; let aDayAdjust = 0; let bDayAdjust = 0; - if (isTemporalObject(a) || isTemporalObject(b)) { + if (isFormattableTemporalObject(a) || isFormattableTemporalObject(b)) { if (!sameTemporalType(a, b)) { throw new TypeErrorCtor('Intl.DateTimeFormat.formatRange accepts two values of the same type'); } @@ -482,7 +482,7 @@ function formatRangeToParts(a, b) { let formatter; let aDayAdjust = 0; let bDayAdjust = 0; - if (isTemporalObject(a) || isTemporalObject(b)) { + if (isFormattableTemporalObject(a) || isFormattableTemporalObject(b)) { if (!sameTemporalType(a, b)) { throw new TypeErrorCtor('Intl.DateTimeFormat.formatRangeToParts accepts two values of the same type'); } @@ -740,7 +740,7 @@ function hasAnyDateTimeOptions(originalOptions) { return hasDateOptions(originalOptions) || hasTimeOptions(originalOptions); } -function isTemporalObject(obj) { +function isFormattableTemporalObject(obj) { return ( ES.IsTemporalDate(obj) || ES.IsTemporalTime(obj) || @@ -753,12 +753,12 @@ function isTemporalObject(obj) { } function toDateTimeFormattable(value) { - if (isTemporalObject(value)) return value; + if (isFormattableTemporalObject(value)) return value; return ES.ToNumber(value); } function sameTemporalType(x, y) { - if (!isTemporalObject(x) || !isTemporalObject(y)) return false; + if (!isFormattableTemporalObject(x) || !isFormattableTemporalObject(y)) return false; if (ES.IsTemporalTime(x) && !ES.IsTemporalTime(y)) return false; if (ES.IsTemporalDate(x) && !ES.IsTemporalDate(y)) return false; if (ES.IsTemporalDateTime(x) && !ES.IsTemporalDateTime(y)) return false; From fa96785442db108a9011a2b3d4495d2a166828e8 Mon Sep 17 00:00:00 2001 From: Philip Chimento Date: Tue, 7 Apr 2026 17:11:31 -0400 Subject: [PATCH 3/5] Polyfill: Various small fixes Got these from running through TypeScript compiler. --- polyfill/lib/calendar.mjs | 3 ++- polyfill/lib/ecmascript.mjs | 7 +++---- polyfill/lib/intl.mjs | 16 ++++++---------- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/polyfill/lib/calendar.mjs b/polyfill/lib/calendar.mjs index 2e6326fa1..25bac61fb 100644 --- a/polyfill/lib/calendar.mjs +++ b/polyfill/lib/calendar.mjs @@ -568,6 +568,7 @@ OneObjectCache.MAX_CACHE_ENTRIES = 1000; * Returns a WeakMap-backed cache that's used to store expensive results * that are associated with a particular ISO Date Record object instance. * + * @param id - calendar ID for the cache * @param obj - object to associate with the cache */ OneObjectCache.getCacheForObject = function (id, obj) { @@ -2035,7 +2036,7 @@ const helperChinese = ObjectAssign({}, nonIsoHelperBase, { const { month, year } = calendarDate; const previousMonthYear = month > 1 ? year : year - 1; - let previousMonthDate = { year: previousMonthYear, month, day: 1 }; + const previousMonthDate = { year: previousMonthYear, month, day: 1 }; const previousMonth = month > 1 ? month - 1 : this.monthsInYear(previousMonthDate, cache); return this.daysInMonth({ year: previousMonthYear, month: previousMonth }, cache); diff --git a/polyfill/lib/ecmascript.mjs b/polyfill/lib/ecmascript.mjs index e5a229c52..25c4d35c2 100644 --- a/polyfill/lib/ecmascript.mjs +++ b/polyfill/lib/ecmascript.mjs @@ -941,10 +941,9 @@ export function ValidateTemporalUnitValue(value, unitGroup, extraValues = []) { if (Call(ArrayPrototypeIncludes, extraValues, [value])) return; for (let index = 0; index < TEMPORAL_UNITS.length; index++) { const unitInfo = TEMPORAL_UNITS[index]; - const singular = unitInfo[0]; const plural = unitInfo[1]; const category = unitInfo[2]; - if (value !== singular && value !== plural) continue; + if (value !== plural) continue; if (unitGroup === 'datetime' || unitGroup === category) return; } throw new RangeErrorCtor(`${value} not allowed as a ${unitGroup} unit`); @@ -2820,7 +2819,7 @@ export function RejectDateTime(year, month, day, hour, minute, second, milliseco export function RejectDateTimeRange(isoDateTime) { const ns = GetUTCEpochNanoseconds(isoDateTime); if (ns.lesser(DATETIME_NS_MIN) || ns.greater(DATETIME_NS_MAX)) { - const dateTimeString = ISODateTimeToString(isoDateTime, 'auto', 'auto', 'never'); + const dateTimeString = ISODateTimeToString(isoDateTime, 'iso8601', 'auto', 'never'); throw new RangeErrorCtor(`${dateTimeString} is outside of supported range`); } } @@ -2830,7 +2829,7 @@ function AssertISODateTimeWithinLimits(isoDateTime) { const ns = GetUTCEpochNanoseconds(isoDateTime); assert( ns.geq(DATETIME_NS_MIN) && ns.leq(DATETIME_NS_MAX), - `${ISODateTimeToString(isoDateTime, 'auto', 'auto', 'never')} is outside the representable range` + `${ISODateTimeToString(isoDateTime, 'iso8601', 'auto', 'never')} is outside the representable range` ); } diff --git a/polyfill/lib/intl.mjs b/polyfill/lib/intl.mjs index 2bb27cf2c..0193050fa 100644 --- a/polyfill/lib/intl.mjs +++ b/polyfill/lib/intl.mjs @@ -430,11 +430,9 @@ function formatRange(a, b) { } const aRecord = extractOverrides(a, this); const bRecord = extractOverrides(b, this); - if (aRecord.formatter) { - assert(bRecord.formatter == aRecord.formatter, 'formatters for same Temporal type should be identical'); - formatter = aRecord.formatter; - formatArgs = [ES.epochNsToMs(aRecord.epochNs, 'floor'), ES.epochNsToMs(bRecord.epochNs, 'floor')]; - } + assert(bRecord.formatter == aRecord.formatter, 'formatters for same Temporal type should be identical'); + formatter = aRecord.formatter; + formatArgs = [ES.epochNsToMs(aRecord.epochNs, 'floor'), ES.epochNsToMs(bRecord.epochNs, 'floor')]; aDayAdjust = aRecord.dayAdjust ?? 0; bDayAdjust = bRecord.dayAdjust ?? 0; } else { @@ -488,11 +486,9 @@ function formatRangeToParts(a, b) { } const aRecord = extractOverrides(a, this); const bRecord = extractOverrides(b, this); - if (aRecord.formatter) { - assert(bRecord.formatter == aRecord.formatter, 'formatters for same Temporal type should be identical'); - formatter = aRecord.formatter; - formatArgs = [ES.epochNsToMs(aRecord.epochNs, 'floor'), ES.epochNsToMs(bRecord.epochNs, 'floor')]; - } + assert(bRecord.formatter == aRecord.formatter, 'formatters for same Temporal type should be identical'); + formatter = aRecord.formatter; + formatArgs = [ES.epochNsToMs(aRecord.epochNs, 'floor'), ES.epochNsToMs(bRecord.epochNs, 'floor')]; aDayAdjust = aRecord.dayAdjust ?? 0; bDayAdjust = bRecord.dayAdjust ?? 0; } else { From 0faae8a927fefb713e6b561d740bbe7cbda1c474 Mon Sep 17 00:00:00 2001 From: Philip Chimento Date: Mon, 27 Apr 2026 16:40:16 -0700 Subject: [PATCH 4/5] Polyfill: Add throw step to AdjustDateDurationRecord As per https://github.com/tc39/proposal-temporal/issues/3309 we change Temporal.Duration.prototype.round step 28.d ! to ?, so we can no longer omit the IsValidDuration check here in the polyfill. (This has no observable effect, since out-of-range durations cause a later step to throw anyway, but we may as well update things here. --- polyfill/lib/ecmascript.mjs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/polyfill/lib/ecmascript.mjs b/polyfill/lib/ecmascript.mjs index 25c4d35c2..6d6a0c3eb 100644 --- a/polyfill/lib/ecmascript.mjs +++ b/polyfill/lib/ecmascript.mjs @@ -770,12 +770,15 @@ export function ToTemporalPartialDurationRecord(temporalDurationLike) { return result; } -export function AdjustDateDurationRecord({ years, months, weeks }, newDays, newWeeks, newMonths) { +export function AdjustDateDurationRecord(dateDuration, newDays, newWeeks, newMonths) { assert(newDays !== undefined, 'days must be provided to AdjustDateDurationRecord'); + const months = newMonths ?? dateDuration.months; + const weeks = newWeeks ?? dateDuration.weeks; + RejectDuration(dateDuration.years, months, weeks, newDays, 0, 0, 0, 0, 0, 0); return { - years, - months: newMonths ?? months, - weeks: newWeeks ?? weeks, + years: dateDuration.years, + months, + weeks, days: newDays }; } From a39f9d83455583ea848fb3e5c11e18aeb00195ce Mon Sep 17 00:00:00 2001 From: Philip Chimento Date: Tue, 7 Apr 2026 20:34:50 -0400 Subject: [PATCH 5/5] Update test262 --- polyfill/test262 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/polyfill/test262 b/polyfill/test262 index ac3a4ba5b..d0c1b4555 160000 --- a/polyfill/test262 +++ b/polyfill/test262 @@ -1 +1 @@ -Subproject commit ac3a4ba5ba9e67be0472a3853e5db9810e43f8cf +Subproject commit d0c1b4555b03dd404873fd6422a4b5da00136500