Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions JSTests/stress/temporal-jit-intrinsics.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
//@ requireOptions("--useTemporal=1")

function shouldBe(actual, expected, msg) {
if (actual !== expected)
throw new Error(`${msg}: expected ${expected} but got ${actual}`);
}

function shouldThrow(fn, errorType, msg) {
try {
fn();
} catch (e) {
if (!(e instanceof errorType))
throw new Error(`${msg}: expected ${errorType.name} but got ${e}`);
return;
}
throw new Error(`${msg}: expected ${errorType.name} to be thrown`);
}

// Ascending, across the 64-bit word boundaries of the Int128 and the rounding edges of the millisecond division.
const nanoseconds = [-8640000000000000000000n, -18446744073709551616n, -9223372036854775809n, -9223372036854775808n, -9223372036854000000n, -1000001n, -1000000n, -1n, 0n, 9223372036853999999n, 9223372036854775807n, 9223372036854775808n, 18446744073709551616n, 8640000000000000000000n];
const milliseconds = [-8640000000000000, -18446744073710, -9223372036855, -9223372036855, -9223372036854, -2, -1, -1, 0, 9223372036853, 9223372036854, 9223372036854, 18446744073709, 8640000000000000];
const instants = nanoseconds.map(ns => new Temporal.Instant(ns));
const zoned = nanoseconds.map(ns => new Temporal.ZonedDateTime(ns, "UTC"));

function compareInstant(x, y) { return Temporal.Instant.compare(x, y); }
function equalsInstant(x, y) { return x.equals(y); }
function compareZoned(x, y) { return Temporal.ZonedDateTime.compare(x, y); }
function equalsZoned(x, y) { return x.equals(y); }
function compareDate(x, y) { return Temporal.PlainDate.compare(x, y); }
function equalsDate(x, y) { return x.equals(y); }
function compareDateTime(x, y) { return Temporal.PlainDateTime.compare(x, y); }
function equalsDateTime(x, y) { return x.equals(y); }
function compareTime(x, y) { return Temporal.PlainTime.compare(x, y); }
function equalsTime(x, y) { return x.equals(y); }

// Each row: [compare, equals, ascending values, a value equal to the first in another calendar or time zone]
const types = [
[compareInstant, equalsInstant, instants, null],
[compareZoned, equalsZoned, zoned, new Temporal.ZonedDateTime(nanoseconds[0], "Asia/Kolkata")],
[compareDate, equalsDate, [new Temporal.PlainDate(-271821, 4, 19), new Temporal.PlainDate(2020, 1, 31), new Temporal.PlainDate(2020, 2, 1), new Temporal.PlainDate(275760, 9, 13)], new Temporal.PlainDate(-271821, 4, 19, "gregory")],
[compareDateTime, equalsDateTime, [new Temporal.PlainDateTime(2020, 1, 31, 23, 59, 59, 999, 999, 998), new Temporal.PlainDateTime(2020, 1, 31, 23, 59, 59, 999, 999, 999), new Temporal.PlainDateTime(2020, 2, 1)], new Temporal.PlainDateTime(2020, 1, 31, 23, 59, 59, 999, 999, 998, "japanese")],
[compareTime, equalsTime, [new Temporal.PlainTime(), new Temporal.PlainTime(12, 30, 15, 1, 2, 3), new Temporal.PlainTime(12, 30, 15, 1, 2, 4), new Temporal.PlainTime(23, 59, 59, 999, 999, 999)], null],
];

