mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
Fix Date setter edge cases and the library profile fences
This commit is contained in:
@@ -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),
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8027-date-setter-order.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8028-date-setter-order-dynamic.mjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user