Fix Date setter edge cases and the library profile fences

This commit is contained in:
Chris Tate
2026-10-23 23:10:56 -05:00
parent 27593e366e
commit 8c94f6f10c
11 changed files with 151 additions and 12 deletions
@@ -255,17 +255,19 @@ export function lowerInheritedDateMethodCall(
loc,
});
if (isDateSetter(name) && name !== "setTime") {
// The field setters change the Date the class's time slot holds.
// The field setters change the Date the class's time slot holds once
// the arguments are evaluated: an argument that calls setTime on the
// instance gives the slot a new Date.
const date = lowerer.declareHiddenLocal("%dateTime", DATE_T);
const current = varRef(date.id, DATE_T, loc);
const time: IrExpr = { kind: "libCall", fn: "date.getTime", args: [current], type: F64, loc };
const next = dateSetterTime(lowerer, call, name, stmts, time);
stmts.push({
kind: "varDecl",
localId: date.id,
init: timeOf(lowerer, value, root, loc),
loc,
});
const current = varRef(date.id, DATE_T, loc);
const time: IrExpr = { kind: "libCall", fn: "date.getTime", args: [current], type: F64, loc };
const next = dateSetterTime(lowerer, call, name, stmts, time);
return done({ kind: "libCall", fn: "date.store", args: [current, next], type: F64, loc });
}
if (name === "setTime") {
@@ -342,9 +342,9 @@ function lowerDateSetter(
/** The new time value of setter `name` called with `call`'s arguments on a
* Date whose time value `time` reads: the arguments evaluate into
* temporaries appended to `stmts` (ToNumber of each, an undefined one is
* NaN), and the result reads `time` after them. Missing optional arguments
* keep their fields; arguments past a setter's last one are evaluated and
* ignored. */
* NaN), and the result reads `time` after them. A missing first argument
* is NaN; missing optional arguments keep their fields; arguments past a
* setter's last one are evaluated and ignored. */
export function dateSetterTime(
lowerer: Lowerer,
call: ts.CallExpression,
@@ -374,7 +374,8 @@ export function dateSetterTime(
loc,
};
const setter = DATE_FIELD_SETTERS.get(name)!;
const used = args.slice(0, setter.max);
// The first argument is required: a missing one is undefined, so NaN.
const used = args.length === 0 ? [number(Number.NaN)] : args.slice(0, setter.max);
const operands = [...used];
while (operands.length < 4) operands.push(number(0));
return {
@@ -102,7 +102,7 @@ function decideTimeValueDate(lowerer: Lowerer, decl: ts.VariableDeclaration): bo
if (!ok) return;
if (ts.isIdentifier(node) && node !== decl.name && node.text === decl.name.text) {
if (lowerer.checker.getSymbolAtLocation(node) === symbol) {
const use = enclosingFunction(node) === fn ? timeValueUse(lowerer, node) : null;
const use = enclosingFunction(node) === fn ? timeValueUse(lowerer, node, symbol) : null;
if (use === null) ok = false;
else if (use === "set") sets = true;
}
@@ -174,7 +174,11 @@ function enclosingFunction(node: ts.Node): ts.FunctionLikeDeclaration | null {
/** How a use of the binding uses its time value: "read" for a use that
* reads only the time value, "set" for a setter call that replaces it, or
* null for any other use. */
function timeValueUse(lowerer: Lowerer, id: ts.Identifier): "read" | "set" | null {
function timeValueUse(
lowerer: Lowerer,
id: ts.Identifier,
symbol: ts.Symbol,
): "read" | "set" | null {
let node: ts.Node = id;
while (ts.isParenthesizedExpression(node.parent)) node = node.parent;
const parent = node.parent;
@@ -192,12 +196,34 @@ function timeValueUse(lowerer: Lowerer, id: ts.Identifier): "read" | "set" | nul
if (isDateSetter(parent.name.text)) return "set";
return TIME_VALUE_METHODS.has(parent.name.text) ? "read" : null;
}
return readsTimeValue(lowerer, node, parent) ? "read" : null;
return readsTimeValue(lowerer, node, parent, symbol) ? "read" : null;
}
/** True when `node` calls a setter on the binding `symbol` names. */
function callsSetterOf(lowerer: Lowerer, node: ts.Node, symbol: ts.Symbol): boolean {
if (
ts.isCallExpression(node) &&
ts.isPropertyAccessExpression(node.expression) &&
isDateSetter(node.expression.name.text)
) {
let receiver = node.expression.expression;
while (ts.isParenthesizedExpression(receiver)) receiver = receiver.expression;
if (ts.isIdentifier(receiver) && lowerer.checker.getSymbolAtLocation(receiver) === symbol)
return true;
}
return (
ts.forEachChild(node, (child) => callsSetterOf(lowerer, child, symbol) || undefined) ?? false
);
}
/** A use of the binding, other than a method call, that reads only its
* time value. */
function readsTimeValue(lowerer: Lowerer, node: ts.Node, parent: ts.Node): boolean {
function readsTimeValue(
lowerer: Lowerer,
node: ts.Node,
parent: ts.Node,
symbol: ts.Symbol,
): boolean {
if (ts.isTemplateSpan(parent)) return parent.expression === node;
// Unary + and -, the number hint.
if (ts.isPrefixUnaryExpression(parent))
@@ -222,6 +248,9 @@ function readsTimeValue(lowerer: Lowerer, node: ts.Node, parent: ts.Node): boole
);
}
if (ts.isBinaryExpression(parent)) {
// A left operand converts after the right one is evaluated: a setter
// there changes the time value the comparison or concatenation reads.
if (parent.left === node && callsSetterOf(lowerer, parent.right, symbol)) return false;
const other = parent.left === node ? parent.right : parent.left;
const otherKind = lowerer.mapTypeOf(lowerer.typeOf(other))?.kind;
if (RELATIONAL.has(parent.operatorToken.kind)) return otherKind === "date";
@@ -2037,6 +2037,28 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
loc,
};
if (left.type.kind === "string" || right.type.kind === "string") {
// A Date on the left converts after the right operand is
// evaluated, which can run its setters.
if (left.type.kind === "date" && !isSafeToRepeat(right)) {
const date = lowerer.declareHiddenLocal("%plus", left.type);
const other = lowerer.declareHiddenLocal("%plus", right.type);
return {
kind: "seqExpr",
stmts: [
{ kind: "varDecl", localId: date.id, init: left, loc },
{ kind: "varDecl", localId: other.id, init: right, loc },
],
result: {
kind: "strConcat",
left: concatOperand(lowerer, varRef(date.id, left.type, loc), expr.left),
right: concatOperand(lowerer, varRef(other.id, right.type, loc), expr.right),
type: STRING,
loc,
},
type: STRING,
loc,
};
}
return {
kind: "strConcat",
left: concatOperand(lowerer, left, expr.left),
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8027-date-setter-order.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8028-date-setter-order-dynamic.mjs"
],
"diags": []
}
+53
View File
@@ -0,0 +1,53 @@
// Date setters and evaluation order: a setter called without arguments
// makes the Date invalid, an argument that calls setTime on the same
// Date subclass instance is seen by the setter, and a comparison or
// concatenation reads its left Date after the right side runs a setter.
export {};
process.env.TZ = "UTC";
const show = (label: string, d: Date): void => {
console.log(label, d.getTime(), Number.isNaN(d.getTime()) ? "invalid" : d.toISOString());
};
// No arguments: the first argument is undefined, so NaN.
const local = new Date(Date.UTC(2026, 5, 15, 9, 30));
// @ts-expect-error: the first argument is required
console.log(local.setHours(), Number.isNaN(local.getTime()));
const utc = new Date(Date.UTC(2026, 5, 15, 9, 30));
// @ts-expect-error: the first argument is required
console.log(utc.setUTCMonth(), utc.setUTCFullYear(2020));
show("utc after", utc);
const kept = new Date(Date.UTC(2026, 5, 15));
const keep = kept;
// @ts-expect-error: the first argument is required
console.log(keep.setMinutes(), kept.getTime());
class Stamp extends Date {
label(): string {
return `stamp ${this.toISOString()}`;
}
}
const stamp = new Stamp(Date.UTC(2026, 0, 1));
// @ts-expect-error: the first argument is required
console.log(stamp.setSeconds(), Number.isNaN(stamp.getTime()));
stamp.setTime(Date.UTC(2026, 0, 2));
// @ts-expect-error: the first argument is required
console.log(stamp.setUTCDate(), stamp.setUTCFullYear(2024, 1, 29), stamp.label());
// An argument that calls setTime on the same instance.
const moved = new Stamp(Date.UTC(2026, 3, 10, 12));
console.log(moved.setUTCHours((moved.setTime(Date.UTC(2000, 0, 1)), 5)), moved.label());
const plain = new Date(Date.UTC(2026, 3, 10, 12));
console.log(plain.setUTCMinutes((plain.setTime(Date.UTC(2001, 0, 1)), 7)));
show("plain", plain);
// The left operand converts after the right operand runs a setter.
const a = new Date(Date.UTC(2026, 0, 1));
const b = new Date(Date.UTC(2026, 0, 2));
console.log(a < (a.setUTCDate(3), b), a > b);
const c = new Date(Date.UTC(2026, 0, 1));
console.log(c >= (c.setTime(0), new Date(1)), c <= new Date(1));
const s = new Date(Date.UTC(2026, 0, 1));
console.log(s + (s.setUTCFullYear(1999), "!"));
const n = new Date(Date.UTC(2026, 0, 1));
console.log(+n === (n.setTime(5), 5), +n);
@@ -0,0 +1,17 @@
// Date setters reached through untyped values: without arguments each
// one makes the Date invalid, and an argument that calls setTime on the
// same Date is seen by the setter.
process.env.TZ = "UTC";
const names = [
"setMilliseconds", "setSeconds", "setMinutes", "setHours", "setDate", "setMonth", "setFullYear",
"setUTCMilliseconds", "setUTCSeconds", "setUTCMinutes", "setUTCHours", "setUTCDate", "setUTCMonth", "setUTCFullYear",
];
for (const name of names) {
const d = new Date(Date.UTC(2026, 5, 15));
console.log(name, d[name](), d.getTime());
}
const d = new Date(Date.UTC(2026, 3, 10, 12));
console.log(d.setUTCHours((d.setTime(Date.UTC(2000, 0, 1)), 5)), d.toISOString());
const a = new Date(Date.UTC(2026, 0, 1));
console.log(a < (a.setUTCDate(3), new Date(Date.UTC(2026, 0, 2))));
@@ -0,0 +1 @@
{ "plain": 2489, "san": 3288 }
@@ -0,0 +1 @@
{ "plain": 2430, "san": 2975 }
+1
View File
@@ -563,6 +563,7 @@ describe("library profile fences", () => {
"stdlib.date.getUTCSeconds",
"stdlib.date.now",
"stdlib.date.parse",
"stdlib.date.setters",
"stdlib.date.toDateString",
"stdlib.date.toISOString",
"stdlib.date.toString",