Merge main

This commit is contained in:
Chris Tate
2026-10-22 05:24:24 -05:00
121 changed files with 4083 additions and 442 deletions
+1 -1
View File
@@ -26,7 +26,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea
| `region-builds` | cpu | a watch-mode build loop over `LifetimeRegion`: each build binds a program in a new region and releases the previous one, which a cache keeps referenced for two builds |
| `template-render` | cpu | template literals, escaping, `map`/`join` |
| `thread-slots`, `thread-slots-cache` | cpu | worker threads reading one published type graph from an `InternTable` and keeping per-thread state for each type they visit: a `ThreadSlots` family, or a direct-mapped cache by id in front of a `Map` by id (the `-cache` entry); under Node.js both are per thread |
| `typed-array-kernels` | cpu | element-wise loops that run to one typed array's length and index others at the same position: `Float64Array` axpy, `Float32Array` blend, `Uint8Array` mask, wrapping `Int32Array` add, `Uint8ClampedArray` brighten, RGBA to gray |
| `typed-array-kernels` | cpu | element-wise loops that run to one typed array's length and index others at the same position: `Float64Array` axpy, `Float32Array` blend, `Uint8Array` mask, wrapping `Int32Array` add, `Uint8ClampedArray` brighten, RGBA to gray, and `reduce`/`some`/`map` callbacks |
| `typed-array-scan`, `typed-array-scan-workers` | cpu | element-by-element scan loops over `Uint8Array`, `Uint16Array`, `Int32Array` and `Float64Array`; the second entry is a worker-enabled executable (it can start a worker), so each access also checks for shared storage |
| `unicode-lexer` | cpu | a hand-written lexer over a 2.7M-unit bundled module whose comments and strings are in several languages: `charCodeAt` scans, line starts |
| `validate-errors` | cpu | throw/catch on a fraction of inputs, small call chains |
@@ -21,6 +21,14 @@ function brighten(src: Uint8ClampedArray, gain: number, out: Uint8ClampedArray):
for (let i = 0; i < out.length; i++) out[i] = src[i]! * gain + 8;
}
// Callback methods over a whole typed array.
function callbacks(samples: Float32Array, limit: number, data: Uint8Array): number {
const energy = samples.reduce((sum, v) => sum + v * v, 0);
const loud = samples.some((v) => v > limit) ? 1 : 0;
const halved = data.map((v) => v >> 1);
return energy / samples.length + loud + halved[halved.length >> 1]!;
}
function gray(rgba: Uint8Array, out: Uint8Array): void {
for (let i = 0; i < out.length; i++) {
const j = i * 4;
@@ -73,6 +81,7 @@ let byteSum = 0;
let intSum = 0;
let brightSum = 0;
let lumaSum = 0;
let callbackSum = 0;
for (let r = 0; r < rounds; r++) {
// Change one input element per round so no round repeats the last.
const at = (r * 7919) & (n - 1);
@@ -88,6 +97,7 @@ for (let r = 0; r < rounds; r++) {
addWrap(ia, ib, io);
brighten(pixels, 0.75 + (r & 7) / 8, bright);
gray(rgba, luma);
if ((r & 3) === 0) callbackSum += callbacks(fo, 140 + (r & 12), masked);
const probe = (r * 104729) & (n - 1);
f64Sum += d[probe]!;
f32Sum += fo[probe]!;
@@ -102,3 +112,4 @@ console.log(`uint8 ${byteSum}`);
console.log(`int32 ${intSum}`);
console.log(`clamped ${brightSum}`);
console.log(`gray ${lumaSum}`);
console.log(`callbacks ${callbackSum.toFixed(3)}`);
+2 -1
View File
@@ -40,10 +40,11 @@
"@scriptc/types": "workspace:*",
"@types/node": "^24.0.0",
"effect": "3.22.2",
"js-yaml": "4.3.0",
"js-yaml": "4.3.2",
"oxfmt": "0.71.0",
"oxlint": "1.86.0",
"rollup": "4.62.2",
"smol-toml": "1.7.1",
"three": "0.186.1",
"tsx": "^4.19.0",
"typescript": "5.9.3",
@@ -314,16 +314,19 @@ test("virtual calls admit every same-named method", () => {
expect(new AmbientReceiverReaders(functions).ignores(caller)).toBe(false);
});
test("synthetic array HOF helpers are constant-callback consumers", () => {
const mod = fixture();
const consumer = mod.functions[1]!;
const entry = mod.functions[2]!;
delete consumer.ownsPrototype;
consumer.name = "%arr.some.0";
const call = entry.body[0]!;
if (call.kind === "exprStmt" && call.expr.kind === "call") call.expr.callee = "%arr.some.0";
expect(facts(mod).get("%arr.some.0")?.get("test")).toBe("predicate");
});
test.each(["%arr.some.0", "%bytes.some.0"])(
"synthetic array and typed-array HOF helpers are constant-callback consumers (%s)",
(name) => {
const mod = fixture();
const consumer = mod.functions[1]!;
const entry = mod.functions[2]!;
delete consumer.ownsPrototype;
consumer.name = name;
const call = entry.body[0]!;
if (call.kind === "exprStmt" && call.expr.kind === "call") call.expr.callee = name;
expect(facts(mod).get(name)?.get("test")).toBe("predicate");
},
);
function factory(name: string, returns: IrExpr[]): IrFunction {
return {
@@ -29,12 +29,16 @@ export function findConstantCallbacks(
mod.functions
.filter(
(fn) =>
// Authored declarations, plus the frontend's synthetic array
// HOF loops (`%arr.<method>.<n>`): those are plain direct-call
// functions, and a literal callback gets a helper of its own.
// Authored declarations, plus the frontend's synthetic array and
// typed-array HOF loops (`%arr.<method>.<n>`, `%bytes.<method>.<n>`):
// those are plain direct-call functions, and a literal callback
// gets a helper of its own.
// Generator declarations are entered through their spawn wrapper,
// which moves the same arguments into the generator's frame.
(fn.ownsPrototype || fn.name.startsWith("%arr.") || fn.generator !== undefined) &&
(fn.ownsPrototype ||
fn.name.startsWith("%arr.") ||
fn.name.startsWith("%bytes.") ||
fn.generator !== undefined) &&
(!fn.async || fn.generator !== undefined) &&
!fn.captures &&
!fn.classCaptures &&
@@ -1,4 +1,5 @@
import type { IntegerCountedForLoop } from "../../ir/integer-loops.js";
import type { IntegerCountedForLoop, IntegerPairedForLoop } from "../../ir/integer-loops.js";
import type { IrExpr } from "../../ir/ir.js";
import type { LlvmEmitterContext } from "./expr-context.js";
import { f64Lit } from "./common.js";
@@ -59,37 +60,51 @@ export function emitCountedLoopSafety(
`${safe} = fcmp ${loop.step > 0 ? "ole" : "oge"} double ${bound.name}, ${f64Lit(loop.guardLimit)}`,
);
}
if (loop.startGuarded) {
// An exact integer of at most 2^53 - 1 in magnitude, and not -0
// (the bit patterns of the value and its round trip agree).
// NaN and infinities fail the ordered range tests.
const start = host.emitExpr(loop.start);
const low = B.tmp(),
high = B.tmp(),
raw = B.tmp(),
wide = B.tmp(),
back = B.tmp(),
bits = B.tmp(),
backBits = B.tmp(),
exact = B.tmp(),
inRange = B.tmp(),
ok = B.tmp();
B.line(`${low} = fcmp oge double ${start.name}, ${f64Lit(-Number.MAX_SAFE_INTEGER)}`);
B.line(`${high} = fcmp ole double ${start.name}, ${f64Lit(Number.MAX_SAFE_INTEGER)}`);
B.line(`${inRange} = and i1 ${low}, ${high}`);
B.line(`${raw} = fptosi double ${start.name} to i64`);
B.line(`${wide} = freeze i64 ${raw}`);
B.line(`${back} = sitofp i64 ${wide} to double`);
B.line(`${bits} = bitcast double ${start.name} to i64`);
B.line(`${backBits} = bitcast double ${back} to i64`);
B.line(`${exact} = icmp eq i64 ${bits}, ${backBits}`);
B.line(`${ok} = and i1 ${inRange}, ${exact}`);
if (safe === "true") safe = ok;
else {
const both = B.tmp();
B.line(`${both} = and i1 ${safe}, ${ok}`);
safe = both;
}
}
if (loop.startGuarded) safe = andSafe(host, safe, emitExactStart(host, loop.start));
return safe;
}
/** The run-time test of a guarded paired loop: each unproven start is an
* exact safe integer. */
export function emitPairedLoopSafety(host: LlvmEmitterContext, loop: IntegerPairedForLoop): string {
let safe = "true";
for (const counter of [loop.low, loop.high])
if (counter.startGuarded) safe = andSafe(host, safe, emitExactStart(host, counter.start));
return safe;
}
function andSafe(host: LlvmEmitterContext, safe: string, ok: string): string {
if (safe === "true") return ok;
const both = host.B.tmp();
host.B.line(`${both} = and i1 ${safe}, ${ok}`);
return both;
}
/** An exact integer of at most 2^53 - 1 in magnitude, and not -0 (the bit
* patterns of the value and its round trip agree). NaN and infinities fail
* the ordered range tests. */
function emitExactStart(host: LlvmEmitterContext, expr: IrExpr): string {
const B = host.B;
const start = host.emitExpr(expr);
const low = B.tmp(),
high = B.tmp(),
raw = B.tmp(),
wide = B.tmp(),
back = B.tmp(),
bits = B.tmp(),
backBits = B.tmp(),
exact = B.tmp(),
inRange = B.tmp(),
ok = B.tmp();
B.line(`${low} = fcmp oge double ${start.name}, ${f64Lit(-Number.MAX_SAFE_INTEGER)}`);
B.line(`${high} = fcmp ole double ${start.name}, ${f64Lit(Number.MAX_SAFE_INTEGER)}`);
B.line(`${inRange} = and i1 ${low}, ${high}`);
B.line(`${raw} = fptosi double ${start.name} to i64`);
B.line(`${wide} = freeze i64 ${raw}`);
B.line(`${back} = sitofp i64 ${wide} to double`);
B.line(`${bits} = bitcast double ${start.name} to i64`);
B.line(`${backBits} = bitcast double ${back} to i64`);
B.line(`${exact} = icmp eq i64 ${bits}, ${backBits}`);
B.line(`${ok} = and i1 ${inRange}, ${exact}`);
return ok;
}
@@ -42,10 +42,15 @@ import {
matchIntegerArrayForLoop,
matchIntegerBytesForLoop,
matchIntegerCountedForLoop,
matchIntegerPairedForLoop,
} from "../../ir/integer-loops.js";
import { emitByteWindowGuard, matchByteWindow } from "./byte-windows.js";
import { countedDenseShape, emitDenseLoopGuard, matchDenseArrayLoop } from "./dense-array-loops.js";
import { emitCountedLoopLimit, emitCountedLoopSafety } from "./counted-loops.js";
import {
emitCountedLoopLimit,
emitCountedLoopSafety,
emitPairedLoopSafety,
} from "./counted-loops.js";
import { emitBytesLoopVersionGuard } from "./bytes-loop-versions.js";
import { mangleLocal } from "../mangle.js";
import { LlvmUnsupportedError } from "./unsupported.js";
@@ -868,6 +873,12 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
emitter.unaliasedArrays,
)
: null;
// Two counters closing in on each other (`lo++, hi--`) take the same
// integer storage as a counted loop's one.
const pairedLoop =
!integerLoop && !countedLoop && emitter.countedLoopsEnabled
? matchIntegerPairedForLoop(s, emitter.numericLocals, emitter.integerRanges)
: null;
const window =
countedLoop && emitter.byteWindowsEnabled
? matchByteWindow(
@@ -1000,8 +1011,10 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
break;
}
let countedJoin: string | null = null;
if (countedLoop?.guarded && !emitter.provenCountedGuards.has(s)) {
const safe = emitCountedLoopSafety(emitter, countedLoop);
if ((countedLoop?.guarded && !emitter.provenCountedGuards.has(s)) || pairedLoop?.guarded) {
const safe = countedLoop
? emitCountedLoopSafety(emitter, countedLoop)
: emitPairedLoopSafety(emitter, pairedLoop!);
const fast = B.newLabel("counted.fast");
const slow = B.newLabel("counted.slow");
countedJoin = B.newLabel("counted.done");
@@ -1016,6 +1029,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
emitter.scopes.push([]);
let integerSlot: string | null = null;
let countedLimit: string | null = null;
const pairedSlots: string[] = [];
if (integerLoop) {
integerSlot = B.slot();
B.entryAllocas.push(
@@ -1060,6 +1074,24 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
: countedLoop.range,
});
countedLimit = emitCountedLoopLimit(emitter, countedLoop);
} else if (pairedLoop) {
for (const counter of [pairedLoop.low, pairedLoop.high]) {
const slot = B.slot();
B.entryAllocas.push(
`${slot} = alloca i64 ; integer induction ${emitter.currentLocals.get(counter.localId)!.name}`,
);
const value = emitter.emitExpr(counter.start);
const start = B.tmp();
B.line(`${start} = fptosi double ${value.name} to i64`);
B.line(`store i64 ${start}, ptr ${slot}`);
pairedSlots.push(slot);
emitter.integerLoopBindings.set(counter.localId, {
slot,
type: "i64",
signed: true,
range: pairedLoop.range,
});
}
} else if (s.init) {
// A multi-declarator head shares the loop's scope. Emitting its
// IR block as an ordinary block would release captured/ref locals
@@ -1141,6 +1173,14 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
`${inBounds} = icmp ${countedLoop.step > 0 ? "slt" : "sgt"} i64 ${index}, ${countedLimit}`,
);
B.condBr(inBounds, lb, exit);
} else if (pairedLoop) {
const low = B.tmp(),
high = B.tmp(),
open = B.tmp();
B.line(`${low} = load i64, ptr ${pairedSlots[0]}`);
B.line(`${high} = load i64, ptr ${pairedSlots[1]}`);
B.line(`${open} = icmp ${pairedLoop.inclusive ? "sle" : "slt"} i64 ${low}, ${high}`);
B.condBr(open, lb, exit);
} else if (s.cond) B.condBr(emitter.emitCondition(s.cond), lb, exit);
else B.br(lb);
B.startBlock(lb);
@@ -1172,6 +1212,14 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
B.line(`${old} = load i64, ptr ${integerSlot}`);
B.line(`${next} = add nsw i64 ${old}, ${countedLoop.step}`);
B.line(`store i64 ${next}, ptr ${integerSlot}`);
} else if (pairedLoop) {
[pairedLoop.low, pairedLoop.high].forEach((counter, i) => {
const old = B.tmp();
const next = B.tmp();
B.line(`${old} = load i64, ptr ${pairedSlots[i]}`);
B.line(`${next} = add nsw i64 ${old}, ${counter.step}`);
B.line(`store i64 ${next}, ptr ${pairedSlots[i]}`);
});
} else if (s.update) emitter.emitStmt(s.update);
B.br(lc);
}
@@ -1180,6 +1228,10 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
emitter.releaseScope(emitter.scopes.pop()!);
if (integerLoop) emitter.integerLoopBindings.delete(integerLoop.localId);
if (countedLoop) emitter.integerLoopBindings.delete(countedLoop.localId);
if (pairedLoop) {
emitter.integerLoopBindings.delete(pairedLoop.low.localId);
emitter.integerLoopBindings.delete(pairedLoop.high.localId);
}
if (integerArrayLoop) emitter.integerArrayBindings.delete(integerArrayLoop.localId);
if (countedJoin) {
B.br(countedJoin);
@@ -1008,12 +1008,15 @@ export const LIB_FN_SYMS: Record<string, string> = {
"dyn.nativeSetIs": "scr_dyn_native_set_is",
"dyn.nativeMapIs": "scr_dyn_native_map_is",
"dyn.nativeUrlIs": "scr_dyn_native_url_is",
"dyn.nativeDateIs": "scr_dyn_native_date_is",
"dyn.nativeDateIs": "scr_dyn_date_instance_is",
"dyn.nativeDateHandleIs": "scr_dyn_native_date_is",
"dyn.globalConstructor": "scr_dyn_global_constructor",
"dyn.builtinFunction": "scr_dyn_builtin_function",
"date.nativeNew": "scr_dyn_native_date_new",
"date.nativeNewFields": "scr_dyn_native_date_new_fields",
"date.nativeNewValue": "scr_dyn_native_date_new_value",
"date.argumentsTime": "scr_dyn_date_arguments_time",
"date.classOperation": "scr_date_class_operation_install",
"date.checkedValue": "scr_dyn_native_date_value",
"dyn.nativeRegexIs": "scr_dyn_native_regex_is",
"dyn.toString": "scr_dyn_to_string_argument",
@@ -1492,6 +1495,10 @@ export const LIB_FN_SYMS: Record<string, string> = {
"dyn.arrayPrototype": "scr_dyn_array_prototype",
"dyn.arrayConstructor": "scr_dyn_array_constructor",
"dyn.objectPrototype": "scr_dyn_object_prototype",
"dyn.errorPrototype": "scr_dyn_error_prototype",
"dyn.inheritsErrorPrototype": "scr_dyn_inherits_error_prototype",
"dyn.errorCaptureStackTrace": "scr_dyn_error_capture_stack_trace",
"dyn.errorCaptureStackTraceFunction": "scr_dyn_error_capture_stack_trace_function",
"dyn.functionApply": "scr_dyn_function_apply",
"dyn.builtinMethod": "scr_dyn_builtin_method",
"dyn.classPrototype": "scr_dyn_class_prototype",
@@ -0,0 +1,552 @@
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 { dynUndefinedExpr, varRef } from "../../../ir/build.js";
import {
BOOL,
DATE_T,
DYN,
F64,
NULL_T,
STRING,
VOID,
typeKey,
type IrExpr,
type IrStmt,
type IrType,
type SrcLoc,
} from "../../../ir/ir.js";
/** The hidden field holding the time value of a class declared `extends
* Date`, the first field of its layout. */
export const DATE_TIME_FIELD = "%time";
/** The class declared `extends Date` at the root of `info`'s base chain, or
* null. */
export function dateRootOf(info: ClassInfo | null | undefined): ClassInfo | null {
for (let c = info ?? null; c; c = c.base) if (c.extendsDate) return c;
return null;
}
/** Whether the program declares a class that extends Date. */
export function programDeclaresDateClass(lowerer: Lowerer): boolean {
for (const info of lowerer.classes.values()) if (info.extendsDate) return true;
return false;
}
/** The Date.prototype methods a class declared `extends Date` inherits.
* Those without a lowering refuse at the call. */
const DATE_PROTOTYPE_METHODS: ReadonlySet<string> = new Set([
"getTime",
"valueOf",
"toISOString",
"toUTCString",
"toJSON",
"toString",
"toDateString",
"toTimeString",
"toGMTString",
"toLocaleString",
"toLocaleDateString",
"toLocaleTimeString",
"getFullYear",
"getUTCFullYear",
"getYear",
"getMonth",
"getUTCMonth",
"getDate",
"getUTCDate",
"getDay",
"getUTCDay",
"getHours",
"getUTCHours",
"getMinutes",
"getUTCMinutes",
"getSeconds",
"getUTCSeconds",
"getMilliseconds",
"getUTCMilliseconds",
"getTimezoneOffset",
"setTime",
"setFullYear",
"setUTCFullYear",
"setYear",
"setMonth",
"setUTCMonth",
"setDate",
"setUTCDate",
"setHours",
"setUTCHours",
"setMinutes",
"setUTCMinutes",
"setSeconds",
"setUTCSeconds",
"setMilliseconds",
"setUTCMilliseconds",
]);
export const DATE_PROTOTYPE_METHOD_NAMES: readonly string[] = [...DATE_PROTOTYPE_METHODS];
export function isDatePrototypeMethod(name: string): boolean {
return DATE_PROTOTYPE_METHODS.has(name);
}
/** Whether `name` is a Date method an instance of `info` inherits from
* Date.prototype: its class chain is rooted at a class declared `extends
* Date` and declares no member of that name. The runtime's view of the
* instance answers it (scr_dyn_date_class_method). */
export function dateInheritedMember(lowerer: Lowerer, info: ClassInfo, name: string): boolean {
return (
dateRootOf(info) !== null &&
DATE_PROTOTYPE_METHODS.has(name) &&
!lowerer.findMethodOn(info, name) &&
!info.fields.has(name)
);
}
/** super(...args) in a class declared `extends Date`: the time value the
* Date constructor computes from the argument vector `args` (one dyn
* array), stored in the hidden field. */
export function dateSuperCall(thisRef: IrExpr, info: ClassInfo, args: IrExpr, loc: SrcLoc): IrStmt {
return {
kind: "fieldSet",
obj: thisRef,
className: info.def.name,
field: DATE_TIME_FIELD,
value: { kind: "libCall", fn: "date.argumentsTime", args: [args], type: DATE_T, loc },
loc,
};
}
/** The time value of `receiver`, an instance of a class rooted at `root`. */
function timeOf(lowerer: Lowerer, receiver: IrExpr, root: ClassInfo, loc: SrcLoc): IrExpr {
return {
kind: "fieldGet",
obj: lowerer.upcastTo(receiver, root.def.name),
className: root.def.name,
field: DATE_TIME_FIELD,
type: DATE_T,
loc,
};
}
/** `receiver.name()` with no arguments where the static class `info` does
* not declare `name`: the subclasses of `info` that declare it run theirs,
* any other instance the Date method. `receiver` is a variable read. */
function instanceMethodCall(
lowerer: Lowerer,
blame: ts.Node,
info: ClassInfo,
receiver: IrExpr,
name: string,
inherited: IrExpr,
loc: SrcLoc,
): IrExpr {
const found = lowerer.findMethodOn(info, name);
if (found) {
if (found.sig.params.length > 0)
lowerer.unsupported(
"SC1090",
blame,
`a '${name}' declaration with parameters in a class that extends Date, called by an inherited Date method`,
);
const args: IrExpr[] = [];
if (lowerer.overrideBelow(info, name)) {
lowerer.noteVirtualEdge(info, name);
return {
kind: "virtualCall",
className: info.def.name,
method: name,
args: [lowerer.upcastTo(receiver, info.def.name), ...args],
type: found.sig.ret,
loc,
};
}
lowerer.noteEdge(`%${found.declarer.def.name}.${name}`);
return {
kind: "call",
callee: `%${found.declarer.def.name}.${name}`,
args: [lowerer.upcastTo(receiver, found.declarer.def.name), ...args],
type: found.sig.ret,
loc,
};
}
let result = inherited;
// Deeper declarations are tested first.
const declaring: ClassInfo[] = [];
const visit = (c: ClassInfo): void => {
for (const sub of c.subclasses) {
if (sub.methods.has(name)) declaring.push(sub);
else visit(sub);
}
};
visit(info);
for (const sub of declaring) {
const call = instanceMethodCall(
lowerer,
blame,
sub,
{ kind: "downcast", value: receiver, type: { kind: "object", className: sub.def.name }, loc },
name,
inherited,
loc,
);
const type = result.type;
result = {
kind: "ternary",
cond: { kind: "instanceOf", value: receiver, className: sub.def.name, type: BOOL, loc },
then: lowerer.coerceToExpected(call, type),
else_: result,
type,
loc,
};
}
return result;
}
/** ToNumber of `value`, the lowered value of `node`. */
function numberValue(lowerer: Lowerer, node: ts.Expression, value: IrExpr, loc: SrcLoc): IrExpr {
if (value.type.kind === "f64") return value;
if (value.type.kind === "dyn")
return { kind: "libCall", fn: "dyn.toNumberCoerce", args: [value], type: F64, loc };
if (lowerer.dynConvertible(value.type))
return {
kind: "libCall",
fn: "dyn.toNumberCoerce",
args: [{ kind: "dynFrom", value, type: DYN, loc }],
type: F64,
loc,
};
return lowerer.coerceInto(node, value, F64);
}
/** A Date.prototype method a class rooted at `root` does not declare,
* called on `receiver` (an instance of `info`, evaluated here once). The
* method reads or sets the hidden time value; toJSON calls the instance's
* toISOString, as Date.prototype.toJSON does. */
export function lowerInheritedDateMethodCall(
lowerer: Lowerer,
call: ts.CallExpression,
name: string,
info: ClassInfo,
receiver: IrExpr,
): IrExpr {
const root = dateRootOf(info)!;
const loc = locOf(call);
const local = lowerer.declareHiddenLocal("%dateReceiver", receiver.type);
const value = varRef(local.id, receiver.type, loc);
const stmts: IrStmt[] = [{ kind: "varDecl", localId: local.id, init: receiver, loc }];
const args = call.arguments.nodes;
if (args.some(ts.isSpreadElement))
lowerer.unsupported("SC1090", call, `spread arguments to 'Date.prototype.${name}'`);
const done = (result: IrExpr): IrExpr => ({
kind: "seqExpr",
stmts,
result,
type: result.type,
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 };
if (args[0]) {
const first = lowerer.lowerExpr(args[0]);
const saved = lowerer.declareHiddenLocal("%dateArgument", first.type);
stmts.push({ kind: "varDecl", localId: saved.id, init: first, loc: locOf(args[0]) });
ms = numberValue(lowerer, args[0], varRef(saved.id, first.type, loc), loc);
}
for (const arg of args.slice(1))
stmts.push({ kind: "exprStmt", expr: lowerer.lowerExpr(arg), loc: locOf(arg) });
const time = lowerer.declareHiddenLocal("%dateTime", DATE_T);
stmts.push(
{
kind: "varDecl",
localId: time.id,
init: { kind: "libCall", fn: "date.newMs", args: [ms], type: DATE_T, loc },
loc,
},
{
kind: "fieldSet",
obj: lowerer.upcastTo(value, root.def.name),
className: root.def.name,
field: DATE_TIME_FIELD,
value: varRef(time.id, DATE_T, loc),
loc,
},
);
return done({
kind: "libCall",
fn: "date.getTime",
args: [varRef(time.id, DATE_T, loc)],
type: F64,
loc,
});
}
for (const arg of args) {
if (name !== "toJSON")
lowerer.noLowering(`Date.prototype.${name} with arguments`, call, DATE_METHOD_HINT);
stmts.push({ kind: "exprStmt", expr: lowerer.lowerExpr(arg), loc: locOf(arg) });
}
if (name === "toJSON") return done(dateClassToJson(lowerer, call, info, value, loc));
const result = dateValueMethod(name, timeOf(lowerer, value, root, loc), loc);
if (!result) lowerer.noLowering(`Date.prototype.${name}`, call, DATE_METHOD_HINT);
return done(result);
}
/** Date.prototype.toJSON on `value` (a variable read of an instance of
* `info`): null for an invalid time, else the instance's toISOString(). */
function dateClassToJson(
lowerer: Lowerer,
blame: ts.Node,
info: ClassInfo,
value: IrExpr,
loc: SrcLoc,
): IrExpr {
const root = dateRootOf(info)!;
const time: IrExpr = {
kind: "libCall",
fn: "date.getTime",
args: [timeOf(lowerer, value, root, loc)],
type: F64,
loc,
};
const type = dateJsonType(lowerer);
const iso = instanceMethodCall(
lowerer,
blame,
info,
value,
"toISOString",
{
kind: "libCall",
fn: "date.toISOStringValue",
args: [timeOf(lowerer, value, root, loc)],
type: STRING,
loc,
},
loc,
);
return {
kind: "ternary",
// An invalid date's time value is NaN, the one value unequal to itself.
cond: { kind: "bin", op: "!==", left: time, right: time, type: BOOL, loc },
then: lowerer.coerceToExpected({ kind: "unitLit", unit: "null", type: NULL_T, loc }, type),
else_: lowerer.coerceToExpected(iso, type),
type,
loc,
};
}
/** `super.name(...)` in a class whose base chain declares no `name`, when
* the chain is rooted at a class declared `extends Date`. */
export function lowerDateSuperMethodCall(
lowerer: Lowerer,
call: ts.CallExpression,
name: string,
cls: ClassInfo,
thisRef: IrExpr,
): IrExpr | null {
if (!dateRootOf(cls) || !isDatePrototypeMethod(name)) return null;
// Date.prototype.toJSON reads this.toISOString, which this class or a
// subclass may declare.
return lowerInheritedDateMethodCall(lowerer, call, name, cls, thisRef);
}
const DATE_CLASS_TIME = "%date.classTime";
const DATE_CLASS_METHOD = "%date.classMethod";
/** The classes of a program rooted at a class declared `extends Date`. */
function dateClasses(lowerer: Lowerer): ClassInfo[] {
return [...lowerer.classes.values()].filter(
(info) => !info.def.runtime && !info.generic && dateRootOf(info) !== null,
);
}
const PARAMS = ["op", "value", "argument"];
/** A lifted `(op, value, argument)` function over checked-dynamic values. */
function liftOperation(lowerer: Lowerer, name: string, body: IrStmt[], loc: SrcLoc): void {
lowerer.liftedFns.push({
name,
params: PARAMS.map((p) => ({ localId: p, name: p, type: DYN })),
returnType: DYN,
locals: PARAMS.map((p) => ({ id: p, name: p, type: DYN, mutable: false })),
body,
loc,
});
}
function opIs(code: number, loc: SrcLoc): IrExpr {
return {
kind: "bin",
op: "===",
left: { kind: "dynCheck", value: varRef("op", DYN, loc), type: F64, loc },
right: { kind: "numLit", value: code, type: F64, loc },
type: BOOL,
loc,
};
}
/** Lifts `%date.classMethod(op, value, argument)` when the program declares
* a class that extends Date: ops 2 to 5 call `value`'s toISOString,
* toJSON(argument), toString and valueOf as checked-dynamic method calls,
* which the class dispatch resolves to each class's declarations and the
* runtime to the inherited Date methods. Called before the class dispatch
* runs over the lowered functions. */
export function noteDateClassMethods(lowerer: Lowerer): void {
if (lowerer.liftedFnNamed(DATE_CLASS_METHOD)) return;
const classes = dateClasses(lowerer);
if (classes.length === 0) return;
const loc = classes[0]!.def.loc;
const value = varRef("value", DYN, loc);
const body: IrStmt[] = ["toISOString", "toJSON", "toString", "valueOf"].map(
(method, index): IrStmt => ({
kind: "if",
cond: opIs(index + 2, loc),
then: [
{
kind: "return",
value: {
kind: "dynInvoke",
recv: value,
method,
calleeName: method,
args: method === "toJSON" ? [varRef("argument", DYN, loc)] : [],
type: DYN,
loc,
},
loc,
},
],
else_: null,
loc,
}),
);
body.push({ kind: "return", value: dynUndefinedExpr(loc), loc });
liftOperation(lowerer, DATE_CLASS_METHOD, body, loc);
}
/** %main's installation of the functions the runtime calls for a
* checked-dynamic value that holds an instance of a class declared `extends
* Date` (scr_date_class_operation_install). `%date.classTime(op, value,
* argument)` answers the time value for op 0 (undefined for any other
* value), sets it from the number `argument` for op 1 and answers the
* class's name for op 6; `%date.classMethod` (noteDateClassMethods) calls
* its methods. */
export function dateClassOperationPrelude(lowerer: Lowerer, loc: SrcLoc): IrStmt[] {
const classes = dateClasses(lowerer);
if (classes.length === 0) return [];
noteDateClassMethods(lowerer);
const value = varRef("value", DYN, loc);
const argument = varRef("argument", DYN, loc);
const body: IrStmt[] = [];
for (const info of classes) {
const root = dateRootOf(info)!;
const type: IrType = { kind: "object", className: info.def.name };
const instance: IrExpr = { kind: "dynCheck", value, type, loc };
body.push({
kind: "if",
cond: {
kind: "libCall",
fn: "dyn.typedRefIs",
args: [value, { kind: "strLit", value: typeKey(type), type: STRING, loc }],
type: BOOL,
loc,
},
then: [
{
kind: "if",
cond: opIs(6, loc),
then: [
{
kind: "return",
value: {
kind: "dynFrom",
value: {
kind: "strLit",
value: info.def.jsName ?? info.def.name,
type: STRING,
loc,
},
type: DYN,
loc,
},
loc,
},
],
else_: null,
loc,
},
{
kind: "if",
cond: opIs(1, loc),
then: [
{
kind: "fieldSet",
obj: lowerer.upcastTo(instance, root.def.name),
className: root.def.name,
field: DATE_TIME_FIELD,
value: {
kind: "libCall",
fn: "date.newMs",
args: [{ kind: "dynCheck", value: argument, type: F64, loc }],
type: DATE_T,
loc,
},
loc,
},
],
else_: null,
loc,
},
{
kind: "return",
value: {
kind: "dynFrom",
value: {
kind: "libCall",
fn: "date.getTime",
args: [timeOf(lowerer, instance, root, loc)],
type: F64,
loc,
},
type: DYN,
loc,
},
loc,
},
],
else_: null,
loc,
});
}
body.push({ kind: "return", value: dynUndefinedExpr(loc), loc });
liftOperation(lowerer, DATE_CLASS_TIME, body, loc);
const closure = (fnName: string): IrExpr => ({
kind: "dynFrom",
value: {
kind: "closure",
fnName,
captures: [],
type: { kind: "func", params: [DYN, DYN, DYN], ret: DYN },
loc,
},
type: DYN,
loc,
});
return [
{
kind: "exprStmt",
expr: {
kind: "libCall",
fn: "date.classOperation",
args: [closure(DATE_CLASS_TIME), closure(DATE_CLASS_METHOD)],
type: VOID,
loc,
},
loc,
},
];
}
@@ -1,6 +1,8 @@
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 { programDeclaresDateClass } from "./date-classes.js";
import {
BOOL,
F64,
@@ -9,6 +11,7 @@ import {
type IrStmt,
type IrType,
NULL_T,
type SrcLoc,
STRING,
typeKey,
} from "../../../ir/ir.js";
@@ -39,7 +42,7 @@ const DATE_TEXT_FNS: ReadonlyMap<string, IrLibFn> = new Map([
["toTimeString", "date.toTimeString"],
]);
const DATE_METHOD_HINT =
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";
/** The Date slice: statics plus read-only Date values backed by one
@@ -52,7 +55,52 @@ export function lowerDateCall(
): IrExpr | null {
if (call.questionDotToken || access.questionDotToken) return null;
const loc = locOf(call);
if (lowerer.stdlibGlobalMember(access, "Date") === "now") {
const member = lowerer.stdlibGlobalMember(access, "Date");
if (member === "now" || member === "parse" || member === "UTC")
return lowerDateStaticCall(lowerer, call, member);
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
// 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)) &&
tryLowerExpression(lowerer, access.expression)?.type.kind === "dyn"
)
return null;
if (name === "toJSON") return lowerDateToJson(lowerer, call, access);
if (call.arguments.nodes.length !== 0) {
lowerer.noLowering(`Date.prototype.${name} with arguments`, call, DATE_METHOD_HINT);
}
const raw = lowerer.lowerExpr(access.expression);
const receiver: IrExpr =
raw.type.kind === "dyn"
? { kind: "libCall", fn: "date.checkedValue", args: [raw], type: { kind: "date" }, loc }
: raw;
if (receiver.type.kind !== "date")
lowerer.badType(access.expression, lowerer.typeOf(access.expression));
const result = dateValueMethod(name, receiver, loc);
if (result) return result;
lowerer.noLowering(
`Date.prototype.${name}`,
call,
DATE_METHOD_HINT,
lowerer.checker.getSymbolAtLocation(access.name),
);
}
/** Date.now(), Date.parse(text) and Date.UTC(...), also called through a
* class declared `extends Date`, which inherits them. Null for other
* names. */
export function lowerDateStaticCall(
lowerer: Lowerer,
call: ts.CallExpression,
name: string,
): IrExpr | null {
const loc = locOf(call);
if (name === "now") {
if (call.arguments.nodes.length !== 0) {
lowerer.noLowering(`Date.now with ${call.arguments.nodes.length} arguments`, call);
}
@@ -60,7 +108,7 @@ export function lowerDateCall(
}
// Date.parse uses the same date-string parser as new Date(dateString)
// (V8's grammar in the runtime), so their NaN and TimeClip behavior agree.
if (lowerer.stdlibGlobalMember(access, "Date") === "parse") {
if (name === "parse") {
if (call.arguments.nodes.length !== 1 || ts.isSpreadElement(call.arguments.nodes[0]!)) {
lowerer.noLowering(`Date.parse with ${call.arguments.nodes.length} arguments`, call);
}
@@ -72,7 +120,7 @@ export function lowerDateCall(
// TimeClip. Omitted trailing arguments complete with the spec's
// defaults (month 0, date 1, time parts 0); tsc pins every present
// argument to number, so the seven-f64 ABI is exact.
if (lowerer.stdlibGlobalMember(access, "Date") === "UTC") {
if (name === "UTC") {
if (
call.arguments.nodes.length < 1 ||
call.arguments.nodes.length > 7 ||
@@ -92,49 +140,38 @@ export function lowerDateCall(
}
return { kind: "libCall", fn: "date.utc", args, type: F64, loc };
}
if (lowerer.mapTypeOf(lowerer.typeOf(access.expression))?.kind !== "date") return null;
if (!lowerer.isStdlibMember(access)) return null;
const name = access.name.text;
if (name === "toJSON") return lowerDateToJson(lowerer, call, access);
if (call.arguments.nodes.length !== 0) {
lowerer.noLowering(`Date.prototype.${name} with arguments`, call, DATE_METHOD_HINT);
}
const raw = lowerer.lowerExpr(access.expression);
const receiver: IrExpr =
raw.type.kind === "dyn"
? { kind: "libCall", fn: "date.checkedValue", args: [raw], type: { kind: "date" }, loc }
: raw;
if (receiver.type.kind !== "date")
lowerer.badType(access.expression, lowerer.typeOf(access.expression));
return null;
}
/** The Date methods without arguments that have a lowering, applied to the
* time value `value` (a `date`): getTime, valueOf, toISOString,
* toUTCString, the calendar getters and the text forms. Null for other
* names. */
export function dateValueMethod(name: string, value: IrExpr, loc: SrcLoc): IrExpr | null {
if (name === "getTime" || name === "valueOf") {
return {
kind: "libCall",
fn: name === "getTime" ? "date.getTime" : "date.valueOf",
args: [receiver],
args: [value],
type: F64,
loc,
};
}
if (name === "toISOString") {
return { kind: "libCall", fn: "date.toISOStringValue", args: [receiver], type: STRING, loc };
return { kind: "libCall", fn: "date.toISOStringValue", args: [value], type: STRING, loc };
}
if (name === "toUTCString") {
return { kind: "libCall", fn: "date.toUTCString", args: [receiver], type: STRING, loc };
return { kind: "libCall", fn: "date.toUTCString", args: [value], type: STRING, loc };
}
const fn = own(DATE_GETTER_FNS, name);
if (fn !== undefined) {
return { kind: "libCall", fn, args: [receiver], type: F64, loc };
return { kind: "libCall", fn, args: [value], type: F64, loc };
}
const textFn = DATE_TEXT_FNS.get(name);
if (textFn !== undefined) {
return { kind: "libCall", fn: textFn, args: [receiver], type: STRING, loc };
return { kind: "libCall", fn: textFn, args: [value], type: STRING, loc };
}
lowerer.noLowering(
`Date.prototype.${name}`,
call,
DATE_METHOD_HINT,
lowerer.checker.getSymbolAtLocation(access.name),
);
return null;
}
/** `d.toJSON(key?)`: Node answers null for an invalid date and the
@@ -1,6 +1,8 @@
import * as ts from "../../ts7/adapter.js";
import { type Lowerer } from "../lowerer.js";
import { locOf } from "../../program.js";
import { tryLowerExpression } from "../expressions/try-lower-expression.js";
import { dynUndefinedExpr } from "../../../ir/build.js";
import {
BOOL,
CAUGHT,
@@ -8,6 +10,7 @@ import {
DYN_CLASS_PROPERTIES,
F64,
type IrExpr,
type IrStmt,
type IrType,
STRING,
} from "../../../ir/ir.js";
@@ -267,6 +270,114 @@ function isErrorClassType(lowerer: Lowerer, type: IrType): boolean {
);
}
/** Error classes that construct when called without `new`. */
const CALLABLE_ERROR_CLASSES: ReadonlySet<string> = new Set([
"%Error",
"%TypeError",
"%RangeError",
"%SyntaxError",
"%ReferenceError",
"%EvalError",
"%URIError",
]);
/** `Error(message?, options?)` and `Error.call(thisArg, message?, options?)`
* (an ES5 constructor function's `Error.call(this)`), and the same for the
* other standard error classes: called as a function, the constructor
* returns a new error and ignores the receiver. `thisArg` is evaluated
* first. Null for other callees. */
export function lowerErrorFunctionCall(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null {
if (call.questionDotToken) return null;
let callee = call.expression;
let args = call.arguments.nodes;
let thisArg: ts.Expression | undefined;
const viaCall =
ts.isPropertyAccessExpression(callee) &&
!callee.questionDotToken &&
callee.name.text === "call" &&
ts.isIdentifier(callee.expression);
if (viaCall) {
callee = (callee as ts.PropertyAccessExpression).expression;
thisArg = args[0];
args = args.slice(1);
}
if (!ts.isIdentifier(callee) || args.some(ts.isSpreadElement)) return null;
if (thisArg && ts.isSpreadElement(thisArg)) return null;
const info = lowerer.builtinErrorInfoOf(lowerer.checker.getSymbolAtLocation(callee));
if (!info || !CALLABLE_ERROR_CLASSES.has(info.def.name)) return null;
const loc = locOf(call);
const stmts: IrStmt[] = [];
if (thisArg) {
const receiver = lowerer.lowerExpr(thisArg);
if (receiver.kind !== "varRef" && receiver.kind !== "unitLit")
stmts.push({ kind: "exprStmt", expr: receiver, loc: receiver.loc });
}
const error: IrExpr = {
kind: "libCall",
fn: "error.newOptions",
args: lowerer.errorConstructorArgs(args, loc, call),
type: { kind: "object", className: info.def.name },
loc,
};
return stmts.length > 0
? { kind: "seqExpr", stmts, result: error, type: error.type, loc }
: error;
}
/** `Error.captureStackTrace(target[, constructorOpt])` (also called as
* `Error.captureStackTrace?.(...)`, which always calls it). On a
* checked-dynamic target (`this` in a JavaScript constructor function) the
* runtime defines target's `stack`. An instance of a class that extends
* Error derives its stack from its name and message when the stack is
* read, so the call only evaluates its arguments. Both arguments are
* evaluated; the second only selects frames in Node. Null for other
* targets and callees. */
export function lowerErrorCaptureStackTraceCall(
lowerer: Lowerer,
call: ts.CallExpression,
access: ts.PropertyAccessExpression,
): IrExpr | null {
if (access.questionDotToken) return null;
if (lowerer.stdlibGlobalMember(access, "Error") !== "captureStackTrace") return null;
const args = call.arguments.nodes;
if (args.length < 1 || args.length > 2 || args.some(ts.isSpreadElement)) return null;
const probed = tryLowerExpression(lowerer, args[0]!);
const loc = locOf(call);
if (
probed?.type.kind === "object" &&
(probed.type.className === "%Error" || lowerer.isSubclassOf(probed.type.className, "%Error"))
) {
const stmts: IrStmt[] = [];
for (const arg of args) {
const value = lowerer.lowerExpr(arg);
if (value.kind !== "varRef") stmts.push({ kind: "exprStmt", expr: value, loc: value.loc });
}
const result = dynUndefinedExpr(loc);
return stmts.length > 0 ? { kind: "seqExpr", stmts, result, type: DYN, loc } : result;
}
if (probed?.type.kind !== "dyn") return null;
const target = lowerer.lowerExpr(args[0]!);
const result: IrExpr = {
kind: "libCall",
fn: "dyn.errorCaptureStackTrace",
args: [target],
type: DYN,
loc,
};
if (args.length === 1) return result;
const local = lowerer.declareHiddenLocal("%captureTarget", DYN);
return {
kind: "seqExpr",
stmts: [
{ kind: "varDecl", localId: local.id, init: target, loc },
{ kind: "exprStmt", expr: lowerer.lowerExprExpecting(args[1]!, DYN), loc },
],
result: { ...result, args: [{ kind: "varRef", localId: local.id, type: DYN, loc }] },
type: DYN,
loc,
};
}
/** `Error.isError(value)`. A catch binding tests its payload against the
* Error class, an `unknown` value tests the checked-dynamic tree's error
* encoding, and a union tests its runtime tag against the arms whose class
@@ -2,8 +2,17 @@ import { primitiveConstructorOr } from "./lower-exprs.js";
import { dynUndefinedExpr, helperLoc, nodeThrowExpr, varRef } from "../../ir/build.js";
import { constructedValue } from "./constructor-returns.js";
import { eventTargetApiMembers, eventTargetInheritedMember } from "./lower-event-target.js";
import {
DATE_PROTOTYPE_METHOD_NAMES,
DATE_TIME_FIELD,
dateInheritedMember,
dateRootOf,
programDeclaresDateClass,
} from "./builtins/date-classes.js";
import { dateValueMethod } from "./builtins/dates.js";
import {
BOOL,
DATE_T,
DYN,
F64,
DYN_CLASS_PROPERTIES as PROPERTY_BAG,
@@ -296,6 +305,32 @@ export function classInstanceOf(
);
const body: IrStmt[] = prototype
? [
// In a program that declares a class extending Date, a Date can
// meet a class test (`date instanceof Moment`): its prototype
// chain holds no class prototype.
...(programDeclaresDateClass(lowerer)
? [
{
kind: "if" as const,
cond: {
kind: "libCall" as const,
fn: "dyn.nativeDateHandleIs" as const,
args: [varRef("value", DYN, loc)],
type: BOOL,
loc,
},
then: [
{
kind: "return" as const,
value: { kind: "boolLit" as const, value: false, type: BOOL, loc },
loc,
},
],
else_: null,
loc,
},
]
: []),
{ kind: "varDecl", localId: "candidate", init: varRef("value", DYN, loc), loc },
{
kind: "while",
@@ -2871,6 +2906,7 @@ export class ClassDynamicDispatch {
!isClassOwnEnumerableFieldName(dispatch.name) ||
(!(info.builtinStream !== undefined && streamProperty(dispatch.name)) &&
!eventTargetInheritedMember(info, dispatch.name) &&
!dateInheritedMember(lowerer, info, dispatch.name) &&
!findMethodOn(lowerer, info, dispatch.name) &&
!findGenericMethodOn(lowerer, info, dispatch.name) &&
dispatch.name !== "constructor" &&
@@ -2987,7 +3023,8 @@ export class ClassDynamicDispatch {
];
if (
((info.builtinStream !== undefined && streamProperty(name)) ||
eventTargetInheritedMember(info, name)) &&
eventTargetInheritedMember(info, name) ||
dateInheritedMember(lowerer, info, name)) &&
dispatch.probe
)
return [
@@ -3200,6 +3237,9 @@ export class ClassDynamicDispatch {
for (const name of [...STREAM_BOOL_PROPERTIES, ...STREAM_NUM_PROPERTIES]) names.add(name);
const inherited = eventTargetApiMembers(info);
if (inherited) for (const name of inherited) names.add(name);
if (dateRootOf(info))
for (const name of DATE_PROTOTYPE_METHOD_NAMES)
if (dateInheritedMember(lowerer, info, name)) names.add(name);
for (let owner: ClassInfo | null = info; owner; owner = owner.base) {
const methodNames = [...owner.methods.keys()];
@@ -3863,6 +3903,70 @@ export class ClassDynamicDispatch {
];
return { kind: "if", cond: condition, then: body, else_: null, loc };
}
// Date.prototype[Symbol.toPrimitive] takes "default" as "string", and a
// class declared `extends Date` inherits Date's toString and valueOf,
// which answer primitives from the time value.
const dateRoot = dateRootOf(info);
if (dateRoot) {
const dateOrder: ("valueOf" | "toString")[] =
hint === "number" ? ["valueOf", "toString"] : ["toString", "valueOf"];
const { receiver } = this.propertyReceiver(plan, loc);
const body: IrStmt[] = [];
for (const method of dateOrder) {
if (!conversionMethod(lowerer, info, method)) {
const time: IrExpr = {
kind: "fieldGet",
obj: upcastTo(lowerer, receiver, dateRoot.def.name),
className: dateRoot.def.name,
field: DATE_TIME_FIELD,
type: DATE_T,
loc,
};
body.push({
kind: "return",
value: lowerer.coerceToExpected(dateValueMethod(method, time, loc)!, DYN),
loc,
});
break;
}
const result = `%toPrimitive.${method}.${helper.locals.length}`;
helper.locals.push({ id: result, name: method, type: DYN, mutable: false });
body.push(
{
kind: "varDecl",
localId: result,
init: {
kind: "call",
callee: this.toPrimitiveCall(lowerer, plan, method, 0, loc),
args: [varRef("p.0", DYN, loc)],
type: DYN,
loc,
},
loc,
},
{
kind: "if",
cond: isPrimitive(varRef(result, DYN, loc), loc),
then: [{ kind: "return", value: varRef(result, DYN, loc), loc }],
else_: null,
loc,
},
);
}
if (body.at(-1)?.kind !== "return")
body.push({
kind: "return",
value: nodeThrowExpr(1, "", "Cannot convert object to primitive value", DYN, loc),
loc,
});
return {
kind: "if",
cond: this.propertyReceiver(plan, loc).condition,
then: body,
else_: null,
loc,
};
}
const order: ("valueOf" | "toString")[] =
hint === "string" ? ["toString", "valueOf"] : ["valueOf", "toString"];
if (!order.some((method) => conversionMethod(lowerer, info, method))) return null;
@@ -4731,9 +4835,11 @@ export class ClassDynamicDispatch {
): IrStmt[] {
const { name, write } = dispatch;
const loc = dispatch.fn.loc;
// The inherited EventTarget/Event members of a subclass instance: the
// runtime's capsule view answers the accessors and method values.
if (eventTargetInheritedMember(info, name)) {
// The inherited EventTarget/Event members of a subclass instance, and
// the inherited Date methods of an instance of a class declared
// `extends Date`: the runtime's capsule view answers the accessors and
// method values.
if (eventTargetInheritedMember(info, name) || dateInheritedMember(lowerer, info, name)) {
const key: IrExpr = { kind: "strLit", value: name, type: STRING, loc };
if (!write)
return [
@@ -1,3 +1,4 @@
import { isErrorPrototypeValue } from "./class-prototypes.js";
import * as ts from "../ts7/adapter.js";
import { literalValues } from "../literal-values.js";
import { isJsSourceFile } from "../program.js";
@@ -377,6 +378,8 @@ export function collectClassMethodMutations(
node.initializer.kind !== ts.SyntaxKind.MissingDeclaration
)
markTarget(node.initializer, node);
if (ts.isPropertyAccessExpression(node) && isErrorPrototypeValue(lowerer, node))
lowerer.errorPrototypeValues = true;
if (ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) {
const base = unwrap(node.expression);
const prototype =
@@ -457,6 +457,28 @@ export function isCompiledPrototypeMember(
);
}
/** `Error.prototype` read as the checked-dynamic object, as a value or for
* a member other than `toString` and `constructor`, which keep the Error
* class's prototype data (`this.toString !== Error.prototype.toString`
* compares class method values). */
export function isErrorPrototypeValue(
lowerer: Lowerer,
expr: ts.PropertyAccessExpression,
): boolean {
let node: ts.Node = expr;
while (ts.isParenthesizedExpression(node.parent)) node = node.parent;
const parent = node.parent;
return (
expr.name.text === "prototype" &&
lowerer.isStdlibGlobal(expr.expression, "Error") &&
!(
ts.isPropertyAccessExpression(parent) &&
parent.expression === node &&
(parent.name.text === "toString" || parent.name.text === "constructor")
)
);
}
/** Named data and method slots have native descriptors. Bare reflection and
* accessor replacement still require a complete view of the prototype. */
export function lowerClassPrototypeData(
@@ -469,6 +491,11 @@ export function lowerClassPrototypeData(
exactClassOfReceiver(lowerer, expr.expression) ??
(stored?.kind === "classval" ? lowerer.classes.get(stored.className) : undefined);
if (!info) return null;
// Error.prototype as a value: the checked-dynamic object an ES5
// constructor function inherits from (`F.prototype =
// Object.create(Error.prototype)`).
if (isErrorPrototypeValue(lowerer, expr))
return { kind: "libCall", fn: "dyn.errorPrototype", args: [], type: DYN, loc: locOf(expr) };
// EventEmitter.prototype and the stream prototypes: the untyped
// constructor's prototype object (builtin-class-dyn.ts).
const builtinKind = builtinClassKind(info.def.name);
@@ -1,10 +1,21 @@
import { varRef } from "../../ir/build.js";
import { BOOL, DYN, F64, STRING, type IrExpr, type IrStmt, type IrType } from "../../ir/ir.js";
import {
BOOL,
DATE_T,
DYN,
F64,
STRING,
type IrExpr,
type IrStmt,
type IrType,
} from "../../ir/ir.js";
import * as ts from "../ts7/adapter.js";
import type { Lowerer } from "./lowerer.js";
import { accessorCall, findMethodOn, type ClassInfo } from "./lower-classes.js";
import { isClassCallback } from "./class-callbacks.js";
import { isSafeToRepeat } from "./expressions/evaluation-safety.js";
import { DATE_TIME_FIELD, dateRootOf } from "./builtins/date-classes.js";
import { dateValueMethod } from "./builtins/dates.js";
/** The ToPrimitive hint of a conversion: `String(x)` and template literals
* take "string", `+` "default", `Number(x)` "number". */
@@ -13,9 +24,12 @@ export type StringConversionHint = "string" | "default";
type ConversionMethod = "sym:toPrimitive" | "valueOf" | "toString";
/** How an instance of one class converts: through one of its methods, or
* through Object.prototype.toString ("[object Object]"). */
type Plan = { method: ConversionMethod; ret: IrType } | { method: null };
/** How an instance of one class converts: through one of its methods,
* through the Date.prototype method `date` of a class declared `extends
* Date`, or through Object.prototype.toString ("[object Object]"). */
type Plan =
| { method: ConversionMethod; ret: IrType }
| { method: null; date?: "toString" | "valueOf" };
const PRIMITIVE_KINDS = new Set(["f64", "string", "bool", "bigint", "undefinedT", "nullT"]);
@@ -138,6 +152,18 @@ function classConversion(
}
const convert = (cls: ClassInfo, object: IrExpr): IrExpr => {
const plan = plans.get(cls)!;
if (plan.method === null && plan.date) {
const root = dateRootOf(cls)!;
const time: IrExpr = {
kind: "fieldGet",
obj: lowerer.upcastTo(object, root.def.name),
className: root.def.name,
field: DATE_TIME_FIELD,
type: DATE_T,
loc,
};
return finish(dateValueMethod(plan.date, time, loc)!);
}
if (plan.method === null)
return finish({ kind: "strLit", value: "[object Object]", type: STRING, loc });
const args: IrExpr[] =
@@ -202,7 +228,10 @@ function depth(info: ClassInfo): number {
}
function samePlan(a: Plan, b: Plan): boolean {
return a.method === b.method;
return (
a.method === b.method &&
(a.method !== null || b.method !== null || a.date === (b as { date?: string }).date)
);
}
/** A conversion method the class declares or inherits (a field of that
@@ -227,11 +256,17 @@ function planOf(lowerer: Lowerer, info: ClassInfo, hint: ConversionHint, node: t
// OrdinaryToPrimitive: "string" tries toString first, which every object
// has (Object.prototype.toString answers a string); "default" and
// "number" try valueOf first, whose Object.prototype version answers the
// object itself.
const order: ConversionMethod[] = hint === "string" ? ["toString"] : ["valueOf", "toString"];
// object itself. Date.prototype[Symbol.toPrimitive] takes "default" as
// "string", and Date.prototype answers both methods with primitives.
const date = dateRootOf(info) !== null;
const order: ConversionMethod[] =
hint === "string" || (date && hint === "default") ? ["toString"] : ["valueOf", "toString"];
for (const method of order) {
const found = declared(lowerer, info, method);
if (!found) continue;
if (!found) {
if (date && method !== "sym:toPrimitive") return { method: null, date: method };
continue;
}
if (!primitive(lowerer, found.ret))
refuse(lowerer, node, info, `a '${method}' method that can return an object`);
return { method, ret: found.ret };
@@ -452,7 +452,7 @@ function withFastCallback(fn: IrFunction, fastT: CallbackType | null, elem: IrTy
* constant-callback census then calls it directly and LLVM can inline the
* callback into the loop (TurboFan's builtin+callback inlining). Callback
* values keep the shared, type-interned helper. */
function callbackSite(fnArg: IrExpr): string {
export function callbackSite(fnArg: IrExpr): string {
return fnArg.kind === "closure" ? `@${fnArg.fnName}` : "";
}
@@ -43,7 +43,7 @@ import { lowerArraySpreadItems, lowerCheckedArrayFrom } from "./array-constructi
import { lowerToNumberArgument } from "../lower-exprs.js";
import { lowerBytesIteratorValue } from "../iterator-adapters.js";
import { lowerArrayCallback } from "./callback-arguments.js";
import { findPredicateOk, predicateToBoolean } from "./array-callbacks.js";
import { callbackSite, findPredicateOk, predicateToBoolean } from "./array-callbacks.js";
import { typeKey } from "../../type-mapper.js";
/** TypedArray.prototype.sort and toSorted. The receiver and comparator
@@ -251,13 +251,28 @@ function lowerBytesCallbackCall(
: method === "findIndex" || method === "findLastIndex"
? F64
: lowerer.irTypeOf(call));
const helper = bytesCallbackHelper(lowerer, method, bytesT, resultT, fnRet, arity, hasInit, loc);
const helper = bytesCallbackHelper(
lowerer,
method,
bytesT,
resultT,
fnRet,
arity,
hasInit,
loc,
callbackSite(fnArg),
);
const args: IrExpr[] = [receiver, fnArg];
if (hasInit) args.push(lowerer.lowerExprExpecting(call.arguments.nodes[1]!, resultT));
return { kind: "call", callee: helper, args, type: resultT, loc };
}
/** The interned helper of one typed-array callback method shape. */
/** The interned helper of one typed-array callback method shape. A literal
* callback gets a helper of its own (`site`), which the backend then calls
* directly, as for the array methods. The visit is a loop on its own
* counter (`for (let i = 0; i < a.length; i++)`, or down from the last
* index), so the counter is an integer and the element reads need no
* index test. */
function bytesCallbackHelper(
lowerer: Lowerer,
method: BytesCallbackMethod,
@@ -267,8 +282,9 @@ function bytesCallbackHelper(
arity: number,
hasInit: boolean,
loc: SrcLoc,
site: string,
): string {
const key = `bytes.${method}:${bytesT.elem}:${typeKey(resultT)}:${typeKey(fnRet)}:${arity}:${hasInit ? "init" : ""}`;
const key = `bytes.${method}:${bytesT.elem}:${typeKey(resultT)}:${typeKey(fnRet)}:${arity}:${hasInit ? "init" : ""}${site}`;
const existing = lowerer.arrHofHelpers.get(key);
if (existing) return existing;
const name = keyedHelperName(lowerer, `%bytes.${method}`, key);
@@ -300,10 +316,20 @@ function bytesCallbackHelper(
type: fnRet,
loc,
});
// The forward bound is the receiver's length itself (fixed for a typed
// array, and equal to `n`), which the emitter's typed-array loops use.
const length = (): IrExpr => ({
kind: "bytesIntrinsic",
method: "length",
receiver: a(),
args: [],
type: F64,
loc,
});
const inRange = (): IrExpr =>
backward
? { kind: "bin", op: ">=", left: i(), right: numLit(0, loc), type: BOOL, loc }
: { kind: "bin", op: "<", left: i(), right: n(), type: BOOL, loc };
: { kind: "bin", op: "<", left: i(), right: length(), type: BOOL, loc };
const advance = (): IrStmt => ({
kind: "assign",
localId: "i.0",
@@ -344,6 +370,7 @@ function bytesCallbackHelper(
...(hasInit ? [{ localId: "z.0", name: "z", type: resultT }] : []),
];
const visit: IrStmt[] = [{ kind: "assign", localId: "v.0", value: at(i()), loc }];
let seeded = false;
const prologue: IrStmt[] = [];
let epilogue: IrStmt[];
switch (method) {
@@ -459,10 +486,12 @@ function bytesCallbackHelper(
loc,
});
} else {
// The seed is the first visited element; the loop starts past it.
seeded = true;
prologue.push(
{
kind: "if",
cond: { kind: "unary", op: "!", operand: inRange(), type: BOOL, loc },
cond: { kind: "bin", op: "===", left: n(), right: numLit(0, loc), type: BOOL, loc },
then: [
{
kind: "throw",
@@ -486,8 +515,16 @@ function bytesCallbackHelper(
else_: null,
loc,
},
{ kind: "varDecl", localId: "acc.0", init: at(i()), loc },
advance(),
{
kind: "varDecl",
localId: "acc.0",
init: at(
backward
? { kind: "bin", op: "-", left: n(), right: numLit(1, loc), type: F64, loc }
: numLit(0, loc),
),
loc,
},
);
}
visit.push({
@@ -506,17 +543,30 @@ function bytesCallbackHelper(
init: { kind: "bytesIntrinsic", method: "length", receiver: a(), args: [], type: F64, loc },
loc,
},
{
kind: "varDecl",
localId: "i.0",
init: backward
? { kind: "bin", op: "-", left: n(), right: numLit(1, loc), type: F64, loc }
: numLit(0, loc),
loc,
},
{ kind: "varDecl", localId: "v.0", init: numLit(0, loc), loc },
...prologue,
{ kind: "for", init: null, cond: inRange(), update: advance(), body: visit, loc },
{
kind: "for",
init: {
kind: "varDecl",
localId: "i.0",
init: backward
? {
kind: "bin",
op: "-",
left: n(),
right: numLit(seeded ? 2 : 1, loc),
type: F64,
loc,
}
: numLit(seeded ? 1 : 0, loc),
loc,
},
cond: inRange(),
update: advance(),
body: visit,
loc,
},
...epilogue,
];
lowerer.liftedFns.push({ name, params, returnType: resultT, locals, body, loc });
@@ -6,7 +6,9 @@ import { varRef } from "../../../ir/build.js";
import { typeKey } from "../../type-mapper.js";
import {
BOOL,
DYN,
F64,
STRING,
type IrExpr,
type IrType,
type SrcLoc,
@@ -69,6 +71,20 @@ export function lowerArrayCallback(
if (valueT.kind === "union") lowerer.requireTruthyUnion(valueT.unionId, argNode);
return { fnArg: truthyFilterCallback(lowerer, valueT, locOf(argNode)), arity: 1 };
}
// The ambient `String` (`values.map(String)`): String(value) converts its
// first argument as a direct call does, whatever the element type; the
// constructor's own value form takes a string.
if (
ts.isIdentifier(argNode) &&
argNode.text === "String" &&
!isJsSourceFile(ts.getSourceFileOfNode(argNode)) &&
lowerer.isStdlibSymbol(lowerer.resolveValueSymbol(argNode) ?? undefined)
) {
const valueT = lead[0];
if (valueT === undefined || valueT.kind === "void")
lowerer.badType(argNode, lowerer.typeOf(argNode));
return { fnArg: stringConversionCallback(lowerer, valueT, argNode), arity: 1 };
}
const full = [...lead, F64, arrT];
// A DYN-receiver HOF's callback (`parsed.flatMap((value) => ...)`):
// the contextual signature types the unannotated param `any` (the
@@ -274,6 +290,49 @@ function runtimeOptionalPairVariant(lowerer: Lowerer, expected: IrType, mapped:
return lowerer.isRuntimeOptionalRecordVariant(strip(expected), strip(mapped));
}
/** `String` as a one-parameter callback over `valueT`: the conversion a
* direct `String(value)` call lowers to (the runtime's String() for unknown
* values and unions holding a symbol, ToString otherwise), interned per
* value type. */
function stringConversionCallback(
lowerer: Lowerer,
valueT: IrType,
node: ts.Expression,
): IrExpr & { type: IrType & { kind: "func" } } {
const loc = locOf(node);
const fnT: IrType & { kind: "func" } = { kind: "func", params: [valueT], ret: STRING };
const key = `mapString:${typeKey(valueT)}`;
let name = lowerer.arrHofHelpers.get(key);
if (!name) {
name = `%arr.mapString.${lowerer.arrHofHelpers.size}`;
lowerer.arrHofHelpers.set(key, name);
const value = varRef("v.0", valueT, loc);
const runtimeString =
valueT.kind === "dyn" ||
(valueT.kind === "union" &&
lowerer.unions.get(valueT.unionId)?.arms.some((arm) => arm.kind === "symbol") === true &&
lowerer.dynConvertible(valueT));
const converted: IrExpr = runtimeString
? {
kind: "libCall",
fn: "dyn.stringConstructor",
args: [lowerer.coerceToExpected(value, DYN)],
type: STRING,
loc,
}
: lowerer.ensureString(value, node);
lowerer.liftedFns.push({
name,
params: [{ localId: "v.0", name: "v", type: valueT }],
returnType: STRING,
locals: [{ id: "v.0", name: "v", type: valueT, mutable: false }],
body: [{ kind: "return", value: converted, loc }],
loc,
});
}
return { kind: "closure", fnName: name, captures: [], type: fnT, loc };
}
/** A native closure for the ambient Boolean callback. Reifying the
* JavaScript constructor itself is unnecessary; Array#filter only observes
* its one-argument ToBoolean result. */
@@ -184,6 +184,21 @@ export function lowerArrayLiteral(
if (arms.length === 1) mapped = arms[0]!;
}
}
// A NON-EMPTY literal whose own type is an array, under a union with
// exactly one array arm and no tuple arm (`[1, { n: true }]` in a
// `Json[]` arm of a recursive `Json`): the checker types each element
// from the arm, so its object literals lower as the arm's elements,
// and the literal's own element type (`number | { n: boolean }`)
// cannot hold them. Build it as the arm; its value only flows into the
// union slot.
if (ctxUnion && expr.elements.nodes.length > 0 && mapped?.kind === "array") {
const def = lowerer.unions.get(ctxUnion.unionId);
const arrayArms = def?.arms.filter((a) => a.kind === "array") ?? [];
const tupleArms =
def?.arms.filter((a) => a.kind === "record" && !!lowerer.shapes.get(a.shapeId)?.tuple) ??
[];
if (arrayArms.length === 1 && tupleArms.length === 0) mapped = arrayArms[0]!;
}
}
// A Date's toJSON() element keeps its null in a literal with no declared
// destination: the lib types say string.
@@ -903,10 +903,15 @@ export function lowerCompoundValueToTarget(
// operator on both values, then the checked conversion into the
// target's declared type (a string result for a number target throws the
// catchable TypeError, as the expanded assignment does).
// A union operand (`fraction += "0"` on a binding the program assigns
// both a number and a string) takes the same runtime operator.
if (
(target.type.kind === "dyn" || rhs.type.kind === "dyn") &&
(isJsSourceFile(ts.getSourceFileOfNode(expr)) ||
(lowerer.dynConvertible(read.type) && lowerer.dynConvertible(rhs.type)))
((target.type.kind === "dyn" || rhs.type.kind === "dyn") &&
(isJsSourceFile(ts.getSourceFileOfNode(expr)) ||
(lowerer.dynConvertible(read.type) && lowerer.dynConvertible(rhs.type)))) ||
((target.type.kind === "union" || rhs.type.kind === "union") &&
lowerer.dynConvertible(read.type) &&
lowerer.dynConvertible(rhs.type))
) {
const left = lowerer.coerceInto(expr.left, read, DYN);
const right = lowerer.coerceInto(expr.right, rhs, DYN);
@@ -1277,6 +1282,48 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
loc,
};
}
// `a[k] = b[k] = v` in a JavaScript file: a member write in value
// position lowers as its statement form and yields the value. The
// receiver, the key and the value are evaluated in that order into
// temporaries first. A receiver held in a temporary must be a
// checked-dynamic value, which the temporary shares; any other receiver
// must be a variable or `this`.
if (
op === ts.SyntaxKind.EqualsToken &&
isJsSourceFile(ts.getSourceFileOfNode(expr)) &&
(ts.isPropertyAccessExpression(expr.left) || ts.isElementAccessExpression(expr.left)) &&
!expr.left.questionDotToken
) {
const target = expr.left;
const receiverNode = target.expression;
const bareReceiver =
ts.isIdentifier(receiverNode) || receiverNode.kind === ts.SyntaxKind.ThisKeyword;
if (bareReceiver || tryLowerExpression(lowerer, receiverNode)?.type.kind === "dyn") {
const stmts: IrStmt[] = [];
const overrides: { node: ts.Expression; value: IrExpr }[] = [];
const hold = (node: ts.Expression, name: string): IrExpr => {
const value = lowerer.lowerExpr(node);
const local = lowerer.declareHiddenLocal(name, value.type);
stmts.push({ kind: "varDecl", localId: local.id, init: value, loc: value.loc });
const held = varRef(local.id, value.type, value.loc);
overrides.push({ node, value: held });
return held;
};
if (!bareReceiver) hold(receiverNode, "%assignReceiver");
if (
ts.isElementAccessExpression(target) &&
!ts.isIdentifier(target.argumentExpression) &&
!ts.isStringLiteral(target.argumentExpression) &&
!ts.isNumericLiteral(target.argumentExpression)
)
hold(target.argumentExpression, "%assignKey");
const result = hold(expr.right, "%assignValue");
stmts.push(
lowerer.withExpressionOverrides(overrides, () => lowerer.lowerExprStatement(expr)),
);
return { kind: "seqExpr", stmts, result, type: result.type, loc };
}
}
lowerer.unsupported(
"SC1090",
expr,
@@ -616,9 +616,25 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre
// An indexed string property is absent for non-integer and missing
// indexes. charAt has different coercion and out-of-range semantics.
const recv = lowerer.lowerExpr(expr.expression);
const rawIndex = lowerer.lowerExpr(expr.argumentExpression);
// A checked-dynamic index (an untyped JavaScript parameter, a parsed
// value) reads the property its key names, as JavaScript does: a code
// unit for an integer index, the length for "length", else undefined.
// The checker's string type does not describe those results, so the
// value stays checked-dynamic and consumers convert it.
if (rawIndex.type.kind === "dyn" && lowerer.dynConvertible(recv.type)) {
const key = lowerRecordPropertyKey(lowerer, rawIndex, expr.argumentExpression);
return {
kind: "dynKeyGet",
value: lowerer.coerceInto(expr.expression, recv, DYN),
key,
type: DYN,
loc: locOf(expr),
};
}
const index = lowerOptionalNumber(
lowerer,
lowerer.lowerExpr(expr.argumentExpression),
rawIndex,
locOf(expr.argumentExpression),
expr.argumentExpression,
);
@@ -11,6 +11,7 @@ import {
DYN,
F64,
type IrExpr,
type IrLocal,
type IrType,
STRING,
type SrcLoc,
@@ -226,7 +227,10 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc:
if (use?.kind === "element") {
const key = runtimeOptionalElementKey(use.access.argumentExpression);
if (key === null) {
return varRef(local.id, local.type, loc);
return (
runtimeOptionalKeyedRecord(lowerer, expr, local, loc) ??
varRef(local.id, local.type, loc)
);
}
return runtimeOptionalReceiverRead(lowerer, expr, local, key, loc);
}
@@ -938,10 +942,43 @@ function moduleFileName(lowerer: Lowerer, sf: ts.SourceFile): string {
: sf.fileName;
}
/** Object.keys/Object.entries require an object receiver, but their first
* argument is not syntactically a member receiver. Recognize that one
* standard-library position so a runtime-optional loop element can be
* checked against the checker-narrowed record arm before builtin lowering. */
/** A computed-key access (`value[key]` after `typeof value === "object"`)
* of a runtime-optional binding the checker narrowed to one record arm of a
* wider union. The binding re-tags to that record or undefined, so the keyed
* read or write evaluates its operands and then throws Node's TypeError for
* an undefined receiver. The other arms, which the checker excluded, fail
* the checked re-tag. Null when the binding stores only the record and
* undefined, or the narrowed record is not one of its arms. */
function runtimeOptionalKeyedRecord(
lowerer: Lowerer,
expr: ts.Identifier,
local: IrLocal,
loc: SrcLoc,
): IrExpr | null {
if (local.type.kind !== "union") return null;
const narrowed = lowerer.mapTypeOf(lowerer.typeOf(expr));
if (narrowed?.kind !== "record") return null;
const fromId = local.type.unionId;
const arms = lowerer.unions.get(fromId)?.arms;
const recordTag = lowerer.armTag(fromId, narrowed);
const undefTag = lowerer.armTag(fromId, UNDEFINED_T);
if (!arms || arms.length <= 2 || recordTag < 0 || undefTag < 0) return null;
const target = lowerer.withUndefinedArm(narrowed);
if (target.kind !== "union") return null;
const trappable = new Set<number>();
for (let tag = 0; tag < arms.length; tag++) {
if (tag !== recordTag && tag !== undefTag) trappable.add(tag);
}
const helper = lowerer.unionRetagHelper(fromId, target.unionId, loc, trappable);
if (!helper) return null;
return lowerer.checkedNarrowCall(helper, varRef(local.id, local.type, loc), target, loc);
}
/** Object.keys, Object.values and Object.entries require an object
* receiver, but their first argument is not syntactically a member
* receiver. Recognize that one standard-library position so a
* runtime-optional loop element can be checked against the checker-narrowed
* record arm before builtin lowering. */
function runtimeOptionalObjectWalkerArg(lowerer: Lowerer, expr: ts.Expression): boolean {
let arg = expr;
while (
@@ -962,7 +999,9 @@ function runtimeOptionalObjectWalkerArg(lowerer: Lowerer, expr: ts.Expression):
return false;
const access = call.expression;
return (
(access.name.text === "keys" || access.name.text === "entries") &&
(access.name.text === "keys" ||
access.name.text === "values" ||
access.name.text === "entries") &&
lowerer.isStdlibMember(access)
);
}
@@ -36,7 +36,6 @@ import { tryLowerExpression } from "./try-lower-expression.js";
import { fenceSymbolFieldCopy } from "../symbol-fields.js";
import { planUnionRetag } from "../union-retag.js";
import { jsHoldsDynObject, runtimeOptionalStorageOperand } from "../lower-exprs.js";
import { builtinGlobalValue } from "./property-access.js";
/** `{ a: 1, b: "x" }` → recordLit. The record type comes from the
* contextual type when tsc has one (annotated declarations, arguments,
@@ -395,11 +394,9 @@ export function lowerDynObjectLiteral(
const lowerValue = (): IrExpr =>
ts.isMethodDeclaration(prop)
? lowerer.lowerLambda(prop, true)
: ts.isShorthandPropertyAssignment(prop) && shorthandBuiltinValue(lowerer, prop)
? shorthandBuiltinValue(lowerer, prop)!
: !boxValue && ts.isObjectLiteralExpression(valueExpr)
? lowerer.lowerExprExpecting(valueExpr, DYN)
: lowerer.lowerExpr(valueExpr as ts.Expression);
: !boxValue && ts.isObjectLiteralExpression(valueExpr)
? lowerer.lowerExprExpecting(valueExpr, DYN)
: lowerer.lowerExpr(valueExpr as ts.Expression);
raw = fenceClosureProbe(lowerer, valueExpr, undefined, lowerValue) ?? lowerValue();
} catch (err) {
// A PURE member read a JS file cannot lower (a namespace object
@@ -801,6 +798,7 @@ export function lowerObjectLiteral(
): IrExpr {
// Unannotated JavaScript accumulator fields have no element contract.
// Preserve their checked arrays instead of assigning never[] a numeric ABI.
// An empty object field is a map the program adds keys to.
if (
!expected &&
isJsSourceFile(ts.getSourceFileOfNode(expr)) &&
@@ -809,6 +807,8 @@ export function lowerObjectLiteral(
ts.isPropertyAssignment(prop) &&
((ts.isArrayLiteralExpression(prop.initializer) &&
prop.initializer.elements.nodes.length === 0) ||
(ts.isObjectLiteralExpression(prop.initializer) &&
prop.initializer.properties.nodes.length === 0) ||
neverTaintedJsType(lowerer, prop.initializer, lowerer.typeOf(prop.initializer))),
)
) {
@@ -3157,20 +3157,6 @@ function lowerDeclaredSpreadMerge(
/** The value of a shorthand property (`{ x }`): the binding `x` refers to.
* The property name's own symbol is the PROPERTY, so resolution goes
* through getShorthandAssignmentValueSymbol. */
/** `{ TypeError }`: a stdlib global in shorthand position reads as the value
* its name reads as elsewhere (the interned constructor or function value),
* or null when the global has no such value. The shorthand's name carries
* the property's symbol, so the identifier path cannot resolve it. */
function shorthandBuiltinValue(
lowerer: Lowerer,
prop: ts.ShorthandPropertyAssignment,
): IrExpr | null {
const symbol = lowerer.checker.getShorthandAssignmentValueSymbol(prop);
if (!symbol || !lowerer.isStdlibSymbol(symbol)) return null;
const js = isJsSourceFile(ts.getSourceFileOfNode(prop));
return builtinGlobalValue(ts.symbolName(symbol), locOf(prop.name), js);
}
export function lowerShorthandValue(
lowerer: Lowerer,
prop: ts.ShorthandPropertyAssignment,
@@ -3187,8 +3173,6 @@ export function lowerShorthandValue(
const callable = lowerer.lowerBuiltinCallableValue(builtin, loc);
if (callable) return callable;
}
const builtinValue = shorthandBuiltinValue(lowerer, prop);
if (builtinValue) return builtinValue;
if (symbol) {
if (lowerer.ctx.selfSymbol === symbol) {
return { kind: "selfRef", type: lowerer.ctx.selfType!, loc };
@@ -6,7 +6,11 @@ import { lowerHistogramProperty } from "../builtins/performance.js";
import { staticFunctionNameRead } from "../static-function-names.js";
import { typedArgumentsArity } from "../typed-arguments.js";
import { isThisParameter } from "../call-signatures.js";
import { localPrivateBrandClass } from "../private-brands.js";
import {
checkedPrivateValue,
localPrivateBrandClass,
privateNameClass,
} from "../private-brands.js";
import { type Lowerer, PoisonError, jsFuncNameOf, neverTaintedJsType, own } from "../lowerer.js";
import {
ARRAY_METHODS,
@@ -91,7 +95,7 @@ import {
runtimeConstructorReceiver,
} from "../builtins/runtime-constructor.js";
import { findGenericMethodOn, lowerStaticFieldRead } from "../lower-classes.js";
import { lowerClassPrototypeData } from "../class-prototypes.js";
import { isErrorPrototypeValue, lowerClassPrototypeData } from "../class-prototypes.js";
import { lowerEnumAccess } from "../lower-enums.js";
import {
hasOptionalChainGuard,
@@ -284,6 +288,24 @@ export function lowerPropertyAccessExpr(
expr: ts.PropertyAccessExpression,
loc: SrcLoc,
): IrExpr {
// `value.#x` on a checked-dynamic value, or on a checked extraction of
// one: the field of the brand-checked receiver (checkedPrivateValue).
if (ts.isPrivateIdentifier(expr.name)) {
const owner = privateNameClass(lowerer, expr.name);
const field = owner?.fields.get(expr.name.text);
const probed = field ? tryLowerExpression(lowerer, expr.expression) : null;
if (owner && field && probed && checkedDynamicValue(probed)) {
const value = checkedDynamicValue(lowerer.lowerExpr(expr.expression))!;
return {
kind: "fieldGet",
obj: checkedPrivateValue(lowerer, owner, expr, value, loc),
className: owner.def.name,
field: expr.name.text,
type: field,
loc,
};
}
}
if (!expr.questionDotToken) {
const navigator = lowerNavigatorMember(lowerer, expr, NODE_COMPAT_VERSION);
if (navigator) return navigator;
@@ -1675,6 +1697,19 @@ export function lowerIntrinsicProperty(
return { kind: "libCall", fn: "dyn.arrayPrototype", args: [], type: DYN, loc: locOf(expr) };
if (lowerer.stdlibGlobalMember(expr, "Object") === "prototype")
return { kind: "libCall", fn: "dyn.objectPrototype", args: [], type: DYN, loc: locOf(expr) };
// Error.prototype as a value (`F.prototype = Object.create(Error.prototype)`)
// and Error.captureStackTrace read as a function (`if
// (Error.captureStackTrace) ...`).
if (isErrorPrototypeValue(lowerer, expr))
return { kind: "libCall", fn: "dyn.errorPrototype", args: [], type: DYN, loc: locOf(expr) };
if (lowerer.stdlibGlobalMember(expr, "Error") === "captureStackTrace")
return {
kind: "libCall",
fn: "dyn.errorCaptureStackTraceFunction",
args: [],
type: DYN,
loc: locOf(expr),
};
// Function.prototype as a VALUE (a prototype-chain link:
// `Object.setPrototypeOf(F.prototype, Function.prototype)`): one
// anonymous function answering undefined, length 0 — a zero-capture
@@ -3547,6 +3582,13 @@ export function classFieldTarget(
* known class instance OR a record, and the member is a field or (class
* receivers) a declared accessor property. Returns the pieces of a
* fieldSet/recordSet/accessor-call (minus value/kind) or null. */
/** The checked-dynamic value `expr` reads: itself, or the value a checked
* extraction validates. Null for other expressions. */
function checkedDynamicValue(expr: IrExpr): IrExpr | null {
if (expr.type.kind === "dyn") return expr;
return expr.kind === "dynCheck" && expr.value.type.kind === "dyn" ? expr.value : null;
}
export function fieldTarget(
lowerer: Lowerer,
access: ts.PropertyAccessExpression,
@@ -3583,6 +3625,30 @@ export function fieldTarget(
return classFieldTarget(lowerer, access.expression, ownerType, access.name.text);
}
}
// `value.#x = v` on a checked-dynamic value: the field of the
// brand-checked receiver (checkedPrivateValue).
if (ts.isPrivateIdentifier(access.name)) {
const owner = privateNameClass(lowerer, access.name);
const probed = owner?.fields.has(access.name.text)
? tryLowerExpression(lowerer, access.expression)
: null;
if (owner && probed && checkedDynamicValue(probed)) {
const receiver = checkedPrivateValue(
lowerer,
owner,
access,
checkedDynamicValue(lowerer.lowerExpr(access.expression))!,
locOf(access),
);
return classFieldTarget(
lowerer,
access.expression,
{ kind: "object", className: owner.def.name },
access.name.text,
receiver,
);
}
}
const stored = ts.isIdentifier(access.expression)
? (lowerer.peekLocal(access.expression)?.type ?? lowerer.globalOf(access.expression)?.type)
: undefined;
@@ -4024,26 +4090,10 @@ function lowerStreamObjectProperty(
return lowerStreamProperty(lowerer, expr, info);
}
/** The error constructors among GLOBAL_CONSTRUCTOR_VALUES. Their statics
* (`Error.stackTraceLimit`, `Error.captureStackTrace`) keep their own
* lowerings, so fenceBuiltinConstructorStaticRead leaves them alone. */
const ERROR_CONSTRUCTOR_VALUES: ReadonlySet<string> = new Set([
"Error",
"TypeError",
"RangeError",
"SyntaxError",
"ReferenceError",
"EvalError",
"URIError",
"AggregateError",
]);
/** The builtin constructors JavaScript sources read as real function
* values rather than identity tokens: the runtime interns one function per
* constructor, and dynamic values answer `.constructor` with the same one,
* so `o.constructor === Object` and `err.constructor === TypeError` are
* exact identity tests, and `err.constructor.name` is the constructor's
* name. */
* so `o.constructor === Object` is an exact identity test. */
const GLOBAL_CONSTRUCTOR_VALUES: ReadonlySet<string> = new Set([
"Object",
"Function",
@@ -4054,7 +4104,6 @@ const GLOBAL_CONSTRUCTOR_VALUES: ReadonlySet<string> = new Set([
"Promise",
"WeakMap",
"WeakSet",
...ERROR_CONSTRUCTOR_VALUES,
]);
/** The builtin global functions JavaScript sources read as interned
@@ -4197,7 +4246,6 @@ function fenceBuiltinConstructorStaticRead(
symbol !== undefined &&
ts.symbolName(symbol).endsWith("Constructor") &&
GLOBAL_CONSTRUCTOR_VALUES.has(ts.symbolName(symbol).slice(0, -"Constructor".length)) &&
!ERROR_CONSTRUCTOR_VALUES.has(ts.symbolName(symbol).slice(0, -"Constructor".length)) &&
lowerer.isStdlibSymbol(symbol)
)
lowerer.stdlibMemberFence(access);
@@ -1,4 +1,5 @@
/* instanceof and in, including private-name and runtime-key in. */
import { dateRootOf } from "../builtins/date-classes.js";
import { keyedHelperName } from "../stable-names.js";
import * as ts from "../../ts7/adapter.js";
import { builtinClassInstanceOfAny, builtinClassKind } from "../builtins/builtin-class-dyn.js";
@@ -266,7 +267,12 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc
stmts: [{ kind: "exprStmt", expr: value, loc }],
result: {
kind: "boolLit",
value: value.type.kind === (date ? "date" : "url"),
value:
value.type.kind === (date ? "date" : "url") ||
// An instance of a class declared `extends Date`.
(date &&
value.type.kind === "object" &&
dateRootOf(lowerer.classes.get(value.type.className)) !== null),
type: BOOL,
loc,
},
@@ -566,7 +572,33 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc
loc,
};
};
const test = caughtInstanceOf(target);
let test = caughtInstanceOf(target);
// A thrown checked-dynamic object whose prototype chain reaches
// Error.prototype (an ES5 constructor function inheriting from it).
if (target.def.name === "%Error" && lowerer.errorPrototypeValues) {
const value = varRef(caughtLocal.id, CAUGHT, loc);
test = {
kind: "logical",
op: "||",
left: test,
right: {
kind: "logical",
op: "&&",
left: { kind: "caughtTest", value, test: "dynamic", type: BOOL, loc },
right: {
kind: "libCall",
fn: "dyn.inheritsErrorPrototype",
args: [{ kind: "caughtToDyn", value, type: DYN, loc }],
type: BOOL,
loc,
},
type: BOOL,
loc,
},
type: BOOL,
loc,
};
}
return duplex
? { kind: "logical", op: "||", left: test, right: caughtInstanceOf(duplex), type: BOOL, loc }
: test;
@@ -591,9 +623,35 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc
) {
return classInstanceOf(lowerer, left, target, loc);
}
if (left.type.kind === "dyn" && target.def.name === "%Error") {
if (left.type.kind === "dyn" && target.def.name === "%Error" && !lowerer.errorPrototypeValues) {
return { kind: "dynTest", test: "error", value: left, type: BOOL, loc };
}
// An object whose prototype chain reaches Error.prototype (an ES5
// constructor function inheriting from it) is an instance as well.
if (left.type.kind === "dyn" && target.def.name === "%Error") {
const local = lowerer.declareHiddenLocal("%instanceofValue", DYN);
const value = varRef(local.id, DYN, loc);
return {
kind: "seqExpr",
stmts: [{ kind: "varDecl", localId: local.id, init: left, loc }],
result: {
kind: "logical",
op: "||",
left: { kind: "dynTest", test: "error", value, type: BOOL, loc },
right: {
kind: "libCall",
fn: "dyn.inheritsErrorPrototype",
args: [value],
type: BOOL,
loc,
},
type: BOOL,
loc,
},
type: BOOL,
loc,
};
}
// `u instanceof TypeError` (and the other BUILTIN error classes) on a
// dyn value: the from_error cache holds the checked-dynamic tree↔error identity edge,
// so the runtime resolves the encoding back to its error and asks the
@@ -23,6 +23,7 @@ import {
type IrExpr,
type IrStmt,
type IrType,
isUnitType,
STRING,
type SrcLoc,
UNDEFINED_T,
@@ -100,6 +101,27 @@ function writeFree(expr: IrExpr): boolean {
}
}
/** A union unary `+`/`-` converts with the runtime's ToNumber: one with an
* arm other than numbers and units (`string | undefined`). Number and unit
* unions keep their native lowering. */
function coercedUnion(lowerer: Lowerer, type: IrType): boolean {
if (type.kind !== "union" || !lowerer.dynConvertible(type)) return false;
const arms = lowerer.unions.get(type.unionId)?.arms ?? [];
return arms.some((arm) => arm.kind !== "f64" && !isUnitType(arm));
}
/** ToNumber of a boolean: 1 for true, 0 for false. */
function booleanToNumber(value: IrExpr, loc: SrcLoc): IrExpr {
return {
kind: "ternary",
cond: value,
then: { kind: "numLit", value: 1, type: F64, loc },
else_: { kind: "numLit", value: 0, type: F64, loc },
type: F64,
loc,
};
}
export function lowerPrefixUnary(lowerer: Lowerer, expr: ts.PrefixUnaryExpression): IrExpr {
const loc = locOf(expr);
switch (expr.operator) {
@@ -111,9 +133,25 @@ export function lowerPrefixUnary(lowerer: Lowerer, expr: ts.PrefixUnaryExpressio
}
// Match the checked numeric boundary used by JS binary arithmetic.
const operand =
raw.type.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(expr))
? { kind: "dynCheck" as const, value: raw, type: F64, loc }
: lowerOptionalNumber(lowerer, raw, loc, expr.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))
? ({
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 };
@@ -123,6 +161,12 @@ export function lowerPrefixUnary(lowerer: Lowerer, expr: ts.PrefixUnaryExpressio
if (raw.type.kind === "dyn") {
return { kind: "libCall", fn: "dyn.toNumberCoerce", args: [raw], type: F64, loc };
}
// A union of primitives (`+match[1]` over `string | undefined`): ToNumber
// over the boxed value, as for a checked-dynamic operand.
if (coercedUnion(lowerer, raw.type)) {
const boxed: IrExpr = { kind: "dynFrom", value: raw, type: DYN, loc };
return { kind: "libCall", fn: "dyn.toNumberCoerce", args: [boxed], type: F64, loc };
}
// Unary + is ToNumber; on an already-number operand it's identity,
// and a STRING operand runs the runtime's ECMA-exact StringToNumber
// (num.fromString — Number(aString)'s lowering, scr_string.c).
@@ -130,6 +174,7 @@ export function lowerPrefixUnary(lowerer: Lowerer, expr: ts.PrefixUnaryExpressio
if (raw.type.kind === "string") {
return { kind: "libCall", fn: "num.fromString", args: [raw], type: F64, loc };
}
if (raw.type.kind === "bool") return booleanToNumber(raw, loc);
const classNumber = classToNumberConversion(lowerer, raw, expr.operand, "unary plus");
if (classNumber) return classNumber;
const operand = lowerOptionalNumber(lowerer, raw, loc, expr.operand);
@@ -321,9 +366,13 @@ export function lowerIncDec(
!isJsSourceFile(ts.getSourceFileOfNode(expr)) &&
raw.type.kind === "dyn" &&
lowerer.mapTypeOf(lowerer.typeOf(access))?.kind === "f64";
// A private name is the field of its class (fieldTarget).
const privateName =
ts.isPropertyAccessExpression(access) && ts.isPrivateIdentifier(access.name);
if (
(isJsSourceFile(ts.getSourceFileOfNode(expr)) || checkedNumber) &&
raw.type.kind === "dyn"
raw.type.kind === "dyn" &&
!privateName
) {
const receiver = save(raw, "%incrementReceiver");
const key = save(
@@ -814,7 +863,8 @@ export function lowerFieldCompoundValue(
probed?.type.kind === "classval" ? lowerer.classes.get(probed.type.className) : undefined;
if (probed && staticClass && hasRuntimeStatics(staticClass))
probed = lowerer.coerceToExpected(probed, DYN);
if (probed?.type.kind === "dyn") {
// A private name is the field of its class (fieldTarget).
if (probed?.type.kind === "dyn" && !ts.isPrivateIdentifier(access.name)) {
const receiver = save(probed, "%compoundReceiver");
const key: IrExpr = {
kind: "strLit",
@@ -142,7 +142,7 @@ import {
lowerCryptoHashMethodCall,
lowerPromisifiedCryptoCall,
} from "./builtins/crypto.js";
import { lowerErrorIsErrorCall } from "./builtins/errors.js";
import { lowerErrorCaptureStackTraceCall, lowerErrorIsErrorCall } from "./builtins/errors.js";
import { lowerPromisifiedDnsCall } from "./lower-dgram.js";
import { lowerDisposableStackUseCall } from "./builtins/disposable-stack.js";
import {
@@ -173,6 +173,7 @@ import {
stringWrapperToString,
symbolFieldInfo,
templateRawTextOf,
primitiveCtorClosure,
} from "./lower-exprs.js";
import { isSafeToDiscard } from "./expressions/evaluation-safety.js";
import { tryLowerExpression } from "./expressions/try-lower-expression.js";
@@ -200,6 +201,11 @@ import {
import { classCallbackCall, isClassCallback } from "./class-callbacks.js";
import { emitterRooted, lowerEmitterMethodCall } from "./lower-event-emitter.js";
import { eventTargetApiMethod, lowerEventTargetMethodCall } from "./lower-event-target.js";
import {
dateRootOf,
isDatePrototypeMethod,
lowerInheritedDateMethodCall,
} from "./builtins/date-classes.js";
import {
lowerConsoleInstanceCall,
lowerConsoleMethodCall,
@@ -1556,6 +1562,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
lowerer.lowerFilterNarrowCall(expr, expr.expression) ??
lowerArrayIsArrayCall(lowerer, expr, expr.expression) ??
lowerErrorIsErrorCall(lowerer, expr, expr.expression) ??
lowerErrorCaptureStackTraceCall(lowerer, expr, expr.expression) ??
lowerBigIntMethodCall(lowerer, expr, expr.expression) ??
lowerSymbolStaticCall(lowerer, expr, expr.expression) ??
lowerSymbolMethodCall(lowerer, expr, expr.expression) ??
@@ -3729,6 +3736,18 @@ function lowerDynReceiverMethodCall(
// IS Node's semantics for every possible dyn value. Shared names would
// need a receiver-kind dispatch — they keep the fence.
if (DYN_STRING_ONLY_METHODS.has(access.name.text) && !ownCallable) {
// An object-typed receiver the member is missing from (a JavaScript
// module object whose members are assigned in a function body,
// `common.repeat(" ", n)`) holds a string or an object with that
// member: dispatch on the receiver's runtime kind.
if (
!checkerUntyped &&
(lowerer.typeOf(access).flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0 &&
!call.questionDotToken &&
!access.questionDotToken &&
!call.arguments.nodes.some(ts.isSpreadElement)
)
return lowerStringOrMemberCall(lowerer, call, access, recv);
const checked = (): IrExpr => dynStringReceiver(lowerer, recv, access);
return (
lowerRegexMethodCall(lowerer, call, access, checked) ??
@@ -3937,6 +3956,98 @@ function lowerDynReceiverMethodCall(
};
}
/** `value.name(...args)` on a dyn receiver that may be a string or an
* object: a string runs the String.prototype lowering, any other value
* calls its member. The member is read before the arguments are
* evaluated, and the arguments are evaluated once for either branch. */
function lowerStringOrMemberCall(
lowerer: Lowerer,
call: ts.CallExpression,
access: ts.PropertyAccessExpression,
recv: IrExpr,
): IrExpr {
const loc = locOf(call);
const receiverLocal = lowerer.declareHiddenLocal("%dynCallReceiver", DYN);
const receiver = varRef(receiverLocal.id, DYN, loc);
const isString: IrExpr = { kind: "dynTest", test: "string", value: receiver, type: BOOL, loc };
const calleeLocal = lowerer.declareHiddenLocal("%dynCallCallee", DYN);
const callee = varRef(calleeLocal.id, DYN, loc);
const stmts: IrStmt[] = [
{ kind: "varDecl", localId: receiverLocal.id, init: recv, loc },
{
kind: "varDecl",
localId: calleeLocal.id,
init: {
kind: "ternary",
cond: isString,
then: dynUndefinedExpr(loc),
else_: {
kind: "dynKeyGet",
key: { kind: "strLit", value: access.name.text, type: STRING, loc: locOf(access) },
value: receiver,
type: DYN,
loc: locOf(access),
},
type: DYN,
loc,
},
loc,
},
];
const args = call.arguments.nodes.map((node) => {
const value = lowerer.lowerExpr(node);
const local = lowerer.declareHiddenLocal("%dynCallArg", value.type);
stmts.push({ kind: "varDecl", localId: local.id, init: value, loc: value.loc });
return { node, value: varRef(local.id, value.type, value.loc) };
});
// String.prototype methods ignore arguments past the ones they read.
const arity = DYN_STRING_ONLY_ARITY[access.name.text] ?? call.arguments.nodes.length;
const stringReceiver = (): IrExpr => ({ kind: "dynCheck", value: receiver, type: STRING, loc });
const lowerString = (): IrExpr | null =>
(call.arguments.nodes.length <= arity
? lowerRegexMethodCall(lowerer, call, access, stringReceiver)
: null) ??
lowerStringMethodCall(
lowerer,
call,
access,
stringReceiver,
call.arguments.nodes.slice(0, arity),
);
const stringCall = lowerer.withExpressionOverrides(args, lowerString);
const memberCall: IrExpr = {
kind: "dynCall",
callee,
receiver,
calleeName: ts.getTextOfNode(access),
args: args.map((arg) => lowerer.coerceToExpected(arg.value, DYN)),
type: DYN,
loc,
};
return {
kind: "seqExpr",
stmts,
result: {
kind: "ternary",
cond: isString,
then: stringCall
? lowerer.coerceToExpected(stringCall, DYN)
: nodeThrowExpr(
0,
"SC1090",
`method calls like '${ts.getTextOfNode(access)}' on a string are not supported yet [SC1090 at ${ts.getSourceFileOfNode(call).fileName}:${ts.getLineAndCharacterOfPosition(ts.getSourceFileOfNode(call), ts.tokenStartOfNode(call)).line + 1}]`,
DYN,
loc,
),
else_: memberCall,
type: DYN,
loc,
},
type: DYN,
loc,
};
}
/** Prototype method names of the checked-dynamic tree-representable kinds (String, Array,
* Object, Function, Number prototypes): a dyn receiver call on one of
* these could be a REAL method on a real value, which a stored-member
@@ -4343,7 +4454,25 @@ export function lowerDynDispatchMethodCall(
}
}
}
// `String`, `Number` or `Boolean` as the callback of an array method
// (`values.map(String)` over an unknown array): the method calls it with
// each element, whatever its type, and cannot observe which function
// value it received, so it gets the constructor's form that converts any
// argument rather than the string-input form typed code stores.
const arrayCallbackReceiver =
ARRAY_CALLBACK_METHODS.has(method) &&
lowerer.checker.isArrayType(lowerer.typeOf(access.expression));
const args = call.arguments.nodes.map((arg, i) => {
const primitive =
i === 0 && arrayCallbackReceiver && ts.isIdentifier(arg)
? stdlibGlobalNameOf(lowerer, arg)
: null;
if (primitive === "String" || primitive === "Number" || primitive === "Boolean")
return lowerer.coerceInto(
arg,
primitiveCtorClosure(lowerer, primitive, locOf(arg), true),
DYN,
);
if (i === 0 && predicate) return lowerer.coerceInto(arg, predicate, DYN);
const undefinedArg = lowerStaticallyUndefinedArgument(lowerer, arg);
return undefinedArg
@@ -4361,6 +4490,21 @@ export function lowerDynDispatchMethodCall(
};
}
/** Array.prototype methods whose first argument is a callback they call
* with the elements. */
const ARRAY_CALLBACK_METHODS = new Set([
"map",
"filter",
"forEach",
"some",
"every",
"find",
"findIndex",
"findLast",
"findLastIndex",
"flatMap",
]);
/** Every String.prototype method name a typed or dyn string lowering
* answers. */
function isStringMethodName(name: string): boolean {
@@ -4374,6 +4518,29 @@ function isStringMethodName(name: string): boolean {
);
}
/** How many arguments each DYN_STRING_ONLY_METHODS method reads. */
const DYN_STRING_ONLY_ARITY: Readonly<Record<string, number>> = {
charCodeAt: 1,
charAt: 1,
startsWith: 2,
endsWith: 2,
substring: 2,
repeat: 1,
trim: 0,
trimStart: 0,
trimEnd: 0,
split: 2,
padStart: 2,
padEnd: 2,
toLowerCase: 0,
toUpperCase: 0,
replace: 2,
replaceAll: 2,
match: 1,
matchAll: 1,
search: 1,
};
/** STR_METHODS ∪ the regex-form names, MINUS everything Array (or any
* other dyn kind's prototype) also declares. */
const DYN_STRING_ONLY_METHODS = new Set([
@@ -9672,6 +9839,10 @@ export function lowerObjectMethodCall(
if (info && !found && eventTargetApiMethod(info, access.name.text)) {
return lowerEventTargetMethodCall(lowerer, call, access, info, lowerReceiver());
}
// The Date methods a class declared `extends Date` does not declare.
if (info && !found && dateRootOf(info) && isDatePrototypeMethod(access.name.text)) {
return lowerInheritedDateMethodCall(lowerer, call, access.name.text, info, lowerReceiver());
}
// GENERIC methods (own type parameters) never enter the methods table:
// they monomorphize per call site and dispatch statically
// (lowerClassGenericMethodCall has the exactness rules).
@@ -10460,6 +10631,17 @@ export function lowerStaticNumberGlobal(
: lowerer.lowerExprExpecting(args[0]!, STRING);
return { kind: "libCall", fn: "num.parseInt", args: [s, radix], type: F64, loc };
}
// parseFloat takes one argument; Node ignores the rest after evaluating
// them (`parseFloat(text, 10)`). Literal and identifier extras have
// nothing to evaluate.
if (
name === "parseFloat" &&
args.length > 1 &&
args
.slice(1)
.every((arg) => ts.isNumericLiteral(arg) || ts.isStringLiteral(arg) || ts.isIdentifier(arg))
)
return lowerStaticNumberGlobal(lowerer, name, call, args.slice(0, 1), loc);
// STATIC parseFloat over string arguments — parseInt's sibling
// (num.parseFloat is ECMA 19.2.4's decimal-literal prefix parse in
// scr_string.c). Other argument types retain their native coercion
@@ -1,3 +1,11 @@
import {
DATE_TIME_FIELD,
dateRootOf,
dateSuperCall,
isDatePrototypeMethod,
lowerDateSuperMethodCall,
} from "./builtins/date-classes.js";
import { lowerDateStaticCall } from "./builtins/dates.js";
import { keyedHelperName } from "./stable-names.js";
import { lowerValueReturningConstructorNew } from "./js-dynamic-objects.js";
import { adaptPromiseExecutor } from "./promise-executor-adapter.js";
@@ -566,6 +574,11 @@ export interface ClassInfo {
* lower-event-target.ts over any class rooted here. Subclass structs
* embed the runtime prefix. */
builtinEventTarget?: "target" | "event";
/** A class declared `extends Date` (builtins/date-classes.ts): its first
* field is the hidden `%time` value, super() sets it with the Date
* constructor's rules, and the Date methods it does not declare lower
* over that value. Subclasses inherit the field. */
extendsDate?: true;
/** The synthetic LifetimeRegion class (@scriptc/experimental,
* lifetime-regions.ts): its constructor is synthesized with the class,
* and its methods lower at their call sites. */
@@ -1953,6 +1966,7 @@ export function collectClassShapeInner(
: (computedBase?.classInfo ?? null);
let genericBase: ts.ExpressionWithTypeArguments | undefined;
let callableBase: ClassInfo["callableBase"];
let extendsDate = false;
let factoryBaseExpression: ts.Expression | undefined;
const adoptCallableBase = (expression: ts.Expression): boolean => {
if (familyMode || inst || mixin) return false;
@@ -2153,6 +2167,11 @@ export function collectClassShapeInner(
if (t && adoptCallableBase(t.expression)) continue;
lowerer.unsupported("SC1090", clause, "extending computed expressions");
}
if (!familyMode && !inst && !mixin && lowerer.isStdlibGlobal(t.expression, "Date")) {
if (t.typeArguments?.nodes) lowerer.unsupported("SC1090", t, "extending generic classes");
extendsDate = true;
continue;
}
const symbol = lowerer.resolveValueSymbol(t.expression);
// Extending a REBINDABLE decorated class (analysis already ran —
// this collection is a class expression or a generic
@@ -4353,7 +4372,8 @@ export function collectClassShapeInner(
const ctorParams: ParamShape[] =
ctor && !mixinForwarding
? lowerer.paramShapes(ctor.parameters.nodes)
: (base?.ctorParams ?? (callableBase ? [{ type: DYN, mode: "dynRest" }] : []));
: (base?.ctorParams ??
(callableBase || extendsDate ? [{ type: DYN, mode: "dynRest" }] : []));
if (
ctor &&
!mixinForwarding &&
@@ -4391,6 +4411,7 @@ export function collectClassShapeInner(
// Redeclared INHERITED fields contribute no slot (their
// initializers assign the prefix slot).
fields: [
...(extendsDate ? [{ name: DATE_TIME_FIELD, type: DATE_T }] : []),
...(base?.def.fields ?? []),
...fieldOrder
.filter((f) => f.redeclared !== true)
@@ -4422,6 +4443,7 @@ export function collectClassShapeInner(
base,
...(genericBase ? { genericBase } : {}),
...(callableBase ? { callableBase } : {}),
...(extendsDate ? { extendsDate: true as const } : {}),
...(factoryBaseExpression ? { factoryBaseExpression } : {}),
subclasses: [],
throwingSetters,
@@ -5668,6 +5690,15 @@ export function lowerStaticFieldRead(
if (info.classDecorators?.valueGlobalId !== undefined) return null;
const loc = locOf(expr);
const found = findStaticOn(lowerer, info, expr.name.text);
// A Date static read as a value through a class declared `extends Date`:
// the statics lower only as calls.
if (
!found &&
dateRootOf(info) &&
["now", "parse", "UTC"].includes(expr.name.text) &&
!(ts.isCallExpression(expr.parent) && expr.parent.expression === expr)
)
lowerer.noLowering(`Date.${expr.name.text} read through a class that extends Date`, expr);
if (hasRuntimeStatics(info) && !found?.field)
return {
kind: "dynKeyGet",
@@ -6486,6 +6517,12 @@ function staticCallOn(
const loc = locOf(call);
const runtimeStatic = directRuntimeStaticCall(lowerer, call, access, info, throughValue);
if (runtimeStatic) return runtimeStatic;
// Date's statics through a class declared `extends Date`
// (`Moment.UTC(...)`), which inherits them.
if (dateRootOf(info) && !findStaticOn(lowerer, info, access.name.text)) {
const date = lowerDateStaticCall(lowerer, call, access.name.text);
if (date) return date;
}
if (
(hasRuntimeStatics(info) && !findStaticOn(lowerer, info, access.name.text)?.field) ||
(info.callableBase &&
@@ -7747,7 +7784,7 @@ function lowerClassCtorInner(lowerer: Lowerer, info: ClassInfo): IrFunction {
lowerer.ctx.argumentsLocal = local;
}
body.push(...declared.prologue);
if (!ctorBase && !info.callableBase) {
if (!ctorBase && !info.callableBase && !info.extendsDate) {
// Node's base-class order: field initializers run at the start
// of construction, the parameter-property assignments open the
// constructor body (probed — a field initializer reading a
@@ -7759,7 +7796,7 @@ function lowerClassCtorInner(lowerer: Lowerer, info: ClassInfo): IrFunction {
body.push(...lowerer.lowerDerivedCtorBody(info, thisLocal));
}
} else {
if (ctorBase || info.callableBase) {
if (ctorBase || info.callableBase || info.extendsDate) {
// Synthetic forwarding params (the inherited ABI signature).
// Nothing references them by symbol — only the super call below,
// which forwards the already-completed values UNCHANGED (defaults
@@ -8678,12 +8715,14 @@ export function lowerDerivedCtorBody(
loc,
},
];
if (info.callableBase) {
if (info.callableBase || info.extendsDate) {
if (superCall.arguments.nodes.some(ts.isSpreadElement))
lowerer.unsupported(
"SC1090",
superCall,
"spreading explicit super arguments into an ordinary constructor",
info.extendsDate
? "spreading super arguments into the Date constructor"
: "spreading explicit super arguments into an ordinary constructor",
);
const pack: IrExpr = {
kind: "dynArrLit",
@@ -8868,6 +8907,7 @@ export function superCallStmt(
type: { kind: "object", className: info.def.name },
loc,
};
if (info.extendsDate) return dateSuperCall(thisRef, info, args[0]!, loc);
if (info.callableBase)
return {
kind: "exprStmt",
@@ -8989,6 +9029,24 @@ export function lowerSuperMethodCall(
access: ts.PropertyAccessExpression,
): IrExpr {
const cls = lowerer.currentClass;
// `super.toISOString()` in a class declared `extends Date`.
if (cls?.extendsDate && !cls.base) {
const thisL = lowerer.resolveThis();
if (!thisL) lowerer.unsupported("SC1080", access);
const thisRef: IrExpr = {
kind: "varRef",
localId: thisL.id,
type: thisL.type,
loc: locOf(call),
};
const date = lowerDateSuperMethodCall(lowerer, call, access.name.text, cls, thisRef);
if (date) return date;
lowerer.unsupported(
"SC1090",
access,
`'super.${access.name.text}' (Date.prototype does not declare it)`,
);
}
if (!cls?.base) {
// tsc rejects super outside derived-class bodies first; defensive.
lowerer.unsupported("SC1090", access, "'super' outside a derived class");
@@ -9033,6 +9091,17 @@ export function lowerSuperMethodCall(
// `super.dispatchEvent(e)`, `super.preventDefault()`: the inherited
// runtime members over this method's own `this` (subclasses cannot
// override them, so the static answer is the runtime's).
if (dateRootOf(cls.base) && isDatePrototypeMethod(access.name.text)) {
const thisL = lowerer.resolveThis();
if (!thisL) lowerer.unsupported("SC1080", access);
const thisRef: IrExpr = {
kind: "varRef",
localId: thisL.id,
type: thisL.type,
loc: locOf(call),
};
return lowerDateSuperMethodCall(lowerer, call, access.name.text, cls.base, thisRef)!;
}
if (eventTargetApiMethod(cls.base, access.name.text)) {
const thisL = lowerer.resolveThis();
if (!thisL) lowerer.unsupported("SC1080", access);
@@ -3057,8 +3057,10 @@ export function lowerFieldCompound(
* returns the zero-capture closure over it. Typed sources use the mapped
* `(value: string) => primitive` signature; a recognized JavaScript
* concise-arrow selector (`() => String`, the picocolors fallback) takes
* dyn so its ordinary coercion signature survives inference residue. The
* dyn bodies run exact ToString/ToNumber/ToBoolean, including object hooks.
* dyn so its ordinary coercion signature survives inference residue, and a
* boxed constructor value takes dyn too (coerceToExpected). The dyn bodies
* run what the constructors do for any argument: String() (a symbol's
* description form included), ToNumber and ToBoolean, with object hooks.
* Interning makes every reference under one ABI the SAME immortal closure,
* so `opt.type === String` compares like JS function identity. */
export function primitiveCtorClosure(
@@ -3081,7 +3083,7 @@ export function primitiveCtorClosure(
? name === "BigInt"
? { kind: "libCall", fn: "dyn.bigintConstructor", args: [v], type: BIGINT_T, loc }
: name === "String"
? { kind: "libCall", fn: "dyn.toStringCoerce", args: [v], type: STRING, loc }
? { kind: "libCall", fn: "dyn.stringConstructor", args: [v], type: STRING, loc }
: name === "Number"
? { kind: "libCall", fn: "dyn.numberConstructor", args: [v], type: F64, loc }
: { kind: "dynTest", test: "truthy", value: v, type: BOOL, loc }
@@ -46,6 +46,7 @@ import type { ClassInfo } from "./lower-classes.js";
import { isSafeToDiscard, isSafeToRepeat } from "./expressions/evaluation-safety.js";
import { symbolFieldDisplayName } from "./symbol-fields.js";
import { eventTargetRootOf } from "./lower-event-target.js";
import { dateRootOf } from "./builtins/date-classes.js";
import { lowerSystemErrorMap } from "./lower-system-errors.js";
import { isPerfEntryType, perfEntryJson } from "./builtins/performance.js";
import { recordMayKeepSource, recordSourceTest } from "./record-sources.js";
@@ -805,8 +806,9 @@ function inspectSupport(
const info = lowerer.classes.get(t.className);
if (!info) return `class '${t.className}' has no inspect lowering`;
// EventTarget and Event subclasses render through Node's inspectors
// for those classes (the runtime's capsule view).
if (eventTargetRootOf(info) !== null) return null;
// for those classes (the runtime's capsule view), and so do classes
// declared `extends Date`.
if (eventTargetRootOf(info) !== null || dateRootOf(info) !== null) return null;
if (isErrorClass(lowerer, t.className)) {
if (userErrorClasses(info).length > 0 && errorHeaderOverridden(lowerer, info))
return "error subclasses whose name, message or stack is an accessor or a prototype property have no inspect lowering yet";
@@ -958,7 +960,10 @@ export function inspectExpr(
loc,
};
case "object":
if (eventTargetRootOf(lowerer.classes.get(t.className)) !== null) {
if (
eventTargetRootOf(lowerer.classes.get(t.className)) !== null ||
dateRootOf(lowerer.classes.get(t.className)) !== null
) {
return {
kind: "libCall",
fn: "insp.dyn",
@@ -149,6 +149,7 @@ import { forkTargetPaths } from "../fork-target.js";
import { workerTargetPaths } from "../worker-target.js";
import { isNativeProxyInitializer } from "./expressions/native-proxy.js";
import { handleEventReaderPrelude } from "./lower-event-target.js";
import { dateClassOperationPrelude } from "./builtins/date-classes.js";
import { cjsAccessorSnapshotStores, jsHoldsDynObject } from "./lower-exprs.js";
import { keptIdentityAssertion } from "./assertion-identity.js";
@@ -3383,6 +3384,7 @@ export function buildMain(lowerer: Lowerer): IrFunction {
...lowerer.mainPrelude,
...cjsModuleRegistryPrelude(lowerer, loc),
...handleEventReaderPrelude(lowerer, loc),
...dateClassOperationPrelude(lowerer, loc),
];
const roots = [lowerer.entry, ...lowerer.forkTargets, ...lowerer.workerTargets];
const isAsync = roots.some((root) => lowerer.asyncInitFiles.has(root));
@@ -3889,9 +3889,6 @@ function stdlibGlobalTokenDestructure(
if (decl.initializer === undefined || !isJsSourceFile(ts.getSourceFileOfNode(decl))) return null;
const globalName = stdlibGlobalNameOf(lowerer, decl.initializer);
if (globalName === null) return null;
// `workerData` is a value (the data the worker was started with), not an
// API surface: its members destructure as any untyped value's do.
if (globalName === "workerData") return null;
if (!ts.isObjectBindingPattern(decl.name)) {
lowerer.unsupported(
"SC1031",
@@ -6118,6 +6115,20 @@ export function jsBindingHasOpenWrites(lowerer: Lowerer, decl: ts.VariableDeclar
)
)
return true;
// A write that can store null or undefined into a binding the checker
// types as a primitive (`offset = match[3] || null`): JavaScript files
// are checked without strict null checks, so the declared type does not
// admit them.
// The declared type is mapped only when such a write exists.
if (
candidates.some(
(node) => node.operatorToken.kind === ts.SyntaxKind.EqualsToken && mayBeNullish(node.right),
)
) {
const primitive = lowerer.mapTypeOf(lowerer.typeOf(decl.name));
if (primitive?.kind === "string" || primitive?.kind === "f64" || primitive?.kind === "bool")
return true;
}
// A plain JavaScript write of a value the declared type cannot hold (the
// checker does not report it): buffer-crc32 replaces its number[] table
// with `new Int32Array(table)`. Only a different representation counts;
@@ -6148,6 +6159,31 @@ export function jsBindingHasOpenWrites(lowerer: Lowerer, decl: ts.VariableDeclar
});
}
/** Whether `node` can evaluate to null or undefined by its form: the
* literals, `void`, and conditional or logical expressions with such an
* operand in result position. */
function mayBeNullish(node: ts.Expression): boolean {
while (ts.isParenthesizedExpression(node)) node = node.expression;
if (node.kind === ts.SyntaxKind.NullKeyword || ts.isVoidExpression(node)) return true;
if (ts.isIdentifier(node)) return node.text === "undefined";
if (ts.isConditionalExpression(node))
return mayBeNullish(node.whenTrue) || mayBeNullish(node.whenFalse);
if (ts.isBinaryExpression(node)) {
const op = node.operatorToken.kind;
if (
op === ts.SyntaxKind.BarBarToken ||
op === ts.SyntaxKind.QuestionQuestionToken ||
op === ts.SyntaxKind.AmpersandAmpersandToken
)
return (
mayBeNullish(node.right) ||
(op === ts.SyntaxKind.AmpersandAmpersandToken && mayBeNullish(node.left))
);
if (op === ts.SyntaxKind.CommaToken) return mayBeNullish(node.right);
}
return false;
}
function immediatelyGuardedAbsenceProbe(lowerer: Lowerer, decl: ts.VariableDeclaration): boolean {
const guard = immediateAbsenceGuard(lowerer, decl);
// `if (!x)`: the falsy test sees a missing value.
@@ -7568,8 +7604,9 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm
// (closures box), the runtime sets the member on OBJ receivers
// and throws Node's TypeErrors on the rest. The receiver is
// PROBED (tryLowerExpression) and claimed exactly when it lowers to
// dyn; typed receivers keep their own lowerings and fences.
if (!expr.left.questionDotToken) {
// dyn; typed receivers keep their own lowerings and fences. A
// private name is the field of its class (fieldTarget).
if (!expr.left.questionDotToken && !ts.isPrivateIdentifier(expr.left.name)) {
const recv = tryLowerExpression(lowerer, expr.left.expression);
if (recv && recv.type.kind === "dyn") {
const loc = locOf(expr);
@@ -500,7 +500,8 @@ import {
lowerTracingChannelProperty,
} from "./builtins/async-context.js";
import { lowerDisposableStackNew } from "./builtins/disposable-stack.js";
import { lowerErrorCodeProperty } from "./builtins/errors.js";
import { lowerErrorCodeProperty, lowerErrorFunctionCall } from "./builtins/errors.js";
import { noteDateClassMethods } from "./builtins/date-classes.js";
import {
lowerProcessProperty,
lowerProcessMethodCall,
@@ -2959,6 +2960,10 @@ export class Lowerer {
/** Named method slots observed before class collection need a callable
* ABI and shared prototype descriptors instead of call-site specialization. */
readonly prototypeMethodAccesses = new Map<string, ts.Node>();
/** Whether the program reads `Error.prototype`, the checked-dynamic
* object an ES5 constructor function can inherit from: only then can a
* value that is not an Error instance be `instanceof Error`. */
errorPrototypeValues = false;
/** Well-known symbol methods (`sym:iterator`) the program may replace
* through a symbol-keyed write: an instance or prototype property, or a
* reflection call. */
@@ -3592,7 +3597,7 @@ export class Lowerer {
this.genericClassInstanceType(decl, ref, mapArgument),
runtimeClassValue: (type) => runtimeClassValueOfType(this, type),
localClassValue: (type) =>
this.instanceResultClass?.declared === type.id
this.instanceResultClass !== null && this.instanceResultClass.declared === type.id
? this.instanceResultClass.value
: (this.localClassValueTypes.get(type.id) ?? null),
mixinClassInstance: (decl) =>
@@ -8212,6 +8217,7 @@ export class Lowerer {
// turn queue more instances. Continue to the joint fixpoint; the initial
// queue above is the only portion whose discovery order differed from
// historical emit order.
noteDateClassMethods(this);
const classDispatch = new ClassDynamicDispatch(extraRoots !== undefined);
// Units in declaration order, so the passes below meet functions, and the
// classes they collect, in an order that does not depend on lowering order.
@@ -12848,6 +12854,25 @@ export class Lowerer {
);
}
/** withExpressionOverride for several nodes at once. */
withExpressionOverrides<T>(
overrides: readonly { node: ts.Expression; value: IrExpr }[],
lower: () => T,
): T {
const previous = overrides.map((override) => this.chainRecvByNode.get(override.node));
for (const override of overrides) this.chainRecvByNode.set(override.node, override.value);
try {
return lower();
} finally {
for (let i = overrides.length - 1; i >= 0; i--) {
const node = overrides[i]!.node;
const value = previous[i];
if (value) this.chainRecvByNode.set(node, value);
else this.chainRecvByNode.delete(node);
}
}
}
withExpressionOverride<T>(node: ts.Expression, value: IrExpr, lower: () => T): T {
const previous = this.chainRecvByNode.get(node);
this.chainRecvByNode.set(node, value);
@@ -12867,6 +12892,7 @@ export class Lowerer {
if (webAssembly !== null) return webAssembly;
refuseWebAssemblyUse(this, expr.expression);
return (
lowerErrorFunctionCall(this, expr) ??
lowerLifetimeRegionCall(this, expr) ??
lowerInternTableCall(this, expr) ??
lowerThreadSlotsCall(this, expr) ??
@@ -160,6 +160,60 @@ function brandCall(
return { kind: "call", callee: helper, args: [receiver, evaluation], type: resultType, loc };
}
/** The class whose body declares the private name `priv` (the nearest
* enclosing one, as JavaScript scoping resolves it), when it is a
* declaration-level class, or null. */
export function privateNameClass(lowerer: Lowerer, priv: ts.PrivateIdentifier): ClassInfo | null {
for (let node: ts.Node | undefined = priv.parent; node; node = node.parent) {
if (!ts.isClassDeclaration(node) && !ts.isClassExpression(node)) continue;
const declares = node.members.nodes.some((member) => {
const name = (member as { name?: ts.PropertyName }).name;
return name !== undefined && ts.isPrivateIdentifier(name) && name.text === priv.text;
});
if (!declares) continue;
const symbol = node.name ? lowerer.checker.getSymbolAtLocation(node.name) : undefined;
const info =
lowerer.currentClass?.decl === node
? lowerer.currentClass
: symbol
? lowerer.classBySymbol.get(symbol)
: undefined;
if (!info || info.localClass || info.generic || info.genericInstance) return null;
return info;
}
return null;
}
/** `value.#x` on a checked-dynamic `value` in a declaration-level class
* `info`: the receiver as an instance of `info` when the value is one (an
* instance of the class or a subclass), else Node's TypeError for a missing
* private member (also for undefined and null). */
export function checkedPrivateValue(
lowerer: Lowerer,
info: ClassInfo,
access: ts.PropertyAccessExpression,
receiver: IrExpr,
loc: SrcLoc,
): IrExpr {
const use: BrandUse = isPrivateWriteTarget(access) ? "write" : "read";
const className = info.def.name;
const resultType: IrType = { kind: "object", className };
const helper = `${PREFIX}${use}:${className}:${access.name.text}`;
if (!lowerer.valueHelpers.has(helper)) {
lowerer.liftedFns.push({
name: helper,
params: [{ localId: "value.0", name: "value", type: DYN }],
returnType: resultType,
locals: [{ id: "value.0", name: "value", type: DYN, mutable: false }],
// finalizePrivateBrands fills the body.
body: [],
loc: helperLoc(loc),
});
lowerer.valueHelpers.set(helper, helper);
}
return { kind: "call", callee: helper, args: [receiver], type: resultType, loc };
}
/** A plain assignment, a destructuring target, or a for-in/of target writes
* the field. Compound assignments and updates read it first, and Node
* reports the failed read. */
@@ -203,7 +257,9 @@ export function finalizePrivateBrands(lowerer: Lowerer, functions: IrFunction[])
const name = spec.slice(spec.lastIndexOf(":") + 1);
const loc = fn.loc;
const value = varRef("value.0", DYN, loc);
const evaluation = varRef("class.0", { kind: "classval", className }, loc);
// A declaration-level class has one brand: the helper takes no class.
const evaluation =
fn.params.length > 1 ? varRef("class.0", { kind: "classval", className }, loc) : null;
const ownerType: IrType = { kind: "object", className };
const body: IrStmt[] = [];
let matches = 0;
@@ -218,21 +274,23 @@ export function finalizePrivateBrands(lowerer: Lowerer, functions: IrFunction[])
const local = `receiver.${matches++}`;
fn.locals.push({ id: local, name: "receiver", type: ownerType, mutable: false });
const receiver = varRef(local, ownerType, loc);
const sameEvaluation: IrExpr = {
kind: "bin",
op: "===",
left: {
kind: "fieldGet",
obj: receiver,
className,
field: `%classEnvironment:${className}`,
type: evaluation.type,
loc,
},
right: evaluation,
type: BOOL,
loc,
};
const sameEvaluation: IrExpr = evaluation
? {
kind: "bin",
op: "===",
left: {
kind: "fieldGet",
obj: receiver,
className,
field: `%classEnvironment:${className}`,
type: evaluation.type,
loc,
},
right: evaluation,
type: BOOL,
loc,
}
: { kind: "boolLit", value: true, type: BOOL, loc };
const match = (typed: IrExpr): IrStmt[] => [
{ kind: "varDecl", localId: local, init: typed, loc },
{
@@ -2279,6 +2279,8 @@ export const AMBIENT_SURFACE_FNS: readonly AmbientSurfaceRow[] = [
"date.newLocal",
"date.nativeNewFields",
"date.nativeNewValue",
"date.argumentsTime",
"date.classOperation",
],
note: "zero arguments, one milliseconds/date-string argument, or local-time year/month fields; values are the read-only TimeClip scalar slice",
},
@@ -541,6 +541,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"date.nativeNew",
"date.nativeNewFields",
"date.nativeNewValue",
"date.argumentsTime",
"date.checkedValue",
"util.parseArgs",
"util.parseEnv",
@@ -608,6 +609,9 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"dyn.objCreateWithProperties",
"dyn.getPrototype",
"dyn.setPrototype",
// Error.captureStackTrace: Node's TypeError for a primitive target, and
// the error string conversion of name and message
"dyn.errorCaptureStackTrace",
// a proxy in the prototype chain can throw
"dyn.builtinInstanceOf",
"dyn.functionInstanceOf",
@@ -272,6 +272,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
"dyn.nativeMapIs": { argTypes: [DYN], result: BOOL },
"dyn.nativeUrlIs": { argTypes: [DYN], result: BOOL },
"dyn.nativeDateIs": { argTypes: [DYN], result: BOOL },
"dyn.nativeDateHandleIs": { argTypes: [DYN], result: BOOL },
/** A builtin constructor (`Object`, `Date`, `RegExp`, `Map`, `Set`,
* `Promise`, `WeakMap`, `WeakSet`) read as a JavaScript value: one
* interned function per name, the same one dynamic values answer for
@@ -291,6 +292,8 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
/** `new Date(value)` in JavaScript: a string argument parses with V8's
* full date parser; every other value constructs as date.nativeNew. */
"date.nativeNewValue": { argTypes: [DYN], result: DYN },
"date.argumentsTime": { argTypes: [DYN], result: DATE_T },
"date.classOperation": { argTypes: [DYN, DYN], result: VOID },
"date.checkedValue": { argTypes: [DYN], result: DATE_T },
"dyn.nativeRegexIs": { argTypes: [DYN], result: BOOL },
/** toString() on a checked-dynamic receiver: runtime kind dispatch
@@ -3670,6 +3673,10 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
"dyn.arrayPrototype": { argTypes: [], result: DYN },
"dyn.arrayConstructor": { argTypes: [], result: DYN },
"dyn.objectPrototype": { argTypes: [], result: DYN },
"dyn.errorPrototype": { argTypes: [], result: DYN },
"dyn.inheritsErrorPrototype": { argTypes: [DYN], result: BOOL },
"dyn.errorCaptureStackTrace": { argTypes: [DYN], result: DYN },
"dyn.errorCaptureStackTraceFunction": { argTypes: [], result: DYN },
"dyn.functionApply": { argTypes: [], result: DYN },
"dyn.builtinMethod": { argTypes: [STRING, STRING], result: DYN },
"dyn.classPrototype": { argTypes: [DYN, DYN], result: DYN },
+1
View File
@@ -349,6 +349,7 @@ const DYN_READ_ONLY = new Set<string>([
"date.nativeNew",
"date.nativeNewFields",
"date.nativeNewValue",
"date.argumentsTime",
// The dynamic operators run the ToPrimitive and coercion helpers of
// dyn.compare, dyn.numberConstructor and dyn.toStringCoerce, then build a
// fresh number, string or bigint. dyn.increment steps dyn.toNumeric's result.
+72 -1
View File
@@ -1,6 +1,10 @@
import { expect, test } from "vitest";
import { BOOL, F64, STRING, arrayOf, type IrExpr, type IrLocal, type IrStmt } from "./ir.js";
import { matchIntegerArrayForLoop, matchIntegerCountedForLoop } from "./integer-loops.js";
import {
matchIntegerArrayForLoop,
matchIntegerCountedForLoop,
matchIntegerPairedForLoop,
} from "./integer-loops.js";
const loc = { file: "integer-array-loop.ts", start: 0, end: 0 };
const number = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc });
@@ -355,3 +359,70 @@ test("a descending loop from an array's last index takes an integer counter for
range: { min: 0, max: 4294967294 },
});
});
test("paired counters closing in on each other run as integers", () => {
const vars = new Map(locals);
for (const id of ["lo", "hi", "head"]) vars.set(id, { id, name: id, type: F64, mutable: true });
const step = (id: string, op: "+" | "-", value = 1): IrStmt => ({
kind: "assign",
localId: id,
value: { kind: "bin", op, left: ref(id), right: number(value), type: F64, loc },
loc,
});
const element = (index: IrExpr): IrExpr => ({
kind: "arrayGet",
arr: { kind: "varRef", localId: "a", type: arrayOf(F64), loc },
index,
type: F64,
loc,
});
const paired = (
lowStart: IrExpr,
highStart: IrExpr,
update = [step("lo", "+"), step("hi", "-")],
) =>
({
kind: "for",
loc,
init: {
kind: "block",
body: [
{ kind: "varDecl", localId: "lo", init: lowStart, loc },
{ kind: "varDecl", localId: "hi", init: highStart, loc },
],
loc,
},
cond: { kind: "bin", op: "<", left: ref("lo"), right: ref("hi"), type: BOOL, loc },
update: { kind: "block", body: update, loc },
body: [{ kind: "exprStmt", expr: element(ref("hi")), loc }],
}) as IrStmt & { kind: "for" };
expect(matchIntegerPairedForLoop(paired(number(0), number(9)), vars)).toMatchObject({
low: { localId: "lo", step: 1, startGuarded: false },
high: { localId: "hi", step: -1, startGuarded: false },
guarded: false,
range: { min: 0, max: 9 },
});
// A start without a known range is tested at run time.
expect(matchIntegerPairedForLoop(paired(number(0), ref("head")), vars)).toMatchObject({
high: { startGuarded: true },
guarded: true,
});
// Counters moving the same way, written by the body, captured, started
// from each other, or never used as an index are refused.
expect(
matchIntegerPairedForLoop(
paired(number(0), number(9), [step("lo", "+"), step("hi", "+")]),
vars,
),
).toBeNull();
const writing = paired(number(0), number(9));
writing.body.push(step("hi", "-", 2));
expect(matchIntegerPairedForLoop(writing, vars)).toBeNull();
const boxed = new Map(vars);
boxed.set("hi", { ...vars.get("hi")!, boxed: true });
expect(matchIntegerPairedForLoop(paired(number(0), number(9)), boxed)).toBeNull();
expect(matchIntegerPairedForLoop(paired(number(0), ref("lo")), vars)).toBeNull();
const unused = paired(number(0), number(9));
unused.body = [];
expect(matchIntegerPairedForLoop(unused, vars)).toBeNull();
});
+114
View File
@@ -281,6 +281,120 @@ export function matchIntegerCountedForLoop(
};
}
/** A for loop whose two counters close in on each other:
*
* for (let lo = a, hi = b; lo < hi; lo++, hi--) { ... }
*
* The first counter only grows and the second only shrinks, by positive
* literal steps, the body writes neither, and the loop runs while the first
* is below (or at most) the second. Every body entry therefore holds both
* between the first start and the second start: exact integers when the
* starts are. A start without a proven integer range is tested at run time
* on a versioned loop, as a counted loop's is. */
export interface IntegerPairedCounter {
localId: string;
start: IrExpr;
/** The signed step of each update. */
step: number;
startGuarded: boolean;
}
export interface IntegerPairedForLoop {
low: IntegerPairedCounter;
high: IntegerPairedCounter;
inclusive: boolean;
guarded: boolean;
/** Both counters' values on every body entry. */
range: IntegerRange;
}
export function matchIntegerPairedForLoop(
stmt: IrStmt & { kind: "for" },
locals: ReadonlyMap<string, IrLocal>,
ranges: IntegerRanges = new Map(),
): IntegerPairedForLoop | null {
const init = stmt.init,
update = stmt.update,
cond = stmt.cond;
if (init?.kind !== "block" || init.body.length !== 2) return null;
if (update?.kind !== "block" || update.body.length !== 2) return null;
if (cond?.kind !== "bin" || (cond.op !== "<" && cond.op !== "<=")) return null;
if (cond.left.kind !== "varRef" || cond.right.kind !== "varRef") return null;
const lowId = cond.left.localId,
highId = cond.right.localId;
if (lowId === highId) return null;
const counter = (id: string): IntegerPairedCounter | null => {
const decl = init.body.find((d) => d.kind === "varDecl" && d.localId === id);
if (decl?.kind !== "varDecl" || !decl.init) return null;
const local = locals.get(id);
if (local?.type.kind !== "f64" || !local.mutable || local.boxed || local.tdz) return null;
const start = decl.init;
// Each start is evaluated once in the head and again on a fallback path.
if (
!stableLoopValue(start, locals, stmt.body, false, lowId) ||
!stableLoopValue(start, locals, stmt.body, false, highId)
)
return null;
const steps = update.body.map((u) => constantStep(u, id)).filter((v) => v !== null);
if (steps.length !== 1 || writesLocal(stmt.body, id)) return null;
const known =
start.kind === "numLit" && Number.isSafeInteger(start.value) && !Object.is(start.value, -0)
? { min: start.value, max: start.value }
: ranges.get(start);
const startGuarded = !known && start.kind !== "numLit" && start.type.kind === "f64";
if (!known && !startGuarded) return null;
if (known && (known.min < -Number.MAX_SAFE_INTEGER || known.max > Number.MAX_SAFE_INTEGER))
return null;
return { localId: id, start, step: steps[0]!, startGuarded };
};
const low = counter(lowId),
high = counter(highId);
if (!low || !high || low.step <= 0 || high.step >= 0) return null;
const startRange = (c: IntegerPairedCounter): IntegerRange =>
c.startGuarded
? { min: -Number.MAX_SAFE_INTEGER, max: Number.MAX_SAFE_INTEGER }
: c.start.kind === "numLit"
? { min: c.start.value, max: c.start.value }
: ranges.get(c.start)!;
const guarded = low.startGuarded || high.startGuarded;
// Only a loop whose counters index something gains from integer storage.
let useful = false,
cost = 0;
const isCounter = (e: IrExpr): boolean =>
(e.kind === "varRef" && (e.localId === lowId || e.localId === highId)) ||
(e.kind === "bin" &&
(e.op === "+" || e.op === "-") &&
(isCounter(e.left) || isCounter(e.right)));
const expr = (e: IrExpr): boolean => {
if (e.kind === "arrayGet" && isCounter(e.index)) useful = true;
if (e.kind === "strIntrinsic" && e.method === "charCodeAt" && e.args[0] && isCounter(e.args[0]))
useful = true;
if (e.kind === "bytesIntrinsic" && e.method === "get" && e.args[0] && isCounter(e.args[0]))
useful = true;
return (!guarded || ++cost <= 80) && everyExprChild(e, expr, bodyStmt);
};
const bodyStmt = (s: IrStmt): boolean => {
if ((s.kind === "arraySet" || s.kind === "bytesSet") && isCounter(s.index)) useful = true;
if (
guarded &&
(s.kind === "for" || s.kind === "forOf" || s.kind === "while" || s.kind === "doWhile")
)
return false;
return (!guarded || ++cost <= 80) && everyStmtChild(s, expr, bodyStmt);
};
if (!stmt.body.every(bodyStmt) || !useful) return null;
return {
low,
high,
inclusive: cond.op === "<=",
guarded,
// A loop whose starts already fail the condition never enters the body.
range: {
min: startRange(low).min,
max: Math.max(startRange(low).min, startRange(high).max),
},
};
}
/** True when a lowered subtree writes `localId`. Local ids are unique per
* function, so typed traversal includes writes
* nested in expressions, branches, nested loops, and try/finally bodies. */
+1
View File
@@ -72,6 +72,7 @@ const DYN_KIND_TESTS: ReadonlySet<string> = new Set([
"dyn.nativeMapIs",
"dyn.nativeUrlIs",
"dyn.nativeDateIs",
"dyn.nativeDateHandleIs",
"dyn.nativeRegexIs",
"dyn.dataViewIs",
"dyn.typeof",
@@ -0,0 +1,68 @@
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, validateModule } from "../src/index.js";
import type { IrFunction, IrModule, IrStmt } from "../src/ir/ir.js";
async function lower(source: string): Promise<IrModule> {
const dir = await mkdtemp(join(tmpdir(), "scriptc-bytes-callbacks-"));
try {
const entry = join(dir, "main.ts");
const outPath = join(dir, "main.ir.json");
await writeFile(entry, source);
const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" });
if (!result.ok)
throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
const mod = deserializeModule(await readFile(outPath, "utf8"));
expect(validateModule(mod)).toEqual([]);
return mod;
} finally {
await rm(dir, { recursive: true, force: true });
}
}
const helpers = (mod: IrModule, method: string): IrFunction[] =>
mod.functions.filter((f) => f.name.startsWith(`%bytes.${method}.`));
const visitLoop = (fn: IrFunction): IrStmt & { kind: "for" } => {
const loop = fn.body.find((s): s is IrStmt & { kind: "for" } => s.kind === "for");
if (!loop) throw new Error(`no loop in ${fn.name}`);
return loop;
};
test("literal callbacks get helpers of their own; function values share one", async () => {
const mod = await lower(`
const a = new Int32Array([1, 2, 3]);
const double = (v: number): number => v * 2;
console.log(a.map((v) => v + 1).join(","), a.map((v) => v - 1).join(","));
console.log(a.map(double).join(","), new Int32Array([4]).map(double).join(","));
`);
expect(helpers(mod, "map")).toHaveLength(3);
});
test("typed-array callback loops run on their own counter", async () => {
const mod = await lower(`
const a = new Float64Array([1.5, 2.5, 3.5]);
console.log(a.some((v) => v > 2), a.reduce((s, v) => s + v));
console.log(a.reduceRight((s, v) => s - v), a.findLastIndex((v) => v < 3));
`);
for (const method of ["some", "reduce"]) {
const loop = visitLoop(helpers(mod, method)[0]!);
// for (let i = <0 or 1 past reduce's seed>; i < a.length; i = i + 1)
expect(loop.init).toMatchObject({ kind: "varDecl", localId: "i.0", init: { kind: "numLit" } });
expect(loop.cond).toMatchObject({
kind: "bin",
op: "<",
right: { kind: "bytesIntrinsic", method: "length" },
});
}
expect(visitLoop(helpers(mod, "reduce")[0]!).init).toMatchObject({ init: { value: 1 } });
// Backward: from n - 2 past reduceRight's seed, and from n - 1.
expect(visitLoop(helpers(mod, "reduceRight")[0]!).init).toMatchObject({
init: { kind: "bin", op: "-", right: { value: 2 } },
});
expect(visitLoop(helpers(mod, "findLastIndex")[0]!).init).toMatchObject({
init: { kind: "bin", op: "-", right: { value: 1 } },
});
});
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6826-map-primitive-constructors.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6827-recursive-union-nested-literals.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6828-recursive-union-object-walkers.ts"
],
"diags": []
}
@@ -1,6 +0,0 @@
{
"order": [
"<repo>/tests/corpus/6870-error-constructor-values.js"
],
"diags": []
}
@@ -1,6 +0,0 @@
{
"order": [
"<repo>/tests/corpus/6871-worker-data-destructuring.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6999-typed-array-callback-loops.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7040-es5-error-constructor.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7041-error-called-without-new.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7042-string-method-names-on-objects.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7043-date-set-time.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7044-nested-empty-object-maps.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7045-instanceof-constructor-values.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7046-parse-float-extra-arguments.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7047-dynamic-string-index.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7048-unary-number-coercions.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7049-member-assignment-values.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7050-date-subclass.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7051-date-subclass-typed.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7052-date-subclass-dynamic.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7053-private-fields-untyped-receivers.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7054-let-bindings-with-null-writes.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7140-paired-loop-counters.ts"
],
"diags": []
}
+38 -3
View File
@@ -664,6 +664,40 @@ bool scr_dyn_native_url_is(const ScrDyn *value);
ScrDyn *scr_dyn_native_search_params(ScrSearchParams *value);
bool scr_dyn_native_search_params_is(const ScrDyn *value);
bool scr_dyn_native_date_is(const ScrDyn *value);
/* Instances of program classes declared `extends Date` (scr_lib.c). */
void scr_date_class_operation_install(ScrDyn *time, ScrDyn *methods);
/* Set by scr_date_class_operation_install; NULL in a program without such
* a class, where every wrapper below answers false or NULL. */
typedef struct {
bool (*time)(const ScrDyn *value, double *time);
ScrDyn *(*to_json)(const ScrDyn *value, const ScrDyn *key);
ScrDyn *(*to_string)(const ScrDyn *value);
ScrDyn *(*value_of)(const ScrDyn *value);
ScrStr *(*name)(const ScrDyn *value);
ScrDyn *(*method)(const ScrDyn *value, const char *key, size_t key_len);
} ScrDateClassHooks;
extern const ScrDateClassHooks *scr_date_class_hooks;
static inline bool scr_dyn_date_class_time(const ScrDyn *value, double *time) {
return scr_date_class_hooks && scr_date_class_hooks->time(value, time);
}
static inline ScrDyn *scr_dyn_date_class_to_json(const ScrDyn *value, const ScrDyn *key) {
return scr_date_class_hooks->to_json(value, key);
}
static inline ScrDyn *scr_dyn_date_class_to_string(const ScrDyn *value) {
return scr_date_class_hooks->to_string(value);
}
static inline ScrDyn *scr_dyn_date_class_value_of(const ScrDyn *value) {
return scr_date_class_hooks->value_of(value);
}
static inline ScrStr *scr_dyn_date_class_name(const ScrDyn *value) {
return scr_date_class_hooks ? scr_date_class_hooks->name(value) : NULL;
}
static inline ScrDyn *scr_dyn_date_class_method(const ScrDyn *value, const char *key, size_t key_len) {
return scr_date_class_hooks ? scr_date_class_hooks->method(value, key, key_len) : NULL;
}
bool scr_dyn_date_instance_is(const ScrDyn *value);
ScrDyn *scr_dyn_date_class_member(const ScrDyn *d, const char *key, size_t key_len);
double scr_dyn_date_arguments_time(const ScrDyn *arguments);
ScrDyn *scr_dyn_native_date_new(const ScrDyn *arguments);
ScrDyn *scr_dyn_native_date_new_fields(const ScrDyn *arguments);
ScrDyn *scr_dyn_native_date_new_value(const ScrDyn *arguments);
@@ -727,9 +761,6 @@ ScrDyn *scr_dyn_array_constructor(void);
/* The interned builtin constructor `name` (Object, Date, RegExp, Map, Set,
* Promise, WeakMap, WeakSet) as a dynamic function value: +1. */
ScrDyn *scr_dyn_global_constructor(const ScrStr *name);
/* The interned function value of an error constructor ("TypeError"), or
* NULL for other names. */
ScrDyn *scr_dyn_error_constructor(const char *name);
/* A builtin global function (setTimeout, setImmediate, process.nextTick, ...)
* as an interned dynamic function value: +1. */
ScrDyn *scr_dyn_builtin_function(const ScrStr *name);
@@ -1053,6 +1084,10 @@ ScrStr *scr_dyn_to_string(const ScrDyn *d, const ScrStr *enc);
* prototype chain has no toString (Node's answer). */
ScrStr *scr_dyn_to_string_method(const ScrDyn *d, const ScrStr *enc, const ScrStr *what);
ScrDyn *scr_dyn_object_prototype(void);
ScrDyn *scr_dyn_error_prototype(void);
bool scr_dyn_inherits_error_prototype(const ScrDyn *value);
ScrDyn *scr_dyn_error_capture_stack_trace(ScrDyn *target);
ScrDyn *scr_dyn_error_capture_stack_trace_function(void);
ScrDyn *scr_dyn_function_apply(void);
ScrDyn *scr_dyn_reflect_apply(ScrDyn *target, ScrDyn *receiver, ScrDyn *arguments_list);
ScrDyn *scr_dyn_apply_array_like(const ScrDyn *target, const ScrDyn *receiver, const ScrDyn *arguments, const char *what);
+10 -2
View File
@@ -845,6 +845,16 @@ static ScrDyn *scr_dyn_invoke_impl(
if (scr_event_ref_hooks &&
scr_event_ref_hooks->invoke(recv, method, args, argc, what, &fast))
return fast;
/* An inherited Date method of an instance of a class declared `extends
* Date`. */
ScrDyn *date_method = scr_dyn_date_class_member(recv, method, strlen(method));
if (date_method) {
scr_dyn_this_push_dyn(recv);
ScrDyn *result = scr_dyn_call(date_method, args, argc, what);
scr_dyn_this_pop();
scr_dyn_release(date_method);
return result;
}
ScrDyn *materialized = scr_dyn_typed_ref_materialize(recv);
if (scr_exc_pending()) {
scr_dyn_release(materialized);
@@ -1913,8 +1923,6 @@ static ScrDyn *dyn_function_constructor(void) {
ScrDyn *scr_dyn_global_constructor(const ScrStr *name) {
if (strcmp(name->data, dyn_s20) == 0) return dyn_function_constructor();
ScrDyn *error = scr_dyn_error_constructor(name->data);
if (error) return error;
ScrDyn *ctor = dyn_global_ctor(name->data);
if (!ctor) scr_trap(dyn_s3);
return ctor;
+34 -1
View File
@@ -1652,7 +1652,40 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) {
* view of its fields does not carry. */
const ScrStr *name = scr_dyn_typed_ref_class_name(d);
ScrStr *out;
if (name && materialized->kind == SCR_DYN_OBJ &&
double time;
ScrStr *date_class = materialized->kind == SCR_DYN_OBJ && scr_dyn_date_class_time(d, &time)
? scr_dyn_date_class_name(d) : NULL;
if (date_class) {
/* An instance of a class declared `extends Date`: the class name and
* the time as Date.prototype.toISOString formats it, then its own
* enumerable properties when it has any. */
ScrStr *iso = scr_insp_date(time);
InspBuf head = {0};
ib_bytes(&head, date_class->data, date_class->len);
scr_str_release(date_class);
ib_char(&head, ' ');
ib_bytes(&head, iso->data, iso->len);
scr_str_release(iso);
ScrStr *prefix = ib_take(&head);
bool fields = false;
for (size_t i = 0; i < materialized->v.obj.len && !fields; i++)
fields = materialized->v.obj.entries[i].enumerable;
if (!fields || recurse > depth) {
scr_dyn_release(materialized);
return prefix;
}
char *cls = malloc(prefix->len + 1);
if (!cls) scr_trap("scriptc: out of memory\n");
memcpy(cls, prefix->data, prefix->len);
cls[prefix->len] = '\0';
scr_str_release(prefix);
size_t nseen = g_nseen;
g_pending_seen = instance;
out = scr_insp_obj(materialized, recurse, depth, cls);
g_pending_seen = NULL;
if (g_nseen > nseen) g_nseen = nseen;
free(cls);
} else if (name && materialized->kind == SCR_DYN_OBJ &&
scr_dyn_obj_get(materialized, "%error", 6) == NULL) {
char *cls = malloc(name->len + 1);
if (!cls) scr_trap("scriptc: out of memory\n");
+317 -84
View File
@@ -37,7 +37,20 @@
static SCR_TL ScrDyn *scr_builtin_object_prototype;
SCR_TL const ScrHandleStreamHooks *scr_handle_stream_hooks;
const ScrDateClassHooks *scr_date_class_hooks = NULL;
/* The checked-dynamic Error.prototype (scr_dyn_error_prototype) is not the
* prototype of scriptc's Error instances, so a change to it would not reach
* them: every mutation throws. */
static SCR_TL ScrDyn *scr_builtin_error_prototype;
static bool scr_dyn_error_prototype_fence(const ScrDyn *value) {
if (!value || value != scr_builtin_error_prototype) return false;
static const char message[] = "Error.prototype mutation is not supported yet";
scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020");
return true;
}
SCR_TL const ScrHandleStreamHooks *scr_handle_stream_hooks = NULL;
/* Constructor capsules expose identity and name/length, but do not model
* per-class static property tables yet. Never report a fabricated empty view. */
@@ -1801,55 +1814,6 @@ static void scr_builtin_object_prototype_cleanup(void) {
scr_dyn_release(prototype);
}
/* The error constructors as values (an error's `constructor`, and the
* globals in JavaScript): one interned function per constructor and thread,
* indexed like scr_error_vts, with the constructor's name and length.
* Calling one through the value is not supported yet, as for the other
* interned constructors (scr_dyn_global_constructor). */
static const char *const scr_error_ctor_names[] = {
"Error", "TypeError", "RangeError", "SyntaxError", NULL,
"ReferenceError", "EvalError", "URIError", "AggregateError",
};
#define SCR_ERROR_CTORS (sizeof scr_error_ctor_names / sizeof scr_error_ctor_names[0])
static SCR_TL ScrDyn *scr_error_ctor_values[SCR_ERROR_CTORS];
static SCR_TL bool scr_error_ctors_registered;
static void scr_error_ctors_cleanup(void) {
for (size_t i = 0; i < SCR_ERROR_CTORS; i++) {
scr_dyn_release(scr_error_ctor_values[i]);
scr_error_ctor_values[i] = NULL;
}
}
static ScrDyn *scr_error_ctor_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) {
(void)args;
(void)argc;
char message[128];
int length = snprintf(message, sizeof message,
"calling the %s constructor through a stored value is not supported yet",
(const char *)closure->fn);
scr_throw_error_msg_code(SCR_ERR_ERROR, message, (size_t)length, "SC2020");
return NULL;
}
ScrDyn *scr_dyn_error_constructor(const char *name) {
for (size_t i = 0; i < SCR_ERROR_CTORS; i++) {
if (!scr_error_ctor_names[i] || strcmp(scr_error_ctor_names[i], name) != 0) continue;
if (!scr_error_ctors_registered) {
scr_error_ctors_registered = true;
scr_atexit(scr_error_ctors_cleanup);
}
if (!scr_error_ctor_values[i]) {
uint32_t length = strcmp(name, "AggregateError") == 0 ? 2 : 1;
scr_error_ctor_values[i] =
scr_dyn_new_func(scr_closure_new((void *)scr_error_ctor_names[i], 0), scr_error_ctor_call, length,
"native:constructor", scr_error_ctor_names[i]);
}
return scr_dyn_retain(scr_error_ctor_values[i]);
}
return NULL;
}
ScrDyn *scr_dyn_object_prototype(void) {
if (!scr_builtin_object_prototype) {
scr_builtin_object_prototype = scr_dyn_alloc(SCR_DYN_OBJ);
@@ -1867,6 +1831,149 @@ ScrDyn *scr_dyn_array_prototype_base(void) {
return scr_dyn_retain(scr_builtin_array_prototype);
}
/* Error.prototype as a checked-dynamic object, for JavaScript constructor
* functions that inherit from it (`F.prototype =
* Object.create(Error.prototype)`): non-enumerable `constructor`, `name`
* ("Error"), `message` ("") and `toString`, on Object.prototype. The Error
* constructor has no checked-dynamic value, so `constructor` is a function
* named Error that throws when called. */
static const ScrDyn *scr_dyn_proto_next(const ScrDyn *object);
static void scr_builtin_error_prototype_cleanup(void) {
ScrDyn *prototype = scr_builtin_error_prototype;
scr_builtin_error_prototype = NULL;
scr_dyn_release(prototype);
}
static ScrDyn *scr_error_prototype_to_string(ScrClosure *closure, ScrDyn *const *args, size_t argc) {
(void)closure; (void)args; (void)argc;
ScrDyn *self = scr_dyn_this_get();
ScrDyn *result = NULL;
if (self->kind == SCR_DYN_OBJ || self->kind == SCR_DYN_ARR || self->kind == SCR_DYN_FUNC) {
ScrStr *text = scr_dyn_error_string(self);
if (text) {
result = scr_dyn_new_str(text);
scr_str_release(text);
}
} else {
ScrJsonBuf message;
scr_jb_init(&message);
scr_jb_puts(&message, "Method Error.prototype.toString called on incompatible receiver ");
ScrStr *shown = self->kind == SCR_DYN_SYMBOL ? self->v.symbol.render(self->v.symbol.value)
: scr_dyn_string_coerce_js(self);
if (shown) {
scr_jb_put_str(&message, shown);
scr_str_release(shown);
}
scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&message));
}
scr_dyn_release(self);
return result;
}
static ScrDyn *scr_error_prototype_constructor(ScrClosure *closure, ScrDyn *const *args, size_t argc) {
(void)closure; (void)args; (void)argc;
static const char message[] = "calling Error.prototype.constructor is not supported yet";
scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020");
return NULL;
}
static void scr_error_prototype_hide(ScrDyn *object, const char *key, size_t key_len) {
ScrDynEntry *entry = scr_dyn_find_entry(object, key, key_len);
entry->enumerable = false;
}
ScrDyn *scr_dyn_error_prototype(void) {
if (!scr_builtin_error_prototype) {
ScrDyn *prototype = scr_dyn_alloc(SCR_DYN_OBJ);
scr_dyn_obj_set(prototype, "constructor", 11,
scr_dyn_new_func(scr_closure_new(NULL, 0), scr_error_prototype_constructor, 1, "native:Error", "Error"));
scr_error_prototype_hide(prototype, "constructor", 11);
ScrStr *name = scr_str_new("Error", 5);
scr_dyn_obj_set(prototype, "name", 4, scr_dyn_new_str(name));
scr_str_release(name);
scr_error_prototype_hide(prototype, "name", 4);
ScrStr *empty = scr_str_new("", 0);
scr_dyn_obj_set(prototype, "message", 7, scr_dyn_new_str(empty));
scr_str_release(empty);
scr_error_prototype_hide(prototype, "message", 7);
scr_dyn_obj_set(prototype, "toString", 8,
scr_dyn_new_func(scr_closure_new(NULL, 0), scr_error_prototype_to_string, 0, "native:Error", "toString"));
scr_error_prototype_hide(prototype, "toString", 8);
scr_builtin_error_prototype = prototype;
scr_atexit(scr_builtin_error_prototype_cleanup);
}
return scr_dyn_retain(scr_builtin_error_prototype);
}
/* Error.captureStackTrace(target): defines a non-enumerable own `stack`
* on target. Node's value is the Error.prototype.toString text of target
* (read without calling an overriding toString) followed by stack frames;
* here, as for scriptc's own Error instances, it is the text alone.
* Answers undefined, or NULL with the exception pending. */
ScrDyn *scr_dyn_error_capture_stack_trace(ScrDyn *target) {
/* An instance of a class that extends Error derives its stack from its
* name and message when the stack is read. */
if (target->kind == SCR_DYN_TYPED_REF) {
ScrDyn *view = scr_dyn_typed_ref_materialize(target);
bool error = view && view->kind == SCR_DYN_OBJ && scr_dyn_obj_get(view, "%error", 6) != NULL;
scr_dyn_release(view);
if (scr_exc_pending()) return NULL;
if (error) return scr_dyn_retain(scr_dyn_undefined());
}
if (target->kind != SCR_DYN_OBJ) {
if (target->kind == SCR_DYN_ARR || target->kind == SCR_DYN_FUNC || target->kind == SCR_DYN_TYPED_REF ||
target->kind == SCR_DYN_HANDLE || target->kind == SCR_DYN_PROXY) {
static const char message[] = "Error.captureStackTrace on this object is not supported yet";
scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020");
} else {
static const char message[] = "invalid_argument";
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
}
return NULL;
}
ScrDynEntry *existing = scr_dyn_find_entry(target, "stack", 5);
if (existing ? existing->accessor || !existing->configurable : target->non_extensible) {
static const char message[] = "Cannot redefine property: stack";
static const char extensible[] = "Cannot define property stack, object is not extensible";
if (existing) scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
else scr_throw_error_msg(SCR_ERR_TYPE, extensible, sizeof extensible - 1);
return NULL;
}
ScrStr *text = scr_dyn_error_string(target);
if (!text) return NULL;
scr_dyn_obj_set_field(target, "stack", 5, scr_dyn_new_str(text));
scr_str_release(text);
ScrDynEntry *entry = scr_dyn_find_entry(target, "stack", 5);
entry->enumerable = false;
entry->writable = entry->configurable = true;
return scr_dyn_retain(scr_dyn_undefined());
}
static ScrDyn *scr_error_capture_stack_trace_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) {
(void)closure;
return scr_dyn_error_capture_stack_trace(argc > 0 ? args[0] : scr_dyn_undefined());
}
/* Error.captureStackTrace read as a value: one function per thread. */
static SCR_TL ScrDyn *scr_error_capture_stack_trace_fn;
static void scr_error_capture_stack_trace_fn_cleanup(void) {
ScrDyn *function = scr_error_capture_stack_trace_fn;
scr_error_capture_stack_trace_fn = NULL;
scr_dyn_release(function);
}
ScrDyn *scr_dyn_error_capture_stack_trace_function(void) {
if (!scr_error_capture_stack_trace_fn) {
scr_error_capture_stack_trace_fn = scr_dyn_new_func(scr_closure_new(NULL, 0),
scr_error_capture_stack_trace_call, 2, "native:Error", "captureStackTrace");
scr_atexit(scr_error_capture_stack_trace_fn_cleanup);
}
return scr_dyn_retain(scr_error_capture_stack_trace_fn);
}
/* `value instanceof Error` for a value that is not an Error instance: true
* when Error.prototype is on its prototype chain. */
bool scr_dyn_inherits_error_prototype(const ScrDyn *value) {
if (!scr_builtin_error_prototype || !value || value->kind != SCR_DYN_OBJ) return false;
for (const ScrDyn *current = scr_dyn_proto_next(value); current; current = scr_dyn_proto_next(current)) {
if (current == scr_builtin_error_prototype) return true;
}
return false;
}
ScrDyn *scr_dyn_new_arr(void) { return scr_dyn_alloc(SCR_DYN_ARR); }
ScrDyn *scr_dyn_new_obj(void) { return scr_dyn_alloc(SCR_DYN_OBJ); }
ScrDyn *scr_dyn_new_obj_with_identity(
@@ -1898,7 +2005,6 @@ ScrDyn *scr_dyn_new_obj_null_proto(void) {
}
static const ScrDyn *scr_dyn_chain_link(const ScrDyn *link);
static const ScrDyn *scr_dyn_proto_next(const ScrDyn *object);
/* A typed capsule can be a prototype when its checked view is an object. */
static bool scr_dyn_typed_ref_prototype(const ScrDyn *value) {
@@ -1993,7 +2099,7 @@ ScrDyn *scr_dyn_set_prototype(ScrDyn *object, ScrDyn *prototype) {
object->kind == SCR_DYN_BIGINT || object->kind == SCR_DYN_SYMBOL) {
return scr_dyn_prototype_valid(prototype) ? scr_dyn_retain(object) : NULL;
}
if (scr_dyn_class_reflection_fence(object) || scr_dyn_class_reflection_fence(prototype)) return NULL;
if (scr_dyn_class_reflection_fence(object) || scr_dyn_error_prototype_fence(object) || scr_dyn_class_reflection_fence(prototype)) return NULL;
if (object->kind == SCR_DYN_TYPED_REF) {
ScrDyn *view = scr_dyn_typed_ref_materialize(object);
if (scr_exc_pending()) { scr_dyn_release(view); return NULL; }
@@ -2109,7 +2215,7 @@ static ScrDyn *scr_dyn_fn_existing_table(const ScrDyn *function) {
}
ScrDyn *scr_dyn_prevent_extensions(ScrDyn *object) {
if (scr_dyn_class_reflection_fence(object)) return NULL;
if (scr_dyn_class_reflection_fence(object) || scr_dyn_error_prototype_fence(object)) return NULL;
if (object->kind == SCR_DYN_PROXY) { scr_dyn_proxy_unsupported("Object.preventExtensions"); return NULL; }
if (scr_dyn_class_integrity_fence(object, "Object.preventExtensions")) return NULL;
ScrDyn *table = scr_dyn_fn_integrity_table(object);
@@ -2140,7 +2246,7 @@ bool scr_dyn_is_extensible(ScrDyn *object) {
}
ScrDyn *scr_dyn_seal(ScrDyn *object) {
if (scr_dyn_class_reflection_fence(object)) return NULL;
if (scr_dyn_class_reflection_fence(object) || scr_dyn_error_prototype_fence(object)) return NULL;
if (object->kind == SCR_DYN_PROXY) { scr_dyn_proxy_unsupported("Object.seal"); return NULL; }
if (scr_dyn_class_integrity_fence(object, "Object.seal")) return NULL;
if (object->symbol_properties) scr_dyn_release(scr_dyn_seal(object->symbol_properties));
@@ -3417,6 +3523,7 @@ const ScrStr *scr_dyn_typed_ref_class_name(const ScrDyn *d) {
return ops ? ops->class_name : NULL;
}
static bool scr_dyn_name_in_strs(const ScrStr *key, ScrStr *const *names, size_t count) {
for (size_t i = 0; i < count; i++) {
if (names[i] == key || scr_str_eq(names[i], (ScrStr *)key)) return true;
@@ -3508,6 +3615,18 @@ ScrDyn *scr_dyn_index_get(const ScrDyn *d, double index) {
}
ScrDyn *scr_dyn_typed_ref_string_view(const ScrDyn *d) {
/* An instance of a class declared `extends Date` converts through its
* toString, declared or inherited. With an exception pending the view is
* the empty string. */
double time;
if (scr_dyn_date_class_time(d, &time)) {
ScrDyn *text = scr_dyn_date_class_to_string(d);
if (text) return text;
ScrStr *empty = scr_str_new("", 0);
ScrDyn *out = scr_dyn_new_str(empty);
scr_str_release(empty);
return out;
}
ScrDyn *view = scr_dyn_typed_ref_materialize(d);
const char *tag = scr_event_ref_hooks ? scr_event_ref_hooks->string_tag(d) : NULL;
if (!tag || view->kind != SCR_DYN_OBJ || scr_dyn_property_owner(view, "toString", 8)) return view;
@@ -3518,12 +3637,28 @@ ScrDyn *scr_dyn_typed_ref_string_view(const ScrDyn *d) {
return out;
}
/* An inherited Date method of an instance of a class declared `extends
* Date`, when the instance's own members do not define `key`: +1, or
* NULL. */
ScrDyn *scr_dyn_date_class_member(const ScrDyn *d, const char *key, size_t key_len) {
ScrDyn *method = scr_dyn_date_class_method(d, key, key_len);
if (!method) return NULL;
ScrDyn *view = scr_dyn_typed_ref_materialize(d);
bool declared = view && !scr_exc_pending() && scr_dyn_property_owner(view, key, key_len);
scr_dyn_release(view);
if (!declared) return method;
scr_dyn_release(method);
return NULL;
}
ScrDyn *scr_dyn_typed_ref_key_get(const ScrDyn *d, const ScrStr *key) {
if (d->v.typed_ref.record) return scr_typed_ref_record_get(d, key);
if (scr_event_ref_hooks) {
ScrDyn *inherited = scr_event_ref_hooks->get(d, key);
if (inherited) return inherited;
}
ScrDyn *date_method = scr_dyn_date_class_member(d, key->data, key->len);
if (date_method) return date_method;
if (!scr_dyn_typed_ref_array_ops(d)) return NULL;
if (scr_typed_ref_is_length(key)) return scr_dyn_new_num((double)((ScrArr *)d->v.typed_ref.ptr)->len);
size_t index;
@@ -3759,6 +3894,32 @@ ScrDyn *scr_array_buffer_constructor(void) {
return scr_dyn_retain(scr_array_buffer_ctor);
}
/* `value instanceof C` for C a global constructor held as a value
* (`{ instanceOf: Date }`, scr_dyn_global_constructor): 1 or 0 by the
* value's native kind, or -1 when callee is not one of them, or for Object
* and for class instances, which these kinds cannot answer (except Date,
* whose subclasses' instances are known). A typed record or array is none
* of these kinds. */
static int scr_dyn_global_constructor_instanceof(const ScrDyn *value, const ScrDyn *callee) {
if (!callee->v.fn.sig || !callee->v.fn.name || strcmp(callee->v.fn.sig, "native:constructor") != 0 ||
strcmp(callee->v.fn.name, "Object") == 0)
return -1;
/* A native Date, or an instance of a class declared `extends Date`. */
if (strcmp(callee->v.fn.name, "Date") == 0) return scr_dyn_date_instance_is(value);
if (value->kind == SCR_DYN_TYPED_REF)
return strncmp(value->v.typed_ref.type_key, "record:", 7) == 0 || scr_dyn_ref_is_array(value) ? 0 : -1;
static const struct { const char *name; int tag; } kinds[] = {
{"Date", SCR_DYNH_DATE}, {"RegExp", SCR_DYNH_REGEXP}, {"Map", SCR_DYNH_MAP}, {"Set", SCR_DYNH_SET},
{"WeakMap", SCR_DYNH_WEAK_MAP}, {"WeakSet", SCR_DYNH_WEAK_SET},
};
if (strcmp(callee->v.fn.name, "Promise") == 0) return value->kind == SCR_DYN_PROMISE;
for (size_t i = 0; i < sizeof kinds / sizeof kinds[0]; i++) {
if (strcmp(callee->v.fn.name, kinds[i].name) == 0)
return value->kind == SCR_DYN_HANDLE && (int)value->v.handle.tag == kinds[i].tag;
}
return -1;
}
bool scr_bytes_instanceof(const ScrDyn *value, const ScrDyn *callee) {
if (!callee || callee->kind != SCR_DYN_FUNC) {
static const char message[] = "Right-hand side of 'instanceof' is not callable";
@@ -3771,6 +3932,38 @@ bool scr_bytes_instanceof(const ScrDyn *value, const ScrDyn *callee) {
if (scr_bytes_ctors[i] && callee->v.fn.clo == scr_bytes_ctors[i]->v.fn.clo)
return scr_dyn_typed_array_is(value, (int)i);
}
int global = scr_dyn_global_constructor_instanceof(value, callee);
if (global >= 0) return global;
/* An ordinary constructor function (an untyped JavaScript function whose
* prototype is an object): OrdinaryHasInstance walks the value's
* prototype chain for callee.prototype. */
{
ScrStr *name = scr_str_new("prototype", 9);
ScrDyn *key = scr_dyn_new_str(name);
scr_str_release(name);
ScrDyn *prototype = scr_dyn_reflect_get((ScrDyn *)callee, key, (ScrDyn *)callee);
scr_dyn_release(key);
if (!prototype) return false; /* the read threw: pending */
if (prototype->kind == SCR_DYN_OBJ) {
bool found = false;
if (value->kind == SCR_DYN_OBJ || value->kind == SCR_DYN_ARR || value->kind == SCR_DYN_FUNC) {
ScrDyn *current = scr_dyn_get_prototype((ScrDyn *)value);
while (current && current->kind != SCR_DYN_NULL) {
if (current == prototype) {
found = true;
break;
}
ScrDyn *next = scr_dyn_get_prototype(current);
scr_dyn_release(current);
current = next;
}
if (current) scr_dyn_release(current);
}
scr_dyn_release(prototype);
return found;
}
scr_dyn_release(prototype);
}
static const char message[] = "instanceof against this native constructor is not supported yet";
scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC1090");
return false;
@@ -6243,6 +6436,7 @@ void scr_dyn_symbol_key_placeholder(ScrDyn *value, ScrDyn *key) {
}
void scr_dyn_symbol_key_set(ScrDyn *value, ScrDyn *key, ScrDyn *stored) {
if (scr_dyn_error_prototype_fence(value)) return;
ScrDyn *receiver = scr_dyn_symbol_receiver(value);
if (!receiver) {
if (!scr_exc_pending()) {
@@ -6393,6 +6587,7 @@ static bool scr_dyn_fn_property_fence(const ScrStr *key) {
}
ScrDyn *scr_dyn_define_property(ScrDyn *target, ScrDyn *key, ScrDyn *descriptor) {
if (scr_dyn_error_prototype_fence(target)) return NULL;
if (target->kind == SCR_DYN_TYPED_REF) {
ScrDyn *view = scr_dyn_typed_ref_materialize(target);
ScrDyn *defined = scr_exc_pending() ? NULL : scr_dyn_define_property(view, key, descriptor);
@@ -7756,17 +7951,6 @@ ScrStr *scr_dyn_to_string(const ScrDyn *d, const ScrStr *enc) {
return out;
}
case SCR_DYN_FUNC: {
/* The interned builtin constructors print with their names, as in Node
* (function Map() { [native code] }); other functions without a source
* text print the anonymous native form. */
if (d->v.fn.sig && strcmp(d->v.fn.sig, "native:constructor") == 0 && d->v.fn.name) {
ScrJsonBuf b;
scr_jb_init(&b);
scr_jb_puts(&b, "function ");
scr_jb_puts(&b, d->v.fn.name);
scr_jb_puts(&b, "() { [native code] }");
return scr_jb_finish(&b);
}
static const char f[] = "function () { [native code] }";
return scr_str_new(f, sizeof f - 1);
}
@@ -7789,6 +7973,9 @@ ScrStr *scr_dyn_to_string(const ScrDyn *d, const ScrStr *enc) {
}
}
static ScrStr *scr_dyn_to_string_method_args(const ScrDyn *d, const ScrStr *enc, const ScrStr *what,
ScrDyn *const *args, size_t argc);
/* The METHOD-CALL spelling `d.toString(enc?)` — scr_dyn_to_string with
* the one receiver whose prototype LACKS the method carved out: a
* null-prototype dictionary (Object.create(null)) has no toString at
@@ -7818,7 +8005,10 @@ ScrStr *scr_dyn_to_string_argument(const ScrDyn *d, const ScrDyn *argument, cons
scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&b));
return scr_str_new("", 0);
}
return scr_dyn_to_string_method(d, NULL, what);
/* A toString the program defines receives the argument
* (`error.toString(compact)`); undefined passes none. */
ScrDyn *args[1] = {(ScrDyn *)argument};
return scr_dyn_to_string_method_args(d, NULL, what, args, argument->kind == SCR_DYN_UNDEF ? 0 : 1);
}
/* The METHOD-CALL spelling `d.valueOf()`: a primitive answers itself (the
@@ -8058,7 +8248,12 @@ bool scr_dyn_number_coerce_js(const ScrDyn *d, double *out) {
return false;
}
if (d->kind == SCR_DYN_TYPED_REF) {
ScrDyn *materialized = scr_dyn_typed_ref_materialize(d);
/* An instance of a class declared `extends Date` converts through its
* valueOf, declared or inherited. */
double time;
ScrDyn *materialized = scr_dyn_date_class_time(d, &time) ? scr_dyn_date_class_value_of(d)
: scr_dyn_typed_ref_materialize(d);
if (!materialized) return false;
bool ok = scr_dyn_number_coerce_js(materialized, out);
scr_dyn_release(materialized);
return ok;
@@ -8161,9 +8356,18 @@ double scr_dyn_number_coerce(const ScrDyn *d) {
static ScrDyn *scr_dyn_add_primitive(const ScrDyn *d);
/* ToPrimitive with the number hint, which differs from the default hint
* only for a Date: its time value. */
* only for a Date: its time value, or the valueOf of an instance of a class
* declared `extends Date`. */
static ScrDyn *scr_dyn_number_primitive(const ScrDyn *d) {
return scr_dyn_is_date(d) ? scr_dyn_date_value_of(d) : scr_dyn_add_primitive(d);
if (scr_dyn_is_date(d)) return scr_dyn_date_value_of(d);
double time;
if (d->kind == SCR_DYN_TYPED_REF && scr_dyn_date_class_time(d, &time)) {
ScrDyn *value = scr_dyn_date_class_value_of(d);
ScrDyn *out = value ? scr_dyn_add_primitive(value) : NULL;
scr_dyn_release(value);
return out;
}
return scr_dyn_add_primitive(d);
}
double scr_dyn_number_constructor(const ScrDyn *d) {
@@ -8224,6 +8428,15 @@ static ScrDyn *scr_dyn_add_primitive(const ScrDyn *d) {
return NULL;
}
if (d->kind == SCR_DYN_TYPED_REF) {
/* A Date's default hint is string: an instance of a class declared
* `extends Date` adds through its toString. */
double time;
if (scr_dyn_date_class_time(d, &time)) {
ScrDyn *text = scr_dyn_date_class_to_string(d);
ScrDyn *out = text ? scr_dyn_add_primitive(text) : NULL;
scr_dyn_release(text);
return out;
}
ScrDyn *view = scr_dyn_typed_ref_materialize(d);
ScrDyn *out = scr_exc_pending() ? NULL : scr_dyn_add_primitive(view);
scr_dyn_release(view);
@@ -8643,7 +8856,7 @@ void scr_dyn_arr_named_put(ScrDyn *arr, const char *key, size_t key_len, ScrDyn
}
void scr_dyn_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value) {
if (scr_dyn_class_reflection_fence(recv)) return;
if (scr_dyn_class_reflection_fence(recv) || scr_dyn_error_prototype_fence(recv)) return;
if (recv->kind == SCR_DYN_PROXY) { scr_dyn_proxy_set(recv, key, value); return; }
if (recv->kind == SCR_DYN_TYPED_REF) {
if (scr_typed_ref_array_set(recv, key, value)) return;
@@ -10471,7 +10684,7 @@ static void scr_json_delete_member(ScrDyn *object, const ScrStr *key) {
* arrays, typed references and handles keep their explicit boundary;
* deleting from a materialized snapshot would lose the mutation. */
void scr_dyn_key_delete(ScrDyn *recv, const ScrStr *key, bool strict) {
if (scr_dyn_class_reflection_fence(recv)) return;
if (scr_dyn_class_reflection_fence(recv) || scr_dyn_error_prototype_fence(recv)) return;
if (recv->kind == SCR_DYN_PROXY) { scr_dyn_proxy_delete(recv, key); return; }
if (recv->kind == SCR_DYN_TYPED_REF && recv->v.typed_ref.commit &&
(!strncmp(recv->v.typed_ref.type_key, "record:", 7) ||
@@ -10697,6 +10910,16 @@ static bool scr_json_live_container(const ScrDyn *value, bool inspect) {
* method, called with the key. Returns the value to serialize (+1), or NULL
* with an exception pending. */
static ScrDyn *scr_json_to_json(ScrDyn *value, const ScrStr *key) {
/* An instance of a class declared `extends Date` converts through its
* toJSON, declared or inherited. */
double time;
if (value->kind == SCR_DYN_TYPED_REF && scr_dyn_date_class_time(value, &time)) {
ScrDyn *name = scr_dyn_new_str((ScrStr *)key);
ScrDyn *converted = scr_dyn_date_class_to_json(value, name);
scr_dyn_release(name);
scr_dyn_release(value);
return converted;
}
/* Inspect a typed reference without replacing its live identity. Later
* members must read the source again after a callback mutates it. A live
* array, or a live record without a `toJSON` field, needs no conversion. */
@@ -11720,18 +11943,21 @@ static ScrDyn *scr_dyn_obj_read_receiver(const ScrDyn *d, const char *key, size_
ScrDyn *own = scr_dyn_obj_get(d, key, key_len);
if (own) return scr_dyn_retain(own);
if (key_len == 11 && memcmp(key, "constructor", 11) == 0) {
/* An error's constructor: the interned function value its global
* reads as (scr_dyn_error_constructor). */
static const char *const names[] = {
"Error", "TypeError", "RangeError", "SyntaxError", NULL,
"ReferenceError", "EvalError", "URIError", "AggregateError",
static const char *const tokens[] = {
"[builtin Error]", "[builtin TypeError]",
"[builtin RangeError]", "[builtin SyntaxError]",
NULL, "[builtin ReferenceError]", "[builtin EvalError]", "[builtin URIError]",
"[builtin AggregateError]",
};
size_t hit = scr_errdyn_find(d, false);
const ScrVt *vt = hit == SIZE_MAX ? NULL : scr_errdyn_cache[hit].err->vt;
for (size_t kind = 0; vt && kind < sizeof names / sizeof names[0]; kind++) {
if (!names[kind]) continue;
for (size_t kind = 0; vt && kind < sizeof tokens / sizeof tokens[0]; kind++) {
if (!tokens[kind]) continue;
if (vt != &scr_error_vts[kind]) continue;
return scr_dyn_error_constructor(names[kind]);
ScrStr *token = scr_str_new(tokens[kind], strlen(tokens[kind]));
ScrDyn *result = scr_dyn_new_str(token);
scr_str_release(token);
return result;
}
}
return scr_dyn_retain(scr_dyn_undefined());
@@ -12886,7 +13112,7 @@ static void scr_dyn_assign_from(ScrDyn *target, const ScrDyn *src, bool define)
* nothing; index-keyed sources (arrays/strings/bytes) copy their index
* keys like Node; the remaining kinds have no own enumerable keys. */
ScrDyn *scr_dyn_assign(ScrDyn *target, const ScrDyn *src) {
if (scr_dyn_class_reflection_fence(target) || scr_dyn_class_reflection_fence(src)) return NULL;
if (scr_dyn_class_reflection_fence(target) || scr_dyn_error_prototype_fence(target) || scr_dyn_class_reflection_fence(src)) return NULL;
if (target->kind == SCR_DYN_UNDEF || target->kind == SCR_DYN_NULL) {
const char *m = "Cannot convert undefined or null to object";
scr_throw_error_msg(SCR_ERR_TYPE, m, strlen(m));
@@ -14680,7 +14906,10 @@ ScrMap *scr_dyn_native_collection_check(const ScrDyn *value, int map, const char
return scr_map_retain(value->v.handle.ptr);
}
ScrStr *scr_dyn_to_string_method(const ScrDyn *d, const ScrStr *enc, const ScrStr *what) {
/* scr_dyn_to_string_method, passing `args` to a toString the receiver's
* chain defines. */
static ScrStr *scr_dyn_to_string_method_args(const ScrDyn *d, const ScrStr *enc, const ScrStr *what,
ScrDyn *const *args, size_t argc) {
/* Object.create receivers must run their inherited formatter before the
* default object tag; Effect shard keys use it to produce distinct hashes. */
const bool object_method = d->kind == SCR_DYN_OBJ && scr_dyn_property_owner(d, "toString", 8);
@@ -14689,7 +14918,7 @@ ScrStr *scr_dyn_to_string_method(const ScrDyn *d, const ScrStr *enc, const ScrSt
if (scr_exc_pending()) { scr_dyn_release(member); return NULL; }
if (member && (object_method || member->kind != SCR_DYN_UNDEF)) {
scr_dyn_this_push_dyn(d);
ScrDyn *value = scr_dyn_call(member, NULL, 0, what->data);
ScrDyn *value = scr_dyn_call(member, args, argc, what->data);
scr_dyn_this_pop();
scr_dyn_release(member);
if (!value) return NULL;
@@ -14711,8 +14940,12 @@ ScrStr *scr_dyn_to_string_method(const ScrDyn *d, const ScrStr *enc, const ScrSt
return scr_dyn_to_string(d, enc);
}
ScrStr *scr_dyn_to_string_method(const ScrDyn *d, const ScrStr *enc, const ScrStr *what) {
return scr_dyn_to_string_method_args(d, enc, what, NULL, 0);
}
ScrDyn *scr_dyn_freeze(ScrDyn *value) {
if (scr_dyn_class_reflection_fence(value)) return NULL;
if (scr_dyn_class_reflection_fence(value) || scr_dyn_error_prototype_fence(value)) return NULL;
if (scr_dyn_class_integrity_fence(value, "Object.freeze")) return NULL;
ScrDyn *table = scr_dyn_fn_integrity_table(value);
if (!table) return scr_dyn_freeze_data(value);
@@ -14723,7 +14956,7 @@ ScrDyn *scr_dyn_freeze(ScrDyn *value) {
}
ScrDyn *scr_dyn_freeze_data(ScrDyn *value) {
if (scr_dyn_class_reflection_fence(value)) return NULL;
if (scr_dyn_class_reflection_fence(value) || scr_dyn_error_prototype_fence(value)) return NULL;
if (scr_dyn_class_integrity_fence(value, "Object.freeze")) return NULL;
if (value->symbol_properties) scr_dyn_release(scr_dyn_freeze_data(value->symbol_properties));
if (value->kind == SCR_DYN_ARR) {
+170 -8
View File
@@ -13008,9 +13008,116 @@ bool scr_dyn_native_date_is(const ScrDyn *value) {
return value && value->kind == SCR_DYN_HANDLE && value->v.handle.tag == SCR_DYNH_DATE;
}
/* Instances of program classes declared `extends Date` keep their time
* value in a compiled field. In checked-dynamic code such an instance is a
* typed reference, and the runtime reaches its time value and methods
* through the functions %main installs (builtins/date-classes.ts), called
* as (op, value, argument): for op 0 the time function answers the time
* value, or undefined for any other value, and for op 1 sets it; for ops 2
* to 5 the method function calls the instance's toISOString,
* toJSON(argument), toString and valueOf. */
enum {
SCR_DATE_CLASS_TIME,
SCR_DATE_CLASS_SET_TIME,
SCR_DATE_CLASS_TO_ISO_STRING,
SCR_DATE_CLASS_TO_JSON,
SCR_DATE_CLASS_TO_STRING,
SCR_DATE_CLASS_VALUE_OF,
SCR_DATE_CLASS_NAME,
};
static SCR_TL ScrDyn *scr_date_class_operation;
static SCR_TL ScrDyn *scr_date_class_methods;
static void scr_date_class_operation_cleanup(void) {
ScrDyn *operation = scr_date_class_operation;
ScrDyn *methods = scr_date_class_methods;
scr_date_class_operation = scr_date_class_methods = NULL;
scr_dyn_release(operation);
scr_dyn_release(methods);
}
static bool scr_date_class_time_of(const ScrDyn *value, double *time);
static ScrDyn *scr_date_class_to_json(const ScrDyn *value, const ScrDyn *key);
static ScrDyn *scr_date_class_to_string(const ScrDyn *value);
static ScrDyn *scr_date_class_value_of(const ScrDyn *value);
static ScrStr *scr_date_class_name(const ScrDyn *value);
static ScrDyn *scr_date_class_method(const ScrDyn *value, const char *key, size_t key_len);
/* The installation also publishes these functions to the rest of the
* runtime (scr_date_class_hooks), so only a program that declares a class
* extending Date links them. */
static const ScrDateClassHooks scr_date_class_hook_table = {
scr_date_class_time_of, scr_date_class_to_json, scr_date_class_to_string,
scr_date_class_value_of, scr_date_class_name, scr_date_class_method,
};
void scr_date_class_operation_install(ScrDyn *time, ScrDyn *methods) {
scr_date_class_hooks = &scr_date_class_hook_table;
if (!scr_date_class_operation) scr_atexit(scr_date_class_operation_cleanup);
scr_dyn_release(scr_date_class_operation);
scr_dyn_release(scr_date_class_methods);
scr_date_class_operation = scr_dyn_retain(time);
scr_date_class_methods = scr_dyn_retain(methods);
}
/* The installed operation `op` on `value`: +1, or NULL with an exception
* pending. */
static ScrDyn *scr_date_class_call(int op, const ScrDyn *value, const ScrDyn *argument) {
ScrDyn *code = scr_dyn_new_num(op);
ScrDyn *args[] = { code, (ScrDyn *)value, (ScrDyn *)(argument ? argument : scr_dyn_undefined()) };
bool time = op < SCR_DATE_CLASS_TO_ISO_STRING || op == SCR_DATE_CLASS_NAME;
ScrDyn *result = scr_dyn_call(time ? scr_date_class_operation : scr_date_class_methods, args, 3, "Date");
scr_dyn_release(code);
return result;
}
/* True, with the time value in *time, when `value` holds an instance of a
* class declared `extends Date`. */
static bool scr_date_class_time_of(const ScrDyn *value, double *time) {
if (!scr_date_class_operation || !value || value->kind != SCR_DYN_TYPED_REF) return false;
ScrDyn *result = scr_date_class_call(SCR_DATE_CLASS_TIME, value, NULL);
bool found = result && result->kind == SCR_DYN_NUM;
if (found) *time = result->v.num;
scr_dyn_release(result);
return found;
}
/* `value instanceof Date`: a Date, or an instance of a class declared
* `extends Date`. */
bool scr_dyn_date_instance_is(const ScrDyn *value) {
double time;
return scr_dyn_native_date_is(value) || scr_dyn_date_class_time(value, &time);
}
/* toJSON(key), toString() or valueOf() of an instance of a class declared
* `extends Date` (scr_dyn_date_class_time answered true): the class's
* declaration or the Date method. +1, or NULL with an exception pending. */
static ScrDyn *scr_date_class_to_json(const ScrDyn *value, const ScrDyn *key) {
return scr_date_class_call(SCR_DATE_CLASS_TO_JSON, value, key);
}
static ScrDyn *scr_date_class_to_string(const ScrDyn *value) {
return scr_date_class_call(SCR_DATE_CLASS_TO_STRING, value, NULL);
}
static ScrDyn *scr_date_class_value_of(const ScrDyn *value) {
return scr_date_class_call(SCR_DATE_CLASS_VALUE_OF, value, NULL);
}
/* The name of the class of an instance of a class declared `extends
* Date`: +1, or NULL for any other value. */
static ScrStr *scr_date_class_name(const ScrDyn *value) {
if (!scr_date_class_operation || !value || value->kind != SCR_DYN_TYPED_REF) return NULL;
ScrDyn *result = scr_date_class_call(SCR_DATE_CLASS_NAME, value, NULL);
ScrStr *name = result && result->kind == SCR_DYN_STR ? scr_str_retain(result->v.str) : NULL;
scr_dyn_release(result);
return name;
}
double scr_dyn_native_date_value(const ScrDyn *value) {
if (!scr_dyn_native_date_is(value)) { scr_dyn_check_fail(NULL, "Date", value); return NAN; }
return ((ScrNativeDate *)value->v.handle.ptr)->milliseconds;
if (scr_dyn_native_date_is(value)) return ((ScrNativeDate *)value->v.handle.ptr)->milliseconds;
double time;
if (scr_dyn_date_class_time(value, &time)) return time;
scr_dyn_check_fail(NULL, "Date", value);
return NAN;
}
/* Method names without another use in this file, in named arrays (see
@@ -13020,13 +13127,14 @@ static const char scr_date_to_time_string_name[] = "toTimeString";
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)args; (void)argc; (void)what;
(void)self; (void)what;
double ms = ((ScrNativeDate *)ptr)->milliseconds;
if (!strcmp(method, "getTime") || !strcmp(method, "valueOf")) return scr_dyn_new_num(ms);
if (!strcmp(method, "toString") || !strcmp(method, scr_date_to_date_string_name) ||
!strcmp(method, scr_date_to_time_string_name)) {
!strcmp(method, scr_date_to_time_string_name) || !strcmp(method, "toUTCString")) {
ScrStr *text = !strcmp(method, "toString") ? scr_date_to_string(ms)
: !strcmp(method, scr_date_to_date_string_name) ? scr_date_to_date_string(ms)
: !strcmp(method, "toUTCString") ? scr_date_to_utc_string(ms)
: scr_date_to_time_string(ms);
ScrDyn *result = scr_dyn_new_str(text);
scr_str_release(text);
@@ -13040,6 +13148,12 @@ static ScrDyn *scr_native_date_invoke(void *ptr, ScrDyn *self, const char *metho
scr_str_release(text);
return result;
}
if (!strcmp(method, "setTime")) {
double time = NAN;
if (argc > 0 && !scr_dyn_number_coerce_js(args[0], &time)) return NULL;
((ScrNativeDate *)ptr)->milliseconds = scr_date_new_ms(time);
return scr_dyn_new_num(((ScrNativeDate *)ptr)->milliseconds);
}
#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);
@@ -13070,13 +13184,25 @@ ScrDyn *scr_dyn_native_date_new(const ScrDyn *arguments) {
else {
ScrDyn *value = arguments->v.arr.items[0];
if (scr_dyn_native_date_is(value)) ms = scr_dyn_native_date_value(value);
else if (value->kind == SCR_DYN_STR) ms = scr_date_parse_get_time(value->v.str);
else if (scr_dyn_date_class_time(value, &ms)) {
} else if (value->kind == SCR_DYN_STR) ms = scr_date_parse_get_time(value->v.str);
else if (!scr_dyn_number_coerce_js(value, &ms)) return NULL;
ms = scr_date_new_ms(ms);
}
return scr_dyn_native_date_from_ms(ms);
}
/* The time value `new Date(...arguments)` constructs (super(...) in a
* class declared `extends Date`), or NaN with an exception pending. */
double scr_dyn_date_arguments_time(const ScrDyn *arguments) {
ScrDyn *date = arguments->v.arr.len > 1 ? scr_dyn_native_date_new_fields(arguments)
: scr_dyn_native_date_new_value(arguments);
if (!date) return NAN;
double time = ((ScrNativeDate *)date->v.handle.ptr)->milliseconds;
scr_dyn_release(date);
return time;
}
/* new Date(value) in JavaScript: a string parses with the full parser;
* every other value takes scr_dyn_native_date_new. */
ScrDyn *scr_dyn_native_date_new_value(const ScrDyn *arguments) {
@@ -13103,11 +13229,11 @@ ScrDyn *scr_dyn_native_date_new_fields(const ScrDyn *arguments) {
/* The Date.prototype methods scr_native_date_invoke answers. */
static const char *const scr_native_date_methods[] = {
"getTime", "valueOf", "toISOString", "toJSON", "toString", scr_date_to_date_string_name,
scr_date_to_time_string_name,
scr_date_to_time_string_name, "toUTCString",
"getFullYear", "getUTCFullYear", "getMonth",
"getUTCMonth", "getDate", "getUTCDate", "getDay", "getUTCDay", "getHours", "getUTCHours",
"getMinutes", "getUTCMinutes", "getSeconds", "getUTCSeconds", "getMilliseconds",
"getUTCMilliseconds", "getTimezoneOffset",
"getUTCMilliseconds", "getTimezoneOffset", "setTime",
};
#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];
@@ -13122,13 +13248,34 @@ static void scr_native_date_methods_cleanup(void) {
}
/* A Date method read as a value and called later: the closure's function
* slot holds the method's name, and the receiver must be a Date. */
* slot holds the method's name, and the receiver must be a Date. On an
* instance of a class declared `extends Date` the method runs over its
* time value; toJSON calls the instance's toISOString. */
static ScrDyn *scr_native_date_method_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) {
const char *method = closure->fn;
ScrDyn *self = scr_dyn_this_get();
ScrDyn *result = NULL;
double time;
if (scr_dyn_native_date_is(self)) {
result = scr_native_date_invoke(self->v.handle.ptr, self, method, args, argc, method);
} else if (scr_dyn_date_class_time(self, &time)) {
if (!strcmp(method, "toJSON")) {
result = isfinite(time) ? scr_date_class_call(SCR_DATE_CLASS_TO_ISO_STRING, self, NULL)
: scr_dyn_new_null();
} else {
ScrNativeDate date = { 1, time };
result = scr_native_date_invoke(&date, self, method, args, argc, method);
if (result && date.milliseconds != time && !(isnan(date.milliseconds) && isnan(time))) {
ScrDyn *stored = scr_dyn_new_num(date.milliseconds);
ScrDyn *set = scr_date_class_call(SCR_DATE_CLASS_SET_TIME, self, stored);
scr_dyn_release(stored);
if (!set) {
scr_dyn_release(result);
result = NULL;
}
scr_dyn_release(set);
}
}
} else {
static const char message[] = "this is not a Date object.";
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
@@ -13137,6 +13284,21 @@ static ScrDyn *scr_native_date_method_call(ScrClosure *closure, ScrDyn *const *a
return result;
}
static ScrDyn *scr_native_date_get(void *ptr, const char *key, size_t key_len);
/* The Date method `key` as a function value when `value` holds an instance
* of a class declared `extends Date`, or NULL. The class's own members are
* read first, by the caller. */
static ScrDyn *scr_date_class_method(const ScrDyn *value, const char *key, size_t key_len) {
if (!scr_date_class_operation) return NULL;
bool named = false;
for (size_t i = 0; i < SCR_NATIVE_DATE_METHODS && !named; i++)
named = strlen(scr_native_date_methods[i]) == key_len && !memcmp(scr_native_date_methods[i], key, key_len);
double time;
if (!named || !scr_dyn_date_class_time(value, &time)) return NULL;
return scr_native_date_get(NULL, key, key_len);
}
/* Property reads of a Date: its methods as function values, one per name
* and thread, and NULL (undefined) for other names. */
static ScrDyn *scr_native_date_get(void *ptr, const char *key, size_t key_len) {
+14 -5
View File
@@ -30,8 +30,8 @@ importers:
specifier: 3.22.2
version: 3.22.2
js-yaml:
specifier: 4.3.0
version: 4.3.0
specifier: 4.3.2
version: 4.3.2
oxfmt:
specifier: 0.71.0
version: 0.71.0
@@ -41,6 +41,9 @@ importers:
rollup:
specifier: 4.62.2
version: 4.62.2
smol-toml:
specifier: 1.7.1
version: 1.7.1
three:
specifier: 0.186.1
version: 0.186.1
@@ -2089,8 +2092,8 @@ packages:
resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
hasBin: true
js-yaml@4.3.0:
resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==}
js-yaml@4.3.2:
resolution: {integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==}
hasBin: true
json-schema-to-ts@1.6.4:
@@ -2458,6 +2461,10 @@ packages:
resolution: {integrity: sha512-QlaZEqcAH3/RtNyet1IPIYPsEWAaYyXXv1Krsi+1L/QHppjX4Ifm8MQsBISz9vE8cHicIq3clogsheili5vhaQ==}
engines: {node: '>= 18'}
smol-toml@1.7.1:
resolution: {integrity: sha512-PPlsspAZ4jbMBu5DMFhfUGDQLu/vrL4SyBROVS37x8ynnVmFIs1VPBz1Co8Xks3TvpIaZXmU85y4DrQ+UyVFoQ==}
engines: {node: '>= 18'}
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
@@ -4305,7 +4312,7 @@ snapshots:
dependencies:
argparse: 2.0.1
js-yaml@4.3.0:
js-yaml@4.3.2:
dependencies:
argparse: 2.0.1
@@ -4694,6 +4701,8 @@ snapshots:
smol-toml@1.5.2: {}
smol-toml@1.7.1: {}
source-map-js@1.2.1: {}
srvx@0.11.16: {}
@@ -0,0 +1,25 @@
// The String, Number and Boolean constructors passed as callbacks: over
// unknown arrays they convert any element, over typed arrays String converts
// each element as String(value) does, and the boxed values keep identity.
const values: unknown[] = [1, "b", true, null, undefined, Symbol("s"), [1, 2], { a: 1 }, 2n];
console.log(values.map(String).join("|"));
console.log(values.slice(0, 5).map(Number), values.map(Boolean));
function joinAll(...parts: unknown[]): string {
return parts.map(String).join(",");
}
console.log(joinAll(1, "a", false, null));
const numbers = [1, 2.5, -0, NaN, Infinity];
console.log(numbers.map(String));
const mixed: (number | string | boolean)[] = [1, "x", false];
console.log(mixed.map(String));
const symbols: (symbol | string)[] = [Symbol("q"), "r", Symbol()];
console.log(symbols.map(String));
console.log([10n, -3n].map(String));
console.log(["1", " 2 ", "x", ""].map(Number), ["", "0", "a"].map(Boolean));
const boxed: unknown = String;
const table: { [name: string]: unknown } = { s: String, n: Number, b: Boolean };
console.log(boxed === String, table.n === Number, table.b === String, typeof boxed);
console.log(values.filter(Boolean).length, values.some(Boolean), numbers.every(Boolean));
@@ -0,0 +1,29 @@
// A recursive JSON-like union with an array arm and an index-signature
// record arm: nested literals mix records into arrays and arrays into
// records at every depth.
type Json = null | boolean | number | string | Json[] | { [key: string]: Json };
function show(value: Json): string {
return JSON.stringify(value);
}
const config: Json = { list: [1, "two", null, { nested: [true, { deeper: [] }] }], flag: false };
console.log(show(config));
console.log(show({ list: [{ n: true }] }));
console.log(show([1, { k: null }, [{ a: [2] }]]));
console.log(show({ a: { b: [{ c: "x" }, 3] } }));
const rows: Json[] = [{ id: 1, tags: ["a", { t: 2 }] }, [null, { x: [] }]];
console.log(rows.map(show).join(" | "));
let value: Json = [];
value = { replaced: [1, { again: [true] }] };
console.log(show(value));
const doc: { [key: string]: Json } = { items: [{ name: "x", parts: [1, { p: "q" }] }] };
console.log(show(doc));
const items = doc["items"];
if (Array.isArray(items)) {
items.push({ added: [9] });
console.log(items.length, show(items));
}
@@ -0,0 +1,83 @@
// Recursive walkers over a JSON-like union: the array branch calls the
// walker for each element, and the record branch reads the narrowed record
// with Object.values, Object.keys and Object.entries.
type Json = null | boolean | number | string | Json[] | { [key: string]: Json };
function depth(value: Json): number {
if (Array.isArray(value)) return 1 + Math.max(0, ...value.map(depth));
if (value !== null && typeof value === "object") return 1 + Math.max(0, ...Object.values(value).map(depth));
return 0;
}
function count(value: Json): number {
let total = 1;
if (Array.isArray(value)) {
for (const item of value) total += count(item);
} else if (value !== null && typeof value === "object") {
for (const item of Object.values(value)) total += count(item);
}
return total;
}
function paths(value: Json, prefix: string): string[] {
if (Array.isArray(value)) return value.flatMap((item, index) => paths(item, `${prefix}[${index}]`));
if (value !== null && typeof value === "object") {
const out: string[] = [];
for (const key of Object.keys(value)) out.push(...paths(value[key]!, `${prefix}.${key}`));
for (const [key, item] of Object.entries(value)) if (item === null) out.push(`${prefix}.${key}=null`);
return out;
}
return [`${prefix}=${String(value)}`];
}
const doc: Json = JSON.parse('{"a":[1,{"b":[true,null]}],"c":{"d":"x"},"e":[]}');
console.log(depth(doc), count(doc));
console.log(paths(doc, "$").join(" "));
console.log(depth(3), count([[[]]]), paths([], "$"));
// A computed-key read or write in a branch that an array hole reaches throws
// Node's TypeError after the key (and, for a write, the value) evaluates.
type Bag = number | string | number[] | { [key: string]: number };
const keys: string[] = [];
function keyOf(k: string): string {
keys.push(k);
return k;
}
function isBag(value: Bag): value is { [key: string]: number } {
return !Array.isArray(value) && typeof value !== "string" && typeof value !== "number";
}
function bump(value: Bag, k: string): string {
if (isBag(value)) {
value[keyOf(k)] = (value[keyOf(k)] ?? 0) + 1;
return String(value[keyOf(k)]);
}
return "-";
}
function put(value: Bag, k: string): string {
if (isBag(value)) {
value[keyOf(k)] = keys.length;
return "put";
}
return "-";
}
function peek(value: Bag, k: string): string {
if (isBag(value)) return typeof value[keyOf(k)];
return "-";
}
const bags: Bag[] = [{ a: 1 }, "s", [1]];
bags.length = 4;
for (const which of ["bump", "put", "peek"]) {
const out: string[] = [];
for (let i = 0; i < bags.length; i++) {
try {
const k = "a" + which.length;
if (which === "bump") out.push(bump(bags[i], k));
else if (which === "put") out.push(put(bags[i], k));
else out.push(peek(bags[i], k));
} catch (e) {
out.push(e instanceof TypeError ? e.message : "other");
}
}
console.log(out.join(" | "));
}
console.log(keys.join(","), JSON.stringify(bags));
@@ -1,55 +0,0 @@
// The error constructors as values in JavaScript: an error's `constructor`
// is the same function the global names, whose name, length, typeof and
// String() are Node's, for errors the program throws, errors the runtime
// throws, and errors read back from catch bindings and containers.
function describe(label, error) {
const ctor = error.constructor;
console.log(label, error.name, error.constructor.name, typeof ctor, ctor.length, String(ctor));
}
describe('Error', new Error('e'));
describe('TypeError', new TypeError('t'));
describe('RangeError', new RangeError('r'));
describe('SyntaxError', new SyntaxError('s'));
describe('ReferenceError', new ReferenceError('f'));
describe('EvalError', new EvalError('v'));
describe('URIError', new URIError('u'));
describe('AggregateError', new AggregateError([], 'a'));
const thrown = [
() => JSON.parse('{'),
() => {
const holder = JSON.parse('{"value":null}');
return holder.value.missing;
},
() => new Array(-1),
() => decodeURIComponent('%'),
() => {
throw new TypeError('own');
},
];
const constructors = { SyntaxError, TypeError, RangeError, URIError };
const caught = [];
for (const run of thrown) {
try {
run();
} catch (error) {
caught.push(error);
console.log('caught', error.constructor.name, error.constructor === constructors[error.name]);
}
}
const byName = new Map();
for (const error of caught) byName.set(error.constructor.name, (byName.get(error.constructor.name) ?? 0) + 1);
console.log('kinds', JSON.stringify([...byName]));
console.log('identity', new TypeError('x').constructor === TypeError, new RangeError('x').constructor === TypeError);
console.log('globals', Error.name, TypeError.length, AggregateError.length, typeof RangeError, String(SyntaxError));
const renamed = new TypeError('x');
renamed.name = 'Custom';
console.log('renamed', renamed.name, renamed.constructor.name);
class ParseFailure extends SyntaxError {}
const failure = new ParseFailure('p');
console.log('subclass', failure.name, failure.constructor.name, failure instanceof SyntaxError);
@@ -1,30 +0,0 @@
// Destructuring the members of a worker's workerData in JavaScript: plain,
// renamed, defaulted, nested and rest bindings read the data the worker was
// started with, as reading `workerData.<member>` does.
'use strict';
const { Worker, isMainThread, parentPort, workerData } = require('node:worker_threads');
if (isMainThread) {
const sab = new SharedArrayBuffer(8);
const data = { name: 'job-7', size: 3, options: { verbose: true, tags: ['a', 'b'] }, counters: new Int32Array(sab), extra: 1, more: 'x' };
const worker = new Worker(__filename, { workerData: data });
worker.on('message', (lines) => {
for (const line of lines) console.log(line);
console.log('main sees', new Int32Array(sab)[0]);
});
} else {
const lines = [];
const { name, size } = workerData;
lines.push(`plain ${name} ${size}`);
const { name: jobName, missing = 'fallback', size: count = 10 } = workerData;
lines.push(`renamed ${jobName} ${missing} ${count}`);
const {
options: { verbose, tags: [firstTag, ...otherTags] },
} = workerData;
lines.push(`nested ${verbose} ${firstTag} ${JSON.stringify(otherTags)}`);
const { name: ignored, size: alsoIgnored, options, counters, ...rest } = workerData;
lines.push(`rest ${JSON.stringify(rest)} ${typeof options} ${ignored === name && alsoIgnored === size}`);
counters[0] = 41;
counters[0] += 1;
parentPort.postMessage(lines);
}
@@ -0,0 +1,93 @@
// Typed-array callback methods with literal callbacks, function values shared
// by several calls, and callbacks chosen at run time: visit order and
// arguments, writes during the visit, early exits, reduce seeds and the
// empty-array TypeError, map conversions, Buffer results, throwing
// callbacks, empty arrays and subarray receivers.
const ints = new Int32Array([5, -3, 8, 0, 12, -7, 4]);
const floats = new Float32Array([0.1, 2.5, -1.25, 3.75, 9.5]);
const visits: string[] = [];
ints.forEach((v, i, a) => {
visits.push(`${i}:${v}:${a === ints}`);
});
console.log("forEach", visits.join(" "));
const writes = new Uint8Array([1, 2, 3, 4, 5, 6]);
writes.forEach((v, i, a) => {
if (i + 1 < a.length) a[i + 1] = a[i + 1]! + v;
});
console.log("forEach writes ahead", writes.join(","));
let calls = 0;
const counted = (v: number): boolean => {
calls++;
return v > 7;
};
console.log("some", ints.some(counted), calls);
calls = 0;
console.log("every", ints.every((v) => (calls++, v < 10)), calls);
calls = 0;
console.log("find", ints.find(counted), calls, ints.find((v) => v > 100));
calls = 0;
console.log("findIndex", ints.findIndex(counted), calls, ints.findIndex((v) => v > 100));
const order: number[] = [];
console.log(
"findLast",
ints.findLast((v, i) => (order.push(i), v < 0)),
ints.findLastIndex((v) => v > 100),
order.join(","),
);
console.log("reduce", ints.reduce((acc, v) => acc + v), ints.reduce((acc, v, i) => acc + v * i, 100));
const right: number[] = [];
console.log(
"reduceRight",
floats.reduceRight((acc, v, i) => (right.push(i), acc - v)),
right.join(","),
floats.reduceRight((acc, v) => acc + v, -1),
);
console.log("single", new Int8Array([42]).reduce(() => -1), new Int8Array([42]).reduceRight(() => -1));
function tryReduce(label: string, run: () => number): void {
try {
console.log(label, run());
} catch (error) {
console.log(label, (error as Error).name, (error as Error).message);
}
}
const emptyU16 = new Uint16Array(0);
const emptyF64 = new Float64Array(0);
tryReduce("empty u16 reduce", () => emptyU16.reduce((a, b) => a + b));
tryReduce("empty f64 reduceRight", () => emptyF64.reduceRight((a, b) => a + b));
tryReduce("empty with init", () => emptyU16.reduce((a, b) => a + b, 7) + emptyF64.reduceRight((a, b) => a + b, 1));
console.log("empty", emptyU16.some(() => true), emptyF64.every(() => false), emptyU16.map((v) => v).length);
console.log("map u8", new Uint8Array([250, 3, 128]).map((v) => v * 3).join(","));
console.log("map clamped", new Uint8ClampedArray([250, 3, 128]).map((v) => v * 1.5 - 10).join(","));
console.log("map i16", new Int16Array([30000, -30000, 7]).map((v, i) => v + i * 10000).join(","));
console.log("map f32", floats.map((v) => v / 3).join(","));
console.log("filter", ints.filter((v, i) => v > 0 && i % 2 === 0).join(","), floats.filter((v) => v > 1).join(","));
const buf = Buffer.from("scriptc");
const upper = buf.map((c) => c - 32);
console.log("buffer map", Buffer.isBuffer(upper), upper.join(","), Buffer.isBuffer(buf.filter((c) => c > 112)));
// One function value at several calls, and callbacks chosen at run time.
const double = (v: number): number => v * 2;
console.log("shared", new Int32Array([1, 2, 3]).map(double).join(","), new Int32Array([4, 5]).map(double).join(","));
const pick = (k: number): ((v: number) => boolean) => (k > 0 ? (v) => v > k : (v) => v < k);
console.log("chosen", ints.filter(pick(4)).join(","), ints.filter(pick(-4)).join(","));
try {
let seen = 0;
floats.forEach((v) => {
seen++;
if (v < 0) throw new RangeError(`negative ${v} after ${seen}`);
});
} catch (error) {
console.log("throw", (error as Error).name, (error as Error).message);
}
const view = ints.subarray(2, 5);
console.log("subarray", view.map((v) => v + 1).join(","), view.reduce((a, v) => a * 10 + v, 0), view.findLastIndex((v) => v === 8));
@@ -0,0 +1,79 @@
// A constructor function that inherits from Error.prototype, the pre-class
// way of declaring an error type.
function ParseFailure(reason, position) {
Error.call(this);
this.name = "ParseFailure";
this.reason = reason;
this.position = position;
this.message = reason + (position === undefined ? "" : " at " + position);
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
} else {
this.stack = new Error().stack || "";
}
}
ParseFailure.prototype = Object.create(Error.prototype);
ParseFailure.prototype.constructor = ParseFailure;
ParseFailure.prototype.toString = function toString(compact) {
return this.name + ": " + this.reason + (compact ? "" : " (position " + this.position + ")");
};
const failure = new ParseFailure("unexpected token", 7);
console.log(failure instanceof ParseFailure, failure instanceof Error, failure.constructor === ParseFailure);
console.log(failure.name, "|", failure.message, "|", failure.reason, failure.position);
console.log(failure.toString(), "|", failure.toString(true), "|", String(failure), "|", `${failure}`);
console.log(Object.keys(failure), JSON.stringify(failure));
console.log(typeof failure.stack, failure.stack.split("\n")[0], Object.keys(failure).includes("stack"));
console.log(Object.prototype.toString.call(failure), Error.isError(failure));
try {
throw new ParseFailure("thrown");
} catch (caught) {
console.log(caught instanceof Error, caught instanceof ParseFailure, caught.reason, caught.message);
}
const values = [new ParseFailure("in a list"), new Error("plain"), { name: "Fake", message: "x" }, "text"];
console.log(values.map((value) => value instanceof Error));
// The inherited members of Error.prototype.
const bare = Object.create(Error.prototype);
console.log(bare instanceof Error, bare.name, JSON.stringify(bare.message), String(bare), Object.keys(bare));
console.log(Error.prototype.name, JSON.stringify(Error.prototype.message), Error.prototype instanceof Error);
const record = Object.create(Error.prototype);
record.name = "N";
record.message = "M";
console.log(String(record), `${Object.create(record)}`);
// captureStackTrace on other objects: the header follows name and message.
const target = { name: "Custom", message: "details", toString() { return "ignored"; } };
Error.captureStackTrace(target);
console.log(target.stack.split("\n")[0], Object.keys(target));
const empty = {};
Error.captureStackTrace(empty);
console.log(empty.stack.split("\n")[0], typeof Error.captureStackTrace);
// A subtype of the subtype.
function StrictFailure(reason) {
ParseFailure.call(this, reason, 0);
this.name = "StrictFailure";
}
StrictFailure.prototype = Object.create(ParseFailure.prototype);
StrictFailure.prototype.constructor = StrictFailure;
const strict = new StrictFailure("strict mode");
console.log(strict instanceof StrictFailure, strict instanceof ParseFailure, strict instanceof Error, strict.toString(true));
// A class that extends Error calls it too, directly or through `?.`.
class TokenError extends Error {
constructor(message) {
super(message);
this.name = "TokenError";
Error.captureStackTrace(this, TokenError);
}
}
class LexError extends TokenError {
constructor(message) {
super(message), (this.name = "LexError"), Error.captureStackTrace?.(this, this.constructor);
}
}
for (const error of [new TokenError("bad token"), new LexError("bad char")]) {
console.log(error.name, error.message, error.stack.split("\n")[0], error instanceof TokenError, Object.keys(error));
}
@@ -0,0 +1,40 @@
// The standard error constructors called as functions construct errors too,
// and Function.prototype.call ignores the receiver it is given.
const plain = Error("plain");
const typed = TypeError("typed");
const range = RangeError();
const withCause = Error("wrapped", { cause: plain });
console.log(plain instanceof Error, plain.message, String(plain));
console.log(typed instanceof TypeError, typed instanceof Error, typed.name, typed.message);
console.log(range instanceof RangeError, JSON.stringify(range.message), String(range));
console.log(withCause.cause === plain, withCause.message);
console.log(SyntaxError("s").name, ReferenceError("r").name, EvalError("e").name, URIError("u").name);
const receiver = { marker: true };
const called = Error.call(receiver, "via call");
console.log(called instanceof Error, called.message, Object.keys(receiver), called === receiver);
const typedCall = TypeError.call(null, "typed via call");
console.log(typedCall instanceof TypeError, typedCall.message);
const noArguments = Error.call();
console.log(noArguments instanceof Error, JSON.stringify(noArguments.message));
let evaluated = 0;
function effect(value) {
evaluated++;
return value;
}
const ordered = Error.call(effect({}), effect("ordered"));
console.log(evaluated, ordered.message);
function Wrapper(message) {
const base = Error.call(this, message);
this.message = base.message;
this.kind = "wrapper";
}
const wrapped = new Wrapper("kept");
console.log(wrapped.message, wrapped.kind, wrapped instanceof Wrapper, wrapped instanceof Error);
try {
throw Error("thrown without new");
} catch (error) {
console.log(error instanceof Error, error.message);
}
@@ -0,0 +1,70 @@
// Objects whose members share names with String.prototype methods, filled
// in after their declaration. A call dispatches on the receiver's runtime
// kind: a string runs the String method, any other value its own member.
var helpers = {};
var initialized;
function setup() {
if (initialized) return helpers;
initialized = true;
function repeat(text, count) {
let out = "";
for (let i = 0; i < count; i += 1) out += text;
return out;
}
function padStart(text, width, fill) {
let out = String(text);
while (out.length < width) out = fill + out;
return out;
}
helpers.repeat = repeat;
helpers.padStart = padStart;
helpers.trim = function (value) {
return "<" + value + ">";
};
helpers.split = (value, separator) => [value, separator, "own"];
helpers.toUpperCase = () => "own upper";
return helpers;
}
const h = setup();
console.log("[" + h.repeat("ab", 3) + "]", "[" + h.repeat("-", 0) + "]");
console.log(h.padStart(7, 3, "0"), h.padStart("long", 2, "*"));
console.log(h.trim(" x "), h.split("a,b", ","), h.toUpperCase());
const indent = h.repeat(" ", 2);
console.log("[" + indent + "]", indent.length);
const lines = ["one", "two"].map((line, index) => h.padStart(index + 1, 3, " ") + " | " + line);
console.log(lines.join("\n"));
// The same names on a missing member throw Node's TypeError.
var partial = {};
function fillPartial() {
partial.present = true;
}
fillPartial();
try {
partial.repeat("x", 2);
} catch (error) {
console.log(error instanceof TypeError, error.message);
}
try {
partial.trim();
} catch (error) {
console.log(error instanceof TypeError, error.message);
}
// Arguments are evaluated once, in order, after the member is read.
const order = [];
var traced = {};
function fillTraced() {
traced.replace = (a, b) => {
order.push("call");
return a + b;
};
}
fillTraced();
function arg(value) {
order.push("arg " + value);
return value;
}
console.log(traced.replace(arg(1), arg(2)), order.join(", "));
+28
View File
@@ -0,0 +1,28 @@
// Date.prototype.setTime on Dates a JavaScript file constructs: the Date
// is one mutable object, so every reference sees the new time value.
function shifted(year, month, day, minutes) {
const date = new Date(Date.UTC(year, month, day, 12, 30, 15, 250));
if (minutes) date.setTime(date.getTime() - minutes * 60000);
return date;
}
const unchanged = shifted(2001, 1, 3, 0);
const moved = shifted(2001, 1, 3, 90);
console.log(unchanged.toISOString(), moved.toISOString());
const alias = moved;
const record = { when: moved };
console.log(moved.setTime(1000), alias.getTime(), record.when.toISOString());
console.log(moved.setTime("2500"), moved.getTime(), moved.setTime(1.9), moved.setTime(-1.9));
console.log(moved.setTime(NaN), moved.getTime(), String(moved), moved.toJSON());
console.log(moved.setTime(8.64e15), moved.setTime(8.64e15 + 1), moved.setTime(-8.64e15 - 1));
console.log(Object.is(moved.setTime(-0), 0), moved.setTime(), moved.setTime(true), moved.setTime(null));
console.log(moved.setTime({ valueOf: () => 86400000 }), moved.toISOString());
try {
moved.setTime({ valueOf: () => { throw new Error("no time"); } });
} catch (error) {
console.log(error.message, moved.toISOString());
}
console.log(JSON.stringify({ at: shifted(1999, 11, 31, 1) }));
const dates = [shifted(2020, 0, 1, 0), shifted(2020, 0, 1, 0)];
dates.forEach((date, index) => date.setTime(date.getTime() + index * 1000));
console.log(dates.map((date) => date.getUTCSeconds()));
@@ -0,0 +1,46 @@
// Object literals whose fields start as empty objects, used as maps the
// program adds keys to, including chained assignment into two of them.
function index(entries) {
const table = {
first: {},
second: {},
all: {},
lists: { first: [], second: [], all: [] },
};
function add(entry) {
if (entry.many) {
table.lists[entry.group].push(entry.name);
table.lists["all"].push(entry.name);
} else {
table[entry.group][entry.name] = table["all"][entry.name] = entry;
}
}
entries.forEach(add);
return table;
}
const table = index([
{ group: "first", name: "a" },
{ group: "second", name: "b" },
{ group: "first", name: "c", many: true },
{ group: "second", name: "a" },
]);
console.log(Object.keys(table.first), Object.keys(table.second), Object.keys(table.all), table.lists);
console.log(table.all.a === table.second.a, table.first.a.group, table.all.b.name, "c" in table.first);
console.log(JSON.stringify(table));
function counters(words) {
const result = { seen: {}, firstAt: {} };
words.forEach((word, position) => {
result.seen[word] = (result.seen[word] || 0) + 1;
if (!(word in result.firstAt)) result.firstAt[word] = position;
});
return result;
}
console.log(JSON.stringify(counters(["x", "y", "x", "z", "y", "x"])));
const settings = { values: {}, names: {} };
const key = String(Math.round(1.6));
settings.values[key] = 2;
settings["names"][key + "!"] = settings.values[key] * 10;
delete settings.values[key];
console.log(JSON.stringify(settings), Object.keys(settings.values).length);
@@ -0,0 +1,43 @@
// `instanceof` against constructors held as values: a property holding a
// global constructor, and constructor functions with prototype chains.
const kinds = [
{ name: "Date", type: Date },
{ name: "RegExp", type: RegExp },
{ name: "Map", type: Map },
{ name: "Set", type: Set },
{ name: "WeakMap", type: WeakMap },
{ name: "Promise", type: Promise },
{ name: "Array", type: Array },
];
function classify(value) {
return kinds
.filter((kind) => typeof value === "object" && value !== null && value instanceof kind.type)
.map((kind) => kind.name)
.join("+") || "none";
}
const samples = [new Date(0), /x/g, new Map([[1, 2]]), new Set([1]), new WeakMap(), Promise.resolve(1), [1, 2], { a: 1 }, null, "text", 4];
console.log(samples.map(classify).join(" "));
function Shape(sides) {
this.sides = sides;
}
Shape.prototype.describe = function () {
return this.sides + " sides";
};
function Square() {
Shape.call(this, 4);
}
Square.prototype = Object.create(Shape.prototype);
Square.prototype.constructor = Square;
const square = new Square();
const shape = new Shape(3);
const types = { Shape, Square };
console.log(square instanceof types.Shape, square instanceof types.Square, shape instanceof types.Square);
console.log(square.describe(), shape.describe(), Object.create(Square.prototype) instanceof types.Shape);
console.log([square, shape, {}, []].map((value) => value instanceof types.Shape));
const notCallable = JSON.parse("{}");
try {
console.log(square instanceof notCallable);
} catch (error) {
console.log(error instanceof TypeError, error.message);
}
@@ -0,0 +1,9 @@
// parseFloat reads one argument; Node ignores any others.
const radix = 10;
const inputs = ["3.25", " -0.5e2xyz", "0x10", "Infinityx", ".5", "-.e1", "", "1_000"];
console.log(inputs.map((text) => parseFloat(text, 10)));
console.log(inputs.map((text) => parseFloat(text, radix, "ignored")));
const fromJson = JSON.parse('{"value":"12.5kg","n":7}');
console.log(parseFloat(fromJson.value, 10), parseFloat(fromJson.n, radix), Object.is(parseFloat("-0", 10), -0));
const values = ["1.5", "2.25"].map((text) => parseFloat(text.replace(/_/g, ""), 10));
console.log(values, values[0] + values[1]);
@@ -0,0 +1,21 @@
// Indexing a string with a key whose type is only known at run time reads
// the property the key names: a code unit, the length, or undefined.
function skipSpaces(text, at) {
let c;
while ((c = text[at]) === " " || c === "\t") at++;
return [at, c];
}
const where = JSON.parse('{"at":2,"name":"length","past":9,"text":"1","neg":-1,"frac":1.5}');
console.log(skipSpaces("ab \tcd", where.at), skipSpaces("ab", where.past));
const s = "hello";
console.log(s[where.at], s[where.name], s[where.past], s[where.text], s[where.neg], s[where.frac]);
console.log(typeof s[where.past], typeof s[where.name], s[where.at] === "l");
function scan(text, start) {
let out = "";
for (let i = start; text[i] !== undefined && text[i] !== ";"; i++) out += text[i];
return out;
}
console.log(scan("key=value;rest", JSON.parse("4")), scan("abc", JSON.parse("1")));
const codes = [];
for (let i = JSON.parse("0"); i < s.length; i++) codes.push(s[i].charCodeAt(0));
console.log(codes);
@@ -0,0 +1,34 @@
// Unary plus and minus on booleans and on values that may be strings or
// undefined, and += on a binding that holds a number or a string.
function step(text, at) {
const open = text[at] === "[";
at += +open;
return [at, +open + +true + +false, -open];
}
const at = JSON.parse("1");
console.log(step("a[b", at), step("ab", at));
let total = 0;
for (const flag of [true, false, true]) total += +flag;
console.log(total, +true, -true, +(1 > 2), -(2 > 1));
const match = "2024-01-02".match(/^(\d+)-(\d+)-(\d+)(?:T(\d+))?$/);
console.log(+match[1], +match[4], -match[2], +match[3] + 1, typeof +match[4], -match[4]);
const optional = [undefined, "7", " 8 ", "", "x"];
console.log(optional.map((value) => +value), optional.map((value) => -value));
let fraction = 0;
const digits = "5";
if (digits) {
fraction = digits.slice(0, 3);
while (fraction.length < 3) fraction += "0";
fraction = +fraction;
}
console.log(fraction, typeof fraction);
let width = 0;
width = digits.repeat(2);
width += 1;
console.log(width, typeof width);
let label = "x";
label += 5;
label += true;
console.log(label);
@@ -0,0 +1,20 @@
// Member assignments used as values: the receiver, the key and the value are
// evaluated in order, and the expression yields the assigned value.
const log = [];
const holder = { get inner() { log.push("receiver"); return this._inner; }, _inner: {} };
function value(v) { log.push("value"); return v; }
function key(k) { log.push("key"); return k; }
const r = holder.inner[key("k")] = value(5);
console.log(r, log.join(","), holder._inner.k);
const store = JSON.parse('{"a":{},"b":{}}');
const t = { id: "x" };
const out = store["a"][t.id] = store.b[t.id] = t;
console.log(out === t, JSON.stringify(store));
let n;
const arr = [0, 0, 0];
n = arr[1] = arr[2] = 7;
console.log(n, arr);
function Counter() { this.count = 0; }
Counter.prototype.bump = function () { return (this.count = this.count + 1); };
const c = new Counter();
console.log(c.bump(), c.bump(), c.count);
+78
View File
@@ -0,0 +1,78 @@
// A class that extends Date: private fields beside the time value, super()
// with each Date constructor form, an overriding toISOString that
// JSON.stringify reaches through Date.prototype.toJSON, and the inherited
// Date methods.
class Moment extends Date {
#zone = "Z";
#precise = true;
constructor(value, zone) {
if (value === undefined) super(0);
else super(value);
if (zone !== undefined) this.#zone = zone;
}
get zone() {
return this.#zone;
}
coarse() {
this.#precise = false;
return this;
}
toISOString() {
const iso = super.toISOString();
const base = this.#precise ? iso : iso.slice(0, 19) + "Z";
return this.#zone === "Z" ? base : base.slice(0, -1) + this.#zone;
}
}
const fromString = new Moment("2001-02-03T04:05:06.789Z");
const fromNumber = new Moment(86400000, "+01:00");
const fromDate = new Moment(new Date(Date.UTC(1999, 11, 31, 23, 59, 59)));
const fromMoment = new Moment(fromString);
const none = new Moment();
console.log(fromString.toISOString(), fromNumber.toISOString(), fromDate.toISOString());
console.log(fromMoment.getTime() === fromString.getTime(), none.getTime(), fromNumber.zone);
console.log(fromString.getUTCFullYear(), fromString.getUTCMonth(), fromString.getUTCDate(), fromString.getUTCHours(), fromString.getUTCMilliseconds());
console.log(fromString instanceof Date, fromString instanceof Moment, new Date(0) instanceof Moment);
console.log(JSON.stringify({ fromString, fromNumber, list: [fromDate.coarse()] }));
console.log(fromNumber.toJSON(), JSON.stringify(fromNumber));
console.log(String(fromDate).slice(0, 15), `${fromDate}`.slice(0, 15), fromDate.toUTCString());
console.log(+fromNumber, +fromNumber - 1000, fromNumber.valueOf(), fromNumber + "" === String(fromNumber));
console.log(fromString > fromNumber, fromString < fromNumber, fromString >= fromMoment);
console.log(fromNumber.setTime(1000), fromNumber.getTime(), fromNumber.toISOString());
console.log(new Date(fromString).toISOString(), new Date(fromNumber).getTime());
console.log(fromString, [fromNumber], { d: fromDate });
const invalid = new Moment("not a date");
console.log(Number.isNaN(invalid.getTime()), JSON.stringify({ invalid }), invalid.toJSON(), String(invalid));
console.log(invalid);
try {
invalid.toISOString();
} catch (error) {
console.log(error instanceof RangeError, error.message);
}
class LocalMoment extends Moment {
constructor(value) {
super(value, "local");
}
toISOString() {
return super.toISOString().replace("local", "");
}
}
const local = new LocalMoment(0);
console.log(local.toISOString(), JSON.stringify([local]), local instanceof Moment, local.zone);
// The field constructor and Date's statics, which the subclass inherits.
class Plain extends Date {}
const fields = new Plain(2020, 0, 15, 10, 30);
console.log(fields.getFullYear(), fields.getMonth(), fields.getDate(), fields.getHours(), fields.getMinutes());
console.log(Plain.UTC(2000, 0, 1), Plain.parse("2000-01-01T00:00:00Z"), typeof Plain.now(), Object.keys(fields));
// Date held as a value answers instanceof for subclass instances.
const constructors = { date: Date };
console.log(fields instanceof constructors.date, local instanceof constructors.date, {} instanceof constructors.date);
// setTime evaluates every argument before it converts the first one.
let base = 1000;
const delayed = { valueOf: () => base };
console.log(fields.setTime(delayed, (base = 2000)), fields.getTime());
+47
View File
@@ -0,0 +1,47 @@
// A TypeScript class that extends Date, and a subclass of it: typed calls
// of the inherited Date methods, conversions, comparisons, JSON and
// console output, with the toISOString override visible to toJSON.
class Stamp extends Date {
#precise: boolean;
label = "stamp";
constructor(value: number | string, precise = false) {
super(value);
this.#precise = precise;
}
override toISOString(): string {
const iso = super.toISOString();
return this.#precise ? iso : iso.slice(0, 19) + "Z";
}
shifted(ms: number): Stamp {
return new Stamp(this.getTime() + ms, this.#precise);
}
}
class Tick extends Stamp {
constructor(value: number) {
super(value, true);
}
seconds(): number {
return this.getUTCSeconds() + this.getUTCMilliseconds() / 1000;
}
}
const a = new Stamp("2001-02-03T04:05:06.789Z");
const b = new Tick(1500);
console.log(a.toISOString(), b.toISOString(), a.getTime(), b.seconds());
console.log(a instanceof Date, b instanceof Stamp, b instanceof Date, a.shifted(1000).toISOString());
console.log(JSON.stringify({ a, b, list: [a] }), JSON.stringify(a), a.toJSON(), b.toJSON());
console.log(String(a).slice(0, 15), `${b}`.slice(0, 15), +a, b.valueOf(), a.getTime() - b.getTime());
console.log(a < b, a > b, a >= b, a == a, a + "!" === String(a) + "!");
console.log(a);
console.log([b], { b });
console.log(a.setTime(5000), a.getTime(), a.toISOString(), a.label);
const items: Stamp[] = [a, b, new Tick(0)];
items.sort((x, y) => x.getTime() - y.getTime());
console.log(items.map((s) => s.toISOString()).join(","));
console.log(items.filter((s) => s instanceof Tick).length);
function describe(value: Stamp | string): string {
return typeof value === "string" ? value : value.toISOString();
}
console.log(describe(b), describe("text"));
@@ -0,0 +1,43 @@
// Instances of a class that extends Date held in values typed only at run
// time: method calls, method values, conversions and instanceof through
// those values, and JSON and console output of the containers.
class Day extends Date {
#label;
constructor(value, label) {
super(value);
this.#label = label;
}
get label() {
return this.#label;
}
toISOString() {
return super.toISOString().slice(0, 10);
}
}
class Plain extends Date {}
const box = JSON.parse('{"items":[],"byName":{}}');
box.items.push(new Day(Date.UTC(2020, 1, 29), "leap"), new Plain(0), new Date(1000), "text");
box.byName.first = box.items[0];
function inspect(value) {
if (!(value instanceof Date)) return "not a date: " + value;
return [value.toISOString(), value.getTime(), value.getUTCDate(), value instanceof Day, value.toJSON()].join(" | ");
}
for (const item of box.items) console.log(inspect(item));
console.log(JSON.stringify(box));
console.log(box.byName.first.label, box.byName.first.getUTCFullYear(), String(box.items[1]).slice(0, 15));
console.log(+box.items[0], box.items[0] - box.items[1], box.items[0] > box.items[2], `${box.items[1]}`.slice(0, 3));
const getTime = box.items[0].getTime;
console.log(getTime.call(box.items[1]), getTime.call(box.items[0]), typeof getTime);
const toISO = box.items[0].toISOString;
console.log(toISO.call(box.items[0]));
console.log(box.items[1].setTime(5000), box.items[1].getTime(), box.items[1].toISOString());
try {
getTime.call({});
} catch (error) {
console.log(error instanceof TypeError);
}
console.log(box.items);
console.log(new Date(box.items[0]).toISOString(), new Date(box.items[1]).getTime());
@@ -0,0 +1,72 @@
// Private fields read and written through receivers whose type is known only
// at run time: static factories and helpers that take the instance as an
// argument, compound assignments and updates, `#x in value`, subclasses, and
// Node's TypeErrors for values that do not carry the field.
class Counter {
#count = 0;
#label = null;
constructor(label) {
this.#label = label;
}
static labeled(label, start) {
let counter = new Counter("pending");
counter.#label = label;
counter.#count = start;
return counter;
}
static read(counter) {
return [counter.#label, counter.#count];
}
static bump(counter, by) {
counter.#count += by;
counter.#count++;
++counter.#count;
return counter.#count;
}
static clear(counter) {
counter.#label = null;
return counter;
}
static owns(value) {
return #count in value;
}
describe() {
return `${this.#label}:${this.#count}`;
}
}
class LimitedCounter extends Counter {
#limit = 3;
static limit(value) {
return value.#limit;
}
}
const made = Counter.labeled("made", 5);
console.log(made.describe(), Counter.read(made), Counter.bump(made, 10), made.describe());
const held = JSON.parse('{"list":[]}');
held.list.push(new Counter("held"), new LimitedCounter("limited"));
for (const item of held.list) console.log(Counter.read(item), Counter.bump(item, 1), item.describe(), Counter.owns(item));
console.log(LimitedCounter.limit(held.list[1]), Counter.clear(held.list[0]).describe());
for (const value of [{}, [], null, undefined, 1, "text", held.list[0]]) {
try {
console.log(Counter.read(value));
} catch (error) {
console.log("read", error instanceof TypeError, error.message);
}
try {
Counter.bump(value, 1);
console.log("bumped");
} catch (error) {
console.log("bump", error instanceof TypeError, error.message);
}
try {
console.log(LimitedCounter.limit(value));
} catch (error) {
console.log("limit", error instanceof TypeError, error.message);
}
try {
console.log("owns", Counter.owns(value));
} catch (error) {
console.log("owns", error instanceof TypeError, error.message);
}
}
@@ -0,0 +1,37 @@
// `let` bindings initialized with a string, number or boolean and later
// assigned values that can be null or undefined. JavaScript files are
// checked without strict null checks, so the declared type of each binding
// is the primitive alone.
const PATTERN = /^(\d+)(?:\.(\d+))?([+-]\d\d:\d\d)?$/;
function parts(text) {
let offset = "Z";
let fraction = 0;
let signed = false;
const match = text.match(PATTERN);
if (match) {
offset = match[3] || null;
fraction = match[2] ? +match[2] : undefined;
signed = match[3] ? match[3][0] === "-" : null;
}
return [offset, fraction, signed];
}
console.log(parts("12"), parts("12.5"), parts("12.25-01:00"), parts("x"));
function pick(flag) {
let name = "default";
name = flag ? name.toUpperCase() : void 0;
let count = 1;
count = flag && count + 1;
let ready = true;
ready = flag === undefined ? undefined : !flag;
return [name, count, ready, typeof name, typeof count];
}
console.log(pick(true), pick(false), pick(undefined));
let last = "start";
const seen = [];
for (const value of ["a", null, "b", undefined]) {
last = value ?? null;
seen.push(last === null ? "null" : last.length);
}
console.log(seen, last);
@@ -60,7 +60,7 @@ await new Promise((resolve) => {
},
(err, value) => {
console.log("array source:", err ?? null, value);
resolve();
resolve(undefined);
},
);
});
@@ -114,7 +114,7 @@ function ticker(log) {
await new Promise((resolve) =>
pipeline(["a", "b"], new PassThrough(), sink(log), () => {
stop();
resolve();
resolve(undefined);
}),
);
console.log("array ticks:", log.join(" "));
@@ -134,7 +134,7 @@ function ticker(log) {
sink(log),
() => {
stop();
resolve();
resolve(undefined);
},
),
);
+81
View File
@@ -0,0 +1,81 @@
// For loops with two counters closing in on each other (`lo++, hi--`):
// in-place reversals, inclusive bounds, larger steps, early exits, starts
// that are fractions, negative zero, NaN, huge or already crossed, nested
// reversals, and counters a closure captures or the body writes.
function reverse(a: number[], from: number, to: number): number[] {
for (let lo = from, hi = to; lo < hi; lo++, hi--) {
const t = a[lo]!;
a[lo] = a[hi]!;
a[hi] = t;
}
return a;
}
const base = (): number[] => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
console.log(reverse(base(), 0, 9).join(","));
console.log(reverse(base(), 2, 6).join(","));
console.log(reverse(base(), 6, 2).join(","));
console.log(reverse(base(), 3, 3).join(","));
// Unproven starts: fractions, negative zero, NaN and huge values keep JS
// semantics (charCodeAt truncates its index; out-of-range reads are NaN).
function codes(s: string, from: number, to: number): string {
const out: number[] = [];
for (let lo = from, hi = to; lo < hi; lo++, hi--) out.push(s.charCodeAt(lo), s.charCodeAt(hi), lo, hi);
return out.join(",");
}
const word = "abcdefgh";
console.log(codes(word, 0.5, 4.5));
console.log(codes(word, -0, 3), Object.is(-0 + 0, 0));
console.log(codes(word, NaN, 4), "|", codes(word, 0, NaN));
console.log(codes(word, 2 ** 53 - 3, 2 ** 53));
console.log(codes(word, -3, 2));
function pairs(n: number): string {
const out: string[] = [];
for (let i = 0, j = n; i <= j; i += 2, j -= 3) out.push(`${i}:${j}`);
return out.join(" ");
}
console.log(pairs(20), "|", pairs(0), "|", pairs(-1));
function firstGap(a: number[]): number {
let found = -1;
for (let lo = 0, hi = a.length - 1; lo < hi; lo++, hi--) {
if (a[lo] === a[hi]) continue;
found = lo;
break;
}
return found;
}
console.log(firstGap([1, 2, 3, 2, 1]), firstGap([1, 2, 3, 4, 2, 1]), firstGap([]));
// The flip loop of a permutation puzzle: the next index comes from the array.
function flips(perm: number[]): number {
const work = perm.slice();
let count = 0;
let head = work[0]!;
while (head !== 0) {
for (let lo = 0, hi = head; lo < hi; lo++, hi--) {
const t = work[lo]!;
work[lo] = work[hi]!;
work[hi] = t;
}
count++;
head = work[0]!;
}
return count;
}
console.log(flips([3, 1, 0, 2]), flips([4, 3, 2, 1, 0]), flips([0, 1, 2]));
// Counters a closure reads, or the body writes, stay ordinary bindings.
const readers: (() => number)[] = [];
for (let lo = 0, hi = 4; lo < hi; lo++, hi--) readers.push(() => lo * 10 + hi);
console.log(readers.map((f) => f()).join(","));
const seen: number[] = [];
for (let lo = 0, hi = 10; lo < hi; lo++, hi--) {
seen.push(lo, hi);
if (lo === 2) hi -= 3;
}
console.log(seen.join(","));
const text = "abcdefgh";
let mirrored = "";
for (let lo = 0, hi = text.length - 1; lo <= hi; lo++, hi--) mirrored += text.charAt(hi) + text.charCodeAt(lo);
console.log(mirrored);
-4
View File
@@ -93,10 +93,6 @@ void inspectReadableStream;
import { callbackify } from "node:util";
const callbackified = callbackify(async (n: number) => n);
void callbackified;
/* Cipheriv values support update(), final(), setAAD(), setAutoPadding(),
* getAuthTag(), and setAuthTag(); the stream.Transform members are fenced. */
const cipherStream = createCipheriv("aes-128-cbc", Buffer.alloc(16), Buffer.alloc(16));
cipherStream.on("data", () => {});
/* ECDH on a curve Node knows but the runtime does not implement, and
* computeSecret(view, encoding), which answers a Buffer where @types/node
* declares a string. */
+63
View File
@@ -0,0 +1,63 @@
// The published js-yaml package compiled as static modules: loading, dumping,
// schemas, custom types and the YAMLException errors it throws.
import yaml from "js-yaml";
const text = `
# config
name: demo
version: 1.2
count: 0x1F
octal: 0o17
neg: -12_000
flt: 6.02e+23
inf: .inf
nan: .NaN
on: yes
off: false
nil: ~
date: 2001-12-14
stamp: 2001-12-14t21:59:43.10-05:00
str: "quoted \\u263A \\t tab"
single: 'it''s'
multi: |
line one
line two
folded: >-
folded
text
base: &base { a: 1, b: [x, y] }
derived:
<<: *base
c: 3
list:
- one
- { k: v }
- [1, 2, 3]
- !!str 123
set: !!set { a, b }
omap: !!omap [ { x: 1 }, { y: 2 } ]
binary: !!binary aGVsbG8=
`;
const doc = yaml.load(text);
console.log(JSON.stringify(doc, (k, v) => (v instanceof Uint8Array ? Array.from(v) : v)));
console.log(doc.stamp instanceof Date, doc.stamp.toISOString(), doc.date.toISOString(), Object.is(doc.nan, NaN) || Number.isNaN(doc.nan));
const docs = yaml.loadAll("---\na: 1\n---\nb: 2\n...\n");
console.log(JSON.stringify(docs));
const items = [];
yaml.loadAll("--- 1\n--- two\n", (d) => items.push(d));
console.log(items);
console.log(yaml.dump(doc, { sortKeys: true, lineWidth: 40, skipInvalid: true }));
console.log(yaml.dump({ a: "multi\nline\n", b: " lead", c: "yes", d: "123", e: null, f: [], g: {}, h: "x: y", i: -0, j: 1e21 }, { indent: 4 }));
console.log(yaml.dump([{ deep: { er: [1, { z: true }] } }], { flowLevel: 2 }));
console.log(yaml.dump({ s: "a'b\"c", u: "é☺", long: "word ".repeat(30).trim() }, { quotingType: '"', forceQuotes: true }));
for (const bad of ["a: [1", "a: b: c", "- a\nb: 1", "key: 'open", "a: !!int x", "? a\n? a\n", "&x a: *y"]) {
try { yaml.load(bad); console.log("no error", bad); }
catch (e) { console.log(e.name, "|", e.reason, "|", e.mark && e.mark.line, e.mark && e.mark.column, "|", e.message.split("\n")[0]); console.log(e.toString(true)); console.log(String(e).split("\n").length); }
}
try { yaml.load("a: 1\na: 2"); } catch (e) { console.log(e instanceof yaml.YAMLException, e instanceof Error, e.message); }
console.log(JSON.stringify(yaml.load("a: 1\na: 2", { json: true })));
console.log(JSON.stringify(yaml.load("x: 1", { schema: yaml.JSON_SCHEMA })), JSON.stringify(yaml.load("x: yes", { schema: yaml.FAILSAFE_SCHEMA })), JSON.stringify(yaml.load("x: yes", { schema: yaml.CORE_SCHEMA })));
const Point = new yaml.Type("!point", { kind: "sequence", construct: (d) => ({ x: d[0], y: d[1] }), instanceOf: Object, represent: (p) => [p.x, p.y] });
const schema = yaml.DEFAULT_SCHEMA.extend([Point]);
console.log(JSON.stringify(yaml.load("p: !point [3, 4]", { schema })));
try { yaml.dump({ f() {} }); } catch (e) { console.log(e.name, e.message); }
console.log(yaml.dump({ f() {}, u: undefined, k: 1 }, { skipInvalid: true }));
+68
View File
@@ -0,0 +1,68 @@
// The published smol-toml package compiled as static modules: parsing every
// value kind, the TomlDate subclass of Date, stringify and TomlError.
import { parse, stringify, TomlDate, TomlError } from "smol-toml";
const text = `
# a document
title = "TOML \\"example\\""
int = +99
neg = -17
hex = 0xDEADBEEF
oct = 0o755
bin = 0b1101
under = 1_000_000
float = 6.626e-34
inf = inf
nan = nan
bool = true
odt1 = 1979-05-27T07:32:00Z
odt2 = 1979-05-27T00:32:00-07:00
odt3 = 1979-05-27T00:32:00.999999-07:00
ldt = 1979-05-27T07:32:00
ld = 1979-05-27
lt = 07:32:00.123
space = 1979-05-27 07:32:00Z
literal = 'C:\\Users\\nodejs\\templates'
multi = """
Roses are red
Violets are blue"""
lmulti = '''
The first newline is
trimmed in raw strings.'''
[owner]
name = "Tom"
"quoted key" = 1
dotted.inner.key = "x"
[database]
ports = [ 8000, 8001, 8002 ]
data = [ ["delta", "phi"], [3.14] ]
temp_targets = { cpu = 79.5, case = 72.0 }
[[products]]
name = "Hammer"
sku = 738594937
[[products]]
[[products]]
name = "Nail"
color = "gray"
`;
const doc = parse(text);
console.log(JSON.stringify(doc, null, 1));
for (const key of ["odt1", "odt2", "odt3", "ldt", "ld", "lt", "space"]) {
const d = doc[key];
console.log(key, d instanceof Date, d instanceof TomlDate, d.isDateTime(), d.isLocal(), d.isDate(), d.isTime(), d.isValid(), d.toISOString(), Number.isNaN(d.getTime()) ? "NaN" : d.getTime());
}
console.log(Number.isNaN(doc.nan), doc.inf, typeof doc.hex, doc.under);
console.log(stringify(doc));
console.log(stringify({ a: new Date(0), b: TomlDate.wrapAsLocalDate(new Date(86400000)), c: new TomlDate("10:20:30"), n: [1, [2, 3]], t: { u: { v: "w" } }, s: "quote\" and \\ backslash", f: 1.5, i: 2, big: 2n ** 40n }));
const d = new TomlDate("2020-02-29T12:00:00+01:00");
console.log(d.toISOString(), JSON.stringify({ d }), String(d.getUTCHours()), d.valueOf());
const bad = new TomlDate("not a date");
console.log(bad.isValid(), Number.isNaN(bad.getTime()));
for (const src of ["a = ", "a = 1\na = 2", "[t]\n[t]", "x = [1,", "k = 1979-13-01", "s = \"unterminated", "= 1", "a = 25:00:00"]) {
try { parse(src); console.log("parsed", JSON.stringify(src)); }
catch (e) { console.log(e instanceof TomlError, e instanceof Error, e.name, e.line, e.column, e.message.split("\n")[0]); }
}
@@ -205,56 +205,56 @@ fenced.ts:94:23 - error SC2020: 'util.callbackify into a callback whose error pa
hint: Node calls back with a null error on success; type the binding's callback as (err: NodeJS.ErrnoException | null, ...) => void
fenced.ts:104:12 - error SC2020: 'crypto.createECDH with curve 'sect163k1'' is part of the standard library types but has no scriptc lowering yet
fenced.ts:100:12 - error SC2020: 'crypto.createECDH with curve 'sect163k1'' is part of the standard library types but has no scriptc lowering yet
103 | import { createECDH } from "node:crypto";
104 | createECDH("sect163k1");
99 | import { createECDH } from "node:crypto";
100 | createECDH("sect163k1");
| ^~~~~~~~~~~
105 | createECDH("prime256v1").computeSecret(Buffer.alloc(65), "hex");
101 | createECDH("prime256v1").computeSecret(Buffer.alloc(65), "hex");
hint: the supported curves are prime192v1, secp224r1, prime256v1, secp384r1, secp521r1, secp192k1, secp224k1, secp256k1, brainpoolP256r1, brainpoolP384r1, and brainpoolP512r1
fenced.ts:105:1 - error SC2020: 'ECDH.computeSecret(key, encoding) with a Buffer or view key' is part of the standard library types but has no scriptc lowering yet
fenced.ts:101:1 - error SC2020: 'ECDH.computeSecret(key, encoding) with a Buffer or view key' is part of the standard library types but has no scriptc lowering yet
104 | createECDH("sect163k1");
105 | createECDH("prime256v1").computeSecret(Buffer.alloc(65), "hex");
100 | createECDH("sect163k1");
101 | createECDH("prime256v1").computeSecret(Buffer.alloc(65), "hex");
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
106 | /* DiffieHellman's Object.prototype members, and crypto.diffieHellman over
102 | /* DiffieHellman's Object.prototype members, and crypto.diffieHellman over
hint: Node reads the second argument as the key's input encoding and answers a Buffer; pass the output encoding third: computeSecret(key, undefined, encoding)
fenced.ts:109:1 - error SC2020: 'DiffieHellman.toString' is part of the standard library types but has no scriptc lowering yet
fenced.ts:105:1 - error SC2020: 'DiffieHellman.toString' is part of the standard library types but has no scriptc lowering yet
108 | import { createDiffieHellman, diffieHellman, type KeyObject } from "node:crypto";
109 | createDiffieHellman(Buffer.from([23])).toString();
104 | import { createDiffieHellman, diffieHellman, type KeyObject } from "node:crypto";
105 | createDiffieHellman(Buffer.from([23])).toString();
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
110 | declare const dhKey: KeyObject;
106 | declare const dhKey: KeyObject;
hint: generateKeys(), computeSecret(), getPrime(), getGenerator(), getPublicKey(), getPrivateKey(), setPublicKey(), setPrivateKey() and verifyError are supported
fenced.ts:111:1 - error SC2020: 'crypto.diffieHellman' is typed by @types/node but has no scriptc lowering yet
fenced.ts:107:1 - error SC2020: 'crypto.diffieHellman' is typed by @types/node but has no scriptc lowering yet
110 | declare const dhKey: KeyObject;
111 | diffieHellman({ privateKey: dhKey, publicKey: dhKey });
106 | declare const dhKey: KeyObject;
107 | diffieHellman({ privateKey: dhKey, publicKey: dhKey });
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
112 | /* WebCrypto beyond the secret-key slice: @types/node declares the whole
108 | /* WebCrypto beyond the secret-key slice: @types/node declares the whole
hint: crypto.diffieHellman takes asymmetric KeyObjects, which the static runtime does not have; the objects of createDiffieHellman, getDiffieHellman and createECDH compute shared secrets
fenced.ts:116:9 - error SC2020: 'crypto.subtle.wrapKey' is part of the standard library types but has no scriptc lowering yet
fenced.ts:112:9 - error SC2020: 'crypto.subtle.wrapKey' is part of the standard library types but has no scriptc lowering yet
115 | async function wrapWithWebCrypto(key: import("node:crypto").webcrypto.CryptoKey): Promise<void> {
116 | await crypto.subtle.wrapKey("raw", key, key, "AES-KW");
111 | async function wrapWithWebCrypto(key: import("node:crypto").webcrypto.CryptoKey): Promise<void> {
112 | await crypto.subtle.wrapKey("raw", key, key, "AES-KW");
| ^~~~~~~~~~~~~~~~~~~~~
117 | }
113 | }
hint: the native WebCrypto surface is crypto.subtle's importKey, generateKey and exportKey for HMAC and AES keys (importKey also for PBKDF2 and HKDF keys), encrypt and decrypt with AES-GCM, AES-CBC or AES-CTR, deriveBits and deriveKey with PBKDF2 or HKDF, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle
fenced.ts:121:15 - error SC2020: 'navigator.hardwareConcurrency' is typed by @types/node but has no scriptc lowering yet
fenced.ts:117:15 - error SC2020: 'navigator.hardwareConcurrency' is typed by @types/node but has no scriptc lowering yet
120 | * compile; the other members fence by name. */
121 | const cores = navigator.hardwareConcurrency;
116 | * compile; the other members fence by name. */
117 | const cores = navigator.hardwareConcurrency;
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~
122 | void cores;
118 | void cores;
hint: of Node's navigator global only typeof navigator and navigator.userAgent compile
@@ -0,0 +1 @@
{ "plain": 3700, "san": 3500 }
@@ -0,0 +1 @@
{ "plain": 3500, "san": 3400 }
@@ -0,0 +1 @@
{ "plain": 4200, "san": 5800 }

Some files were not shown because too many files have changed in this diff Show More