for (const [compare, equals, values, other] of types) {
noInline(compare);
noInline(equals);
const copies = values.map(value => value.constructor.from(value));
for (let i = 0; i < testLoopCount; i++) {
const j = i % (values.length - 1);
shouldBe(compare(values[j], values[j + 1]), -1, `${compare.name} ${j}`);
shouldBe(compare(values[j + 1], values[j]), 1, `${compare.name} ${j} reversed`);
shouldBe(compare(values[j], copies[j]), 0, `${compare.name} ${j} copy`);
shouldBe(equals(values[j], values[j + 1]), false, `${equals.name} ${j}`);
shouldBe(equals(values[j], copies[j]), true, `${equals.name} ${j} copy`);
// -2^64 and 2^64 differ only in the high word.
shouldBe(equals(values[j], values[values.length - 1 - j]), j === values.length - 1 - j, `${equals.name} ${j} mirrored`);
if (other) {
shouldBe(compare(values[0], other), 0, `${compare.name} in another calendar or time zone`);
shouldBe(equals(values[0], other), false, `${equals.name} in another calendar or time zone`);
}
}
shouldBe(compare(values[0], values[0].toString()), 0, `${compare.name} with a string`);
shouldBe(equals(values[0], values[0].toString()), true, `${equals.name} with a string`);
shouldThrow(() => compare(values[0], 42), TypeError, `${compare.name} with a number`);
shouldThrow(() => equals(Object.create(values[0].constructor.prototype), values[0]), TypeError, `${equals.name} on a wrong receiver`);
}

function instantMilliseconds(x) { return x.epochMilliseconds; }
noInline(instantMilliseconds);
function zonedMilliseconds(x) { return x.epochMilliseconds; }
noInline(zonedMilliseconds);
for (let i = 0; i < testLoopCount; i++) {
const j = i % nanoseconds.length;
shouldBe(instantMilliseconds(instants[j]), milliseconds[j], `Instant epochMilliseconds ${j}`);
shouldBe(zonedMilliseconds(zoned[j]), milliseconds[j], `ZonedDateTime epochMilliseconds ${j}`);
}
{
const instant = new Temporal.Instant(0n);
Object.defineProperty(instant, "epochMilliseconds", { value: 7 });
shouldBe(instantMilliseconds(instant), 7, "epochMilliseconds own property");
}
shouldThrow(() => zonedMilliseconds(Object.create(Temporal.ZonedDateTime.prototype)), TypeError, "epochMilliseconds on a wrong receiver");

// Values beyond int32 and int52 are doubles.
const names = ["years", "months", "weeks", "days", "hours", "minutes", "seconds", "milliseconds"];
const durations = [
new Temporal.Duration(1, 2, 3, 4, 5, 6, 7, 8, 9, 10),
new Temporal.Duration(-1, -2, -3, -4, -5, -6, -7, -8, -9, -10),
new Temporal.Duration(0, 0, 0, 2 ** 31),
new Temporal.Duration(0, 0, 0, 0, 0, 0, Number.MAX_SAFE_INTEGER),
new Temporal.Duration(0, 0, 0, 0, 0, 0, 0, -(2 ** 62)),
];
for (const name of names) {
const get = new Function("d", `return d.${name};`);
noInline(get);
const getter = Object.getOwnPropertyDescriptor(Temporal.Duration.prototype, name).get;
const expected = durations.map(d => getter.call(d));
for (let i = 0; i < testLoopCount; i++) {
const j = i % durations.length;
shouldBe(get(durations[j]), expected[j], `Duration ${name} ${j}`);
}
shouldBe(get({ [name]: "own" }), "own", `${name} on a plain object`);
shouldThrow(() => get(Object.create(Temporal.Duration.prototype)), TypeError, `${name} on a wrong receiver`);
}

// The operand is only a Temporal object on the path that uses it, so nothing may be hoisted above the check.
function guarded(x, n) {
let s = 0;
for (let i = 0; i < n; i++) {
if (x instanceof Temporal.Instant)
s += x.epochMilliseconds + Temporal.Instant.compare(x, x) + (x.equals(x) ? 1 : 0);
if (x instanceof Temporal.Duration)
s += x.hours;
}
return s;
}
noInline(guarded);
const epoch = new Temporal.Instant(0n);
for (let i = 0; i < testLoopCount; i++) {
shouldBe(guarded(epoch, 4), 4, "guarded Instant");
shouldBe(guarded(durations[0], 4), 20, "guarded Duration");
shouldBe(guarded(i, 4), 0, "guarded number");
}

// Replacing a getter on the prototype invalidates code that inlined it.
Object.defineProperty(Temporal.Duration.prototype, "hours", { get() { return 1; } });
shouldBe(guarded(durations[0], 4), 4, "guarded Duration with hours redefined");
83 changes: 83 additions & 0 deletions JSTests/stress/temporal-options-cache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
//@ requireOptions("--useTemporal=1")

