mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-27 06:43:19 +08:00
Keep Dates as time values where no use observes their identity
This commit is contained in:
@@ -559,21 +559,25 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
|
||||
? "scr_stream_dyn_async_iterator_open"
|
||||
: LIB_FN_SYMS[e.fn]);
|
||||
if (sym === undefined) throw new LlvmUnsupportedError(`libCall:${e.fn}`, e.loc);
|
||||
// The runtime's Date functions take and answer time values: a Date
|
||||
// operand passes its time value, read at once from the borrowed object,
|
||||
// and a Date result is a new Date object.
|
||||
const args = READ_ONLY_STRING_LIB_FNS.has(e.fn)
|
||||
? emitBorrowedInputs(host, e.args)
|
||||
: e.args.map((a) =>
|
||||
borrowsJsonInputs(e.fn) || borrowsIteratorInputs(e.fn)
|
||||
? emitBorrowedInput(host, a)
|
||||
: host.emitExpr(a),
|
||||
a.type.kind === "date"
|
||||
? { name: emitDateTimeArg(host, a), type: F64 }
|
||||
: borrowsJsonInputs(e.fn) || borrowsIteratorInputs(e.fn)
|
||||
? emitBorrowedInput(host, a)
|
||||
: host.emitExpr(a),
|
||||
);
|
||||
// getTime and valueOf answer the time value the argument already is.
|
||||
if ((e.fn === "date.getTime" || e.fn === "date.valueOf") && e.args[0]?.type.kind === "date")
|
||||
return { name: args[0]!.name, type: e.type };
|
||||
// The deep comparisons' number test: computed here, with no runtime call.
|
||||
if (e.fn === "assert.sameValue")
|
||||
return { name: emitSameValueF64(host, args[0]!.name, args[1]!.name), type: e.type };
|
||||
// The runtime's Date functions take and answer time values: a Date
|
||||
// operand passes its time value, and a Date result is a new Date object.
|
||||
const callArgs = args.map((a) =>
|
||||
a.type.kind === "date" ? { name: emitDateTime(host, a.name), type: F64 } : a,
|
||||
);
|
||||
const callArgs = args;
|
||||
if (e.type.kind === "date") {
|
||||
const ms = emitSymbolLibCall(host, { ...e, type: F64 }, sym, callArgs);
|
||||
host.declare(`declare ptr @scr_date_new(double)`);
|
||||
@@ -584,6 +588,19 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
|
||||
return emitSymbolLibCall(host, e, sym, callArgs);
|
||||
}
|
||||
|
||||
/** The time value of a Date argument. A Date that a runtime Date function
|
||||
* produces for this argument alone (`new Date(ms).getTime()`, a
|
||||
* time-value local's read) passes the function's time value and creates
|
||||
* no Date object. */
|
||||
function emitDateTimeArg(host: LlvmEmitterContext, date: IrExpr): string {
|
||||
// A time-value local's read is its time value itself.
|
||||
if (date.kind === "libCall" && date.fn === "date.fromTime")
|
||||
return host.emitExpr(date.args[0]!).name;
|
||||
if (date.kind === "libCall" && date.fn.startsWith("date."))
|
||||
return emitGenericLibCall(host, { ...date, type: F64 }).name;
|
||||
return emitDateTime(host, emitBorrowedInput(host, date).name);
|
||||
}
|
||||
|
||||
/** A Date object's time value (ScrDate.ms). */
|
||||
function emitDateTime(host: LlvmEmitterContext, date: string): string {
|
||||
const B = host.B;
|
||||
|
||||
@@ -330,6 +330,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"date.parse": "scr_date_parse",
|
||||
"date.getTime": "scr_date_get_time",
|
||||
"date.valueOf": "scr_date_get_time",
|
||||
"date.fromTime": "scr_date_get_time",
|
||||
"date.parseGetTime": "scr_date_parse",
|
||||
"date.utc": "scr_date_utc",
|
||||
"date.newLocal": "scr_date_new_local",
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
|
||||
/** Date methods whose receiver is only read for its time value: a Date
|
||||
* local used as their receiver can store the time value instead of a Date
|
||||
* object (builtins/dates.ts lowers them). */
|
||||
const TIME_VALUE_METHODS = new Set([
|
||||
"getTime",
|
||||
"valueOf",
|
||||
"toISOString",
|
||||
"toJSON",
|
||||
"toString",
|
||||
"toDateString",
|
||||
"toTimeString",
|
||||
"toUTCString",
|
||||
"getFullYear",
|
||||
"getUTCFullYear",
|
||||
"getMonth",
|
||||
"getUTCMonth",
|
||||
"getDate",
|
||||
"getUTCDate",
|
||||
"getDay",
|
||||
"getUTCDay",
|
||||
"getHours",
|
||||
"getUTCHours",
|
||||
"getMinutes",
|
||||
"getUTCMinutes",
|
||||
"getSeconds",
|
||||
"getUTCSeconds",
|
||||
"getMilliseconds",
|
||||
"getUTCMilliseconds",
|
||||
"getTimezoneOffset",
|
||||
]);
|
||||
|
||||
const RELATIONAL = new Set([
|
||||
ts.SyntaxKind.LessThanToken,
|
||||
ts.SyntaxKind.LessThanEqualsToken,
|
||||
ts.SyntaxKind.GreaterThanToken,
|
||||
ts.SyntaxKind.GreaterThanEqualsToken,
|
||||
]);
|
||||
|
||||
/** True when a local `const` (or a `let` that is never assigned again)
|
||||
* holding a Date can keep the Date's time value instead of a Date object:
|
||||
* no use observes the Date's identity. Every use reads the time value as
|
||||
* a Date method receiver, a String() or template operand, a relational or
|
||||
* string `+` operand, or `new Date(d)`'s argument, inside the declaring
|
||||
* function, or inside the block or loop that scopes a module-level
|
||||
* binding (no closure captures it). Top-level module bindings keep the
|
||||
* object. Other uses box the time value into a new Date at the use. */
|
||||
export function isTimeValueDateDeclaration(
|
||||
lowerer: Lowerer,
|
||||
decl: ts.VariableDeclaration,
|
||||
): boolean {
|
||||
const known = lowerer.timeValueDateDecisions.get(decl);
|
||||
if (known !== undefined) return known;
|
||||
const answer = decideTimeValueDate(lowerer, decl);
|
||||
lowerer.timeValueDateDecisions.set(decl, answer);
|
||||
return answer;
|
||||
}
|
||||
|
||||
function decideTimeValueDate(lowerer: Lowerer, decl: ts.VariableDeclaration): boolean {
|
||||
if (!ts.isIdentifier(decl.name) || decl.initializer === undefined) return false;
|
||||
const sf = decl.getSourceFile();
|
||||
if (sf.isDeclarationFile || /\.[cm]?jsx?$/.test(sf.fileName)) return false;
|
||||
const list = decl.parent;
|
||||
if (!ts.isVariableDeclarationList(list)) return false;
|
||||
const isConst = (list.flags & ts.NodeFlags.Const) !== 0;
|
||||
const isLet = (list.flags & ts.NodeFlags.Let) !== 0;
|
||||
if (!isConst && !isLet) return false;
|
||||
if (!ts.isVariableStatement(list.parent) && !ts.isForStatement(list.parent)) return false;
|
||||
const fn = enclosingFunction(decl);
|
||||
// The uses to check: the function body, or for a binding inside a block
|
||||
// or loop at module level (a local of the module's initializer, not a
|
||||
// module binding) the block or loop that scopes it.
|
||||
let scope: ts.Node | undefined;
|
||||
if (fn !== null) scope = fn.body;
|
||||
else if (!inClassStaticBlock(decl)) scope = moduleBlockScope(decl);
|
||||
if (scope === undefined) return false;
|
||||
// The lib Date interface by its symbol first: mapping every candidate
|
||||
// binding's type would intern its unions before the lowering does.
|
||||
const typeSymbol = lowerer.typeOf(decl.name).getSymbol();
|
||||
if (
|
||||
typeSymbol === undefined ||
|
||||
ts.symbolName(typeSymbol) !== "Date" ||
|
||||
!typeSymbol.declarations.some(
|
||||
(d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()),
|
||||
)
|
||||
)
|
||||
return false;
|
||||
if (lowerer.mapTypeOf(lowerer.typeOf(decl.name))?.kind !== "date") return false;
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(decl.name);
|
||||
if (!symbol) return false;
|
||||
let ok = true;
|
||||
const visit = (node: ts.Node): void => {
|
||||
if (!ok) return;
|
||||
if (ts.isIdentifier(node) && node !== decl.name && node.text === decl.name.text) {
|
||||
if (lowerer.checker.getSymbolAtLocation(node) === symbol) {
|
||||
if (enclosingFunction(node) !== fn || !readsTimeValue(lowerer, node)) ok = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
};
|
||||
visit(scope);
|
||||
return ok;
|
||||
}
|
||||
|
||||
/** The block or for statement that scopes a module-level binding which is
|
||||
* not a module binding: a declaration in a block or loop body, or in a
|
||||
* for statement's initializer. Undefined for a top-level declaration. */
|
||||
function moduleBlockScope(decl: ts.VariableDeclaration): ts.Node | undefined {
|
||||
const list = decl.parent;
|
||||
if (!list || !ts.isVariableDeclarationList(list)) return undefined;
|
||||
if (ts.isForStatement(list.parent)) return list.parent;
|
||||
const statement = list.parent;
|
||||
if (!ts.isVariableStatement(statement) || ts.isSourceFile(statement.parent)) return undefined;
|
||||
for (let cur: ts.Node | undefined = statement.parent; cur; cur = cur.parent) {
|
||||
if (ts.isBlock(cur) || ts.isCaseBlock(cur)) return cur;
|
||||
if (ts.isSourceFile(cur)) return undefined;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function inClassStaticBlock(node: ts.Node): boolean {
|
||||
for (let cur = node.parent; cur; cur = cur.parent) {
|
||||
if (ts.isClassStaticBlockDeclaration(cur)) return true;
|
||||
if (ts.isSourceFile(cur)) return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function enclosingFunction(node: ts.Node): ts.FunctionLikeDeclaration | null {
|
||||
for (let cur = node.parent; cur; cur = cur.parent) {
|
||||
if (
|
||||
ts.isFunctionDeclaration(cur) ||
|
||||
ts.isFunctionExpression(cur) ||
|
||||
ts.isArrowFunction(cur) ||
|
||||
ts.isMethodDeclaration(cur) ||
|
||||
ts.isConstructorDeclaration(cur) ||
|
||||
ts.isGetAccessorDeclaration(cur) ||
|
||||
ts.isSetAccessorDeclaration(cur)
|
||||
)
|
||||
return cur;
|
||||
if (ts.isClassStaticBlockDeclaration(cur) || ts.isSourceFile(cur)) return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** A use of the binding that reads only its time value. */
|
||||
function readsTimeValue(lowerer: Lowerer, id: ts.Identifier): boolean {
|
||||
let node: ts.Node = id;
|
||||
while (ts.isParenthesizedExpression(node.parent)) node = node.parent;
|
||||
const parent = node.parent;
|
||||
if (ts.isPropertyAccessExpression(parent) && parent.expression === node) {
|
||||
const call = parent.parent;
|
||||
return (
|
||||
!parent.questionDotToken &&
|
||||
ts.isCallExpression(call) &&
|
||||
call.expression === parent &&
|
||||
!call.questionDotToken &&
|
||||
TIME_VALUE_METHODS.has(parent.name.text) &&
|
||||
lowerer.isStdlibMember(parent)
|
||||
);
|
||||
}
|
||||
if (ts.isTemplateSpan(parent)) return parent.expression === node;
|
||||
if (ts.isCallExpression(parent)) {
|
||||
return (
|
||||
parent.arguments.nodes.length === 1 &&
|
||||
parent.arguments.nodes[0] === node &&
|
||||
ts.isIdentifier(parent.expression) &&
|
||||
lowerer.isStdlibGlobal(parent.expression, "String")
|
||||
);
|
||||
}
|
||||
if (ts.isNewExpression(parent)) {
|
||||
return (
|
||||
parent.arguments?.nodes.length === 1 &&
|
||||
parent.arguments.nodes[0] === node &&
|
||||
ts.isIdentifier(parent.expression) &&
|
||||
lowerer.isStdlibGlobal(parent.expression, "Date")
|
||||
);
|
||||
}
|
||||
if (ts.isBinaryExpression(parent)) {
|
||||
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";
|
||||
if (parent.operatorToken.kind === ts.SyntaxKind.PlusToken) return otherKind === "string";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { type Lowerer, PoisonError } from "../lowerer.js";
|
||||
import {
|
||||
BOOL,
|
||||
BYTES_ELEMENT_NAME,
|
||||
DATE_T,
|
||||
DYN,
|
||||
F64,
|
||||
type IrExpr,
|
||||
@@ -170,6 +171,12 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc:
|
||||
// when the checker types this USE as a single arm, maybeNarrow
|
||||
// bridges the tagged representation with a unionNarrow.
|
||||
const local = lowerer.resolveLocal(expr);
|
||||
// A Date local holding its time value reads as that Date: only uses
|
||||
// that read the time value reach here (date-values.ts).
|
||||
if (local && lowerer.timeValueDateLocals.has(local)) {
|
||||
const time: IrExpr = { kind: "varRef", localId: local.id, type: F64, loc };
|
||||
return { kind: "libCall", fn: "date.fromTime", args: [time], type: DATE_T, loc };
|
||||
}
|
||||
if (local) {
|
||||
// A switch can dispatch past a lexical declaration. Keep this
|
||||
// previously refused codec form out until switch-scope TDZ boxes
|
||||
|
||||
@@ -28,6 +28,7 @@ import * as ts from "../ts7/adapter.js";
|
||||
import { processDataMemberValue, programReadsExitCode } from "./builtins/process.js";
|
||||
import { requireInitFlagStmt } from "./require-initialization.js";
|
||||
import { isJsonRequireDecl } from "./json-require.js";
|
||||
import { isTimeValueDateDeclaration } from "./date-values.js";
|
||||
import { bindingSource } from "../binding-source.js";
|
||||
import { lowerConstructorReturn } from "./constructor-returns.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
@@ -63,6 +64,7 @@ import {
|
||||
BOOL,
|
||||
BYTES_U8,
|
||||
CAUGHT,
|
||||
DATE_T,
|
||||
DYN,
|
||||
F64,
|
||||
type IrExpr,
|
||||
@@ -5234,6 +5236,16 @@ export function lowerVarDecl(
|
||||
if (alias) return alias;
|
||||
}
|
||||
|
||||
// A Date local whose identity no use observes stores its time value.
|
||||
if (isTimeValueDateDeclaration(lowerer, decl)) {
|
||||
const loc = locOf(decl);
|
||||
const date = lowerer.lowerExprExpecting(decl.initializer!, DATE_T);
|
||||
const local = lowerer.declareLocal(decl.name, decl.name.text, F64, isLet);
|
||||
lowerer.timeValueDateLocals.add(local);
|
||||
const init: IrExpr = { kind: "libCall", fn: "date.getTime", args: [date], type: F64, loc };
|
||||
return { kind: "varDecl", localId: local.id, init, loc };
|
||||
}
|
||||
|
||||
// A NULLISH generic binding (`const i: I<A & B> = null as any` — the
|
||||
// declared type has no mapping, the value is provably null/undefined
|
||||
// forever): no storage exists; reads know the value (member reads and
|
||||
|
||||
@@ -2331,6 +2331,10 @@ export class Lowerer {
|
||||
readonly runtimeOptionalFields = new Set<string>();
|
||||
/** Bindings initialized by a Date's `toJSON()` (analyzeDateJsonResults). */
|
||||
readonly dateJsonBindings = new Set<ts.Symbol>();
|
||||
/** isTimeValueDateDeclaration's answers, and the locals that hold a
|
||||
* Date's time value instead of a Date object (date-values.ts). */
|
||||
readonly timeValueDateDecisions = new Map<ts.Node, boolean>();
|
||||
readonly timeValueDateLocals = new Set<IrLocal>();
|
||||
/** Record bindings whose flow class reaches storage outside the class:
|
||||
* they keep the declared layout, so an initializer from unchecked reads
|
||||
* converts once instead of the binding adopting the widened variant. */
|
||||
|
||||
@@ -4738,6 +4738,10 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
"date.getMilliseconds": { argTypes: [DATE_T], result: F64 },
|
||||
"date.getUTCMilliseconds": { argTypes: [DATE_T], result: F64 },
|
||||
"date.getTimezoneOffset": { argTypes: [DATE_T], result: F64 },
|
||||
/** A Date object for a time value a local holds (date-values.ts): a
|
||||
* lib-call argument passes the time value itself, and any other use
|
||||
* creates the Date. */
|
||||
"date.fromTime": { argTypes: [F64], result: DATE_T },
|
||||
/** Date.prototype.toString / toDateString / toTimeString and String(date):
|
||||
* V8's local-time formats with the host zone's long name, "Invalid Date"
|
||||
* for an invalid date. Owned (+1) string; never throws. */
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
import { compile, deserializeModule } from "../src/index.js";
|
||||
import { emitLlvmModule } from "../src/backend/llvm/emitter.js";
|
||||
import type { IrModule } from "../src/ir/ir.js";
|
||||
|
||||
async function lowerSource(source: string): Promise<IrModule> {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-date-time-values-"));
|
||||
try {
|
||||
const entry = join(dir, "main.ts");
|
||||
await writeFile(entry, source);
|
||||
const outPath = join(dir, "main.ir.json");
|
||||
const result = await compile(entry, { outPath, outDir: dir, outputKind: "ir" });
|
||||
if (!result.ok) throw new Error(result.diagnostics.map((d) => d.message).join("\n"));
|
||||
return deserializeModule(await readFile(outPath, "utf8"));
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
/** The IR type of each local named `name` in function `fn`. */
|
||||
function localKinds(mod: IrModule, fn: string, name: string): string[] {
|
||||
const f = mod.functions.find((candidate) => candidate.name === fn);
|
||||
if (!f) throw new Error(`no function ${fn}`);
|
||||
return f.locals.filter((local) => local.name === name).map((local) => local.type.kind);
|
||||
}
|
||||
|
||||
test("a Date local read only for its time value stores the time value", async () => {
|
||||
const mod = await lowerSource(`
|
||||
function format(ms: number): string {
|
||||
const d = new Date(ms);
|
||||
const later = new Date(ms + 1);
|
||||
return d.toISOString() + String(d) + \`\${later}\` + (d < later) + d.getUTCHours() + new Date(d).getTime();
|
||||
}
|
||||
console.log(format(0));
|
||||
`);
|
||||
expect(localKinds(mod, "format", "d")).toEqual(["f64"]);
|
||||
expect(localKinds(mod, "format", "later")).toEqual(["f64"]);
|
||||
// Reading the time value creates no Date object.
|
||||
const llvm = emitLlvmModule(mod, { pointerBits: 64 });
|
||||
const body = /^define internal [^\n]*@sc_f_format\([^]*?^}/m.exec(llvm)?.[0] ?? "";
|
||||
expect(body).not.toContain("@scr_date_new(");
|
||||
});
|
||||
|
||||
test("a Date local whose identity a use can observe keeps the Date object", async () => {
|
||||
const mod = await lowerSource(`
|
||||
const keep: Date[] = [];
|
||||
function uses(ms: number): boolean {
|
||||
const compared = new Date(ms);
|
||||
const stored = new Date(ms);
|
||||
const passed = new Date(ms);
|
||||
const captured = new Date(ms);
|
||||
const returned = new Date(ms);
|
||||
let reassigned = new Date(ms);
|
||||
reassigned = new Date(ms + 1);
|
||||
keep.push(stored);
|
||||
const read = (): number => captured.getTime();
|
||||
console.log(String(passed), read(), returned.getTime(), reassigned.getTime());
|
||||
return compared === compared && pass(passed) === passed && back(returned) === returned;
|
||||
}
|
||||
function pass(d: Date): Date {
|
||||
return d;
|
||||
}
|
||||
function back(d: Date): Date {
|
||||
return d;
|
||||
}
|
||||
const top = new Date(0);
|
||||
console.log(uses(0), top.getTime());
|
||||
`);
|
||||
for (const name of ["compared", "stored", "passed", "captured", "returned", "reassigned"]) {
|
||||
expect(localKinds(mod, "uses", name), name).toEqual(["date"]);
|
||||
}
|
||||
});
|
||||
|
||||
test("a Date in a block or loop at module level stores the time value unless a use observes it", async () => {
|
||||
const mod = await lowerSource(`
|
||||
const seen: Date[] = [];
|
||||
let total = 0;
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const read = new Date(i * 1000);
|
||||
const kept = new Date(i * 1000);
|
||||
seen.push(kept);
|
||||
total += read.getUTCSeconds() + read.toISOString().length;
|
||||
}
|
||||
{
|
||||
const block = new Date(5);
|
||||
const captured = new Date(6);
|
||||
const later = (): number => captured.getTime();
|
||||
total += block.getTime() + later();
|
||||
}
|
||||
const top = new Date(7);
|
||||
console.log(total, seen.length, top.getTime());
|
||||
`);
|
||||
expect(localKinds(mod, "%init.0", "read")).toEqual(["f64"]);
|
||||
expect(localKinds(mod, "%init.0", "block")).toEqual(["f64"]);
|
||||
expect(localKinds(mod, "%init.0", "kept")).toEqual(["date"]);
|
||||
expect(localKinds(mod, "%init.0", "captured")).toEqual(["date"]);
|
||||
});
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5966-date-time-values.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// Date locals that are only read for their time value (method receivers,
|
||||
// String() and template operands, relational and string `+` operands,
|
||||
// `new Date(d)`) print and compare as Dates; locals whose identity a use
|
||||
// can observe keep it.
|
||||
export {};
|
||||
process.env.TZ = "America/New_York";
|
||||
|
||||
function describe(ms: number): string {
|
||||
const d = new Date(ms);
|
||||
const later = new Date(ms + 3_600_000);
|
||||
const copy = new Date(d);
|
||||
return [
|
||||
d.toISOString(),
|
||||
d.toUTCString(),
|
||||
String(d),
|
||||
`${later}`,
|
||||
"at " + d,
|
||||
d < later,
|
||||
later >= d,
|
||||
d.getHours(),
|
||||
d.getUTCDay(),
|
||||
d.getTimezoneOffset(),
|
||||
copy.getTime() === d.getTime(),
|
||||
d.toJSON(),
|
||||
].join(" | ");
|
||||
}
|
||||
console.log(describe(Date.UTC(2026, 2, 8, 6, 30)));
|
||||
console.log(describe(Date.UTC(2026, 10, 1, 5, 30)));
|
||||
function invalid(): string {
|
||||
const d = new Date(NaN);
|
||||
return [String(d), d.getTime(), String(d.toJSON()), d < new Date(0), `${d}`].join(" | ");
|
||||
}
|
||||
console.log(invalid());
|
||||
|
||||
function sum(days: number): number {
|
||||
let total = 0;
|
||||
for (let i = 0; i < days; i++) {
|
||||
const day = new Date(Date.UTC(2026, 0, 1) + i * 86_400_000);
|
||||
total += day.getUTCDate() + day.getUTCMonth() + day.toISOString().length;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
console.log(sum(400));
|
||||
|
||||
// Identity-observing uses keep the Date object.
|
||||
const kept: Date[] = [];
|
||||
function identities(ms: number): string {
|
||||
const a = new Date(ms);
|
||||
const b = a;
|
||||
const stored = new Date(ms);
|
||||
kept.push(stored);
|
||||
const captured = new Date(ms);
|
||||
const read = (): number => captured.getTime();
|
||||
const passed = new Date(ms);
|
||||
return [a === b, kept.includes(stored), kept[0] === stored, read(), same(passed, passed)].join(",");
|
||||
}
|
||||
function same(x: Date, y: Date): boolean {
|
||||
return x === y;
|
||||
}
|
||||
console.log(identities(5));
|
||||
const held: unknown = kept[0];
|
||||
console.log(held instanceof Date, (held as Date) === kept[0]);
|
||||
|
||||
// Dates in blocks and loops at module level, read for their time value or
|
||||
// kept, stored and captured.
|
||||
const moduleKept: Date[] = [];
|
||||
let moduleTotal = "";
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const read = new Date(Date.UTC(2026, 2, 8 + i, 6, 30));
|
||||
const kept = new Date(read.getTime());
|
||||
moduleKept.push(kept);
|
||||
moduleTotal += `${read.toISOString()} ${read.getHours()} ${String(read)}\n`;
|
||||
}
|
||||
{
|
||||
const block = new Date(Date.UTC(2026, 10, 1, 5, 59));
|
||||
const captured = new Date(block);
|
||||
const later = (): string => captured.toISOString();
|
||||
moduleTotal += `${block.getTimezoneOffset()} ${later()} ${captured === moduleKept[0]}`;
|
||||
}
|
||||
console.log(moduleTotal, moduleKept.map((d) => d.getUTCDate()).join(","));
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3096, "san": 3319 }
|
||||
Reference in New Issue
Block a user