Make Dates mutable objects with Node's setters and conversions

This commit is contained in:
Chris Tate
2026-10-23 22:08:02 -05:00
parent 1df99a8414
commit 27593e366e
64 changed files with 1061 additions and 180 deletions
+1
View File
@@ -10,6 +10,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea
| `atomics-contended` | cpu | four worker threads updating `SharedArrayBuffer` counters with `Atomics`: one shared counter, one counter per worker, a `compareExchange` loop, and `Atomics.wait`/`notify` handoffs |
| `channel-messages` | cpu | 200,000 MessageChannel messages in one thread, half numbers and half small objects, all posted before the port delivers any |
| `csv-numbers` | cpu | `split`, `Number`, `parseInt`, `toFixed` |
| `date-math` | cpu | schedules built with Date setters (local and UTC), weekday and month-end rules through the getters, Dates sorted by time value |
| `cyclic-graphs` | cpu | object graphs with back references built and dropped: trees with parent and sibling links, rings of records sharing a long-lived registry, closures capturing their owner |
| `dictionary-walks` | cpu | `Record<string, number>` dictionaries walked with for-in and `Object.keys`/`values`/`entries` while entries are added, updated and deleted |
| `dynamic-methods` | cpu | untyped JavaScript calling builtin methods on values it does not type: string `slice`/`indexOf`/`toUpperCase`/`padStart`, array `push`/`pop`/`includes`, `Map` and `Set`, and an object's own methods |
@@ -0,0 +1,6 @@
{
"entry": "workloads/date-math.ts",
"args": [
"40"
]
}
+52
View File
@@ -0,0 +1,52 @@
// Calendar arithmetic on Dates: schedules built by adjusting Dates with the
// UTC and local setters, weekday and month-end rules read back through the
// getters, Dates compared and sorted by time value, and ISO text for a
// sample. Argument: the number of rounds.
process.env.TZ = "America/New_York";
const rounds = Number(process.argv[2] ?? "40");
let seed = 2026;
function random(): number {
seed = (Math.imul(seed, 1103515245) + 12345) >>> 0;
return seed / 4294967296;
}
/** The next business day at 09:00 local time on or after `from`. */
function nextBusinessDay(from: Date): Date {
const day = new Date(from.getTime());
day.setHours(9, 0, 0, 0);
if (day < from) day.setDate(day.getDate() + 1);
while (day.getDay() === 0 || day.getDay() === 6) day.setDate(day.getDate() + 1);
return day;
}
/** The last day of `date`'s month, in UTC. */
function monthEnd(date: Date): number {
const end = new Date(date.getTime());
end.setUTCDate(1);
end.setUTCMonth(end.getUTCMonth() + 1);
end.setUTCDate(0);
return end.getUTCDate();
}
let checksum = 0;
let sample = "";
for (let round = 0; round < rounds; round++) {
const due: Date[] = [];
const cursor = new Date(Date.UTC(2020 + (round % 7), 0, 1, 12));
for (let i = 0; i < 4000; i++) {
cursor.setUTCMinutes(cursor.getUTCMinutes() + Math.floor(random() * 4000));
const next = nextBusinessDay(cursor);
checksum += next.getDate() + monthEnd(next) + next.getHours();
if (i % 3 === 0) next.setMonth(next.getMonth() + 1, 15);
due.push(next);
}
due.sort((a, b) => +a - +b);
let previous = due[0]!;
for (const date of due) {
checksum += (+date - +previous) / 3600000;
previous = date;
}
sample = due[due.length >> 1]!.toISOString();
}
console.log(Math.round(checksum), sample);
@@ -1,6 +1,7 @@
- **Date:** Spread constructor arguments, setters, thrown Date values, and locale formatters are unsupported.
Dates are objects: `===` compares identity, `new Date(date)` makes a new Date, and Dates compile in arrays (`includes`, `indexOf`, `lastIndexOf`, `join`, and default `sort` by `toString()` text), in `Date | undefined` and `Date | null` slots, as Map and Set keys and values, and in records, tuples, class fields, closures, promises and generators.
Unions of a Date with other non-nullish types are unsupported.
Zero-argument, single number/string, and local-time year/month field constructors, `getTime`, `valueOf`, `toISOString`, `toUTCString`, `toJSON` (a `string | null`: null for an invalid date, as in Node), `toString`, `toDateString`, `toTimeString`, `String(date)` and template literal conversion, relational comparison, calendar getters, `getTimezoneOffset`, and single-string `Date.parse` compile.
- **Date:** Spread constructor arguments and locale formatters are unsupported.
Dates are objects with identity: `===` and `==` compare identity, `new Date(date)` makes a new Date, setters change the one Date every reference holds, and Dates compile in arrays (`includes`, `indexOf`, `lastIndexOf`, `join`, and default `sort` by `toString()` text), in unions with other types, as Map and Set keys and values, and in records, tuples, class fields, closures, promises and generators.
Thrown and rejected Dates keep their identity; `structuredClone` and `postMessage` copy them.
Zero-argument, single number/string, and local-time year/month field constructors, `getTime`, `valueOf`, `toISOString`, `toUTCString`, `toJSON` (a `string | null`: null for an invalid date, as in Node), `toString`, `toDateString`, `toTimeString`, the local and UTC calendar getters and setters, `setTime`, `getTimezoneOffset`, and single-string `Date.parse` compile.
Unary `+`, `-` and `Number(date)` read the time value; `String(date)`, template literals and `+` with a string read the `toString()` text; `date[Symbol.toPrimitive](hint)` compiles with a literal hint.
A Date converted to `unknown` is the same Date object.
See [Dates and timezones](#dates-and-timezones) for timezone data.
@@ -649,6 +649,15 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
if (MAY_THROW_LIB_FNS.has(e.fn) && LIB_FN_SYMS[e.fn] === undefined) {
throw new LlvmUnsupportedError(`libCall:${e.fn}`, e.loc);
}
// A setter's write: the time value into the Date object (ScrDate.ms).
if (e.fn === "date.store") {
const date = emitBorrowedInput(host, e.args[0]!).name;
const ms = host.emitExpr(e.args[1]!).name;
const field = B.tmp();
B.line(`${field} = getelementptr inbounds i8, ptr ${date}, i64 8 ; ->ms`);
B.line(`store double ${ms}, ptr ${field}`);
return { name: ms, type: e.type };
}
// queueMicrotask's checked-dynamic form carries the AsyncLocalStorage
// context like the static one (lib-process.ts).
const sym =
@@ -334,6 +334,8 @@ export const LIB_FN_SYMS: Record<string, string> = {
"date.getTime": "scr_date_get_time",
"date.valueOf": "scr_date_get_time",
"date.fromTime": "scr_date_get_time",
"date.setFields": "scr_date_set_fields",
"date.timeClip": "scr_date_new_ms",
"date.parseGetTime": "scr_date_parse",
"date.utc": "scr_date_utc",
"date.newLocal": "scr_date_new_local",
@@ -622,7 +622,7 @@ export function unsupportedTypeDiag(typeText: string, loc: SrcLoc): ScrDiagnosti
code: "SC2001",
message:
`values of type '${typeText}' cannot be compiled yet ` +
`(supported: number, string, boolean, arrays, Maps, Sets, RegExp, read-only Date values, functions, classes, records, unions of those, and 'unknown')`,
`(supported: number, string, boolean, arrays, Maps, Sets, RegExp, Dates, functions, classes, records, unions of those, and 'unknown')`,
loc,
milestone: "M2",
};
@@ -5,6 +5,7 @@ import {
DYN,
DYN_HANDLE_KINDS,
F64,
STRING,
type IrExpr,
type IrStmt,
type IrType,
@@ -285,6 +286,43 @@ export function lowerAbstractEquality(
};
return negated ? { kind: "unary", op: "!", operand: result, type: BOOL, loc } : result;
}
// Two Dates are equal when they are the same object, as with ===.
if (left.type.kind === "date" && right.type.kind === "date")
return { kind: "bin", op: negated ? "!==" : "===", left, right, type: BOOL, loc };
// A Date and a string or number compare through the Date's default
// primitive, its toString() text.
if (left.type.kind === "date" || right.type.kind === "date") {
const otherType = left.type.kind === "date" ? right.type : left.type;
if (otherType.kind === "string" || otherType.kind === "f64") {
// Both operands evaluate before the Date converts.
const l = lowerer.declareHiddenLocal("%looseA", left.type);
const r = lowerer.declareHiddenLocal("%looseB", right.type);
const stmts: IrStmt[] = [
{ kind: "varDecl", localId: l.id, init: left, loc },
{ kind: "varDecl", localId: r.id, init: right, loc },
];
const primitive = (value: IrExpr): IrExpr => {
if (value.type.kind !== "date") return value;
const text: IrExpr = {
kind: "libCall",
fn: "date.toString",
args: [value],
type: STRING,
loc,
};
return otherType.kind === "string"
? text
: { kind: "libCall", fn: "num.fromString", args: [text], type: F64, loc };
};
const a = primitive(varRef(l.id, left.type, loc));
const b = primitive(varRef(r.id, right.type, loc));
const result: IrExpr =
otherType.kind === "string"
? { kind: "strEq", negated, left: a, right: b, type: BOOL, loc }
: { kind: "bin", op: negated ? "!==" : "===", left: a, right: b, type: BOOL, loc };
return { kind: "seqExpr", stmts, result, type: BOOL, loc };
}
}
if (!supportsPair(lowerer, left.type, right.type)) return null;
if (left.kind === "strLit" && right.kind === "strLit") {
return { kind: "boolLit", value: (left.value === right.value) !== negated, type: BOOL, loc };
@@ -2,7 +2,13 @@ import * as ts from "../../ts7/adapter.js";
import { type Lowerer } from "../lowerer.js";
import { locOf } from "../../program.js";
import { type ClassInfo } from "../lower-classes.js";
import { DATE_METHOD_HINT, dateJsonType, dateValueMethod } from "./dates.js";
import {
DATE_METHOD_HINT,
dateJsonType,
dateSetterTime,
dateValueMethod,
isDateSetter,
} from "./dates.js";
import { dynUndefinedExpr, varRef } from "../../../ir/build.js";
import {
BOOL,
@@ -248,6 +254,20 @@ export function lowerInheritedDateMethodCall(
type: result.type,
loc,
});
if (isDateSetter(name) && name !== "setTime") {
// The field setters change the Date the class's time slot holds.
const date = lowerer.declareHiddenLocal("%dateTime", DATE_T);
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") {
// Every argument is evaluated before setTime converts the first one.
let ms: IrExpr = { kind: "numLit", value: Number.NaN, type: F64, loc };
@@ -2,12 +2,15 @@ import * as ts from "../../ts7/adapter.js";
import { type Lowerer, own } from "../lowerer.js";
import { locOf } from "../../program.js";
import { tryLowerExpression } from "../expressions/try-lower-expression.js";
import { lowerToNumberArguments } from "../lower-exprs.js";
import { programDeclaresDateClass } from "./date-classes.js";
import {
BOOL,
DATE_T,
F64,
type IrExpr,
type IrLibFn,
type IrLocal,
type IrStmt,
type IrType,
NULL_T,
@@ -36,6 +39,32 @@ const DATE_GETTER_FNS: Readonly<Record<string, IrLibFn>> = {
getTimezoneOffset: "date.getTimezoneOffset",
};
/** The setters that change calendar fields: the field code
* scr_date_set_fields takes (scr_number.h SCR_DATE_SET_*; 8 marks the UTC
* forms) and the most arguments each takes. */
const DATE_FIELD_SETTERS: ReadonlyMap<string, { code: number; max: number }> = new Map([
["setMilliseconds", { code: 0, max: 1 }],
["setSeconds", { code: 1, max: 2 }],
["setMinutes", { code: 2, max: 3 }],
["setHours", { code: 3, max: 4 }],
["setDate", { code: 4, max: 1 }],
["setMonth", { code: 5, max: 2 }],
["setFullYear", { code: 6, max: 3 }],
["setUTCMilliseconds", { code: 8, max: 1 }],
["setUTCSeconds", { code: 9, max: 2 }],
["setUTCMinutes", { code: 10, max: 3 }],
["setUTCHours", { code: 11, max: 4 }],
["setUTCDate", { code: 12, max: 1 }],
["setUTCMonth", { code: 13, max: 2 }],
["setUTCFullYear", { code: 14, max: 3 }],
]);
/** Whether `name` is a Date setter scriptc lowers: setTime or a field
* setter. */
export function isDateSetter(name: string): boolean {
return name === "setTime" || DATE_FIELD_SETTERS.has(name);
}
const DATE_TEXT_FNS: ReadonlyMap<string, IrLibFn> = new Map([
["toString", "date.toString"],
["toDateString", "date.toDateString"],
@@ -43,11 +72,11 @@ const DATE_TEXT_FNS: ReadonlyMap<string, IrLibFn> = new Map([
]);
export const DATE_METHOD_HINT =
"getTime(), valueOf(), toISOString(), toUTCString(), toJSON(), toString(), toDateString(), toTimeString(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale formatters have no lowering";
"getTime(), valueOf(), toISOString(), toUTCString(), toJSON(), toString(), toDateString(), toTimeString(), the local/UTC calendar getters and setters, setTime(), and getTimezoneOffset() are supported; locale formatters have no lowering";
/** The Date slice: statics plus read-only Date values backed by one
* TimeClip'd millisecond scalar. Construction lives in lowerNew;
* identity and mutating methods remain fenced. */
/** Date statics and methods on Date objects (a TimeClip'd millisecond
* time value with identity): getters, text forms and setters.
* Construction lives in lowerNew. */
export function lowerDateCall(
lowerer: Lowerer,
call: ts.CallExpression,
@@ -67,16 +96,18 @@ export function lowerDateCall(
if (lowerer.mapTypeOf(lowerer.typeOf(access.expression))?.kind !== "date") return null;
if (!lowerer.isStdlibMember(access)) return null;
const name = access.name.text;
// A Date a JavaScript file constructs is a mutable object; setTime on
// it takes the dynamic method call. So does every Date method on a
// A Date a JavaScript file constructs is a checked-dynamic value; a
// setter on it takes the dynamic method call, which changes the Date the
// handle holds. So does every Date method on a
// checked-dynamic receiver when the program declares a class that extends
// Date: the value can be an instance, whose class may declare the method.
if (
(name === "setTime" || programDeclaresDateClass(lowerer)) &&
(isDateSetter(name) || programDeclaresDateClass(lowerer)) &&
tryLowerExpression(lowerer, access.expression)?.type.kind === "dyn"
)
return null;
if (name === "toJSON") return lowerDateToJson(lowerer, call, access);
if (isDateSetter(name)) return lowerDateSetter(lowerer, call, access, name);
if (call.arguments.nodes.length !== 0) {
lowerer.noLowering(`Date.prototype.${name} with arguments`, call, DATE_METHOD_HINT);
}
@@ -253,3 +284,150 @@ export function isDateJsonSource(lowerer: Lowerer, node: ts.Node): boolean {
lowerer.isStdlibMember(access)
);
}
/** `d.setTime(t)` and the field setters (`d.setHours(h, m?, s?, ms?)` ...)
* on a Date object: the receiver, then the arguments in order, then the
* new time value (scr_date_set_fields over the time value the Date holds
* once the arguments have run) stored into the Date and answered. Missing
* optional arguments keep their fields; arguments past a setter's last
* one are evaluated and ignored. A Date local that holds its time value
* (date-values.ts) takes the new time value itself. */
function lowerDateSetter(
lowerer: Lowerer,
call: ts.CallExpression,
access: ts.PropertyAccessExpression,
name: string,
): IrExpr {
const loc = locOf(call);
const nodes = call.arguments.nodes;
if (nodes.some((node) => ts.isSpreadElement(node)))
lowerer.noLowering(`Date.prototype.${name} with spread arguments`, call, DATE_METHOD_HINT);
const timeLocal = timeValueLocalOf(lowerer, access.expression);
const stmts: IrStmt[] = [];
let receiver: IrExpr | null = null;
if (timeLocal === null) {
// A checked-dynamic receiver takes the dynamic method call
// (lowerDateCall), so the value here is a Date object.
const value = lowerer.lowerExpr(access.expression);
if (value.type.kind !== "date")
lowerer.badType(access.expression, lowerer.typeOf(access.expression));
const date = lowerer.declareHiddenLocal("%date", DATE_T);
stmts.push({ kind: "varDecl", localId: date.id, init: value, loc });
receiver = { kind: "varRef", localId: date.id, type: DATE_T, loc };
}
const time: IrExpr =
receiver === null
? { kind: "varRef", localId: timeLocal!.id, type: F64, loc }
: { kind: "libCall", fn: "date.getTime", args: [receiver], type: F64, loc };
const next = dateSetterTime(lowerer, call, name, stmts, time);
if (receiver === null) {
stmts.push({ kind: "assign", localId: timeLocal!.id, value: next, loc });
return {
kind: "seqExpr",
stmts,
result: { kind: "varRef", localId: timeLocal!.id, type: F64, loc },
type: F64,
loc,
};
}
return {
kind: "seqExpr",
stmts,
result: { kind: "libCall", fn: "date.store", args: [receiver, next], type: F64, loc },
type: F64,
loc,
};
}
/** 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. */
export function dateSetterTime(
lowerer: Lowerer,
call: ts.CallExpression,
name: string,
stmts: IrStmt[],
time: IrExpr,
): IrExpr {
const loc = locOf(call);
const converted = lowerToNumberArguments(
lowerer,
call.arguments.nodes as readonly ts.Expression[],
);
stmts.push(...converted.stmts);
const args: IrExpr[] = converted.values.map((value) => {
if (value.kind === "numLit") return value;
const arg = lowerer.declareHiddenLocal("%dateArg", F64);
stmts.push({ kind: "varDecl", localId: arg.id, init: value, loc });
return { kind: "varRef", localId: arg.id, type: F64, loc };
});
const number = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc });
if (name === "setTime")
return {
kind: "libCall",
fn: "date.timeClip",
args: [args[0] ?? number(Number.NaN)],
type: F64,
loc,
};
const setter = DATE_FIELD_SETTERS.get(name)!;
const used = args.slice(0, setter.max);
const operands = [...used];
while (operands.length < 4) operands.push(number(0));
return {
kind: "libCall",
fn: "date.setFields",
args: [time, number(setter.code), number(used.length), ...operands],
type: F64,
loc,
};
}
/** The local a Date binding that holds its time value lowers to, for an
* identifier `node` that names one (date-values.ts); null otherwise. */
function timeValueLocalOf(lowerer: Lowerer, node: ts.Expression): IrLocal | null {
while (ts.isParenthesizedExpression(node)) node = node.expression;
if (!ts.isIdentifier(node)) return null;
const local = lowerer.resolveLocal(node);
return local !== undefined && local !== null && lowerer.timeValueDateLocals.has(local)
? local
: null;
}
/** `d[Symbol.toPrimitive](hint)` on a Date with a literal hint: "number"
* answers the time value, "string" and "default" the toString() text (a
* Date's default hint is a string). Null for other calls. */
export function lowerDateToPrimitiveCall(
lowerer: Lowerer,
call: ts.CallExpression,
access: ts.ElementAccessExpression,
): IrExpr | null {
const key = access.argumentExpression;
if (
!ts.isPropertyAccessExpression(key) ||
key.name.text !== "toPrimitive" ||
!ts.isIdentifier(key.expression) ||
!lowerer.isStdlibGlobal(key.expression, "Symbol") ||
lowerer.mapTypeOf(lowerer.typeOf(access.expression))?.kind !== "date"
)
return null;
const loc = locOf(call);
const hint = call.arguments.nodes.length === 1 ? call.arguments.nodes[0]! : null;
const text = hint !== null && ts.isStringLiteralLike(hint) ? hint.text : null;
if (text !== "number" && text !== "string" && text !== "default")
return lowerer.noLowering(
'Date.prototype[Symbol.toPrimitive] without a "number", "string" or "default" literal hint',
call,
);
const raw = lowerer.lowerExpr(access.expression);
const date: IrExpr =
raw.type.kind === "dyn"
? { kind: "libCall", fn: "date.checkedValue", args: [raw], type: DATE_T, loc }
: raw;
return text === "number"
? { kind: "libCall", fn: "date.valueOf", args: [date], type: F64, loc }
: { kind: "libCall", fn: "date.toString", args: [date], type: STRING, loc };
}
@@ -1,5 +1,6 @@
import * as ts from "../ts7/adapter.js";
import type { Lowerer } from "./lowerer.js";
import { isDateSetter } from "./builtins/dates.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
@@ -41,12 +42,14 @@ const RELATIONAL = new Set([
/** True when a local `const` (or a `let` that is never assigned again)
* initialized with `new 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. */
* of a Date object: no use observes the Date's identity. Every use reads
* the time value as a Date method receiver (a setter replaces it), a
* String(), Number() or template operand, a unary + or - 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,
@@ -94,17 +97,21 @@ function decideTimeValueDate(lowerer: Lowerer, decl: ts.VariableDeclaration): bo
const symbol = lowerer.checker.getSymbolAtLocation(decl.name);
if (!symbol) return false;
let ok = true;
let sets = false;
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;
const use = enclosingFunction(node) === fn ? timeValueUse(lowerer, node) : null;
if (use === null) ok = false;
else if (use === "set") sets = true;
}
return;
}
ts.forEachChild(node, visit);
};
visit(scope);
if (ok && sets) lowerer.timeValueDateWrites.add(decl);
return ok;
}
@@ -164,29 +171,46 @@ function enclosingFunction(node: ts.Node): ts.FunctionLikeDeclaration | null {
return null;
}
/** A use of the binding that reads only its time value. */
function readsTimeValue(lowerer: Lowerer, id: ts.Identifier): boolean {
/** 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 {
let node: ts.Node = id;
while (ts.isParenthesizedExpression(node.parent)) node = node.parent;
const parent = node.parent;
if (parent === undefined) return null;
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 (
parent.questionDotToken ||
!ts.isCallExpression(call) ||
call.expression !== parent ||
call.questionDotToken ||
!lowerer.isStdlibMember(parent)
)
return null;
if (isDateSetter(parent.name.text)) return "set";
return TIME_VALUE_METHODS.has(parent.name.text) ? "read" : null;
}
return readsTimeValue(lowerer, node, parent) ? "read" : null;
}
/** 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 {
if (ts.isTemplateSpan(parent)) return parent.expression === node;
// Unary + and -, the number hint.
if (ts.isPrefixUnaryExpression(parent))
return (
parent.operator === ts.SyntaxKind.PlusToken || parent.operator === ts.SyntaxKind.MinusToken
);
if (ts.isCallExpression(parent)) {
return (
parent.arguments.nodes.length === 1 &&
parent.arguments.nodes[0] === node &&
ts.isIdentifier(parent.expression) &&
lowerer.isStdlibGlobal(parent.expression, "String")
(lowerer.isStdlibGlobal(parent.expression, "String") ||
lowerer.isStdlibGlobal(parent.expression, "Number"))
);
}
if (ts.isNewExpression(parent)) {
@@ -1600,6 +1600,19 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
loc,
};
}
// A Date against an untyped value: the same Date object, whose
// checked-dynamic handle the untyped value would hold.
if (dynSide.type.kind === "dyn" && scalarSide.type.kind === "date") {
const boxed = lowerer.coerceToExpected(scalarSide, DYN);
return {
kind: "dynScalarEq",
left: scalarSide === left ? boxed : left,
right: scalarSide === right ? boxed : right,
...(negated ? { negated: true as const } : {}),
type: BOOL,
loc,
};
}
if (
dynSide.type.kind === "dyn" &&
(scalarSide.type.kind === "f64" ||
@@ -112,6 +112,7 @@ import { dynUndefinedExpr, nodeThrowExpr, numLit, strLit, varRef } from "../../.
import { typedArrayLacksMember } from "../builtins/typed-array-members.js";
import { TEMPORAL_GLOBAL } from "../../builtin-modules.js";
import { tryLowerExpression } from "./try-lower-expression.js";
import { DATE_METHOD_HINT } from "../builtins/dates.js";
import {
cjsClassExprWholeExportOf,
cjsExportAssignmentOf,
@@ -2055,6 +2056,9 @@ export function lowerIntrinsicProperty(
);
}
if (kind === "date") {
// A Date held as a checked-dynamic value (a Date a JavaScript file
// constructs) answers its method values at run time.
if (tryLowerExpression(lowerer, expr.expression)?.type.kind === "dyn") return null;
const methods = new Set([
"getTime",
"valueOf",
@@ -2083,7 +2087,7 @@ export function lowerIntrinsicProperty(
lowerer.noLowering(
`Date.prototype.${name}`,
expr,
"getTime(), valueOf(), toISOString(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering",
DATE_METHOD_HINT,
lowerer.checker.getSymbolAtLocation(expr.name),
);
}
@@ -288,6 +288,16 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc
if (!target) {
const date = lowerer.isStdlibGlobal(expr.right, "Date");
const url = lowerer.isStdlibGlobal(expr.right, "URL");
// A catch binding holds a thrown Date as its checked-dynamic handle.
const caughtDate = date ? lowerer.caughtLocalOf(expr.left) : null;
if (caughtDate)
return {
kind: "libCall",
fn: "dyn.nativeDateIs",
args: [{ kind: "caughtToDyn", value: varRef(caughtDate.id, CAUGHT, loc), type: DYN, loc }],
type: BOOL,
loc,
};
if ((date || url) && !lowerer.caughtLocalOf(expr.left)) {
const value = lowerer.lowerExpr(expr.left);
if (value.type.kind === "dyn")
@@ -281,6 +281,7 @@ export function lowerTypeofExpr(lowerer: Lowerer, expr: ts.TypeOfExpression, loc
promise: "object",
bytes: "object",
regex: "object",
date: "object",
generator: "object",
classval: "function",
moduleNs: "object",
@@ -120,6 +120,11 @@ function coercedUnion(lowerer: Lowerer, type: IrType): boolean {
return arms.some((arm) => arm.kind !== "f64" && !isUnitType(arm));
}
/** ToNumber of a Date: its time value (valueOf, the number hint). */
function dateTimeValue(date: IrExpr, loc: SrcLoc): IrExpr {
return { kind: "libCall", fn: "date.valueOf", args: [date], type: F64, loc };
}
/** ToNumber of a boolean: 1 for true, 0 for false. */
function booleanToNumber(value: IrExpr, loc: SrcLoc): IrExpr {
return {
@@ -145,23 +150,25 @@ export function lowerPrefixUnary(lowerer: Lowerer, expr: ts.PrefixUnaryExpressio
const operand =
raw.type.kind === "bool"
? booleanToNumber(raw, loc)
: coercedUnion(lowerer, raw.type)
? ({
kind: "libCall",
fn: "dyn.toNumberCoerce",
args: [{ kind: "dynFrom", value: raw, type: DYN, loc }],
type: F64,
loc,
} satisfies IrExpr)
: raw.type.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(expr))
: raw.type.kind === "date"
? dateTimeValue(raw, loc)
: coercedUnion(lowerer, raw.type)
? ({
kind: "libCall",
fn: "dyn.toNumberCoerce",
args: [raw],
args: [{ kind: "dynFrom", value: raw, type: DYN, loc }],
type: F64,
loc,
} satisfies IrExpr)
: lowerOptionalNumber(lowerer, raw, loc, expr.operand);
: raw.type.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(expr))
? ({
kind: "libCall",
fn: "dyn.toNumberCoerce",
args: [raw],
type: F64,
loc,
} satisfies IrExpr)
: lowerOptionalNumber(lowerer, raw, loc, expr.operand);
if (operand.type.kind !== "f64") operatorUnsupported(lowerer, expr, "-", [raw.type]);
if (operand.kind === "numLit") return { ...operand, value: -operand.value, loc };
return { kind: "unary", op: "-", operand, type: F64, loc };
@@ -185,6 +192,8 @@ export function lowerPrefixUnary(lowerer: Lowerer, expr: ts.PrefixUnaryExpressio
return { kind: "libCall", fn: "num.fromString", args: [raw], type: F64, loc };
}
if (raw.type.kind === "bool") return booleanToNumber(raw, loc);
// A Date's number hint answers its time value.
if (raw.type.kind === "date") return dateTimeValue(raw, loc);
const classNumber = classToNumberConversion(lowerer, raw, expr.operand, "unary plus");
if (classNumber) return classNumber;
const operand = lowerOptionalNumber(lowerer, raw, loc, expr.operand);
@@ -1,5 +1,6 @@
import { keyedHelperName } from "./stable-names.js";
import { perKeyOrder } from "./key-order-witness.js";
import { lowerDateToPrimitiveCall } from "./builtins/dates.js";
import { isForwardedTupleSpread, lowerForwardingApply } from "./typed-arguments.js";
import { guardNullProto, nullProtoRecordShape } from "./null-prototype.js";
import { lowerDynStreamMemberCall } from "./dyn-streams.js";
@@ -1816,6 +1817,8 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
// The element spelling of an unsupported native Web method must fence
// before lowering the callee into a checked-dynamic keyed read.
if (ts.isElementAccessExpression(expr.expression)) {
const datePrimitive = lowerDateToPrimitiveCall(lowerer, expr, expr.expression);
if (datePrimitive) return datePrimitive;
const timersDispose = lowerMockTimersElementCall(lowerer, expr, expr.expression);
if (timersDispose) return timersDispose;
const symbolMethod = lowerClassSymbolMethodCall(lowerer, expr);
@@ -2456,6 +2459,9 @@ function lowerNumberConstructorValue(
};
const optionalNumber = lowerOptionalStringNumber(lowerer, arg, loc);
if (optionalNumber) return optionalNumber;
// A Date's number hint answers its time value.
if (arg.type.kind === "date")
return { kind: "libCall", fn: "date.valueOf", args: [arg], type: F64, loc };
// A program class instance converts through its own methods.
const classNumber = classToNumberConversion(lowerer, arg, argNode);
if (classNumber) return classNumber;
@@ -10426,6 +10426,29 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
const time: IrExpr = { kind: "libCall", fn: "date.getTime", args: [arg], type: F64, loc };
return { kind: "libCall", fn: "date.newMs", args: [time], type: DATE_T, loc };
}
// A union (`Date | string | number`) or checked-dynamic value: the
// runtime's constructor reads a Date's time value, parses a string
// and converts anything else to a primitive and a number.
if (
(arg.type.kind === "union" || arg.type.kind === "dyn") &&
lowerer.dynConvertible(arg.type)
) {
const value: IrExpr = {
kind: "libCall",
fn: "date.nativeNewValue",
args: [
{
kind: "dynArrLit",
elems: [lowerer.coerceToExpected(arg, DYN)],
type: DYN,
loc,
},
],
type: DYN,
loc,
};
return { kind: "libCall", fn: "date.checkedValue", args: [value], type: DATE_T, loc };
}
lowerer.noLowering(
`new Date of '${lowerer.fmt(arg.type)}' values`,
args[0]!,
@@ -2320,6 +2320,7 @@ export function typeofAnswer(arm: IrType): string | null {
case "set":
case "generator":
case "regex":
case "date":
case "bytes":
case "url":
case "searchParams":
@@ -183,13 +183,9 @@ export function lowerGenMethodCall(
);
}
arg = lowerer.lowerExpr(argNode);
if (arg.type.kind === "date") {
lowerer.unsupported(
"SC1090",
argNode,
".throw() with a Date value (the exception channel cannot preserve Date's object kind; throw an Error or primitive instead)",
);
}
// A Date throws as its checked-dynamic handle, the same Date object
// (the throw statement's rule).
if (arg.type.kind === "date") arg = lowerer.coerceToExpected(arg, DYN);
if (arg.type.kind === "void" || arg.type.kind === "caught" || isUnitType(arg.type)) {
lowerer.unsupported(
"SC1090",
@@ -1624,6 +1624,7 @@ const THROWN_AS_DYN: ReadonlySet<IrType["kind"]> = new Set([
"bytes",
"func",
"regex",
"date",
]);
export function lowerStmt(lowerer: Lowerer, stmt: ts.Statement): IrStmt | IrStmt[] | null {
@@ -1828,9 +1829,7 @@ export function lowerStmt(lowerer: Lowerer, stmt: ts.Statement): IrStmt | IrStmt
return { kind: "rethrow", localId: caughtLocal.id, loc: locOf(stmt) };
}
// Exception-representable values move into the runtime's exception
// cell. `throw f()` of a void call has no value; Date has numeric
// storage internally, but cannot preserve its object kind through the
// untyped catch boundary. Reject both rather than changing JS identity.
// cell. `throw f()` of a void call has no value.
let value = lowerer.lowerExpr(stmt.expression);
if (value.type.kind === "void")
lowerer.badType(stmt.expression, lowerer.typeOf(stmt.expression));
@@ -1842,19 +1841,12 @@ export function lowerStmt(lowerer: Lowerer, stmt: ts.Statement): IrStmt | IrStmt
} else if (value.type.kind === "record" && lowerer.dynConvertible(value.type)) {
value = { kind: "dynFrom", value, liveRef: true, type: DYN, loc: locOf(stmt.expression) };
} else if (THROWN_AS_DYN.has(value.type.kind) && lowerer.dynConvertible(value.type)) {
// Arrays, Maps, Sets, typed arrays, Buffers, functions, regexes and
// unions over them keep their identity and contents the same way:
// the catch binding, `typeof`, `instanceof`, `Array.isArray`,
// inspection and typed views read the thrown object.
// Arrays, Maps, Sets, typed arrays, Buffers, functions, regexes,
// Dates and unions over them keep their identity and contents the
// same way: the catch binding, `typeof`, `instanceof`,
// `Array.isArray`, inspection and typed views read the thrown object.
value = lowerer.coerceInto(stmt.expression, value, DYN);
}
if (value.type.kind === "date") {
lowerer.unsupported(
"SC1090",
stmt.expression,
"throwing Date values (catch bindings cannot preserve Date's object kind; throw an Error or primitive instead)",
);
}
// A dyn throw rides the REF cell arm (scr_throw_ref over the checked-dynamic tree
// node — identity preserved through catch bindings, caughtToDyn
// passes it back by reference). The JS-lane traced-call shape:
@@ -5450,7 +5442,12 @@ export function lowerVarDecl(
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);
const local = lowerer.declareLocal(
decl.name,
decl.name.text,
F64,
isLet || lowerer.timeValueDateWrites.has(decl),
);
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 };
@@ -239,7 +239,7 @@ test("optional union widening keeps refused and missing contracts current", () =
unionId: lowerer.unions.recursiveRef(recursive, () => "Recursive"),
};
expect(lowerer.withUndefinedArmOf(pending)).not.toBeNull();
lowerer.unions.finalizeRecursive(recursive, [{ kind: "date" }, STRING]);
lowerer.unions.finalizeRecursive(recursive, [{ kind: "promise", inner: F64 }, STRING]);
expect(lowerer.withUndefinedArmOf(pending)).toBeNull();
expect(lowerer.withUndefinedArmOf(pending)).toBeNull();
@@ -2362,6 +2362,9 @@ export class Lowerer {
* Date's time value instead of a Date object (date-values.ts). */
readonly timeValueDateDecisions = new Map<ts.Node, boolean>();
readonly timeValueDateLocals = new Set<IrLocal>();
/** Declarations of those locals that a setter call writes
* (date-values.ts). */
readonly timeValueDateWrites = new Set<ts.VariableDeclaration>();
/** 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. */
@@ -2329,6 +2329,13 @@ export const AMBIENT_SURFACE_FNS: readonly AmbientSurfaceRow[] = [
],
note: "zero arguments, one milliseconds/date-string argument, or local-time year/month fields; values are the read-only TimeClip scalar slice",
},
{
id: "stdlib.date.setters",
kind: "stdlib",
name: "Date.prototype setters",
fns: ["date.setFields", "date.timeClip", "date.store"],
note: "setTime and the local and UTC field setters, with their optional arguments",
},
{
id: "stdlib.date.UTC",
kind: "stdlib",
@@ -4858,6 +4858,15 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
* lib-call argument passes the time value itself, and any other use
* creates the Date. */
"date.fromTime": { argTypes: [F64], result: DATE_T },
/** Date.prototype's field setters over a time value
* (scr_date_set_fields): time value, setter code, argument count and four
* arguments; answers the new time value. */
"date.setFields": { argTypes: [F64, F64, F64, F64, F64, F64, F64], result: F64 },
/** TimeClip of a number (setTime's argument). */
"date.timeClip": { argTypes: [F64], result: F64 },
/** Stores a time value into a Date object and answers it (a setter's
* write). */
"date.store": { argTypes: [DATE_T, F64], result: F64 },
/** 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. */
+3 -10
View File
@@ -855,9 +855,8 @@ export function funcOf(params: IrType[], ret: IrType): IrType {
* promises nullable-only; declared maybe-promise unions are admitted by
* maybePromiseUnionArmsOk. */
export function unionContainerArmsOk(arms: IrType[]): boolean {
return (
arms.every((a, i) => a.kind !== "promise" || arms.every((b, j) => j === i || isUnitType(b))) &&
dateUnionArmsOk(arms)
return arms.every(
(a, i) => a.kind !== "promise" || arms.every((b, j) => j === i || isUnitType(b)),
);
}
@@ -868,13 +867,7 @@ export function unionContainerArmsOk(arms: IrType[]): boolean {
* non-thenable path). Data arms must not be thenable; the type mapper
* checks that against the checker types before minting such a union. */
export function maybePromiseUnionArmsOk(arms: IrType[]): boolean {
return arms.filter((a) => a.kind === "promise").length <= 1 && dateUnionArmsOk(arms);
}
/** A Date arm has a union home beside undefined and null arms only: no
* narrowing contract tells it from other data arms yet. */
function dateUnionArmsOk(arms: IrType[]): boolean {
return arms.every((a, i) => a.kind !== "date" || arms.every((b, j) => j === i || isUnitType(b)));
return arms.filter((a) => a.kind === "promise").length <= 1;
}
/** Canonical, injective text form of an IrType — the building block of
+3 -16
View File
@@ -55,7 +55,6 @@ import {
typeEquals,
typeKey,
maybePromiseUnionArmsOk,
unionContainerArmsOk,
VOID,
} from "./ir.js";
import { DYN_CLASS_PROPERTIES, callSiteReturnType, classValueConstructor } from "./ir.js";
@@ -814,9 +813,6 @@ export function validateModule(mod: IrModule): IrValidationError[] {
loc: noLoc,
});
}
if (arm.kind === "date" && !unionContainerArmsOk(u.arms)) {
errors.push({ message: `union ${u.id}: date arm ${i} beside non-unit arms`, loc: noLoc });
}
if (arm.kind === "record" && !recordsById.has(arm.shapeId)) {
errors.push({
message: `union ${u.id}: arm ${i} references undeclared shape "${arm.shapeId}"`,
@@ -6334,15 +6330,10 @@ function validateFunction(
}
} else {
// throw: any exception-representable payload (the throw
// statement's rule). Date cannot retain its object kind in the
// untyped exception cell.
// statement's rule).
if (e.arg === null) {
err("genResume throw with no payload", e.loc);
} else if (
e.arg.type.kind === "void" ||
e.arg.type.kind === "caught" ||
e.arg.type.kind === "date"
) {
} else if (e.arg.type.kind === "void" || e.arg.type.kind === "caught") {
err(`genResume throw of a ${e.arg.type.kind} value`, e.loc);
}
}
@@ -6783,11 +6774,7 @@ function validateFunction(
checkExpr(s.value);
// dyn throws are allowed: the dyn node rides the REF cell arm by
// reference (the JS-lane `throw err` of a dyn argument).
if (
s.value.type.kind === "void" ||
s.value.type.kind === "caught" ||
s.value.type.kind === "date"
) {
if (s.value.type.kind === "void" || s.value.type.kind === "caught") {
err(`throw of a ${s.value.type.kind} value`, s.loc);
}
break;
+7
View File
@@ -4251,6 +4251,13 @@
"status": "static",
"note": "one date string, using V8's date parser shared with new Date(dateString)"
},
{
"id": "stdlib.date.setters",
"kind": "stdlib",
"name": "Date.prototype setters",
"status": "static",
"note": "setTime and the local and UTC field setters, with their optional arguments"
},
{
"id": "stdlib.date.toDateString",
"kind": "stdlib",
+4 -6
View File
@@ -8,12 +8,10 @@ test("hand-built fib module validates", () => {
expect(validateModule(fibModule)).toEqual([]);
});
test("validator rejects Date union arms beside other data arms", () => {
const bad = structuredClone(fibModule);
bad.unions = [{ id: "u-date", arms: [{ kind: "date" }, { kind: "string" }] }];
expect(validateModule(bad).map((e) => e.message)).toContain(
"union u-date: date arm 0 beside non-unit arms",
);
test("validator accepts Date union arms beside other arms", () => {
const mixed = structuredClone(fibModule);
mixed.unions = [{ id: "u-date", arms: [{ kind: "date" }, { kind: "string" }] }];
expect(validateModule(mixed)).toEqual([]);
const nullable = structuredClone(fibModule);
nullable.unions = [{ id: "u-date", arms: [{ kind: "date" }, { kind: "undefinedT" }] }];
expect(validateModule(nullable)).toEqual([]);
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8020-date-setters.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8021-date-coercions.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8022-date-unions.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8023-date-clone-throw.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8024-date-setters-dynamic.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8025-date-subclass-setters.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8026-date-subclass-setters-dynamic.mjs"
],
"diags": []
}
+123 -3
View File
@@ -11660,6 +11660,56 @@ double scr_date_new_local(double y, double mo, double d,
return scr_date_from_local(scr_date_v8_make_day(y, mo, d), scr_date_v8_make_time(h, mi, s, ms));
}
/* Date.prototype's setters over a time value, as V8's builtins-date.cc
* computes them: the fields of `t` (local fields for the local setters)
* with the first `argc` of them replaced, in the order the setter names
* them, by `a`..`d`, through MakeDay/MakeTime/MakeDate, then UTC(t) for
* the local setters and TimeClip. An invalid `t` stays invalid, except
* that setFullYear and setUTCFullYear start from +0: January 1 of the
* epoch year at midnight (local midnight for setFullYear). Returns the new
* time value; the caller stores it into the Date. Never throws. */
double scr_date_set_fields(double t, double setter_code, double argc_count,
double a, double b, double c, double d) {
int32_t setter = (int32_t)setter_code, argc = (int32_t)argc_count;
bool utc = (setter & SCR_DATE_SET_UTC) != 0;
int32_t field = setter & ~SCR_DATE_SET_UTC;
ScrDateParts p;
if (isnan(t)) {
if (field != SCR_DATE_SET_FULL_YEAR) return NAN;
p = (ScrDateParts){1970, 0, 1, 4, 0, 0, 0, 0, 0};
} else if (utc) {
if (!scr_date_utc_parts(t, &p)) return NAN;
} else if (!scr_date_local_parts(t, &p)) {
return NAN;
}
double y = (double)p.year, mo = p.month, dt = p.date;
double h = p.hours, mi = p.minutes, s = p.seconds, ms = p.milliseconds;
double args[4] = {a, b, c, d};
double *slots[7] = {&y, &mo, &dt, &h, &mi, &s, &ms};
/* The first field each setter names, in slots' order. */
int first;
switch (field) {
case SCR_DATE_SET_MILLISECONDS: first = 6; break;
case SCR_DATE_SET_SECONDS: first = 5; break;
case SCR_DATE_SET_MINUTES: first = 4; break;
case SCR_DATE_SET_HOURS: first = 3; break;
case SCR_DATE_SET_DATE: first = 2; break;
case SCR_DATE_SET_MONTH: first = 1; break;
case SCR_DATE_SET_FULL_YEAR: first = 0; break;
default: return NAN;
}
/* Each setter takes at most the fields from its own down to the next
* larger unit's end: setHours up to milliseconds, setMonth up to the
* date, setFullYear up to the date. */
int last = first <= 2 ? 2 : 6;
for (int i = 0; i < argc && first + i <= last && i < 4; i++) *slots[first + i] = args[i];
double day = scr_date_v8_make_day(y, mo, dt);
double time = scr_date_v8_make_time(h, mi, s, ms);
if (!utc) return scr_date_from_local(day, time);
if (!isfinite(day) || !isfinite(time)) return NAN;
return scr_date_new_ms(day * 86400000.0 + time);
}
/* ── V8's date-string parser, the fallback ─────────────────────────────
* scr_date_parse answers the bounded grammars directly and asks
* this parser only when they produce NaN. It follows V8's DateParser
@@ -13203,6 +13253,27 @@ ScrDate *scr_dyn_native_date_check(const ScrDyn *value, const ScrDynPath *path)
static const char scr_date_to_date_string_name[] = "toDateString";
static const char scr_date_to_time_string_name[] = "toTimeString";
/* The field setters a checked-dynamic Date answers (scr_date_set_fields). */
static const struct {
const char *name;
int code, max;
} scr_native_date_setters[] = {
{"setMilliseconds", SCR_DATE_SET_MILLISECONDS, 1},
{"setSeconds", SCR_DATE_SET_SECONDS, 2},
{"setMinutes", SCR_DATE_SET_MINUTES, 3},
{"setHours", SCR_DATE_SET_HOURS, 4},
{"setDate", SCR_DATE_SET_DATE, 1},
{"setMonth", SCR_DATE_SET_MONTH, 2},
{"setFullYear", SCR_DATE_SET_FULL_YEAR, 3},
{"setUTCMilliseconds", SCR_DATE_SET_UTC | SCR_DATE_SET_MILLISECONDS, 1},
{"setUTCSeconds", SCR_DATE_SET_UTC | SCR_DATE_SET_SECONDS, 2},
{"setUTCMinutes", SCR_DATE_SET_UTC | SCR_DATE_SET_MINUTES, 3},
{"setUTCHours", SCR_DATE_SET_UTC | SCR_DATE_SET_HOURS, 4},
{"setUTCDate", SCR_DATE_SET_UTC | SCR_DATE_SET_DATE, 1},
{"setUTCMonth", SCR_DATE_SET_UTC | SCR_DATE_SET_MONTH, 2},
{"setUTCFullYear", SCR_DATE_SET_UTC | SCR_DATE_SET_FULL_YEAR, 3},
};
static ScrDyn *scr_native_date_invoke(void *ptr, ScrDyn *self, const char *method,
ScrDyn *const *args, size_t argc, const char *what) {
(void)self; (void)what;
@@ -13232,6 +13303,25 @@ static ScrDyn *scr_native_date_invoke(void *ptr, ScrDyn *self, const char *metho
((ScrDate *)ptr)->ms = scr_date_new_ms(time);
return scr_dyn_new_num(((ScrDate *)ptr)->ms);
}
for (size_t i = 0; i < sizeof scr_native_date_setters / sizeof *scr_native_date_setters; i++) {
if (strcmp(method, scr_native_date_setters[i].name)) continue;
/* V8's order: ToNumber of the first argument, then the time value,
* then (unless the date is invalid) ToNumber of the optional ones the
* setter takes. */
int code = scr_native_date_setters[i].code, max = scr_native_date_setters[i].max;
double values[4] = {NAN, 0, 0, 0};
if (argc > 0 && !scr_dyn_number_coerce_js(args[0], &values[0])) return NULL;
double t = ((ScrDate *)ptr)->ms;
int used = argc < (size_t)max ? (int)argc : max;
if (used < 1) used = 1;
if (!isnan(t) || (code & ~SCR_DATE_SET_UTC) == SCR_DATE_SET_FULL_YEAR) {
for (int k = 1; k < used; k++)
if (!scr_dyn_number_coerce_js(args[k], &values[k])) return NULL;
}
double next = scr_date_set_fields(t, code, used, values[0], values[1], values[2], values[3]);
((ScrDate *)ptr)->ms = next;
return scr_dyn_new_num(next);
}
#define DATE_GET(name, fn) if (!strcmp(method, name)) return scr_dyn_new_num(fn(ms))
DATE_GET("getFullYear", scr_date_get_full_year_local);
DATE_GET("getUTCFullYear", scr_date_get_full_year_utc);
@@ -13311,7 +13401,9 @@ static const char *const scr_native_date_methods[] = {
"getFullYear", "getUTCFullYear", "getMonth",
"getUTCMonth", "getDate", "getUTCDate", "getDay", "getUTCDay", "getHours", "getUTCHours",
"getMinutes", "getUTCMinutes", "getSeconds", "getUTCSeconds", "getMilliseconds",
"getUTCMilliseconds", "getTimezoneOffset", "setTime",
"getUTCMilliseconds", "getTimezoneOffset", "setTime", "setMilliseconds", "setSeconds",
"setMinutes", "setHours", "setDate", "setMonth", "setFullYear", "setUTCMilliseconds",
"setUTCSeconds", "setUTCMinutes", "setUTCHours", "setUTCDate", "setUTCMonth", "setUTCFullYear",
};
#define SCR_NATIVE_DATE_METHODS (sizeof scr_native_date_methods / sizeof scr_native_date_methods[0])
static SCR_TL ScrDyn *scr_native_date_method_values[SCR_NATIVE_DATE_METHODS];
@@ -13397,12 +13489,40 @@ static ScrDyn *scr_native_date_get(void *ptr, const char *key, size_t key_len) {
return NULL;
}
/* structuredClone of a Date: a new Date with the same time value. */
static ScrDyn *scr_native_date_clone(void *h) {
return scr_dyn_native_date_from_ms(((ScrDate *)h)->ms);
}
/* A Date crossing threads (postMessage, workerData) carries its time value
* in a plain allocation any thread may read and free. */
static void *scr_native_date_share(void *h) {
double *ms = malloc(sizeof *ms);
if (!ms) scr_trap("scriptc: out of memory\n");
*ms = ((ScrDate *)h)->ms;
return ms;
}
static ScrDyn *scr_native_date_adopt(void *payload) {
return scr_dyn_native_date_from_ms(*(double *)payload);
}
static void scr_native_date_drop(void *payload) { free(payload); }
static const ScrDynHandleTransfer scr_native_date_transfer = {
.share = scr_native_date_share, .adopt = scr_native_date_adopt, .drop = scr_native_date_drop};
/* A checked-dynamic handle for the Date object `date` (borrowed; the
* handle retains it), so the value stays the same object. */
ScrDyn *scr_dyn_native_date(ScrDate *date) {
static const ScrDynHandleOps ops = {
"Date", &scr_date_retain_v, &scr_date_release_v, &scr_native_date_invoke,
&scr_native_date_get, NULL, NULL, NULL,
.cls = "Date",
.retain = &scr_date_retain_v,
.release = &scr_date_release_v,
.invoke = &scr_native_date_invoke,
.get = &scr_native_date_get,
.clone = &scr_native_date_clone,
.transfer = &scr_native_date_transfer,
};
scr_dyn_handle_install(SCR_DYNH_DATE, &ops);
return scr_dyn_new_handle(date, SCR_DYNH_DATE);
+17
View File
@@ -111,6 +111,23 @@ double scr_date_now(void); /* integer ms since epoch, like Node */
extern SCR_TL const double *scr_date_mock_now;
double scr_date_new_ms(double ms); /* TimeClip (NaN when invalid) */
double scr_date_get_time(double ms);
/* Date.prototype's setters (setMilliseconds ... setFullYear and their UTC
* forms) over a time value: the new time value for `argc` arguments
* `a`..`d` (scr_lib.c). `setter` is an SCR_DATE_SET_* code; it and `argc`
* are whole numbers passed as doubles, as the compiler's lib calls pass
* numbers. */
enum {
SCR_DATE_SET_MILLISECONDS = 0,
SCR_DATE_SET_SECONDS = 1,
SCR_DATE_SET_MINUTES = 2,
SCR_DATE_SET_HOURS = 3,
SCR_DATE_SET_DATE = 4,
SCR_DATE_SET_MONTH = 5,
SCR_DATE_SET_FULL_YEAR = 6,
SCR_DATE_SET_UTC = 8,
};
double scr_date_set_fields(double t, double setter, double argc,
double a, double b, double c, double d);
/* A Date object: the TimeClip'd time value in milliseconds (NaN for an
* invalid date). Dates are immutable; their identity is the pointer.
* Retain/release follow the runtime's RC conventions (SIZE_MAX immortal). */
+115
View File
@@ -0,0 +1,115 @@
// Date setters: every local and UTC setter with each argument count, the
// time values they return, normalization of out-of-range fields, local
// setters across a daylight saving transition, setters on invalid dates,
// explicit undefined arguments, fractional arguments, and the TimeClip
// range. A setter changes the one Date object every alias holds.
export {};
process.env.TZ = "America/Chicago";
const show = (label: string, d: Date): void => {
console.log(label, d.getTime(), Number.isNaN(d.getTime()) ? "invalid" : d.toISOString());
};
const d = new Date(Date.UTC(2026, 0, 31, 10, 20, 30, 400));
console.log(d.setMilliseconds(999), d.getMilliseconds());
console.log(d.setSeconds(5), d.setSeconds(6, 7));
console.log(d.setMinutes(1), d.setMinutes(2, 3), d.setMinutes(4, 5, 6));
console.log(d.setHours(7), d.setHours(8, 9), d.setHours(10, 11, 12), d.setHours(13, 14, 15, 16));
console.log(d.setDate(15), d.setMonth(1), d.setMonth(2, 3));
console.log(d.setFullYear(2025), d.setFullYear(2024, 1), d.setFullYear(2023, 4, 6));
show("local", d);
const u = new Date(Date.UTC(2026, 0, 31));
console.log(u.setUTCMilliseconds(1), u.setUTCSeconds(2, 3), u.setUTCMinutes(4, 5, 6));
console.log(u.setUTCHours(7, 8, 9, 10), u.setUTCDate(1), u.setUTCMonth(5, 30));
console.log(u.setUTCFullYear(1999), u.setUTCFullYear(2000, 1, 29));
show("utc", u);
// Fields out of range carry into the next larger field, both ways.
const n = new Date(Date.UTC(2026, 0, 31, 12));
n.setUTCMonth(1);
show("Jan 31 + month", n);
n.setUTCDate(0);
show("day 0", n);
n.setUTCMonth(13);
show("month 13", n);
n.setUTCHours(-1);
show("hour -1", n);
n.setUTCMinutes(60 * 24 + 1);
show("minutes past a day", n);
n.setUTCSeconds(-0.5);
show("fractional seconds", n);
n.setUTCMilliseconds(1.9);
show("fractional ms", n);
// Local setters in a zone with daylight saving time: 2026-03-08 02:30
// does not exist in Chicago and 2026-11-01 01:30 happens twice.
const spring = new Date(2026, 2, 8, 0, 0);
spring.setHours(2, 30);
show("spring gap", spring);
console.log(spring.getHours(), spring.getMinutes(), spring.getTimezoneOffset());
const fall = new Date(2026, 10, 1, 0, 0);
fall.setHours(1, 30);
show("fall overlap", fall);
fall.setMinutes(fall.getMinutes() + 60);
show("fall overlap + 60 min", fall);
const walk = new Date(2026, 2, 7, 12);
for (let i = 0; i < 3; i++) {
walk.setDate(walk.getDate() + 1);
console.log("walk", walk.getDate(), walk.getHours(), walk.getTimezoneOffset());
}
// Invalid dates: setFullYear starts from +0 local time, the others keep NaN.
const bad = new Date(NaN);
console.log(bad.setHours(1), bad.setDate(2), bad.setUTCMonth(3), bad.setMilliseconds(4));
console.log(bad.setFullYear(2020));
show("after setFullYear", bad);
const badUtc = new Date("not a date");
console.log(badUtc.setUTCFullYear(2021, 6));
show("after setUTCFullYear", badUtc);
// Explicit undefined and NaN arguments make the date invalid; a missing
// optional argument keeps the current field.
const opt = new Date(Date.UTC(2026, 5, 15, 9));
console.log(opt.setUTCHours(10, undefined), Number.isNaN(opt.getTime()));
const opt2 = new Date(Date.UTC(2026, 5, 15, 9));
console.log(opt2.setUTCMonth(Number.NaN), opt2.setUTCMonth(2));
// setTime and the TimeClip range.
const t = new Date(0);
console.log(t.setTime(8.64e15), t.setTime(8.64e15 + 1), t.setTime(-8.64e15), t.setTime(1.5));
console.log(t.setTime(-0), Object.is(t.getTime(), 0), t.setTime(Number.NaN));
const far = new Date(Date.UTC(275760, 8, 13));
console.log(far.getTime(), far.setUTCMilliseconds(1));
// Every alias sees the change: a second binding, an array slot, a Map
// value, a record field, a closure and a parameter.
const shared = new Date(Date.UTC(2026, 6, 4));
const alias = shared;
const list = [shared, new Date(Date.UTC(2000, 0, 1))];
const byName = new Map<string, Date>([["shared", shared]]);
const holder = { when: shared };
const bump = (): number => shared.setUTCDate(shared.getUTCDate() + 1);
const nextYear = (date: Date): void => {
date.setUTCFullYear(date.getUTCFullYear() + 1);
};
alias.setUTCHours(6);
list[0]!.setUTCMinutes(30);
byName.get("shared")!.setUTCSeconds(15);
holder.when.setUTCMilliseconds(250);
bump();
nextYear(shared);
show("shared", shared);
console.log(alias === shared, list[0] === shared, byName.get("shared") === holder.when);
console.log(list.map((x) => x.toISOString()).join(" "));
// A Date a loop adjusts in place.
const cursor = new Date(Date.UTC(2026, 0, 1));
let weekdays = 0;
while (cursor.getUTCFullYear() === 2026) {
const day = cursor.getUTCDay();
if (day !== 0 && day !== 6) weekdays++;
cursor.setUTCDate(cursor.getUTCDate() + 1);
}
console.log("weekdays in 2026", weekdays);
show("cursor", cursor);
+47
View File
@@ -0,0 +1,47 @@
// Dates converted to primitives as Node converts them: unary +, Number()
// and relational operators read the time value (the number hint), while +
// with a string, template literals and String() read the text (a Date's
// default hint is a string). Symbol.toPrimitive, valueOf and toString answer
// the same values; == compares two Dates by identity and a Date with a
// primitive through its default primitive. typeof, truthiness and
// instanceof treat a Date as an object, invalid ones included.
export {};
process.env.TZ = "UTC";
const a = new Date(Date.UTC(2026, 9, 23, 8, 30));
const b = new Date(Date.UTC(2026, 9, 24, 8, 30));
const bad = new Date(Number.NaN);
console.log(+a, -a, +b - +a, (+b - +a) / 3600000, Number(a), Math.max(+a, +b) === b.getTime());
console.log(a + "!");
console.log(`at ${a}`);
console.log(String(b));
console.log("" + a === a.toString(), a + "1" === a.toString() + "1");
console.log(a[Symbol.toPrimitive]("number"), a[Symbol.toPrimitive]("string") === a.toString());
console.log(a[Symbol.toPrimitive]("default") === a.toString(), a.valueOf() === a.getTime());
console.log(a < b, a <= b, a > b, a >= b, +b > a.getTime() + 1);
const same = a;
const twin = new Date(a.getTime());
console.log(a == same, a == twin, a === twin, a != twin, a.getTime() === twin.getTime());
console.log(a == (a.toString() as unknown), a == (a.getTime() as unknown));
console.log(typeof a, typeof bad, a instanceof Date, bad instanceof Date);
console.log(Object.prototype.toString.call(a), !a, !!bad, bad ? "truthy" : "falsy");
// Invalid dates.
console.log(+bad, +bad - +a, `${bad}`, String(bad), bad + "", Number.isNaN(bad.getTime()));
console.log(bad.toJSON(), JSON.stringify({ when: bad, at: a }));
try {
bad.toISOString();
} catch (error) {
console.log((error as Error).name, (error as Error).message);
}
// Sorting and summing through the number hint.
const days = [b, a, new Date(Date.UTC(2026, 0, 1))];
days.sort((x, y) => +x - +y);
console.log(days.map((x) => x.getUTCDate()).join(","));
let span = 0;
for (let i = 1; i < days.length; i++) span += +days[i]! - +days[i - 1]!;
console.log(span / 86400000);
+68
View File
@@ -0,0 +1,68 @@
// Dates in unions with other arms: Date | string, Date | number and
// Date | a record, narrowed with instanceof and typeof, in arrays, Map
// values, optional fields and function results. A Date arm keeps its
// identity, so a setter through a union-typed alias changes the original.
export {};
process.env.TZ = "UTC";
type When = Date | string | number;
function describe(when: When): string {
if (when instanceof Date) return `date ${when.toISOString()}`;
if (typeof when === "string") return `text ${when}`;
return `ms ${when}`;
}
function normalize(when: When): Date {
return when instanceof Date ? when : new Date(when);
}
const fixed = new Date(Date.UTC(2026, 9, 23));
const values: When[] = [fixed, "2026-01-02T03:04:05Z", 0, new Date(Number.NaN)];
for (const value of values) {
const text = value instanceof Date && Number.isNaN(value.getTime()) ? "invalid" : describe(value);
console.log(text);
}
console.log(normalize(fixed) === fixed, normalize("2000-01-01").getUTCFullYear(), normalize(1e12).getUTCFullYear());
const held: When = fixed;
if (held instanceof Date) held.setUTCHours(12);
console.log(fixed.getUTCHours(), held === fixed);
type Stamp = Date | { label: string; at: number };
const stamps: Stamp[] = [fixed, { label: "start", at: 5 }];
for (const stamp of stamps) {
console.log(stamp instanceof Date ? stamp.getUTCDate() : `${stamp.label}@${stamp.at}`);
}
const byKey = new Map<string, Date | string>();
byKey.set("a", fixed);
byKey.set("b", "later");
for (const [key, value] of byKey) console.log(key, value instanceof Date ? value.getTime() : value);
interface Task {
name: string;
due?: Date;
done: Date | null;
}
const tasks: Task[] = [
{ name: "write", due: fixed, done: null },
{ name: "ship", done: new Date(Date.UTC(2026, 11, 1)) },
];
for (const task of tasks) {
const due = task.due ? task.due.toISOString().slice(0, 10) : "none";
const done = task.done === null ? "open" : task.done.toISOString().slice(0, 10);
console.log(task.name, due, done);
}
tasks[0]!.due!.setUTCDate(30);
console.log(fixed.getUTCDate(), tasks[0]!.due === fixed);
function latest(dates: (Date | undefined)[]): Date | string {
let best: Date | undefined;
for (const date of dates) if (date && (best === undefined || date > best)) best = date;
return best ?? "none";
}
const found = latest([undefined, fixed, new Date(0)]);
console.log(found === fixed, latest([]));
console.log(JSON.stringify({ values, stamps }));
+32
View File
@@ -0,0 +1,32 @@
// Dates through structuredClone and MessageChannel, which copy them as
// new Dates with the same time value, and Dates thrown and rejected as
// values, which keep their identity.
import { MessageChannel } from "node:worker_threads";
process.env.TZ = "UTC";
const at = new Date(Date.UTC(2026, 9, 23, 7));
const copy = structuredClone(at);
console.log(copy instanceof Date, copy === at, copy.getTime() === at.getTime());
copy.setUTCFullYear(2000);
console.log(at.getUTCFullYear(), copy.getUTCFullYear());
const nested = structuredClone({ at, list: [at, at], invalid: new Date(Number.NaN) });
console.log(nested.list[0] instanceof Date, nested.at === at, nested.list.length);
console.log(nested.at.toISOString(), Number.isNaN(nested.invalid.getTime()));
try {
throw at;
} catch (error) {
console.log(error instanceof Date, error === at);
}
const { port1, port2 } = new MessageChannel();
port2.on("message", (data: { when: Date; again: Date }) => {
const { when, again } = data;
console.log("message", when instanceof Date, when === at, again instanceof Date, when.toISOString());
port2.close();
Promise.reject(at).catch((reason: unknown) => {
console.log("rejected", reason instanceof Date, reason === at);
});
});
port1.postMessage({ when: at, again: at });
@@ -0,0 +1,24 @@
// Date setters in JavaScript, on Dates held as untyped values: each setter
// converts its arguments with ToNumber (valueOf runs, undefined is NaN) and
// changes the one Date object every alias holds.
process.env.TZ = "Europe/Berlin";
const d = new Date(Date.UTC(2026, 2, 28, 12));
const alias = d;
const box = { d };
console.log(d.setHours(1, 30), d.getHours(), d.getTimezoneOffset());
console.log(alias.setDate(29), box.d.getHours(), d.getTimezoneOffset());
console.log(d.setMinutes("15"), d.setSeconds(true), d.setMilliseconds(null));
console.log(d.setUTCFullYear(2030, 11, 31), d.toISOString());
console.log(d.setMonth(undefined), Number.isNaN(d.getTime()));
console.log(d.setFullYear(2001), d.toISOString());
let calls = 0;
const later = { valueOf() { calls++; return 45; } };
console.log(d.setUTCMinutes(later, later), calls, d.toISOString());
const other = new Date(0);
console.log(other.setHours(5), other.getHours(), typeof d.setMonth);
const list = [new Date(0), new Date(86400000)];
list.forEach((x, i) => x.setUTCHours(i + 1));
console.log(list.map((x) => x.toISOString()).join(" "));
@@ -0,0 +1,30 @@
// Setters on instances of a class declared `extends Date`: each changes
// the instance's time value, which the inherited getters, the subclass's
// own methods and toISOString then read; invalid instances follow the same
// rules as Dates.
export {};
process.env.TZ = "Asia/Kolkata";
class Deadline extends Date {
label: string;
constructor(label: string, ms: number) {
super(ms);
this.label = label;
}
extend(days: number): this {
this.setDate(this.getDate() + days);
return this;
}
describe(): string {
return `${this.label} ${this.toISOString()}`;
}
}
const due = new Deadline("report", Date.UTC(2026, 9, 23, 18, 30));
console.log(due.describe());
console.log(due.setHours(9, 15), due.getHours(), due.getMinutes());
console.log(due.extend(10).describe());
console.log(due.setUTCMonth(0, 31), due.setMonth(1), due.getDate(), due.getMonth());
console.log(due.setFullYear(2024, 1, 29), due.describe());
console.log(due.setTime(Number.NaN), Number.isNaN(due.getTime()), due.setUTCFullYear(2030));
console.log(due.describe(), due instanceof Date, due instanceof Deadline);
@@ -0,0 +1,17 @@
// Setters on instances of a JavaScript class declared `extends Date`, held
// as untyped values: the inherited setters change the instance's time value.
process.env.TZ = "America/Sao_Paulo";
class Shift extends Date {
start() {
this.setHours(8, 0, 0, 0);
return this;
}
}
const shift = new Shift(Date.UTC(2026, 4, 5, 15));
console.log(shift.start().toISOString(), shift.getHours());
console.log(shift.setDate(31), shift.getMonth(), shift.setUTCHours(0), shift.toISOString());
const values = [shift, new Shift(0)];
for (const value of values) value.setUTCMinutes(45);
console.log(values.map((value) => value.toISOString()).join(" "), values[0] === shift);
-10
View File
@@ -21,8 +21,6 @@ const fmt = new Intl.NumberFormat();
// Constructors.
const d = new Date(...([2024, 0] as const)); // spread field arguments stay fenced
const mutableDate = new Date(0);
mutableDate.setUTCFullYear(2024); // mutation is outside the read-only Date slice
const wr = new WeakRef({});
const px = new Proxy({ a: 1 }, {});
const buf = new ArrayBuffer(8);
@@ -73,14 +71,6 @@ function pick(a: string | number, b: number | boolean): boolean {
}
const picked = pick(1, 2);
function throwDate(): void {
throw new Date(0); // the exception cell cannot preserve Date's object kind
}
throwDate();
function* dateGenerator(): Generator<number, void, unknown> {
yield 1;
}
dateGenerator().throw(new Date(0));
// Related classes may store one object under different union tags. A
// structural arm beside them leaves no identity-preserving comparison.
class FenceBase {
@@ -16,7 +16,7 @@ assert-class-fences.ts:12:16 - error SC2020: 'assert.Assert instances used as va
hint: bind `new Assert(...)` with literal options to a const and only call methods on it (`const a = new Assert({ diff: 'full' }); a.strictEqual(x, y)`)
assert-class-fences.ts:13:22 - error SC2001: values of type 'object | Assert' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, read-only Date values, functions, classes, records, unions of those, and 'unknown')
assert-class-fences.ts:13:22 - error SC2001: values of type 'object | Assert' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, Dates, functions, classes, records, unions of those, and 'unknown')
12 | const passed = new Assert();
13 | function useInstance(a: Assert | object): void {
@@ -7,7 +7,7 @@ class-interface-refinements.ts:8:26 - error SC2002: record shapes must match exa
hint: width subtyping compiles as a copy (each expected field copied off the source; extra fields drop) — this pair is outside the copy's rules; build a literal with exactly the expected fields instead
class-interface-refinements.ts:12:16 - error SC2001: values of type 'AddedStorage' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, read-only Date values, functions, classes, records, unions of those, and 'unknown')
class-interface-refinements.ts:12:16 - error SC2001: values of type 'AddedStorage' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, Dates, functions, classes, records, unions of those, and 'unknown')
11 | interface AddedStorage extends Box { extra: string; }
12 | function added(view: AddedStorage): string { return view.extra; }
@@ -23,7 +23,7 @@ class-interface-refinements.ts:13:13 - error SC2004: uses of 'added' inherit the
hint: the declaration of 'added' did not compile — fix the diagnostic reported there and these sites clear with it
class-interface-refinements.ts:16:18 - error SC2001: values of type 'ChangedMethod' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, read-only Date values, functions, classes, records, unions of those, and 'unknown')
class-interface-refinements.ts:16:18 - error SC2001: values of type 'ChangedMethod' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, Dates, functions, classes, records, unions of those, and 'unknown')
15 | interface ChangedMethod extends Box { read(): number; }
16 | function changed(view: ChangedMethod): number { return view.read(); }
@@ -5,7 +5,7 @@ date-formatters.ts:4:1 - error SC2020: 'Date.prototype.toLocaleTimeString' is pa
| ^~~~~~~~~~~~~~~~~~~~~~~~~
5 | date.toLocaleString();
hint: getTime(), valueOf(), toISOString(), toUTCString(), toJSON(), toString(), toDateString(), toTimeString(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale formatters have no lowering
hint: getTime(), valueOf(), toISOString(), toUTCString(), toJSON(), toString(), toDateString(), toTimeString(), the local/UTC calendar getters and setters, setTime(), and getTimezoneOffset() are supported; locale formatters have no lowering
date-formatters.ts:5:1 - error SC2020: 'Date.prototype.toLocaleString' is part of the standard library types but has no scriptc lowering yet
@@ -14,4 +14,4 @@ date-formatters.ts:5:1 - error SC2020: 'Date.prototype.toLocaleString' is part o
| ^~~~~~~~~~~~~~~~~~~~~
6 |
hint: getTime(), valueOf(), toISOString(), toUTCString(), toJSON(), toString(), toDateString(), toTimeString(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale formatters have no lowering
hint: getTime(), valueOf(), toISOString(), toUTCString(), toJSON(), toString(), toDateString(), toTimeString(), the local/UTC calendar getters and setters, setTime(), and getTimezoneOffset() are supported; locale formatters have no lowering
@@ -1,4 +1,4 @@
dynamic-representable-types.ts:7:7 - error SC2001: values of type '[number, (string | undefined)?]' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, read-only Date values, functions, classes, records, unions of those, and 'unknown')
dynamic-representable-types.ts:7:7 - error SC2001: values of type '[number, (string | undefined)?]' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, Dates, functions, classes, records, unions of those, and 'unknown')
6 | const bare: any = 41;
7 | const optTuple: [number, string?] = [1];
+1 -1
View File
@@ -12,7 +12,7 @@ generics.ts:30:10 - error SC1090: '.join()' on arrays of this element type (numb
| ^~~~~~~~~~~
31 | }
generics.ts:44:7 - error SC2001: values of type 'typeof Box' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, read-only Date values, functions, classes, records, unions of those, and 'unknown')
generics.ts:44:7 - error SC2001: values of type 'typeof Box' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, Dates, functions, classes, records, unions of those, and 'unknown')
43 | // The uninstantiated family as a value: no thunk, no single ctor ABI.
44 | const BoxAlias = Box;
@@ -1,4 +1,4 @@
maps-surface.ts:21:11 - error SC2001: values of type '(v: number, k: string, theMap: Map<string, number>) => void' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, read-only Date values, functions, classes, records, unions of those, and 'unknown')
maps-surface.ts:21:11 - error SC2001: values of type '(v: number, k: string, theMap: Map<string, number>) => void' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, Dates, functions, classes, records, unions of those, and 'unknown')
20 | // The forEach callback receives (value, key) — no third map parameter.
21 | m.forEach((v, k, theMap) => console.log(k, v));
@@ -5,7 +5,7 @@ regex-match-destructuring-fences.ts:6:16 - error SC1031: rest bindings over rege
| ^~~~~~~
7 | const { input = "none" } = m;
regex-match-destructuring-fences.ts:6:19 - error SC2001: values of type '{ [n: number]: string; input: string; 0: string; length: number; toString(): string; toLocaleString(): string; toLocaleString(locales: string | string[], options?: (NumberFormatOptions & DateTimeFormatOptions) | undefined): string; ... 39 more ...; with(index: number, value: string): string[]; }' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, read-only Date values, functions, classes, records, unions of those, and 'unknown')
regex-match-destructuring-fences.ts:6:19 - error SC2001: values of type '{ [n: number]: string; input: string; 0: string; length: number; toString(): string; toLocaleString(): string; toLocaleString(locales: string | string[], options?: (NumberFormatOptions & DateTimeFormatOptions) | undefined): string; ... 39 more ...; with(index: number, value: string): string[]; }' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, Dates, functions, classes, records, unions of those, and 'unknown')
5 | const m = /(a)(b)?/.exec("ab")!;
6 | const { index, ...rest } = m;
@@ -43,7 +43,7 @@ retired-engine-2103-optional-tuple-elements.ts:19:1 - error SC2004: uses of 'f2'
hint: the declaration of 'f2' did not compile — fix the diagnostic reported there and these sites clear with it
retired-engine-2103-optional-tuple-elements.ts:20:7 - error SC2001: values of type '[(string | undefined)?, (number | undefined)?]' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, read-only Date values, functions, classes, records, unions of those, and 'unknown')
retired-engine-2103-optional-tuple-elements.ts:20:7 - error SC2001: values of type '[(string | undefined)?, (number | undefined)?]' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, Dates, functions, classes, records, unions of those, and 'unknown')
19 | f2([]);
20 | const t: [string?, number?] = ["x"];
@@ -59,7 +59,7 @@ retired-engine-2103-optional-tuple-elements.ts:21:13 - error SC2004: uses of 't'
hint: the declaration of 't' did not compile — fix the diagnostic reported there and these sites clear with it
retired-engine-2103-optional-tuple-elements.ts:22:7 - error SC2001: values of type '[(string | undefined)?, (number | undefined)?]' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, read-only Date values, functions, classes, records, unions of those, and 'unknown')
retired-engine-2103-optional-tuple-elements.ts:22:7 - error SC2001: values of type '[(string | undefined)?, (number | undefined)?]' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, Dates, functions, classes, records, unions of those, and 'unknown')
21 | console.log(t.length, `${t[0]} ${t[1]}`);
22 | const full: [string?, number?] = ["y", 7];
@@ -75,14 +75,14 @@ retired-engine-2103-optional-tuple-elements.ts:23:13 - error SC2004: uses of 'fu
hint: the declaration of 'full' did not compile — fix the diagnostic reported there and these sites clear with it
retired-engine-2103-optional-tuple-elements.ts:24:7 - error SC2001: values of type '[(string | undefined)?, (number | undefined)?]' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, read-only Date values, functions, classes, records, unions of those, and 'unknown')
retired-engine-2103-optional-tuple-elements.ts:24:7 - error SC2001: values of type '[(string | undefined)?, (number | undefined)?]' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, Dates, functions, classes, records, unions of those, and 'unknown')
23 | console.log(full.length, `${full[0]} ${full[1]}`);
24 | const none: [string?, number?] = [];
| ^~~~
25 | console.log(none.length);
retired-engine-2103-optional-tuple-elements.ts:24:34 - error SC2001: values of type '[(string | undefined)?, (number | undefined)?]' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, read-only Date values, functions, classes, records, unions of those, and 'unknown')
retired-engine-2103-optional-tuple-elements.ts:24:34 - error SC2001: values of type '[(string | undefined)?, (number | undefined)?]' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, Dates, functions, classes, records, unions of those, and 'unknown')
23 | console.log(full.length, `${full[0]} ${full[1]}`);
24 | const none: [string?, number?] = [];
@@ -98,7 +98,7 @@ retired-engine-2103-optional-tuple-elements.ts:25:13 - error SC2004: uses of 'no
hint: the declaration of 'none' did not compile — fix the diagnostic reported there and these sites clear with it
retired-engine-2103-optional-tuple-elements.ts:27:7 - error SC2001: values of type '[string, ...number[]]' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, read-only Date values, functions, classes, records, unions of those, and 'unknown')
retired-engine-2103-optional-tuple-elements.ts:27:7 - error SC2001: values of type '[string, ...number[]]' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, Dates, functions, classes, records, unions of those, and 'unknown')
26 | // A rest-element tuple rides the same island representation.
27 | const r: [string, ...number[]] = ["head", 1, 2, 3];
+29 -52
View File
@@ -55,88 +55,65 @@ stdlib-fence.ts:23:11 - error SC2020: 'new Date with spread arguments' is part o
22 | // Constructors.
23 | const d = new Date(...([2024, 0] as const)); // spread field arguments stay fenced
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
24 | const mutableDate = new Date(0);
24 | const wr = new WeakRef({});
hint: new Date(), new Date(milliseconds), new Date(dateString), and new Date(year, monthIndex, ...) with listed arguments are supported
stdlib-fence.ts:25:1 - error SC2020: 'Date.prototype.setUTCFullYear with arguments' is part of the standard library types but has no scriptc lowering yet
stdlib-fence.ts:24:7 - error SC2020: 'WeakRef<{}>' is part of the standard library types but has no scriptc lowering yet
24 | const mutableDate = new Date(0);
25 | mutableDate.setUTCFullYear(2024); // mutation is outside the read-only Date slice
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
26 | const wr = new WeakRef({});
hint: getTime(), valueOf(), toISOString(), toUTCString(), toJSON(), toString(), toDateString(), toTimeString(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale formatters have no lowering
stdlib-fence.ts:26:7 - error SC2020: 'WeakRef<{}>' is part of the standard library types but has no scriptc lowering yet
25 | mutableDate.setUTCFullYear(2024); // mutation is outside the read-only Date slice
26 | const wr = new WeakRef({});
23 | const d = new Date(...([2024, 0] as const)); // spread field arguments stay fenced
24 | const wr = new WeakRef({});
| ^~
27 | const px = new Proxy({ a: 1 }, {});
25 | const px = new Proxy({ a: 1 }, {});
hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations)
stdlib-fence.ts:26:12 - error SC2020: 'new WeakRef' is part of the standard library types but has no scriptc lowering yet
stdlib-fence.ts:24:12 - error SC2020: 'new WeakRef' is part of the standard library types but has no scriptc lowering yet
25 | mutableDate.setUTCFullYear(2024); // mutation is outside the read-only Date slice
26 | const wr = new WeakRef({});
23 | const d = new Date(...([2024, 0] as const)); // spread field arguments stay fenced
24 | const wr = new WeakRef({});
| ^~~~~~~~~~~~~~~
27 | const px = new Proxy({ a: 1 }, {});
25 | const px = new Proxy({ a: 1 }, {});
hint: deref()-after-collect exposes GC timing — genuinely dynamic; hold a strong reference instead
stdlib-fence.ts:41:31 - error SC1090: '.split()' on a 'RegExp | string' separator (pass a string, or a regex literal) is not supported yet
stdlib-fence.ts:39:31 - error SC1090: '.split()' on a 'RegExp | string' separator (pass a string, or a regex literal) is not supported yet
40 | const norm = "abc".normalize();
41 | const limited = "a,b,c".split(Math.random() ? "," : /,/, 2); // union separators stay fenced; limits lower
38 | const norm = "abc".normalize();
39 | const limited = "a,b,c".split(Math.random() ? "," : /,/, 2); // union separators stay fenced; limits lower
| ^~~~~~~~~~~~~~~~~~~~~~~~~
42 | const fixed = (1.5).toFixed(); // digit-free toFixed lowers now (the static ties-up integer)
40 | const fixed = (1.5).toFixed(); // digit-free toFixed lowers now (the static ties-up integer)
stdlib-fence.ts:43:19 - error SC2020: 'Number.prototype.toLocaleString without a locale' is part of the standard library types but has no scriptc lowering yet
stdlib-fence.ts:41:19 - error SC2020: 'Number.prototype.toLocaleString without a locale' is part of the standard library types but has no scriptc lowering yet
42 | const fixed = (1.5).toFixed(); // digit-free toFixed lowers now (the static ties-up integer)
43 | const localized = (1234.5).toLocaleString();
40 | const fixed = (1.5).toFixed(); // digit-free toFixed lowers now (the static ties-up integer)
41 | const localized = (1234.5).toLocaleString();
| ^~~~~~~~~~~~~~~~~~~~~~~~~
44 | // Unlowered call FORMS of lowered members. (push is fully variadic now —
42 | // Unlowered call FORMS of lowered members. (push is fully variadic now —
hint: the default locale is the host environment's, which a compiled binary cannot carry — pass it explicitly: x.toLocaleString("en-US")
stdlib-fence.ts:49:1 - error SC2020: '.forEach with 2 arguments' is part of the standard library types but has no scriptc lowering yet
stdlib-fence.ts:47:1 - error SC2020: '.forEach with 2 arguments' is part of the standard library types but has no scriptc lowering yet
48 | const joined = nums.join(); // the omitted separator lowers
49 | nums.forEach((x) => console.log(x), { unused: true }); // thisArg stays fenced
46 | const joined = nums.join(); // the omitted separator lowers
47 | nums.forEach((x) => console.log(x), { unused: true }); // thisArg stays fenced
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
50 | const pos = "abc".includes("b", 1); // the position form lowers now (indexOf's clamp)
48 | const pos = "abc".includes("b", 1); // the position form lowers now (indexOf's clamp)
hint: the thisArg parameter has no lowering — use an arrow function
stdlib-fence.ts:77:9 - error SC1090: throwing Date values (catch bindings cannot preserve Date's object kind; throw an Error or primitive instead) are not supported yet
stdlib-fence.ts:84:10 - error SC1090: comparisons of related class values beside structural union members (narrow first: check a discriminant field, or compare with '!== undefined'/'!== null' for unit arms) are not supported yet
76 | function throwDate(): void {
77 | throw new Date(0); // the exception cell cannot preserve Date's object kind
| ^~~~~~~~~~~
78 | }
stdlib-fence.ts:83:23 - error SC1090: .throw() with a Date value (the exception channel cannot preserve Date's object kind; throw an Error or primitive instead) is not supported yet
82 | }
83 | dateGenerator().throw(new Date(0));
| ^~~~~~~~~~~
84 | // Related classes may store one object under different union tags. A
stdlib-fence.ts:94:10 - error SC1090: comparisons of related class values beside structural union members (narrow first: check a discriminant field, or compare with '!== undefined'/'!== null' for unit arms) are not supported yet
93 | function relatedStrict(a: FenceValue, b: FenceValue): boolean {
94 | return a === b;
83 | function relatedStrict(a: FenceValue, b: FenceValue): boolean {
84 | return a === b;
| ^~~~~~~
95 | }
85 | }
stdlib-fence.ts:97:10 - error SC2020: 'Object.is over related class values beside structural union members' is part of the standard library types but has no scriptc lowering yet
stdlib-fence.ts:87:10 - error SC2020: 'Object.is over related class values beside structural union members' is part of the standard library types but has no scriptc lowering yet
96 | function relatedSameValue(a: FenceValue, b: FenceSub): boolean {
97 | return Object.is(a, b);
86 | function relatedSameValue(a: FenceValue, b: FenceSub): boolean {
87 | return Object.is(a, b);
| ^~~~~~~~~~~~~~~
98 | }
88 | }
hint: narrow first: check a discriminant field, or compare with '!== undefined'/'!== null' for unit arms
+2 -2
View File
@@ -12,14 +12,14 @@ tuples.ts:12:6 - error SC1090: tuple indexing with a non-literal index (tuples a
| ^
13 |
tuples.ts:15:7 - error SC2001: values of type '[string, (number | undefined)?]' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, read-only Date values, functions, classes, records, unions of those, and 'unknown')
tuples.ts:15:7 - error SC2001: values of type '[string, (number | undefined)?]' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, Dates, functions, classes, records, unions of those, and 'unknown')
14 | // Optional and rest elements have no fixed arity/shape.
15 | const opt: [string, number?] = ["x"];
| ^~~
16 | const rest: [string, ...number[]] = ["y", 1, 2];
tuples.ts:16:7 - error SC2001: values of type '[string, ...number[]]' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, read-only Date values, functions, classes, records, unions of those, and 'unknown')
tuples.ts:16:7 - error SC2001: values of type '[string, ...number[]]' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, Dates, functions, classes, records, unions of those, and 'unknown')
15 | const opt: [string, number?] = ["x"];
16 | const rest: [string, ...number[]] = ["y", 1, 2];
@@ -1,4 +1,4 @@
union-clone-override-width.ts:8:18 - error SC2001: values of type '{ kind: "left"; left: number; payload: WiderPayload; } | { kind: "right"; right: number; payload: WiderPayload; }' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, read-only Date values, functions, classes, records, unions of those, and 'unknown')
union-clone-override-width.ts:8:18 - error SC2001: values of type '{ kind: "left"; left: number; payload: WiderPayload; } | { kind: "right"; right: number; payload: WiderPayload; }' cannot be compiled yet (supported: number, string, boolean, arrays, Maps, Sets, RegExp, Dates, functions, classes, records, unions of those, and 'unknown')
7 | // copy its record. The inferred spread result keeps the wider payload.
8 | const result = { ...source, payload };
@@ -0,0 +1 @@
{ "plain": 2891, "san": 4454 }
@@ -0,0 +1 @@
{ "plain": 2589, "san": 3385 }
@@ -0,0 +1 @@
{ "plain": 2786, "san": 3724 }
@@ -0,0 +1 @@
{ "plain": 2847, "san": 33492 }
@@ -0,0 +1 @@
{ "plain": 2645, "san": 3529 }
@@ -0,0 +1 @@
{ "plain": 2383, "san": 3249 }
@@ -0,0 +1 @@
{ "plain": 2483, "san": 3294 }