function shouldBe(actual, expected, msg) {
if (actual !== expected)
throw new Error(`${msg}: expected ${expected} but got ${actual}`);
}

// Options bags are read through a cache keyed on their structure and Object.prototype's.
const duration = new Temporal.Duration(0, 0, 0, 0, 25, 31, 45);
function round(options) { return duration.round(options).toString(); }
function from(bag) { return Temporal.Duration.from(bag).toString(); }
function until(options) { return new Temporal.PlainDate(2020, 1, 31).until(new Temporal.PlainDate(2021, 3, 1), options).toString(); }

function warm() {
shouldBe(round({ smallestUnit: "hour" }), "PT26H", "warm round");
shouldBe(from({ days: 1, hours: 2 }), "P1DT2H", "warm from");
shouldBe(until({ largestUnit: "month" }), "P13M1D", "warm until");
}
warm();
warm();

// A getter for an option key installed on Object.prototype.
{
const log = [];
Object.defineProperty(Object.prototype, "roundingMode", { configurable: true, get() { log.push("roundingMode"); return "floor"; } });
Object.defineProperty(Object.prototype, "years", { configurable: true, get() { log.push("years"); return 2; } });
shouldBe(round({ smallestUnit: "hour" }), "PT25H", "Object.prototype roundingMode");
shouldBe(from({ days: 1 }), "P2Y1D", "Object.prototype years");
shouldBe(log.join(), "roundingMode,years", "Object.prototype getters called");
delete Object.prototype.roundingMode;
delete Object.prototype.years;
warm();
}

// Keys added to and deleted from the options object, and keys only on its prototype.
{
const options = { smallestUnit: "hour" };
options.roundingMode = "floor";
shouldBe(round(options), "PT25H", "added key");
delete options.roundingMode;
shouldBe(round(options), "PT26H", "deleted key");
shouldBe(round({ smallestUnit: "hour", get roundingMode() { return "floor"; } }), "PT25H", "own getter");
shouldBe(round(Object.create({ smallestUnit: "hour", roundingMode: "floor" })), "PT25H", "keys on the prototype");
const bag = { days: 1, hours: 2 };
bag.minutes = 3;
shouldBe(from(bag), "P1DT2H3M", "added duration field");
}

// Converting an object value runs user code that can change the options read after it.
{
const options = { smallestUnit: "day", roundingMode: "floor" };
options.largestUnit = { toString() { options.smallestUnit = "minute"; options.roundingMode = "ceil"; options.roundingIncrement = 5; return "hour"; } };
shouldBe(round(options), "PT25H35M", "toString changes later options");
const bag = { hours: 2, seconds: 1 };
bag.days = { valueOf() { bag.hours = 5; bag.minutes = 9; delete bag.seconds; return 1; } };
shouldBe(from(bag), "P1DT5H9M", "valueOf changes later duration fields");
}

// Options from another realm, whose Object.prototype has a key.
{
const options = createGlobalObject().eval("Object.prototype.smallestUnit = 'minute'; ({})");
shouldBe(round(options), "PT25H32M", "another realm's Object.prototype");
shouldBe(until(createGlobalObject().eval("({ largestUnit: 'year' })")), "P1Y1M1D", "another realm's options");
}

// Calls remain correct across full GC.
for (let i = 0; i < 3; i++) {
shouldBe(round({ smallestUnit: "hour", ["key" + i]: i }), "PT26H", `fresh shape ${i}`);
fullGC();
shouldBe(round({ smallestUnit: "hour", largestUnit: "day" }), "P1DT2H", `after GC ${i}`);
}

// Dictionaries take the uncached path. Object.prototype stays a dictionary, so this comes last.
{
const options = $vm.toCacheableDictionary({ smallestUnit: "hour" });
shouldBe(round(options), "PT26H", "dictionary options");
options.largestUnit = "day";
shouldBe(round(options), "P1DT2H", "key added to dictionary options");
$vm.toCacheableDictionary(Object.prototype);
shouldBe(round({ smallestUnit: "hour" }), "PT26H", "dictionary Object.prototype");
Object.prototype.largestUnit = "day";
shouldBe(round({ smallestUnit: "hour" }), "P1DT2H", "key added to dictionary Object.prototype");
}
56 changes: 56 additions & 0 deletions JSTests/stress/temporal-plaindatetime-from-plaindate-limits.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//@ requireOptions("--useTemporal=1")

// ToTemporalDateTime on a PlainDate creates the date-time at midnight with ? CreateTemporalDateTime (step 2.c.iii),
// so a PlainDate whose midnight is outside ISODateTimeWithinLimits must throw RangeError. -271821-04-19 is a valid
// PlainDate (PlainDate limits are checked at noon) but its midnight is one day before the earliest instant.

function shouldBe(actual, expected) {
if (actual !== expected)
throw new Error(`Expected ${expected} but got ${actual}`);
}

function shouldThrow(func, errorType) {
let threw = false;
try { func(); } catch (e) {
threw = true;
if (!(e instanceof errorType))
throw new Error(`Expected ${errorType.name} but got ${e.constructor.name}: ${e.message}`);
}
if (!threw)
throw new Error(`Expected ${errorType.name} but no exception was thrown`);
}

const earliestDate = Temporal.PlainDate.from("-271821-04-19");
const nextDate = Temporal.PlainDate.from("-271821-04-20");
const dateTime = new Temporal.PlainDateTime(2000, 1, 1);

shouldThrow(() => Temporal.PlainDateTime.from(earliestDate), RangeError);
// The overflow option is read before the date-time is created.
{
let overflowRead = false;
shouldThrow(() => Temporal.PlainDateTime.from(earliestDate, { get overflow() { overflowRead = true; return "constrain"; } }), RangeError);
shouldBe(overflowRead, true);
}
shouldThrow(() => Temporal.PlainDateTime.compare(earliestDate, dateTime), RangeError);
shouldThrow(() => Temporal.PlainDateTime.compare(dateTime, earliestDate), RangeError);
shouldThrow(() => dateTime.equals(earliestDate), RangeError);

// until/since convert `other` (step 1) before reading options (step 3).
for (const method of ["until", "since"]) {
let optionsRead = false;
const options = {
get largestUnit() {
optionsRead = true;
return undefined;
}
};
shouldThrow(() => dateTime[method](earliestDate, options), RangeError);
shouldBe(optionsRead, false);
}

// The next day's midnight is within the limits.
shouldBe(Temporal.PlainDateTime.from(nextDate).toString(), "-271821-04-20T00:00:00");
shouldBe(Temporal.PlainDateTime.compare(nextDate, dateTime), -1);
shouldBe(dateTime.equals(nextDate), false);
shouldBe(dateTime.since(nextDate, { largestUnit: "days" }).days, 100010957);
shouldBe(Temporal.PlainDateTime.from(new Temporal.PlainDate(275760, 9, 13)).toString(), "+275760-09-13T00:00:00");
80 changes: 80 additions & 0 deletions JSTests/stress/temporal-sort-with-compare-function.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
//@ requireOptions("--useTemporal=1")

function shouldBe(actual, expected, msg) {
if (actual !== expected)
throw new Error(`${msg}: expected ${expected} but got ${actual}`);
}

function shouldThrow(fn, errorType, msg) {
try {
fn();
} catch (e) {
if (!(e instanceof errorType))
throw new Error(`${msg}: expected ${errorType.name} but got ${e}`);
return;
}
throw new Error(`${msg}: expected ${errorType.name} to be thrown`);
}

// Sorting with the compare function itself must give the same stable order as calling it from a JS comparator.
function checkSort(values, compare, msg) {
const expected = [...values].sort((a, b) => compare(a, b));
const actual = [...values].sort(compare);
for (let i = 0; i < values.length; i++)
shouldBe(actual[i], expected[i], `${msg} [${i}]`);
}

// Pseudo-random milliseconds with ties, so that stability is visible.
const milliseconds = [];
for (let i = 0, seed = 12345; i < 100; i++) {
seed = (seed * 1103515245 + 12345) % 2147483648;
milliseconds.push(i % 7 ? (seed - 1073741824) * 1e6 + (i % 5) * 1e3 : 0);
}
const instants = milliseconds.map(ms => Temporal.Instant.fromEpochMilliseconds(ms));
instants.push(new Temporal.Instant(-8640000000000000000000n), new Temporal.Instant(8640000000000000000000n), new Temporal.Instant(-1n));
const zoned = instants.map((instant, i) => instant.toZonedDateTimeISO(i & 1 ? "America/New_York" : "+05:30"));
const dates = zoned.map((z, i) => z.toPlainDate().withCalendar(i % 3 ? "iso8601" : "japanese"));
const durations = milliseconds.map((ms, i) => Temporal.Duration.from(i % 5 ? { days: i % 4, hours: Math.abs(Math.trunc(ms / 1e6) % 1000), microseconds: i } : { hours: -Math.abs(Math.trunc(ms / 1e6) % 1000), nanoseconds: -i }));
// A day and 24 hours are equal without relativeTo.
durations.push(Temporal.Duration.from({ hours: 24 }), Temporal.Duration.from({ days: 1 }), Temporal.Duration.from({ seconds: Number.MAX_SAFE_INTEGER, nanoseconds: 999999999 }));

checkSort(instants, Temporal.Instant.compare, "Instant");
checkSort(zoned, Temporal.ZonedDateTime.compare, "ZonedDateTime");
checkSort(dates, Temporal.PlainDate.compare, "PlainDate");
checkSort(zoned.map(z => z.toPlainDateTime()), Temporal.PlainDateTime.compare, "PlainDateTime");
checkSort(zoned.map(z => z.toPlainTime()), Temporal.PlainTime.compare, "PlainTime");
checkSort(dates.map(d => d.withCalendar("iso8601").toPlainYearMonth()), Temporal.PlainYearMonth.compare, "PlainYearMonth");
checkSort(durations, Temporal.Duration.compare, "Duration");
{
const day = Temporal.Duration.from({ days: 1 });
const hours23 = Temporal.Duration.from({ hours: 23 });
const hours24 = Temporal.Duration.from({ hours: 24 });
const sorted = [hours24, day, hours23].sort(Temporal.Duration.compare);
shouldBe(sorted[0], hours23, "23 hours first");
shouldBe(sorted[1], hours24, "then 24 hours");
shouldBe(sorted[2], day, "then a day");
}

// Elements without the internal slots, and Durations with years, months or weeks, take the generic path: they are
// converted, or the compare function throws.
checkSort([...instants.slice(0, 20), zoned[3], instants[5].toString()], Temporal.Instant.compare, "Instant with ZonedDateTime and string");
shouldThrow(() => [instants[0], 42].sort(Temporal.Instant.compare), TypeError, "Instant.compare with a number");
shouldThrow(() => [Temporal.Duration.from({ months: 1 }), Temporal.Duration.from({ days: 30 })].sort(Temporal.Duration.compare), RangeError, "Duration.compare with months");

// Holes and undefined never reach the comparator.
{
const sorted = [instants[3], undefined, instants[1], , instants[2]].sort(Temporal.Instant.compare);
const expected = [instants[1], instants[2], instants[3]].sort((a, b) => Temporal.Instant.compare(a, b));
for (let i = 0; i < expected.length; i++)
shouldBe(sorted[i], expected[i], `sparse [${i}]`);
shouldBe(sorted[3], undefined, "undefined after the values");
shouldBe(4 in sorted, false, "hole moved to the end");
}

// The DFG sorts arrays of at most 16 elements itself, calling the compare function for each pair.
function sortInstants(values) { return values.sort(Temporal.Instant.compare); }
noInline(sortInstants);
const small = instants.slice(0, 12);
const expected = [...small].sort((a, b) => Temporal.Instant.compare(a, b));
for (let i = 0; i < testLoopCount; i++)
shouldBe(sortInstants([...small])[i % small.length], expected[i % small.length], "hot sort");
Loading