diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 5a100e374..f131747fa 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -773,17 +773,22 @@ jobs:
run: >-
pnpm exec vitest run tests/harness/worktree-files.test.ts
--testNamePattern "workspace reset keeps Linux Sandbox paths POSIX"
+ # The Node host checks with tsc-ts, the default, over the named pipe it
+ # serves on Windows, and with tsgo.
- name: Windows scriptc run smoke
shell: pwsh
run: |
zig version
- $output = & node packages/cli/dist/main.js run tests/corpus/001-hello.ts --backend llvm
- if ($LASTEXITCODE -ne 0) {
- exit $LASTEXITCODE
- }
- $actual = ($output -join "`n").Trim()
- if ($actual -ne "hello world") {
- throw "unexpected scriptc output: $actual"
+ foreach ($checker in @("tsc-ts", "tsgo")) {
+ $env:SCRIPTC_CHECKER = $checker
+ $output = & node packages/cli/dist/main.js run tests/corpus/001-hello.ts --backend llvm
+ if ($LASTEXITCODE -ne 0) {
+ exit $LASTEXITCODE
+ }
+ $actual = ($output -join "`n").Trim()
+ if ($actual -ne "hello world") {
+ throw "unexpected scriptc output with ${checker}: $actual"
+ }
}
- name: Windows PE subsystem selection
env:
diff --git a/docs/content/docs/cli.mdx b/docs/content/docs/cli.mdx
index 8c57d2e2a..8b0353620 100644
--- a/docs/content/docs/cli.mdx
+++ b/docs/content/docs/cli.mdx
@@ -206,7 +206,7 @@ With `--sparse`, the profile leaves out the functions the runs never reached. Th
A runtime pack directory to use instead of an installed @scriptc/runtime-<target> package.
SCRIPTC_CHECKER
- The TypeScript 7 API server that checks the program: tsc-ts (the default), which scriptc includes, or tsgo, from the typescript package scriptc also includes. Both give the same diagnostics and generated code. On Windows the default is tsgo, because tsc-ts's API server does not yet accept the named pipe scriptc connects over there.
+ The TypeScript 7 API server that checks the program: tsc-ts (the default), which scriptc includes, or tsgo, from the typescript package scriptc also includes. Both give the same diagnostics and generated code.
```console
diff --git a/packages/compiler/package.json b/packages/compiler/package.json
index a1d66963d..a358ae545 100644
--- a/packages/compiler/package.json
+++ b/packages/compiler/package.json
@@ -40,7 +40,7 @@
},
"dependencies": {
"@scriptc/runtime": "workspace:*",
- "tsc-ts": "0.0.7",
+ "tsc-ts": "0.0.8",
"typescript": "7.0.2",
"typescript5": "npm:typescript@5.9.3"
},
diff --git a/packages/compiler/src/backend/llvm/appended-array-literals.test.ts b/packages/compiler/src/backend/llvm/appended-array-literals.test.ts
index c483b7848..11a9704f6 100644
--- a/packages/compiler/src/backend/llvm/appended-array-literals.test.ts
+++ b/packages/compiler/src/backend/llvm/appended-array-literals.test.ts
@@ -57,10 +57,11 @@ test("only empty literals stored where the module appends construct small", () =
expect(found.has(idle)).toBe(false);
expect(found.has(returned)).toBe(false);
- // Only `fill` builds arrays.
+ // Only `fill` builds arrays. They may hold int32 slots (element kind 7),
+ // which does not change the choice of constructor.
const llvm = emitLlvmModule(module(body, ["xs", "ys"]));
- expect(llvm.match(/call ptr @scr_arr_new_small\(i32 0\)/g)).toHaveLength(2);
- expect(llvm.match(/call ptr @scr_arr_new\(i32 0, i64 0\)/g)).toHaveLength(2);
+ expect(llvm.match(/call ptr @scr_arr_new_small\(i32 [07]\)/g)).toHaveLength(2);
+ expect(llvm.match(/call ptr @scr_arr_new\(i32 [07], i64 0\)/g)).toHaveLength(2);
});
test("a binding with the same id in another function does not count", () => {
diff --git a/packages/compiler/src/backend/llvm/dense-array-access.ts b/packages/compiler/src/backend/llvm/dense-array-access.ts
index 9158d12cd..95612e57c 100644
--- a/packages/compiler/src/backend/llvm/dense-array-access.ts
+++ b/packages/compiler/src/backend/llvm/dense-array-access.ts
@@ -33,7 +33,7 @@ import {
import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
import { compactRefLoadLines, compactRefStoreLines } from "./classes.js";
import { elemAccess } from "./shapes.js";
-import { exactInteger, widenInteger } from "./integer-values.js";
+import { indexInteger } from "./integer-values.js";
import { integerOrNaNIndex } from "./wrapping-sums.js";
const SCR_ARR_VALUE = 1;
@@ -99,14 +99,17 @@ function emitDenseGuard(
): DenseGuard {
const B = host.B;
const size = host.sizeType;
- const integer = exactInteger(host, index, indexExpr);
- const nanIndex = integer ? null : integerOrNaNIndex(host, index, indexExpr);
+ const integer = indexInteger(host, index, indexExpr);
+ const nanIndex = integer === null ? integerOrNaNIndex(host, index, indexExpr) : null;
let wide: string;
let integral: string | null = null;
- if (integer) {
- // A proven integer needs no double round-trip. Negative signed values
- // compare above any length or capacity as unsigned 64-bit integers.
- wide = widenInteger(host, integer);
+ if (integer !== null) {
+ // A proven integer, or a wide view that equals the double at every
+ // index below a length, needs no double round-trip. Negative signed
+ // values compare above any length or capacity as unsigned 64-bit
+ // integers, and every index that fails the comparisons takes the
+ // double's path.
+ wide = integer;
} else if (nanIndex) {
// An exact integer or NaN is an index exactly when it is ordered.
wide = nanIndex.wide;
@@ -239,9 +242,10 @@ function statePointer(
return statePtr;
}
-/** How an array's dense slots hold its elements: the access kind, or
- * `cref` for references to compact class instances (SCR_ELEM_CREF: a 4-byte
- * slot holding the address over 8; LlEmitter.compactArrayElem). */
+/** How an array's dense slots hold its elements: the access kind (`i32`
+ * for int32 arrays, ir/int32-arrays.ts), or `cref` for references to compact
+ * class instances (SCR_ELEM_CREF: a 4-byte slot holding the address over 8;
+ * LlEmitter.compactArrayElem). */
type SlotKind = ArrayAccess | "cref";
function slotKind(host: LlvmEmitterContext, acc: ArrayAccess, element: IrType): SlotKind {
@@ -249,14 +253,14 @@ function slotKind(host: LlvmEmitterContext, acc: ArrayAccess, element: IrType):
}
/** The dense slot type: boolean arrays store one byte per element, compact
- * references four, every other element kind an 8-byte slot. */
+ * references and int32 arrays four, every other element kind an 8-byte slot. */
function slotType(kind: SlotKind): "i8" | "i32" | "i64" {
- return kind === "bool" ? "i8" : kind === "cref" ? "i32" : "i64";
+ return kind === "bool" ? "i8" : kind === "cref" || kind === "i32" ? "i32" : "i64";
}
/** Bytes per dense slot (scr_arr_slot_width). */
-function slotWidth(kind: SlotKind): number {
- return kind === "bool" ? 1 : kind === "cref" ? 4 : 8;
+export function slotWidth(kind: SlotKind): number {
+ return kind === "bool" ? 1 : kind === "cref" || kind === "i32" ? 4 : 8;
}
/** Bytes per dense slot of an array of `element` (scr_arr_slot_width). */
@@ -264,6 +268,17 @@ export function denseSlotWidth(host: LlvmEmitterContext, element: IrType): numbe
return slotWidth(slotKind(host, elemAccess(element), element));
}
+/** The runtime accessor family of an access: int32 arrays answer through
+ * the double-valued entry points, which convert their slots. */
+export function runtimeAccess(acc: ArrayAccess): "f64" | "bool" | "ref" {
+ return acc === "i32" ? "f64" : acc;
+}
+
+/** The LLVM type of an access's values at the emitter's interface. */
+function valueType(acc: ArrayAccess): "double" | "i1" | "ptr" {
+ return acc === "f64" || acc === "i32" ? "double" : acc === "bool" ? "i1" : "ptr";
+}
+
function valuePointer(
host: LlvmEmitterContext,
guard: Pick,
@@ -285,7 +300,13 @@ function loadState(host: LlvmEmitterContext, guard: DenseGuard): string {
return state;
}
-export type ArrayAccess = "f64" | "bool" | "ref";
+/** The ScrElemKind of int32 arrays (scr_array.h). */
+export const SCR_ELEM_I32 = 7;
+
+/** How a dense array's slots are read and written: doubles, booleans,
+ * references, or the int32 slots of SCR_ELEM_I32 arrays (ir/int32-arrays.ts),
+ * whose values are doubles at the emitter's interface. */
+export type ArrayAccess = "f64" | "bool" | "ref" | "i32";
/** Before a self-append (`a[i] = a[i] + suffix`) replaces a reference
* element: when `index` lies in the hole-free prefix and the slot still
@@ -341,15 +362,33 @@ export function emitDenseArrayGet(
retain: boolean,
proven = false,
): string {
+ return emitDenseArrayRead(host, array, index, indexExpr, acc, element, getter, retain, proven)
+ .value;
+}
+
+/** emitDenseArrayGet, with the int32 of an int32 array's element: the
+ * slot itself on the dense path, the runtime getter's double converted
+ * (exactly: the element is an int32) on the other. */
+export function emitDenseArrayRead(
+ host: LlvmEmitterContext,
+ array: string,
+ index: LlValue,
+ indexExpr: IrExpr | undefined,
+ acc: ArrayAccess,
+ element: IrType,
+ getter: string,
+ retain: boolean,
+ proven = false,
+): { value: string; int32: string | null } {
const B = host.B;
- const ty = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr";
+ const ty = valueType(acc);
const ret = acc === "bool" ? "zeroext i1" : ty;
const kind = slotKind(host, acc, element);
host.declare(`declare ${ret} @${getter}(ptr, double)`);
if (acc === "ref" && retain && !isRefCounted(element)) {
const t = B.tmp();
B.line(`${t} = call ${ty} @${getter}(ptr ${array}, double ${index.name})`);
- return t;
+ return { value: t, int32: null };
}
const slow = B.newLabel("arr.get.slow"),
join = B.newLabel("arr.get.join"),
@@ -380,10 +419,16 @@ export function emitDenseArrayGet(
B.startBlock(valueLabel);
const ptr = valuePointer(host, guard, kind);
let fast: string;
+ let fastInt: string | null = null;
let fastLabel = valueLabel;
if (acc === "f64") {
fast = B.tmp();
B.line(`${fast} = load double, ptr ${ptr}${host.fieldAliasAttachment(ptr)}`);
+ } else if (acc === "i32") {
+ fastInt = B.tmp();
+ fast = B.tmp();
+ B.line(`${fastInt} = load i32, ptr ${ptr}${host.fieldAliasAttachment(ptr)}`);
+ B.line(`${fast} = sitofp i32 ${fastInt} to double`);
} else if (acc === "bool") {
const raw = B.tmp();
fast = B.tmp();
@@ -410,11 +455,21 @@ export function emitDenseArrayGet(
const slowValue = B.tmp();
const attrs = acc === "ref" && retain ? " cold" : READ_FALLBACK;
B.line(`${slowValue} = call ${ty} @${getter}(ptr ${array}, double ${index.name})${attrs}`);
+ let slowInt: string | null = null;
+ if (fastInt !== null) {
+ slowInt = B.tmp();
+ B.line(`${slowInt} = fptosi double ${slowValue} to i32`);
+ }
B.br(join);
B.startBlock(join);
const result = B.tmp();
B.line(`${result} = phi ${ty} [ ${fast}, %${fastLabel} ], [ ${slowValue}, %${slow} ]`);
- return result;
+ let int32: string | null = null;
+ if (fastInt !== null && slowInt !== null) {
+ int32 = B.tmp();
+ B.line(`${int32} = phi i32 [ ${fastInt}, %${fastLabel} ], [ ${slowInt}, %${slow} ]`);
+ }
+ return { value: result, int32 };
}
/** `scr_arr_get_number`: a numeric element, or NaN for every missing,
@@ -425,13 +480,14 @@ export function emitDenseArrayNumber(
index: LlValue,
indexExpr: IrExpr | undefined,
proven = false,
+ acc: "f64" | "i32" = "f64",
): string {
return emitDenseArrayGet(
host,
array,
index,
indexExpr,
- "f64",
+ acc,
{ kind: "f64" },
"scr_arr_get_number",
false,
@@ -486,7 +542,9 @@ export function emitDenseArrayState(
* value's reference. A dense index stores the slot, marks it present, and
* extends the length when it lands at or past it; a replaced present
* reference is released after the new edge is published (a release can
- * collect cycles). Every other index takes the unchanged runtime call. */
+ * collect cycles). Every other index takes the unchanged runtime call.
+ * `int32`, for an int32 array, is the value as an i32 when the caller has
+ * it at hand. */
export function emitDenseArraySet(
host: LlvmEmitterContext,
array: string,
@@ -496,11 +554,12 @@ export function emitDenseArraySet(
element: IrType,
value: string,
proven = false,
+ int32: string | null = null,
): void {
const B = host.B;
- const ty = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr";
+ const ty = valueType(acc);
const kind = slotKind(host, acc, element);
- const setter = `scr_arr_set_${acc}`;
+ const setter = `scr_arr_set_${runtimeAccess(acc)}`;
host.declare(`declare void @${setter}(ptr, double, ${acc === "bool" ? "i1 zeroext" : ty})`);
const call = (attrs: string): void =>
B.line(`call void @${setter}(ptr ${array}, double ${index.name}, ${ty} ${value})${attrs}`);
@@ -508,6 +567,13 @@ export function emitDenseArraySet(
call("");
return;
}
+ // An int32 array's stores are proven int32s (ir/int32-arrays.ts); both
+ // dense paths below store the converted value.
+ let int32Value: string | null = int32;
+ if (acc === "i32" && int32Value === null) {
+ int32Value = B.tmp();
+ B.line(`${int32Value} = fptosi double ${value} to i32`);
+ }
const slow = B.newLabel("arr.set.slow"),
join = B.newLabel("arr.set.join"),
filledLabel = B.newLabel("arr.set.filled");
@@ -526,6 +592,8 @@ export function emitDenseArraySet(
const storeValue = (valuePtr: string): void => {
if (acc === "f64") {
B.line(`store double ${value}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
+ } else if (acc === "i32") {
+ B.line(`store i32 ${int32Value}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
} else if (acc === "bool") {
const bits = B.tmp();
B.line(`${bits} = zext i1 ${value} to i8`);
@@ -713,12 +781,21 @@ export function emitDenseArrayPop(
B.line(`${wide} = zext i8 ${raw} to i64`);
raw = wide;
} else if (slotTy === "i32") {
- // The 8-byte slot of a compact reference is its address.
- const wide = B.tmp(),
- address = B.tmp();
- B.line(`${wide} = zext i32 ${raw} to i64`);
- B.line(`${address} = shl nuw nsw i64 ${wide}, 3`);
- raw = address;
+ if (kind === "cref") {
+ // The 8-byte slot of a compact reference is its address.
+ const wide = B.tmp(),
+ address = B.tmp();
+ B.line(`${wide} = zext i32 ${raw} to i64`);
+ B.line(`${address} = shl nuw nsw i64 ${wide}, 3`);
+ raw = address;
+ } else {
+ // The slot bits of an int32 element's double, as the f64 path returns them.
+ const number = B.tmp(),
+ bits = B.tmp();
+ B.line(`${number} = sitofp i32 ${raw} to double`);
+ B.line(`${bits} = bitcast double ${number} to i64`);
+ raw = bits;
+ }
}
B.line(`store ${slotTy} 0, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
// Only an array with states has a state byte to clear.
@@ -755,8 +832,8 @@ export function emitDenseArrayPush(
const B = host.B;
const size = host.sizeType;
const kind = element === null ? acc : slotKind(host, acc, element);
- const ty = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr";
- const pusher = `scr_arr_push_${acc}`;
+ const ty = valueType(acc);
+ const pusher = `scr_arr_push_${runtimeAccess(acc)}`;
host.declare(`declare double @${pusher}(ptr, ${acc === "bool" ? "i1 zeroext" : ty})`);
const header = (field: number): string => {
const ptr = B.tmp();
@@ -794,6 +871,10 @@ export function emitDenseArrayPush(
const valuePtr = valuePointer(host, { offset: len, data }, kind);
if (acc === "f64") {
B.line(`store double ${value}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
+ } else if (acc === "i32") {
+ const int32 = B.tmp();
+ B.line(`${int32} = fptosi double ${value} to i32`);
+ B.line(`store i32 ${int32}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
} else if (acc === "bool") {
const bits = B.tmp();
B.line(`${bits} = zext i1 ${value} to i8`);
diff --git a/packages/compiler/src/backend/llvm/emit-function.ts b/packages/compiler/src/backend/llvm/emit-function.ts
index 16e654393..b6df7bcca 100644
--- a/packages/compiler/src/backend/llvm/emit-function.ts
+++ b/packages/compiler/src/backend/llvm/emit-function.ts
@@ -61,6 +61,7 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string {
const B = new BlockBuilder();
emitter.B = B;
emitter.currentFunctionName = fn.name;
+ emitter.findInt32Allocations(fn);
emitter.currentTier = fn.tier;
// Fiber switches restore the active exception cell before control
// returns to a synchronous frame (emitPendingCheck).
@@ -138,6 +139,8 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string {
emitter.speculativeFunction = fn.name;
emitter.speculation = null;
emitter.speculationResume = null;
+ emitter.int32Kinds = null;
+ emitter.int32KindResume = null;
emitter.finalFieldBorrows =
!fn.async &&
!fn.generator &&
diff --git a/packages/compiler/src/backend/llvm/emit-statements.ts b/packages/compiler/src/backend/llvm/emit-statements.ts
index b530f794d..c2846a9d4 100644
--- a/packages/compiler/src/backend/llvm/emit-statements.ts
+++ b/packages/compiler/src/backend/llvm/emit-statements.ts
@@ -34,9 +34,16 @@ import { dynHoleRead } from "./loop-array-borrows.js";
import { endsWithJump, matchStringSelfConcat } from "../../ir/analysis.js";
import { charAtUnitRead, emitDiscardedSplice } from "./expr-containers.js";
import { InternalCompilerError } from "../../errors.js";
-import { emitDenseArrayGet, emitDenseArrayPop, emitDenseArraySet } from "./dense-array-access.js";
+import {
+ emitDenseArrayGet,
+ emitDenseArrayPop,
+ emitDenseArraySet,
+ SCR_ELEM_I32,
+} from "./dense-array-access.js";
import { emitBorrowedInput } from "./borrowed-inputs.js";
-import { f64Lit, llvmCommentText, loadArrCount, sizeToDouble } from "./common.js";
+import { f64Lit, llvmCommentText, loadArrCount, SCR_ARR_FIELD, sizeToDouble } from "./common.js";
+import { planInt32KindLoop, type Int32KindLoopPlan } from "./int32-kind-loops.js";
+import { withinInt32 } from "../../ir/integer-ranges.js";
import { resultSlotStore } from "../result-slots.js";
import {
matchIntegerArrayForLoop,
@@ -411,7 +418,21 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
const v = emitter.emitExpr(s.value);
if (s.arr.type.kind !== "array")
throw new InternalCompilerError("llvm emitter bug: arraySet on non-array");
- const acc = elemAccess(s.arr.type.elem);
+ if (emitter.checkedInt32Array(s.arr)) {
+ // An int32 array with checked stores: the int32 arm stores an int32
+ // value, and widens the array for any other first.
+ emitter.emitPublishedArrayGuard(arr.name, s.arr.type, 0, idx.name);
+ const elem = s.arr.type.elem;
+ const proven = emitter.provenDenseIndex(s.arr, s.index);
+ emitter.emitByElementKind(arr.name, null, (acc, widened) => {
+ if (acc === "i32") emitter.emitCheckedInt32Store(arr.name, v.name, s.value, widened);
+ emitDenseArraySet(emitter, arr.name, idx, s.index, acc, elem, v.name, proven);
+ return null;
+ });
+ break;
+ }
+ const acc = emitter.arrayAccess(s.arr, s.arr.type.elem);
+ const int32 = emitter.emitInt32KindStoreCheck(s.arr, v, s.value);
emitter.emitPublishedArrayGuard(arr.name, s.arr.type, 0, idx.name);
if (acc === "ref") emitter.toHeap(v);
emitDenseArraySet(
@@ -423,6 +444,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
s.arr.type.elem,
v.name,
emitter.provenDenseIndex(s.arr, s.index),
+ int32,
);
break;
}
@@ -478,12 +500,19 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void {
B.condBr(dropsLast, pop, call);
B.startBlock(pop);
const elem = s.arr.type.elem;
- const acc = elemAccess(elem);
- const raw = emitDenseArrayPop(emitter, arr.name, acc, elem, call, call);
- if (acc === "ref" && isRefCounted(elem)) {
- const old = B.tmp();
- B.line(`${old} = inttoptr i64 ${raw} to ptr`);
- emitter.releaseHeap(old, elem);
+ if (emitter.checkedInt32Array(s.arr)) {
+ emitter.emitByElementKind(arr.name, null, (acc) => {
+ emitDenseArrayPop(emitter, arr.name, acc, elem, call, call);
+ return null;
+ });
+ } else {
+ const acc = emitter.arrayAccess(s.arr, elem);
+ const raw = emitDenseArrayPop(emitter, arr.name, acc, elem, call, call);
+ if (acc === "ref" && isRefCounted(elem)) {
+ const old = B.tmp();
+ B.line(`${old} = inttoptr i64 ${raw} to ptr`);
+ emitter.releaseHeap(old, elem);
+ }
}
B.br(done);
B.startBlock(call);
@@ -1826,7 +1855,7 @@ export function emitTryCatch(emitter: LlEmitter, s: IrStmt & { kind: "tryCatch"
* case, refcounted/boxed case-body locals are NULL-reset up front and
* the scope-exit releases rely on NULL tolerance. */
export function emitSwitch(emitter: LlEmitter, s: IrStmt & { kind: "switch" }): void {
- const unitSwitch = charAtUnitSwitch(s);
+ const unitSwitch = charAtUnitSwitch(emitter, s);
if (unitSwitch !== null) return emitSwitch(emitter, unitSwitch);
const B = emitter.B;
const discKind = s.disc.type.kind;
@@ -2098,9 +2127,26 @@ export function emitLoopCopies(
)
: null;
if (!plan) {
- const proven = allowed ? planProvenLoop(emitter, loop) : null;
- if (proven) emitProvenLoop(emitter, proven, exit, emitCopy);
- else emitCopy(exit);
+ const ordinary = (exit: string): void => {
+ const proven = allowed ? planProvenLoop(emitter, loop) : null;
+ if (proven) emitProvenLoop(emitter, proven, exit, emitCopy);
+ else emitCopy(exit);
+ };
+ const kinds =
+ allowed &&
+ emitter.speculativeLoopsEnabled &&
+ emitter.int32Slots.checkedInt32Arrays.size > 0 &&
+ emitter.int32Kinds === null &&
+ emitter.speculation === null &&
+ emitter.speculationResume === null
+ ? planInt32KindLoop(
+ loop,
+ (id) => emitter.checkedInt32Local(id),
+ (e) => withinInt32(emitter.integerRanges.get(e)),
+ )
+ : null;
+ if (kinds) emitInt32KindLoop(emitter, kinds, exit, kinds.writes.size > 0 ? emitCopy : ordinary);
+ else ordinary(exit);
return;
}
const B = emitter.B;
@@ -2243,6 +2289,124 @@ export function emitLoopCopies(
}
}
+/** An int32 kind loop (int32-kind-loops.ts). The entry test reads the kind
+ * of each of the plan's arrays: when every one holds int32 slots the fast
+ * copy runs, when every one holds doubles the wide copy, which reads and
+ * writes them as double arrays, and otherwise (a plan with several arrays)
+ * the mixed copy, which tests their kinds at each access. A failed store of
+ * the fast copy re-executes in the third copy, which finishes its
+ * iteration with kind tests and then enters the mixed copy, or the wide
+ * copy when the plan has one array (it just widened). Each copy is emitted
+ * by `ordinary`: the loop's plain copy when a store may fail, since a
+ * proven integer loop keeps locals in shadows of its own copy that another
+ * copy cannot resume from. */
+function emitInt32KindLoop(
+ emitter: LlEmitter,
+ plan: Int32KindLoopPlan,
+ exit: string,
+ ordinary: (exit: string) => void,
+): void {
+ const B = emitter.B;
+ const fn = emitter.currentFunctionName;
+ const keys = new Set([...plan.arrays.keys()].map((id) => `${fn}\0${id}`));
+ const several = keys.size > 1;
+ let allInt32 = "true",
+ allWide = "true";
+ const and = (all: string, next: string): string => {
+ if (all === "true") return next;
+ const both = B.tmp();
+ B.line(`${both} = and i1 ${all}, ${next}`);
+ return both;
+ };
+ for (const ref of plan.arrays.values()) {
+ const arr = emitter.emitStableReceiver(ref, []);
+ const kindPtr = B.tmp(),
+ kind = B.tmp(),
+ isInt32 = B.tmp();
+ B.line(
+ `${kindPtr} = getelementptr inbounds %ScrArr, ptr ${arr.name}, i32 0, i32 ${SCR_ARR_FIELD.elem}`,
+ );
+ emitter.markMemoryPointer(kindPtr, "array:header");
+ B.line(`${kind} = load i8, ptr ${kindPtr}${emitter.fieldAliasAttachment(kindPtr)}`);
+ B.line(`${isInt32} = icmp eq i8 ${kind}, ${SCR_ELEM_I32}`);
+ allInt32 = and(allInt32, isInt32);
+ if (several) {
+ const isWide = B.tmp();
+ B.line(`${isWide} = xor i1 ${isInt32}, true`);
+ allWide = and(allWide, isWide);
+ }
+ }
+ const fast = B.newLabel("kind.fast"),
+ wide = B.newLabel("kind.wide"),
+ mixed = several ? B.newLabel("kind.mixed") : null;
+ if (mixed === null) B.condBr(allInt32, fast, wide);
+ else {
+ const other = B.newLabel("kind.other");
+ B.condBr(allInt32, fast, other);
+ B.startBlock(other);
+ B.condBr(allWide, wide, mixed);
+ }
+ const resume = new Map();
+ for (const s of plan.writes) resume.set(s, { label: B.newLabel("kind.resume"), placed: false });
+ // The header the third copy enters: the mixed copy's, or the wide copy's.
+ let header: string | null = null;
+ const capture = (label: string): void => {
+ header = label;
+ };
+ B.startBlock(wide);
+ if (resume.size > 0 && mixed === null) emitter.speculationHeader = capture;
+ emitter.int32Kinds = { int32: new Set(), wide: keys, resume: new Map() };
+ try {
+ ordinary(exit);
+ } finally {
+ emitter.speculationHeader = null;
+ emitter.int32Kinds = null;
+ }
+ if (mixed !== null) {
+ B.startBlock(mixed);
+ if (resume.size > 0) emitter.speculationHeader = capture;
+ try {
+ ordinary(exit);
+ } finally {
+ emitter.speculationHeader = null;
+ }
+ }
+ if (resume.size > 0) {
+ if (header === null)
+ throw new InternalCompilerError("llvm emitter bug: int32 kind loop copy without a header");
+ // A failed store finishes its iteration here, as in a speculative
+ // integer loop's third copy.
+ B.startBlock(B.newLabel("kind.tail"));
+ emitter.speculationResume = resume;
+ emitter.speculationHeader = () => {
+ B.br(header!);
+ B.startBlock(B.newLabel("kind.unused"));
+ };
+ try {
+ ordinary(exit);
+ } finally {
+ emitter.speculationResume = null;
+ emitter.speculationHeader = null;
+ }
+ }
+ B.startBlock(fast);
+ if (![...resume.values()].every((r) => r.placed)) {
+ // A write the third copy never reached: this copy tests the kinds.
+ ordinary(exit);
+ return;
+ }
+ emitter.int32Kinds = {
+ int32: keys,
+ wide: new Set(),
+ resume: new Map([...resume].map(([s, r]) => [s, r.label])),
+ };
+ try {
+ ordinary(exit);
+ } finally {
+ emitter.int32Kinds = null;
+ }
+}
+
/** At the top of a loop copy's header: lets a speculative loop find its
* ordinary copy's header, and keeps its third copy out of the loop. */
function enterLoopHeader(emitter: LlEmitter, header: string): void {
@@ -2518,7 +2682,10 @@ function nullableStringSwitch(
* one-unit string literals, as a switch on `s.charCodeAt(i)` with the units
* as cases: no character string is built. An index out of range answers
* NaN, which matches no case, as "" matches no one-unit literal. */
-function charAtUnitSwitch(s: IrStmt & { kind: "switch" }): (IrStmt & { kind: "switch" }) | null {
+function charAtUnitSwitch(
+ emitter: LlEmitter,
+ s: IrStmt & { kind: "switch" },
+): (IrStmt & { kind: "switch" }) | null {
const disc = s.disc;
if (disc.kind !== "strIntrinsic" || disc.method !== "charAt" || disc.args.length !== 1)
return null;
@@ -2531,7 +2698,7 @@ function charAtUnitSwitch(s: IrStmt & { kind: "switch" }): (IrStmt & { kind: "sw
return null;
return {
...s,
- disc: charAtUnitRead(disc),
+ disc: charAtUnitRead(emitter, disc),
cases: s.cases.map((c) =>
c.test?.kind === "strLit"
? {
diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts
index 8e14541af..fdaaf595d 100644
--- a/packages/compiler/src/backend/llvm/emitter.ts
+++ b/packages/compiler/src/backend/llvm/emitter.ts
@@ -79,8 +79,18 @@ import {
} from "./emit-journal.js";
import { specializeNumericCalls } from "../../ir/numeric-call-specialization.js";
import { versionLoopBoundParameters } from "../../ir/parameter-versions.js";
+import {
+ allocatedLiteral,
+ fillAllocationLength,
+ selectInt32Arrays,
+} from "../../ir/int32-arrays.js";
import { everyExpr, everyExprChild, everyStmtChild, everyStmtList } from "../../ir/traverse.js";
-import { INT32_RANGE, type IntegerRange, type IntegerRanges } from "../../ir/integer-ranges.js";
+import {
+ INT32_RANGE,
+ withinInt32,
+ type IntegerRange,
+ type IntegerRanges,
+} from "../../ir/integer-ranges.js";
import { unshareNodes } from "../../ir/unshare.js";
import { copyStableCaptures } from "../../ir/capture-values.js";
import { resolveSelfCaptures } from "../../ir/self-captures.js";
@@ -100,7 +110,7 @@ import {
} from "./int32-abi.js";
import { type Int32Local } from "./int32-locals.js";
import { findIntegerClones, NO_INTEGER_CLONES, type IntegerClones } from "./integer-clones.js";
-import { exactInteger, narrowInteger } from "./integer-values.js";
+import { exactInteger, narrowInteger, widenInteger } from "./integer-values.js";
import { findInitializerBindings } from "../../ir/initializer-bindings.js";
import { findConstantNumericTables, type ConstantNumericTable } from "../../ir/constant-tables.js";
import {
@@ -147,7 +157,12 @@ import {
} from "./inline-unions.js";
import { OptionalArrayReads, type LocalArrayRead } from "./local-array-reads.js";
import { CheckedNarrows } from "./checked-narrows.js";
-import { emitDenseArraySet, emitDenseSlotUnlink } from "./dense-array-access.js";
+import {
+ emitDenseArraySet,
+ emitDenseSlotUnlink,
+ SCR_ELEM_I32,
+ type ArrayAccess,
+} from "./dense-array-access.js";
import { matchMapRead, type MapReadLifetimes } from "./map-read-lifetimes.js";
import { type MapEntryFusion } from "./map-entry-fusion.js";
import { emitBorrowedFieldSequence, guardedValue } from "./borrowed-receivers.js";
@@ -168,7 +183,7 @@ import {
} from "./string-slices.js";
import { LazyCaptures } from "./lazy-captures.js";
import { StackCaptures } from "./stack-captures.js";
-import { emitFieldAbsentTest, f64Lit } from "./common.js";
+import { emitFieldAbsentTest, f64Lit, SCR_ARR_FIELD } from "./common.js";
import { ffiExtendsNarrowIntegers } from "../targets.js";
import {
emitLiteralExpr,
@@ -483,39 +498,65 @@ function versionParameters(mod: IrModule): IrModule {
return versionLoopBoundParameters(mod, mangleFunction);
}
-export function emitLlvmModule(input: IrModule, options: LlvmTargetOptions = {}): string {
+/** The int32 arrays the int32 slot analysis keeps in `mod` (proven and
+ * checked), under the inputs LlEmitter gives it (selectInt32Arrays). */
+function provenInt32Arrays(mod: IrModule): ReadonlySet {
+ const slots = analyzeInt32Slots(mod, {
+ dynamicStores: dynamicFieldStores(mod),
+ initializerBindings: findInitializerBindings(mod),
+ });
+ return new Set([...slots.int32Arrays, ...slots.checkedInt32Arrays]);
+}
+
+/** The emitter for `input` after the module passes every emission runs. */
+function moduleEmitter(input: IrModule, options: LlvmTargetOptions): LlEmitter {
// Facts are keyed by node identity: emit the same module whether it was
// lowered in memory or read back from its serialized form.
const mod = unshareNodes(input);
// Keep source storage intact for debugger inspection in dev builds.
return new LlEmitter(
options.debugSources === undefined
- ? versionParameters(
- addRecordResultEntries(
- scalarizeNumericRecords(
- specializeCallbackCalls(
- specializeNumericCalls(
- foldConstantGlobals(
- copyStableCaptures(
- resolveSelfCaptures(
- hoistFinalFieldReads(
- freezeInitOnlyGlobals(
- holdCharLocalsAsUnits(dropUnwrittenPropertyBags(mod)),
+ ? selectInt32Arrays(
+ versionParameters(
+ addRecordResultEntries(
+ scalarizeNumericRecords(
+ specializeCallbackCalls(
+ specializeNumericCalls(
+ foldConstantGlobals(
+ copyStableCaptures(
+ resolveSelfCaptures(
+ hoistFinalFieldReads(
+ freezeInitOnlyGlobals(
+ holdCharLocalsAsUnits(dropUnwrittenPropertyBags(mod)),
+ ),
),
),
),
),
+ mangleFunction,
),
mangleFunction,
),
- mangleFunction,
),
),
),
+ provenInt32Arrays,
)
: mod,
options,
- ).emit();
+ );
+}
+
+export function emitLlvmModule(input: IrModule, options: LlvmTargetOptions = {}): string {
+ return moduleEmitter(input, options).emit();
+}
+
+/** emitLlvmModule's text as parts that concatenate to it, without the copy
+ * that joining them makes: a large program's text is 150 MB. */
+export function emitLlvmModuleParts(input: IrModule, options: LlvmTargetOptions = {}): string[] {
+ return moduleEmitter(input, options)
+ .emitParts()
+ .flatMap((part, index) => (index === 0 ? [part] : ["\n", part]));
}
/** Keep large translation units below the host's single-string limit. */
@@ -525,35 +566,7 @@ export function emitLlvmModuleSource(
): string | readonly string[] {
// IR validation, contracts and the frontend's disposal run after lowering without checks.
relieveHeapPressure(true);
- const mod = unshareNodes(input);
- const parts = new LlEmitter(
- options.debugSources === undefined
- ? versionParameters(
- addRecordResultEntries(
- scalarizeNumericRecords(
- specializeCallbackCalls(
- specializeNumericCalls(
- foldConstantGlobals(
- copyStableCaptures(
- resolveSelfCaptures(
- hoistFinalFieldReads(
- freezeInitOnlyGlobals(
- holdCharLocalsAsUnits(dropUnwrittenPropertyBags(mod)),
- ),
- ),
- ),
- ),
- ),
- mangleFunction,
- ),
- mangleFunction,
- ),
- ),
- ),
- )
- : mod,
- options,
- ).emitParts();
+ const parts = moduleEmitter(input, options).emitParts();
const length = parts.reduce((total, part) => total + part.length, Math.max(0, parts.length - 1));
if (length <= 256 * 1024 * 1024) return parts.join("\n");
return parts.flatMap((part, index) => (index === 0 ? [part] : ["\n", part]));
@@ -734,6 +747,228 @@ export class LlEmitter {
return `, !tbaa !${tag}`;
}
+ /** The current function's int32 array allocations: the array literals
+ * its int32 arrays are written with. They construct SCR_ELEM_I32 arrays. */
+ int32Allocations = new Set();
+ /** The fill temporaries (`new Array(n).fill(v)`) of those allocations:
+ * the same arrays, so int32 too. */
+ int32ArrayTemps = new Set();
+ /** The same for the current function's int32 arrays with checked stores,
+ * which are only allocated by fills (ir/int32-arrays.ts
+ * checkableInt32Allocations): their temporaries' literals, with the fill's
+ * length (a literal constructs SCR_ELEM_I32 storage when that length is at
+ * least CHECKED_INT32_MIN_LENGTH, SCR_ELEM_F64 otherwise), and their
+ * temporaries. */
+ checkedAllocations = new Map();
+ checkedArrayTemps = new Set();
+ /** The fill calls (`.fill(v)`) of those allocations: a value that is not
+ * an int32 widens the new array first. */
+ checkedFills = new Set();
+
+ /** Collect the int32 allocations of `fn`, about to be emitted. */
+ findInt32Allocations(fn: IrFunction): void {
+ this.int32Allocations.clear();
+ this.int32ArrayTemps.clear();
+ this.checkedAllocations.clear();
+ this.checkedArrayTemps.clear();
+ this.checkedFills.clear();
+ const { int32Arrays, checkedInt32Arrays } = this.int32Slots;
+ if (int32Arrays.size === 0 && checkedInt32Arrays.size === 0) return;
+ everyStmtList(fn.body, {
+ stmt: (s) => {
+ if (s.kind !== "varDecl" && s.kind !== "assign") return true;
+ const key = `${fn.name}\0${s.localId}`;
+ const checked = checkedInt32Arrays.has(key);
+ if (!checked && !int32Arrays.has(key)) return true;
+ const value = s.kind === "varDecl" ? s.init : s.value;
+ const allocation = value && allocatedLiteral(value);
+ if (!allocation) return true;
+ const length = checked ? fillAllocationLength(value) : null;
+ if (length !== null) this.checkedAllocations.set(allocation.literal, length);
+ else if (!checked) this.int32Allocations.add(allocation.literal);
+ if (allocation.temporary !== null)
+ (checked ? this.checkedArrayTemps : this.int32ArrayTemps).add(allocation.temporary);
+ if (checked && value !== allocation.literal) this.checkedFills.add(value!);
+ return true;
+ },
+ expr: () => true,
+ });
+ }
+
+ /** Whether `arr` is one of the current function's int32 arrays
+ * (ir/int32-arrays.ts, kept by the int32 slot analysis). */
+ int32Array(arr: IrExpr): boolean {
+ if (arr.kind !== "varRef") return false;
+ if (this.int32KindArray(arr)) return true;
+ if (this.int32Slots.int32Arrays.size === 0) return false;
+ return (
+ this.int32ArrayTemps.has(arr.localId) ||
+ this.int32Slots.int32Arrays.has(`${this.currentFunctionName}\0${arr.localId}`)
+ );
+ }
+
+ /** Whether `arr` is one of the current function's int32 arrays with
+ * checked stores: int32 slots until a store widens it to doubles. Every
+ * access tests its kind (emitByElementKind), except in the fast copy of
+ * an int32 kind loop, which reads and writes it as an int32 array. */
+ checkedInt32Array(arr: IrExpr): boolean {
+ if (arr.kind !== "varRef" || this.int32KindArray(arr)) return false;
+ if (this.int32Kinds?.wide.has(`${this.currentFunctionName}\0${arr.localId}`)) return false;
+ return this.checkedInt32Local(arr.localId);
+ }
+
+ /** Whether the local `localId` holds one of the current function's int32
+ * arrays with checked stores. */
+ checkedInt32Local(localId: string): boolean {
+ if (this.int32Slots.checkedInt32Arrays.size === 0) return false;
+ return (
+ this.checkedArrayTemps.has(localId) ||
+ this.int32Slots.checkedInt32Arrays.has(`${this.currentFunctionName}\0${localId}`)
+ );
+ }
+
+ /** Whether `arr` is an int32 array with checked stores that the fast copy
+ * of the int32 kind loop being emitted holds as an int32 array. */
+ int32KindArray(arr: IrExpr): boolean {
+ return (
+ this.int32Kinds !== null &&
+ arr.kind === "varRef" &&
+ this.int32Kinds.int32.has(`${this.currentFunctionName}\0${arr.localId}`)
+ );
+ }
+
+ /** Before a store of `value` (of `expr`) into `arr`, in the fast copy of
+ * an int32 kind loop: continue when it is an int32 that is not -0, and
+ * otherwise leave for the statement's resume point, where it re-executes
+ * with kind tests. Returns the value as an i32 when it is at hand as an
+ * integer, or null when the store converts the double itself. */
+ emitInt32KindStoreCheck(arr: IrExpr, value: LlValue, expr: IrExpr): string | null {
+ if (!this.int32KindArray(arr) || withinInt32(this.integerRanges.get(expr))) return null;
+ const integer = exactInteger(this, value, expr);
+ if (integer !== null && withinInt32(integer.range)) return narrowInteger(this, integer);
+ const resume = this.int32KindResume;
+ if (resume === null)
+ throw new InternalCompilerError("llvm emitter bug: an int32 kind store without a resume");
+ const B = this.B;
+ const ok = B.tmp(),
+ int = B.tmp();
+ if (integer !== null) {
+ // An exact integer: a range test on its 64-bit form.
+ const wide = widenInteger(this, integer),
+ low = B.tmp(),
+ high = B.tmp();
+ B.line(`${low} = icmp sge i64 ${wide}, -2147483648`);
+ B.line(`${high} = icmp sle i64 ${wide}, 2147483647`);
+ B.line(`${ok} = and i1 ${low}, ${high}`);
+ const store = B.newLabel("arr.store.i32");
+ B.condBr(ok, store, resume);
+ B.startBlock(store);
+ B.line(`${int} = trunc i64 ${wide} to i32`);
+ return int;
+ }
+ this.declare("declare i32 @llvm.fptosi.sat.i32.f64(double)");
+ const back = B.tmp(),
+ exact = B.tmp(),
+ bits = B.tmp(),
+ notNegativeZero = B.tmp();
+ B.line(`${int} = call i32 @llvm.fptosi.sat.i32.f64(double ${value.name})`);
+ B.line(`${back} = sitofp i32 ${int} to double`);
+ B.line(`${exact} = fcmp oeq double ${back}, ${value.name}`);
+ B.line(`${bits} = bitcast double ${value.name} to i64`);
+ B.line(`${notNegativeZero} = icmp ne i64 ${bits}, -9223372036854775808`);
+ B.line(`${ok} = and i1 ${exact}, ${notNegativeZero}`);
+ const store = B.newLabel("arr.store.i32");
+ B.condBr(ok, store, resume);
+ B.startBlock(store);
+ return int;
+ }
+
+ /** Emit an access to the checked int32 array `array` once per element
+ * kind: `arm("i32", widened)` while it holds int32 slots and
+ * `arm("f64", widened)` after it widened, joined after both. `widened` is
+ * the label of the double arm, where an int32 arm's store of a value that
+ * is not an int32 continues after widening the array. Returns the phi of
+ * the arms' results (of LLVM type `ty`), or null when `ty` is null. */
+ emitByElementKind(
+ array: string,
+ ty: string | null,
+ arm: (acc: "i32" | "f64", widened: string) => string | null,
+ ): string | null {
+ const B = this.B;
+ const narrow = B.newLabel("arr.kind.i32"),
+ wide = B.newLabel("arr.kind.f64"),
+ join = B.newLabel("arr.kind.join");
+ const kindPtr = B.tmp(),
+ kind = B.tmp(),
+ isInt32 = B.tmp();
+ B.line(
+ `${kindPtr} = getelementptr inbounds %ScrArr, ptr ${array}, i32 0, i32 ${SCR_ARR_FIELD.elem}`,
+ );
+ this.markMemoryPointer(kindPtr, "array:header");
+ B.line(`${kind} = load i8, ptr ${kindPtr}${this.fieldAliasAttachment(kindPtr)}`);
+ B.line(`${isInt32} = icmp eq i8 ${kind}, ${SCR_ELEM_I32}`);
+ B.condBr(isInt32, narrow, wide);
+ B.startBlock(narrow);
+ const narrowValue = arm("i32", wide);
+ const narrowEnd = B.isTerminated() ? null : B.currentLabel();
+ if (narrowEnd !== null) B.br(join);
+ B.startBlock(wide);
+ const wideValue = arm("f64", wide);
+ const wideEnd = B.isTerminated() ? null : B.currentLabel();
+ if (wideEnd !== null) B.br(join);
+ B.startBlock(join);
+ // Arms that end in their own branches (a read's shared join) leave this
+ // join without predecessors.
+ if (narrowEnd === null && wideEnd === null) B.terminate("unreachable");
+ if (ty === null) return null;
+ const arms: string[] = [];
+ if (narrowEnd !== null) arms.push(`[ ${narrowValue}, %${narrowEnd} ]`);
+ if (wideEnd !== null) arms.push(`[ ${wideValue}, %${wideEnd} ]`);
+ const result = B.tmp();
+ B.line(`${result} = phi ${ty} ${arms.join(", ")}`);
+ return result;
+ }
+
+ /** Before storing the double `value` (of `expr`) into an int32 arm
+ * (emitByElementKind) of the checked int32 array `array`: continue in this
+ * arm when it is an int32 that is not -0, or widen the array and continue
+ * at `widened`. A value proven an int32 needs no test. */
+ emitCheckedInt32Store(array: string, value: string, expr: IrExpr, widened: string): void {
+ if (withinInt32(this.integerRanges.get(expr))) return;
+ const B = this.B;
+ this.declare("declare i32 @llvm.fptosi.sat.i32.f64(double)");
+ this.declare("declare void @scr_arr_widen_i32(ptr)");
+ const int = B.tmp(),
+ back = B.tmp(),
+ exact = B.tmp(),
+ bits = B.tmp(),
+ notNegativeZero = B.tmp(),
+ ok = B.tmp();
+ B.line(`${int} = call i32 @llvm.fptosi.sat.i32.f64(double ${value})`);
+ B.line(`${back} = sitofp i32 ${int} to double`);
+ B.line(`${exact} = fcmp oeq double ${back}, ${value}`);
+ B.line(`${bits} = bitcast double ${value} to i64`);
+ B.line(`${notNegativeZero} = icmp ne i64 ${bits}, -9223372036854775808`);
+ B.line(`${ok} = and i1 ${exact}, ${notNegativeZero}`);
+ const store = B.newLabel("arr.store.i32"),
+ widen = B.newLabel("arr.widen");
+ B.condBr(ok, store, widen);
+ B.startBlock(widen);
+ B.line(`call void @scr_arr_widen_i32(ptr ${array})`);
+ B.br(widened);
+ B.startBlock(store);
+ }
+
+ /** The access an element operation on `arr` (of element type `elem`)
+ * takes: int32 arrays read and write their 4-byte slots. */
+ arrayAccess(arr: IrExpr, elem: IrType): ArrayAccess {
+ if (this.checkedInt32Array(arr))
+ throw new InternalCompilerError(
+ "llvm emitter bug: a checked int32 array without its kind test",
+ );
+ return this.int32Array(arr) ? "i32" : elemAccess(elem);
+ }
+
/** Whether an element access at `index` of `arr` lies in the hole-free
* prefix: the counter of a dense array loop's fast copy indexing one of
* the arrays its entry test covered. */
@@ -1180,6 +1415,14 @@ export class LlEmitter {
* (`enterLoopHeader`): records the ordinary copy's header, or sends the
* third copy's header there. */
speculationHeader: ((header: string) => void) | null = null;
+ /** A copy of an int32 kind loop (int32-kind-loops.ts) being emitted: the
+ * int32 arrays with checked stores it reads and writes as int32 arrays
+ * (the fast copy) or as double arrays (an ordinary copy entered only
+ * after they widened), as `function\0local` keys, and where each of the
+ * fast copy's writes resumes in the third copy. */
+ int32Kinds: { int32: Set; wide: Set; resume: Map } | null = null;
+ /** The resume label of the fast-copy write being emitted. */
+ int32KindResume: string | null = null;
/** The function being emitted, for speculative globals. */
speculativeFunction = "";
/** Every function that writes each number global (computed on demand). */
@@ -1201,6 +1444,10 @@ export class LlEmitter {
* (findIntegerOrNaNLocals in wrapping-sums.ts). */
integerOrNaNLocals: ReadonlyMap = new Map();
bytesBounds: ReadonlySet = new Set();
+ /** The charCodeAt reads standing for charAt reads the emitter rewrote
+ * (one-unit tests and switches, charAtUnitRead), with the charAt each
+ * stands for: a loop version's proof for the charAt covers its read. */
+ readonly charAtUnitReads = new Map();
/** Worker programs: typed-array accesses whose views an enclosing version
* guard proved thread-owned. */
bytesOwned: ReadonlySet = new Set();
@@ -5269,10 +5516,14 @@ export class LlEmitter {
const loop =
s.kind === "while" || s.kind === "doWhile" || s.kind === "for" || s.kind === "forOf";
if (loop) this.loopDepth++;
+ // An int32 kind loop's fast-copy write leaves for its resume point.
+ const kindResume = this.int32KindResume;
+ this.int32KindResume = this.int32Kinds?.resume.get(s) ?? null;
try {
this.emitStmtBody(s);
} finally {
this.B.debugLocation = previous;
+ this.int32KindResume = kindResume;
if (loop) this.loopDepth--;
}
}
@@ -6519,7 +6770,7 @@ export class LlEmitter {
return tagInSet(this, uName, unionId, tags);
}
- arrPush(arr: string, acc: "f64" | "bool" | "ref", value: string, element: IrType | null): string {
+ arrPush(arr: string, acc: ArrayAccess, value: string, element: IrType | null): string {
return arrPush(this, arr, acc, value, element);
}
diff --git a/packages/compiler/src/backend/llvm/expr-bytes.ts b/packages/compiler/src/backend/llvm/expr-bytes.ts
index c16ae856a..41ddf2b9d 100644
--- a/packages/compiler/src/backend/llvm/expr-bytes.ts
+++ b/packages/compiler/src/backend/llvm/expr-bytes.ts
@@ -1118,7 +1118,9 @@ export function emitInlineAtomicsOp(
if (op !== 0 && op !== 1 && op !== 3 && ATOMIC_RMW[op] === undefined) return null;
const elem = array.type.elem;
const B = host.B;
- const receiver = host.emitExpr(array).name;
+ // The instruction and scr_atomics_op borrow the array: an unchanged local
+ // owns it across the later operands and the operation.
+ const receiver = emitBorrowedInput(host, array).name;
const idxValue = host.emitExpr(index);
const operand = host.emitExpr(value);
const swap = host.emitExpr(replacement);
diff --git a/packages/compiler/src/backend/llvm/expr-containers.ts b/packages/compiler/src/backend/llvm/expr-containers.ts
index 2f4cd98e9..1ba5da818 100644
--- a/packages/compiler/src/backend/llvm/expr-containers.ts
+++ b/packages/compiler/src/backend/llvm/expr-containers.ts
@@ -60,7 +60,10 @@ import {
emitDenseArrayNumber,
emitDenseArrayPop,
emitDenseArrayPush,
+ runtimeAccess,
+ type ArrayAccess,
} from "./dense-array-access.js";
+import { INT32_ARRAY_METHODS } from "../../ir/int32-arrays.js";
import { emitUnionWiden } from "./expr-records.js";
import { registerReplay } from "./emit-journal.js";
@@ -115,7 +118,7 @@ export function tagInSet(
export function arrPush(
host: LlvmEmitterContext,
arr: string,
- acc: "f64" | "bool" | "ref",
+ acc: ArrayAccess,
value: string,
element: IrType | null,
): string {
@@ -126,7 +129,7 @@ export function arrPush(
export function emitArrayValues(
host: LlvmEmitterContext,
arr: string,
- acc: "f64" | "bool" | "ref",
+ acc: ArrayAccess,
element: IrType,
values: LlValue[],
prepend = false,
@@ -142,12 +145,15 @@ export function emitArrayValues(
}
if (values.length === 1) {
if (!prepend) return arrPush(host, arr, acc, values[0]!.name, element);
- const ty = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr";
+ const flat = runtimeAccess(acc);
+ const ty = flat === "f64" ? "double" : flat === "bool" ? "i1" : "ptr";
host.declare(
- `declare double @scr_arr_unshift_${acc}(ptr, ${ty}${acc === "bool" ? " zeroext" : ""})`,
+ `declare double @scr_arr_unshift_${flat}(ptr, ${ty}${flat === "bool" ? " zeroext" : ""})`,
);
const result = B.tmp();
- B.line(`${result} = call double @scr_arr_unshift_${acc}(ptr ${arr}, ${ty} ${values[0]!.name})`);
+ B.line(
+ `${result} = call double @scr_arr_unshift_${flat}(ptr ${arr}, ${ty} ${values[0]!.name})`,
+ );
return result;
}
const capacity = Math.min(values.length, 64);
@@ -165,8 +171,9 @@ export function emitArrayValues(
const slot = B.tmp(),
pointer = B.tmp();
const value = values[start + i]!.name;
+ // The runtime takes double bits for int32 arrays too, and converts.
const pack =
- acc === "f64"
+ acc === "f64" || acc === "i32"
? `bitcast double ${value} to i64`
: acc === "bool"
? `zext i1 ${value} to i64`
@@ -286,14 +293,11 @@ export function emitArrayCopyLoop(
host.B.line(`call double @scr_arr_push_spread(ptr ${dst}, ptr ${src})`);
}
-/** The `charCodeAt` reads that stand for `charAt` reads the emitter
- * rewrote, such as one-unit tests and switches: a loop version's proof
- * for the charAt covers its read. */
-const unitReadOrigins = new WeakMap();
-
/** `s.charCodeAt(i)` in place of `s.charAt(i)`, the same index read as a
- * code unit. */
+ * code unit. The emitter records the charAt it stands for
+ * (charAtUnitReads), so a loop version's proof for the charAt covers it. */
export function charAtUnitRead(
+ host: LlvmEmitterContext,
charAt: IrExpr & { kind: "strIntrinsic" },
): IrExpr & { kind: "strIntrinsic" } {
const read: IrExpr & { kind: "strIntrinsic" } = {
@@ -304,7 +308,7 @@ export function charAtUnitRead(
type: F64,
loc: charAt.loc,
};
- unitReadOrigins.set(read, charAt);
+ host.charAtUnitReads.set(read, charAt);
return read;
}
@@ -312,7 +316,7 @@ export function charAtUnitRead(
* below the length (bytes-loop-versions.ts). */
function provenStringRead(host: LlvmEmitterContext, e: IrExpr): boolean {
if (host.bytesBounds.has(e)) return true;
- const origin = unitReadOrigins.get(e);
+ const origin = host.charAtUnitReads.get(e);
return origin !== undefined && host.bytesBounds.has(origin);
}
@@ -610,7 +614,15 @@ export function emitArrIntrinsic(
if (e.receiver.type.kind !== "array")
throw new InternalCompilerError("llvm emitter bug: arrIntrinsic on non-array");
const elem = e.receiver.type.elem;
- const acc = elemAccess(elem);
+ // Int32 arrays (ir/int32-arrays.ts) reach only these methods; the others
+ // take the runtime family's flat access. One with checked stores tests
+ // its kind at each inline access (emitByElementKind) and otherwise takes
+ // the double entry points, which serve both kinds.
+ const checked = host.checkedInt32Array(e.receiver);
+ const access = checked ? "f64" : host.arrayAccess(e.receiver, elem);
+ if ((access === "i32" || checked) && !INT32_ARRAY_METHODS.has(e.method))
+ throw new InternalCompilerError(`llvm emitter bug: ${e.method} on an int32 array`);
+ const acc = runtimeAccess(access);
const accTy = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr";
const accArg = acc === "bool" ? "i1 zeroext" : accTy;
const method = e.method;
@@ -631,6 +643,13 @@ export function emitArrIntrinsic(
e.receiver.kind === "varRef"
? host.constantNumericTables.get(e.receiver.localId)
: undefined;
+ if (!table && elem.kind === "f64" && checked) {
+ const proven = host.provenDenseIndex(e.receiver, e.args[0]!);
+ const value = host.emitByElementKind(r.name, "double", (kind) =>
+ emitDenseArrayNumber(host, r.name, index, e.args[0], proven, kind),
+ )!;
+ return { name: value, type: e.type };
+ }
if (!table && elem.kind === "f64")
return {
name: emitDenseArrayNumber(
@@ -639,6 +658,7 @@ export function emitArrIntrinsic(
index,
e.args[0],
host.provenDenseIndex(e.receiver, e.args[0]!),
+ access === "i32" ? "i32" : "f64",
),
type: e.type,
};
@@ -675,7 +695,22 @@ export function emitArrIntrinsic(
// no-op zero-argument call.
const vs = e.args.map((a) => host.emitExpr(a));
host.emitPublishedArrayGuard(r.name, e.receiver.type, 1, f64Lit(0), f64Lit(vs.length));
- return { name: emitArrayValues(host, r.name, acc, elem, vs), type: e.type };
+ if (checked && vs.length > 0) {
+ // Each value appends in its own kind test: one that is not an int32
+ // widens the array before it is pushed.
+ let length = "";
+ vs.forEach((v, i) => {
+ length = host.emitByElementKind(r.name, "double", (kind, widened) => {
+ if (kind === "i32") host.emitCheckedInt32Store(r.name, v.name, e.args[i]!, widened);
+ return arrPush(host, r.name, kind, v.name, elem);
+ })!;
+ });
+ return { name: length, type: e.type };
+ }
+ // In an int32 kind loop's fast copy, every value is checked before
+ // the first appends.
+ vs.forEach((v, i) => host.emitInt32KindStoreCheck(e.receiver, v, e.args[i]!));
+ return { name: emitArrayValues(host, r.name, access, elem, vs), type: e.type };
}
case "pushSpread": {
// `a.push(...src)`: append src's elements in order (borrowed src,
@@ -749,7 +784,13 @@ export function emitArrIntrinsic(
let slowLabel = "";
if (method === "pop") {
slowLabel = B.newLabel("remove.slow");
- denseRaw = emitDenseArrayPop(host, r.name, acc, elem, la, slowLabel);
+ const empty = la,
+ slow = slowLabel;
+ denseRaw = checked
+ ? host.emitByElementKind(r.name, "i64", (kind) =>
+ emitDenseArrayPop(host, r.name, kind, elem, empty, slow),
+ )!
+ : emitDenseArrayPop(host, r.name, access, elem, la, slowLabel);
denseLabel = B.currentLabel();
B.br(lp);
B.startBlock(slowLabel);
@@ -938,6 +979,14 @@ export function emitArrIntrinsic(
case "fillUndefined": {
const args = e.args.map((arg) => host.emitExpr(arg));
host.emitPublishedArrayGuard(r.name, e.receiver.type, 7);
+ if (host.checkedFills.has(e)) {
+ // A new int32 array with checked stores, filled with a value that
+ // may not be an int32: widen it first then.
+ const filled = B.newLabel("arr.fill.kind");
+ host.emitCheckedInt32Store(r.name, args[0]!.name, e.args[0]!, filled);
+ B.br(filled);
+ B.startBlock(filled);
+ }
const helper =
e.method === "copyWithin"
? "scr_arr_copy_within"
diff --git a/packages/compiler/src/backend/llvm/expr-context.ts b/packages/compiler/src/backend/llvm/expr-context.ts
index 9dd6bbcac..df6dfc608 100644
--- a/packages/compiler/src/backend/llvm/expr-context.ts
+++ b/packages/compiler/src/backend/llvm/expr-context.ts
@@ -15,11 +15,23 @@ export interface LlValue {
/** An exact integer snapshot, excluding negative zero, with no discarded
* high bits and the proven range of its original value. */
integer?: { name: string; type: "i32" | "i64"; signed: boolean; range: IntegerRange };
+ /** An i64 holding the exact integer that the arithmetic producing this
+ * number computes, when that integer has no proven safe range: its
+ * absolute value is at most `bound`, and the double equals it whenever
+ * its absolute value is below `below` (a zero may be -0). Index consumers
+ * use it in place of a round trip (integer-values.ts `indexInteger`). */
+ wideInteger?: WideInteger;
slot?: boolean;
/** A sequence local held in a capture box; release the box itself. */
boxed?: boolean;
}
+export interface WideInteger {
+ name: string;
+ below: number;
+ bound: number;
+}
+
export type ExprOf = Extract;
export type LibCallExpr = ExprOf<"libCall">;
type LibCallPrefixOf = T extends `${infer Prefix}.${string}` ? Prefix : never;
diff --git a/packages/compiler/src/backend/llvm/expr-primitives.ts b/packages/compiler/src/backend/llvm/expr-primitives.ts
index 72392956a..1eec6a965 100644
--- a/packages/compiler/src/backend/llvm/expr-primitives.ts
+++ b/packages/compiler/src/backend/llvm/expr-primitives.ts
@@ -8,6 +8,7 @@ import { LlvmUnsupportedError } from "./unsupported.js";
import type { LlvmEmitterContext, ExprOf, LlValue } from "./expr-context.js";
import { BYTES_ELEM_NUM, closureIdentityEqual, f64Lit } from "./common.js";
+import { CHECKED_INT32_MIN_LENGTH } from "../../ir/int32-arrays.js";
import { emitStringInputs } from "./string-lifetimes.js";
import { emitStringParts, numberPart, stringParts } from "./string-construction.js";
import { emitBorrowedInputs } from "./borrowed-inputs.js";
@@ -16,10 +17,12 @@ import {
int32Number,
integerNumber,
narrowInteger,
+ wideArithmetic,
widenInteger,
} from "./integer-values.js";
import {
bitwiseIdentityOperand,
+ INT32_RANGE,
integerArithmeticRange,
isBitwiseOp,
withinInt32,
@@ -32,7 +35,15 @@ import {
emitPowerOfTwoRemainder,
emitSignedIntegerRemainder,
} from "./integer-remainder.js";
-import { denseSlotWidth, emitDenseArrayGet, emitDenseArrayState } from "./dense-array-access.js";
+import {
+ denseSlotWidth,
+ emitDenseArrayRead,
+ emitDenseArrayState,
+ runtimeAccess,
+ SCR_ELEM_I32,
+ slotWidth,
+ type ArrayAccess,
+} from "./dense-array-access.js";
import { emitWrappingSum, markWrappingOperands } from "./wrapping-sums.js";
export function emitLiteralExpr(
@@ -337,7 +348,14 @@ export function emitOperatorExpr(
if (arith[e.op] !== undefined) {
B.line(`${t} = ${arith[e.op]} double ${l.name}, ${r.name}`);
const uint32 = emitWrappingSum(host, e, l, r, li, ri);
- if (uint32 !== undefined) return { name: t, type: e.type, uint32 };
+ // The exact integer beside the double, for index consumers.
+ const wideInteger = wideArithmetic(host, e.op, l, li, r, ri);
+ if (uint32 !== undefined || wideInteger) {
+ const value: LlValue = { name: t, type: e.type };
+ if (uint32 !== undefined) value.uint32 = uint32;
+ if (wideInteger) value.wideInteger = wideInteger;
+ return value;
+ }
} else B.line(`${t} = fcmp ${cmp[e.op]} double ${l.name}, ${r.name}`);
} else if (bit[e.op] !== undefined) {
if (e.left.type.kind !== "f64" || e.right.type.kind !== "f64") {
@@ -648,7 +666,7 @@ export function emitStringExpr(
// equals no unit, as "" equals no one-unit literal.
const unit = charAtUnitTest(e);
if (unit !== null) {
- const code = host.emitExpr(charAtUnitRead(unit.charAt));
+ const code = host.emitExpr(charAtUnitRead(host, unit.charAt));
const t = B.tmp();
B.line(
`${t} = fcmp ${e.negated ? "une" : "oeq"} double ${code.name}, ${f64Lit(unit.code)}`,
@@ -682,7 +700,7 @@ export function emitStringExpr(
// the code unit, as the equality above does.
const unit = charAtUnitTest(e);
if (unit !== null) {
- const code = host.emitExpr(charAtUnitRead(unit.charAt));
+ const code = host.emitExpr(charAtUnitRead(host, unit.charAt));
const t = B.tmp();
B.line(
`${t} = fcmp ${charAtUnitPredicate(e.op, unit.left)} double ${code.name}, ${f64Lit(unit.code)}`,
@@ -886,9 +904,46 @@ export function emitContainerExpr(
// the plain elements up to the next one (scr_arr_reserve_spread).
const firstSpread = e.elems.findIndex((_, i) => spreadSet.has(i));
const capacity = small ? 0 : (e.capacity ?? (firstSpread < 0 ? e.elems.length : firstSpread));
- B.line(`${arr} = ${arrNewCall(host.shapeHost, elem, String(capacity), small)}`);
+ // An int32 array's allocation (ir/int32-arrays.ts) constructs
+ // SCR_ELEM_I32 storage; its elements are proven int32s. The empty
+ // temporary of a fill that allocates an int32 array with checked
+ // stores takes SCR_ELEM_I32 storage when the fill's length (which
+ // evaluates without effects) is at least CHECKED_INT32_MIN_LENGTH, and
+ // SCR_ELEM_F64 storage otherwise.
+ const fillLength = host.checkedAllocations.get(e);
+ if (fillLength !== undefined && e.elems.length === 0) {
+ const length = host.emitExpr(fillLength);
+ const long = B.tmp(),
+ kind = B.tmp();
+ B.line(`${long} = fcmp oge double ${length.name}, ${f64Lit(CHECKED_INT32_MIN_LENGTH)}`);
+ B.line(`${kind} = select i1 ${long}, i32 ${SCR_ELEM_I32}, i32 0`);
+ host.declare(
+ small
+ ? `declare ptr @scr_arr_new_small(i32)`
+ : `declare ptr @scr_arr_new(i32, ${host.sizeType})`,
+ );
+ B.line(
+ small
+ ? `${arr} = call ptr @scr_arr_new_small(i32 ${kind})`
+ : `${arr} = call ptr @scr_arr_new(i32 ${kind}, ${host.sizeType} ${capacity})`,
+ );
+ return host.own({ name: arr, type: e.type });
+ }
+ const int32 = host.int32Allocations.has(e);
+ if (int32) {
+ host.declare(
+ small
+ ? `declare ptr @scr_arr_new_small(i32)`
+ : `declare ptr @scr_arr_new(i32, ${host.sizeType})`,
+ );
+ B.line(
+ small
+ ? `${arr} = call ptr @scr_arr_new_small(i32 ${SCR_ELEM_I32})`
+ : `${arr} = call ptr @scr_arr_new(i32 ${SCR_ELEM_I32}, ${host.sizeType} ${capacity})`,
+ );
+ } else B.line(`${arr} = ${arrNewCall(host.shapeHost, elem, String(capacity), small)}`);
const out = host.own({ name: arr, type: e.type });
- const acc = elemAccess(elem);
+ const acc: ArrayAccess = int32 ? "i32" : elemAccess(elem);
let pending: LlValue[] = [];
let buffer: string | undefined;
const flush = (): void => {
@@ -909,7 +964,8 @@ export function emitContainerExpr(
B.line(
`call void @scr_arr_reserve_spread(ptr ${arr}, ptr ${v.name}, ${host.sizeType} ${plain})`,
);
- host.emitArrayCopyLoop(arr, v.name, acc);
+ // A literal with spreads is never an int32 allocation.
+ host.emitArrayCopyLoop(arr, v.name, runtimeAccess(acc));
return;
}
pending.push(host.emitExpr(el));
@@ -954,33 +1010,65 @@ export function emitContainerExpr(
// Ref-element reads return +1 (the inline dense path retains like
// the runtime getter); own registers the owned temp in the frame.
const elem = e.arr.type.elem;
- const acc = elemAccess(elem);
- const t = emitDenseArrayGet(
+ if (host.checkedInt32Array(e.arr)) {
+ const proven = host.provenDenseIndex(e.arr, e.index);
+ const value = host.emitByElementKind(
+ arr.name,
+ "double",
+ (kind) =>
+ emitDenseArrayRead(
+ host,
+ arr.name,
+ idx,
+ e.index,
+ kind,
+ elem,
+ "scr_arr_get_f64",
+ true,
+ proven,
+ ).value,
+ )!;
+ return { name: value, type: e.type };
+ }
+ const acc = host.arrayAccess(e.arr, elem);
+ const read = emitDenseArrayRead(
host,
arr.name,
idx,
e.index,
acc,
elem,
- `scr_arr_get_${acc}`,
+ `scr_arr_get_${runtimeAccess(acc)}`,
true,
host.provenDenseIndex(e.arr, e.index),
);
- return host.own({ name: t, type: e.type });
+ if (read.int32 !== null)
+ // An int32 array's element: its i32 is exact for index consumers.
+ return {
+ name: read.value,
+ type: e.type,
+ uint32: read.int32,
+ integer: { name: read.int32, type: "i32", signed: true, range: INT32_RANGE },
+ };
+ return host.own({ name: read.value, type: e.type });
}
case "arrayHas": {
const arr = host.emitStableReceiver(e.arr, [e.index]);
const idx = host.emitExpr(e.index);
if (e.arr.type.kind !== "array")
throw new InternalCompilerError("llvm emitter bug: arrayHas on non-array");
- const t = emitDenseArrayState(
- host,
- arr.name,
- idx,
- e.index,
- "has",
- denseSlotWidth(host, e.arr.type.elem),
- );
+ const t = host.checkedInt32Array(e.arr)
+ ? host.emitByElementKind(arr.name, "i1", (kind) =>
+ emitDenseArrayState(host, arr.name, idx, e.index, "has", slotWidth(kind)),
+ )!
+ : emitDenseArrayState(
+ host,
+ arr.name,
+ idx,
+ e.index,
+ "has",
+ host.int32Array(e.arr) ? slotWidth("i32") : denseSlotWidth(host, e.arr.type.elem),
+ );
return { name: t, type: e.type };
}
case "arrayState": {
@@ -988,14 +1076,18 @@ export function emitContainerExpr(
const idx = host.emitExpr(e.index);
if (e.arr.type.kind !== "array")
throw new InternalCompilerError("llvm emitter bug: arrayState on non-array");
- const t = emitDenseArrayState(
- host,
- arr.name,
- idx,
- e.index,
- "state",
- denseSlotWidth(host, e.arr.type.elem),
- );
+ const t = host.checkedInt32Array(e.arr)
+ ? host.emitByElementKind(arr.name, "double", (kind) =>
+ emitDenseArrayState(host, arr.name, idx, e.index, "state", slotWidth(kind)),
+ )!
+ : emitDenseArrayState(
+ host,
+ arr.name,
+ idx,
+ e.index,
+ "state",
+ host.int32Array(e.arr) ? slotWidth("i32") : denseSlotWidth(host, e.arr.type.elem),
+ );
return { name: t, type: e.type };
}
case "arrIntrinsic":
diff --git a/packages/compiler/src/backend/llvm/int32-kind-loops.ts b/packages/compiler/src/backend/llvm/int32-kind-loops.ts
new file mode 100644
index 000000000..12e38c1f0
--- /dev/null
+++ b/packages/compiler/src/backend/llvm/int32-kind-loops.ts
@@ -0,0 +1,135 @@
+import type { IrExpr, IrStmt } from "../../ir/ir.js";
+import { everyExprChild, everyStmtChild } from "../../ir/traverse.js";
+import { MAX_LOOP_NODES, SIMPLE_STATEMENTS } from "./speculative-integers.js";
+
+/** Int32 kind loops.
+ *
+ * An int32 array with checked stores (ir/int32-slots.ts) holds int32 slots
+ * (or doubles, when its fill was short) until a store of a value that is
+ * not an int32 widens it to doubles in place, so an access to it tests its
+ * element kind first (LlEmitter.emitByElementKind). An innermost loop that
+ * accesses such arrays tests their kinds once on entry instead
+ * (emitLoopCopies). The fast copy runs when every one of them holds int32
+ * slots and reads and writes them as int32 arrays; the wide copy runs when
+ * every one holds doubles and accesses them as double arrays; a loop over
+ * several arrays whose kinds differ runs the mixed copy, which tests the
+ * kinds at each access. A store of the fast copy whose value is not an
+ * int32 branches to the same statement in a third copy, which re-executes
+ * it with kind tests (widening the array) and finishes the iteration, then
+ * enters the mixed copy, or the wide copy when the loop has one array.
+ * Re-execution is exact because such a store's statement evaluates nothing
+ * but locals, literals, number arithmetic and element reads before it
+ * writes.
+ *
+ * Nothing else can widen these arrays: they never reach code other than
+ * the compiled element operations of their own function
+ * (ir/int32-arrays.ts). */
+export interface Int32KindLoopPlan {
+ /** The arrays the fast copy reads and writes as int32 arrays, by local,
+ * with one of their references in the loop. */
+ arrays: Map;
+ /** Element writes and pushes whose value may not be an int32: the
+ * statements the fast copy may leave at. */
+ writes: Set;
+}
+
+type KindLoop = Extract;
+
+/** A number or boolean expression whose evaluation has no effect: locals,
+ * literals, arithmetic, comparisons and element reads of local arrays. */
+function isPure(e: IrExpr): boolean {
+ if (e.type.kind !== "f64" && e.type.kind !== "bool") return false;
+ switch (e.kind) {
+ case "numLit":
+ case "boolLit":
+ case "varRef":
+ return true;
+ case "bin":
+ return (
+ (e.left.type.kind === "f64" || e.left.type.kind === "bool") &&
+ isPure(e.left) &&
+ isPure(e.right)
+ );
+ case "unary":
+ return isPure(e.operand);
+ case "ternary":
+ return isPure(e.cond) && isPure(e.then) && isPure(e.else_);
+ case "arrayGet":
+ case "arrayHas":
+ return e.arr.kind === "varRef" && isPure(e.index);
+ default:
+ return false;
+ }
+}
+
+/** The plan for `loop`, or null. `checked` names the int32 array locals
+ * with checked stores of the function; `provenInt32` the values the static
+ * range analysis proves int32s (stores that never leave the fast copy). */
+export function planInt32KindLoop(
+ loop: KindLoop,
+ checked: (localId: string) => boolean,
+ provenInt32: (e: IrExpr) => boolean,
+): Int32KindLoopPlan | null {
+ const arrays = new Map();
+ const writes = new Set();
+ const assigned = new Set();
+ let nodes = 0;
+ let rejected = false;
+ const reject = (): false => {
+ rejected = true;
+ return false;
+ };
+ const array = (e: IrExpr): string | null =>
+ e.kind === "varRef" && checked(e.localId) ? e.localId : null;
+ const push = (e: IrExpr): (IrExpr & { kind: "arrIntrinsic" }) | null =>
+ e.kind === "arrIntrinsic" && e.method === "push" && array(e.receiver) !== null ? e : null;
+
+ const expr = (e: IrExpr): boolean => {
+ if (rejected) return false;
+ nodes++;
+ if (e.kind === "varRef" && checked(e.localId) && !arrays.has(e.localId))
+ arrays.set(e.localId, e);
+ // A push the statement walk did not take as a write: its values must
+ // be proven int32s, since it cannot leave the fast copy.
+ const pushed = push(e);
+ if (pushed && !pushed.args.every(provenInt32)) return reject();
+ return everyExprChild(e, expr, nested);
+ };
+ // Statements inside expressions run mid-expression, where the fast copy
+ // cannot leave: they may not write these arrays.
+ const nested = (s: IrStmt): boolean => {
+ nodes++;
+ if (s.kind === "assign" || s.kind === "varDecl") assigned.add(s.localId);
+ if (s.kind === "arraySet" && array(s.arr) !== null && !provenInt32(s.value)) return reject();
+ return everyStmtChild(s, expr, nested);
+ };
+ const stmt = (s: IrStmt): boolean => {
+ if (rejected) return false;
+ nodes++;
+ if (!SIMPLE_STATEMENTS.has(s.kind)) return reject();
+ if (s.kind === "assign" || s.kind === "varDecl") assigned.add(s.localId);
+ if (s.kind === "arraySet" && array(s.arr) !== null && !provenInt32(s.value)) {
+ if (!isPure(s.index) || !isPure(s.value)) return reject();
+ writes.add(s);
+ return everyStmtChild(s, expr, stmt);
+ }
+ const pushed = s.kind === "exprStmt" ? push(s.expr) : null;
+ if (pushed && !pushed.args.every(provenInt32)) {
+ if (!pushed.args.every(isPure)) return reject();
+ writes.add(s);
+ expr(pushed.receiver);
+ pushed.args.forEach(expr);
+ return !rejected;
+ }
+ return everyStmtChild(s, expr, stmt);
+ };
+
+ if (loop.cond) expr(loop.cond);
+ if (loop.kind === "for" && loop.update) stmt(loop.update);
+ for (const s of loop.body) stmt(s);
+ if (rejected || nodes > MAX_LOOP_NODES || arrays.size === 0) return null;
+ // A loop that rebinds one of the arrays could reach another array of
+ // either kind.
+ for (const id of arrays.keys()) if (assigned.has(id)) return null;
+ return { arrays, writes };
+}
diff --git a/packages/compiler/src/backend/llvm/integer-values.ts b/packages/compiler/src/backend/llvm/integer-values.ts
index 63e279d04..ddbcdd8da 100644
--- a/packages/compiler/src/backend/llvm/integer-values.ts
+++ b/packages/compiler/src/backend/llvm/integer-values.ts
@@ -1,6 +1,6 @@
import type { IrExpr, IrType } from "../../ir/ir.js";
import type { IntegerRange } from "../../ir/integer-ranges.js";
-import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
+import type { LlValue, LlvmEmitterContext, WideInteger } from "./expr-context.js";
export type LlInteger = NonNullable;
@@ -51,6 +51,89 @@ export function exactInteger(
return { name, type: "i64", signed: true, range };
}
+/** Doubles hold every integer of smaller magnitude exactly. */
+const EXACT_DOUBLES = 2 ** 53;
+/** Wide views stay within this magnitude: a product of two int32s reaches
+ * 2^62, one exact term of at most 2^53 may be added to it, and no i64
+ * result wraps. */
+const WIDE_BOUND = 2 ** 62 + 2 ** 53;
+/** Array and string indexes are below 2^32 - 1: a wide view exact below
+ * this many is exact at every index a length can admit. */
+const INDEX_LIMIT = 2 ** 32;
+
+function magnitude(range: IntegerRange): number {
+ return Math.max(Math.abs(range.min), Math.abs(range.max));
+}
+
+/** The wide view of `l op r` for `+`, `-` and `*` when the exact operands
+ * have no proven result range (their product or sum may pass 2^53).
+ *
+ * - A product of two exact integers equals its double whenever its
+ * magnitude is below 2^53: the double product of integers is exact there.
+ * - A sum or difference of two exact integers likewise.
+ * - A sum or difference of a wide view (exact below T) and an exact integer
+ * of magnitude at most B equals its double below min(T - B, 2^53): the
+ * view's magnitude is then below T, so its double is exact, and the
+ * double sum of two exact integers below 2^53 is exact.
+ *
+ * Every i64 result stays within 2^62 + 2^53 in magnitude, so none wraps. A view
+ * exact below fewer than 2^32 helps no index and is not kept. */
+export function wideArithmetic(
+ host: LlvmEmitterContext,
+ op: string,
+ l: LlValue,
+ li: LlInteger | null,
+ r: LlValue,
+ ri: LlInteger | null,
+): WideInteger | null {
+ if (op !== "+" && op !== "-" && op !== "*") return null;
+ const view = (value: LlValue, integer: LlInteger | null): WideInteger | null =>
+ integer
+ ? { name: "", below: Infinity, bound: magnitude(integer.range) }
+ : (value.wideInteger ?? null);
+ const lv = view(l, li),
+ rv = view(r, ri);
+ if (!lv || !rv) return null;
+ let below: number, bound: number;
+ if (op === "*") {
+ if (lv.below !== Infinity || rv.below !== Infinity) return null;
+ below = EXACT_DOUBLES;
+ bound = lv.bound * rv.bound;
+ } else if (lv.below === Infinity && rv.below === Infinity) {
+ below = EXACT_DOUBLES;
+ bound = lv.bound + rv.bound;
+ } else if (lv.below === Infinity || rv.below === Infinity) {
+ const [exact, wide] = lv.below === Infinity ? [lv, rv] : [rv, lv];
+ below = Math.min(wide.below - exact.bound, EXACT_DOUBLES);
+ bound = exact.bound + wide.bound;
+ } else return null;
+ if (!(bound <= WIDE_BOUND) || below < INDEX_LIMIT) return null;
+ const operand = (value: LlValue, integer: LlInteger | null): string =>
+ integer ? widenInteger(host, integer) : value.wideInteger!.name;
+ const left = operand(l, li),
+ right = operand(r, ri);
+ const name = host.B.tmp();
+ host.B.line(
+ `${name} = ${op === "+" ? "add" : op === "-" ? "sub" : "mul"} nsw i64 ${left}, ${right}`,
+ );
+ return { name, below, bound };
+}
+
+/** An i64 index for a consumer that compares it, unsigned, with a length or
+ * capacity below 2^32 and takes the double's path otherwise: the exact
+ * integer of `value`, or its wide view, which equals the double at every
+ * such index. Null when the index needs the double round trip. */
+export function indexInteger(
+ host: LlvmEmitterContext,
+ value: LlValue,
+ expr?: IrExpr,
+): string | null {
+ const integer = exactInteger(host, value, expr);
+ if (integer) return widenInteger(host, integer);
+ const wide = value.wideInteger;
+ return wide && wide.below >= INDEX_LIMIT ? wide.name : null;
+}
+
export function widenInteger(host: LlvmEmitterContext, value: LlInteger): string {
if (value.type === "i64") return value.name;
const wide = host.B.tmp();
diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts
index ecd3d3b5b..4a9578e5e 100644
--- a/packages/compiler/src/backend/llvm/lib-shared.ts
+++ b/packages/compiler/src/backend/llvm/lib-shared.ts
@@ -973,6 +973,7 @@ export const LIB_FN_SYMS: Record = {
"dyn.typedRefIs": "scr_dyn_typed_ref_is_key",
"dyn.classIs": "scr_dyn_class_is_key",
"dyn.iterPack": "scr_dyn_iter_pack",
+ "dyn.mathFold": "scr_dyn_math_fold",
"dyn.destructureRest": "scr_dyn_destructure_rest",
"dyn.iterator": "scr_dyn_iterator",
"dyn.asyncIteratorOpen": "scr_dyn_async_iterator_open",
@@ -1584,6 +1585,8 @@ export const LIB_FN_SYMS: Record = {
"regex.resultProp": "scr_regex_result_prop",
"array.namedKeys": "scr_arr_named_keys",
"array.hasNamedKey": "scr_arr_has_named_key",
+ "array.hasProperties": "scr_arr_has_properties",
+ "array.undefinedKey": "scr_arr_undefined_key",
// queueMicrotask's checked-dynamic form (borrowed dyn; a non-function
// throws synchronously), the minted setImmediate value, and the
// timers/promises immediate — all mark the loop live.
diff --git a/packages/compiler/src/backend/llvm/local-array-reads.ts b/packages/compiler/src/backend/llvm/local-array-reads.ts
index 8df187736..1c540295a 100644
--- a/packages/compiler/src/backend/llvm/local-array-reads.ts
+++ b/packages/compiler/src/backend/llvm/local-array-reads.ts
@@ -13,7 +13,7 @@ import { analyzeCallLifetimes, type CallLifetimes } from "./call-lifetimes.js";
import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
import { ReferenceEffects } from "./reference-effects.js";
import { SCR_ARR_FIELD, loadArrCount, scrArrStateAt } from "./common.js";
-import { exactInteger, widenInteger } from "./integer-values.js";
+import { indexInteger } from "./integer-values.js";
import { READ_FALLBACK } from "./dense-array-access.js";
import { compactRefLoadLines } from "./classes.js";
@@ -476,7 +476,8 @@ function emitDenseReferenceArrayRead(
no: string,
slow: string,
join: string,
- loadType: "ptr" | "cref" | "i64" | "i8" = "ptr",
+ /** `i32`: an int32 array's slot, presented as the int32. */
+ loadType: "ptr" | "cref" | "i64" | "i8" | "i32" = "ptr",
): void {
const B = host.B;
const header = (field: number): string => {
@@ -567,7 +568,7 @@ function emitDenseReferenceArrayRead(
statePtr,
data,
cap,
- loadType === "i8" ? 1 : loadType === "cref" ? 4 : 8,
+ loadType === "i8" ? 1 : loadType === "cref" || loadType === "i32" ? 4 : 8,
offset,
);
host.markMemoryPointer(statePtr, "array:present");
@@ -578,9 +579,10 @@ function emitDenseReferenceArrayRead(
B.startBlock(valueLabel);
const valuePtr = B.tmp(),
raw = B.tmp();
- // Boolean arrays store one byte per element, compact references four,
- // every other kind an 8-byte slot.
- const slotType = loadType === "i8" ? "i8" : loadType === "cref" ? "i32" : "i64";
+ // Boolean arrays store one byte per element, compact references and int32
+ // arrays four, every other kind an 8-byte slot.
+ const slotType =
+ loadType === "i8" ? "i8" : loadType === "cref" || loadType === "i32" ? "i32" : "i64";
B.line(
`${valuePtr} = getelementptr inbounds ${slotType}, ptr ${data}, ${host.sizeType} ${offset}`,
);
@@ -638,11 +640,11 @@ export function emitBorrowedArrayRead(
return;
}
const index = host.emitExpr(read.index);
- // A proven integer index (an int32 local, say) needs no double
- // round-trip; a negative one compares above the capacity as unsigned.
- const exact =
- loopIndex === null && host.sizeType === "i64" ? exactInteger(host, index, read.index) : null;
- const integerIndex = loopIndex ?? (exact ? widenInteger(host, exact) : null);
+ // A proven integer index (an int32 local, say), or a wide view exact at
+ // every index below the capacity, needs no double round-trip; a negative
+ // one compares above the capacity as unsigned.
+ const integerIndex =
+ loopIndex ?? (host.sizeType === "i64" ? indexInteger(host, index, read.index) : null);
const slow = B.newLabel("borrow.array.slow"),
join = B.newLabel("borrow.array.join");
emitDenseReferenceArrayRead(
@@ -680,13 +682,10 @@ export function emitLocalArrayRead(
const array = host.emitStableReceiver(read.array, [read.index]);
const loopIndex = inline ? host.emitIntegerLoopIndex(read.index) : null;
const index = host.emitExpr(read.index);
- // A proven integer index (a counter with a known range, say) needs no
- // double round trip, as in a borrowed read.
- const exact =
- inline && loopIndex === null && host.sizeType === "i64"
- ? exactInteger(host, index, read.index)
- : null;
- const integerIndex = loopIndex ?? (exact ? widenInteger(host, exact) : null);
+ // As in emitBorrowedArrayRead: an exact integer or wide view needs no
+ // round trip, and only the loop's own counter is an induction variable.
+ const integerIndex =
+ loopIndex ?? (inline && host.sizeType === "i64" ? indexInteger(host, index, read.index) : null);
const nullable = read.type.kind === "union" ? host.nullableUnions.get(read.type.unionId) : null;
if (nullable) {
// A nullable union is the element pointer itself: borrowed from an
@@ -743,8 +742,15 @@ export function emitLocalArrayRead(
const no = B.newLabel("local.array.missing"),
join = B.newLabel("local.array.join");
const scalar = isScalarElement(read.element);
+ // An int32 array's arms join on the int32 element, converted to the
+ // payload's double once at the join, so a consumer converting the payload
+ // back to an integer folds to the element.
+ const int32 = scalar && read.element.kind === "f64" && host.int32Array(read.array);
+ const element = int32 ? B.slot() : null;
+ if (element) B.entryAllocas.push(`${element} = alloca i32`);
const storeValue = (value: string): void => {
- if (scalar) {
+ if (element) B.line(`store i32 ${value}, ptr ${element}`);
+ else if (scalar) {
// Slot bits for f64; a canonical 0/1 for bool, like scr_union_new_bool.
let bits = value;
if (read.element.kind === "bool") {
@@ -761,7 +767,37 @@ export function emitLocalArrayRead(
}
B.line(`store i32 ${read.presentTag}, ptr ${tag}`);
};
- if (inline)
+ if (inline && scalar && host.checkedInt32Array(read.array))
+ // An int32 array with checked stores: its int32 arm stores the element's
+ // double bits, as the double arm does.
+ host.emitByElementKind(array.name, null, (kind) => {
+ const present =
+ kind === "i32"
+ ? (value: string): void => {
+ const number = B.tmp(),
+ bits = B.tmp();
+ B.line(`${number} = sitofp i32 ${value} to double`);
+ B.line(`${bits} = bitcast double ${number} to i64`);
+ storeValue(bits);
+ }
+ : storeValue;
+ const loadType = kind === "i32" ? "i32" : "i64";
+ const induction = loopIndex !== null;
+ emitDenseReferenceArrayRead(
+ host,
+ array,
+ index,
+ integerIndex,
+ induction,
+ present,
+ no,
+ slow,
+ join,
+ loadType,
+ );
+ return null;
+ });
+ else if (inline)
emitDenseReferenceArrayRead(
host,
array,
@@ -772,7 +808,13 @@ export function emitLocalArrayRead(
no,
slow,
join,
- !scalar ? referenceLoad(host, read.array.type) : read.element.kind === "bool" ? "i8" : "i64",
+ !scalar
+ ? referenceLoad(host, read.array.type)
+ : read.element.kind === "bool"
+ ? "i8"
+ : int32
+ ? "i32"
+ : "i64",
);
else B.br(slow);
B.startBlock(slow);
@@ -799,7 +841,8 @@ export function emitLocalArrayRead(
B.line(
`${value} = call double @scr_arr_get_f64(ptr ${array.name}, double ${index.name})${READ_FALLBACK}`,
);
- B.line(`${bits} = bitcast double ${value} to i64`);
+ if (element) B.line(`${bits} = fptosi double ${value} to i32`);
+ else B.line(`${bits} = bitcast double ${value} to i64`);
} else {
host.declare(`declare zeroext i1 @scr_arr_get_bool(ptr, double)`);
B.line(
@@ -807,7 +850,8 @@ export function emitLocalArrayRead(
);
B.line(`${bits} = zext i1 ${value} to i64`);
}
- B.line(`store i64 ${bits}, ptr ${payload}`);
+ if (element) B.line(`store i32 ${bits}, ptr ${element}`);
+ else B.line(`store i64 ${bits}, ptr ${payload}`);
B.line(`store i32 ${read.presentTag}, ptr ${tag}`);
} else {
const value = B.tmp(),
@@ -821,10 +865,20 @@ export function emitLocalArrayRead(
}
B.br(join);
B.startBlock(no);
- B.line(`store i64 0, ptr ${payload}`);
+ if (element) B.line(`store i32 0, ptr ${element}`);
+ else B.line(`store i64 0, ptr ${payload}`);
B.line(`store i32 ${read.missingTag}, ptr ${tag}`);
B.br(join);
B.startBlock(join);
+ if (element) {
+ const value = B.tmp(),
+ number = B.tmp(),
+ bits = B.tmp();
+ B.line(`${value} = load i32, ptr ${element}`);
+ B.line(`${number} = sitofp i32 ${value} to double`);
+ B.line(`${bits} = bitcast double ${number} to i64`);
+ B.line(`store i64 ${bits}, ptr ${payload}`);
+ }
B.line(`store ptr ${box}, ptr ${localSlot}`);
return read.borrow || scalar ? null : { slot: payload, type: read.element };
}
diff --git a/packages/compiler/src/backend/llvm/reference-writes.ts b/packages/compiler/src/backend/llvm/reference-writes.ts
index 6d1593a55..df2ec5032 100644
--- a/packages/compiler/src/backend/llvm/reference-writes.ts
+++ b/packages/compiler/src/backend/llvm/reference-writes.ts
@@ -107,6 +107,7 @@ const INERT_LIB_CALLS = new Set([
"regex.resultProp",
"array.namedKeys",
"array.hasNamedKey",
+ "array.hasProperties",
"process.cwd",
"process.argv",
"process.execArgv",
diff --git a/packages/compiler/src/backend/llvm/speculative-integers.ts b/packages/compiler/src/backend/llvm/speculative-integers.ts
index a309a9785..00fabb788 100644
--- a/packages/compiler/src/backend/llvm/speculative-integers.ts
+++ b/packages/compiler/src/backend/llvm/speculative-integers.ts
@@ -35,12 +35,12 @@ export interface SpeculativeLoopPlan {
type SpeculativeLoop = Extract;
/** The fast copy duplicates the loop; keep the duplicated code small. */
-const MAX_LOOP_NODES = 400;
+export const MAX_LOOP_NODES = 400;
/** Statements whose emission is plain control flow or a single write.
* Nested loops, switches and try statements keep the loop on the ordinary
* path (their emission keeps state that the two copies would not share). */
-const SIMPLE_STATEMENTS = new Set([
+export const SIMPLE_STATEMENTS: ReadonlySet = new Set([
"varDecl",
"assign",
"exprStmt",
diff --git a/packages/compiler/src/contracts/int32-elements.ts b/packages/compiler/src/contracts/int32-elements.ts
index 8284d40ab..ca61a8e49 100644
--- a/packages/compiler/src/contracts/int32-elements.ts
+++ b/packages/compiler/src/contracts/int32-elements.ts
@@ -122,7 +122,8 @@ const COPIES = new Set([
const COPY_IN = new Set(["pushSpread", "unshiftSpread", "spliceInsert"]);
/** Library calls that only read array arguments. */
-const READ_ONLY_LIB = /^(console\.|inspect\.|json\.stringify|math\.|insp\.)/;
+const READ_ONLY_LIB =
+ /^(console\.|inspect\.|json\.stringify|math\.|insp\.|array\.(hasProperties|namedKeys|hasNamedKey)$)/;
/** Library calls that always throw (the value they stand for never exists). */
const THROWING_LIB = /^error\.\w*Throw$/;
@@ -872,6 +873,9 @@ export class Int32ElementContracts {
};
const isNumberArray = (t: IrType): boolean => t.kind === "array" && t.elem.kind === "f64";
+ // Live views handed straight to a read-only library call (util.inspect
+ // of an array with properties) are only read through.
+ const readOnlyViews = new Set();
const expr = (e: IrExpr): void => {
switch (e.kind) {
case "assignExpr":
@@ -1000,6 +1004,7 @@ export class Int32ElementContracts {
case "libCall":
if (!READ_ONLY_LIB.test(e.fn))
for (const a of e.args) escape(a, a.loc, `the library call ${e.fn}`);
+ else for (const a of e.args) if (a.kind === "dynFrom") readOnlyViews.add(a);
break;
case "callValue":
for (const a of e.args) escape(a, a.loc, "a call through a function value");
@@ -1008,7 +1013,10 @@ export class Int32ElementContracts {
case "dynFrom":
// Plain arrays and records cross into dynamic code as deep copies;
// class instances and live references stay connected.
- if (e.liveRef === true || this.containsObject(e.value.type, new Set()))
+ if (
+ !readOnlyViews.has(e) &&
+ (e.liveRef === true || this.containsObject(e.value.type, new Set()))
+ )
escape(e.value, e.loc, "dynamic (any or unknown) code");
break;
default:
diff --git a/packages/compiler/src/frontend/dts-paths.ts b/packages/compiler/src/frontend/dts-paths.ts
index 3b2f32413..009d1c333 100644
--- a/packages/compiler/src/frontend/dts-paths.ts
+++ b/packages/compiler/src/frontend/dts-paths.ts
@@ -34,6 +34,31 @@ export function packageShimRoot(): string {
: join(installedDeclarationRoot, "shims");
}
+/** The directories of files scriptc itself ships, each with the prefix
+ * that names its files independently of where scriptc is installed
+ * (compilerOwnedFileName). IR artifacts spell source locations in them this
+ * way (serializeModuleParts). */
+export function compilerOwnedFileRoots(): readonly { from: string; to: string }[] {
+ return [{ from: tsgoPath(packageShimRoot()) + "/", to: "scriptc:shims/" }];
+}
+
+/** The name of a file scriptc itself ships (a package shim or one of the
+ * shipped declaration files) that does not depend on where scriptc is
+ * installed: `scriptc:shims/` or
+ * `scriptc:`. Null for every other file. Names and
+ * artifacts that would otherwise spell the installation's absolute path use
+ * it, so two installations of one compiler emit the same output. */
+export function compilerOwnedFileName(file: string): string | null {
+ const path = tsgoPath(file);
+ for (const { from, to } of compilerOwnedFileRoots()) {
+ if (path.startsWith(from)) return to + path.slice(from.length);
+ }
+ for (const declaration of [ambientDtsPath(), overridesDtsPath(), fallbackDtsPath()]) {
+ if (path === declaration) return `scriptc:${path.slice(path.lastIndexOf("/") + 1)}`;
+ }
+ return null;
+}
+
/** Path of the shipped ambient declarations — the always-shipped CORE
* (comptime, setTimeout). Part of EVERY program scriptc
* builds, the project-world preflight program included. */
diff --git a/packages/compiler/src/frontend/input-tracker.test.ts b/packages/compiler/src/frontend/input-tracker.test.ts
index 4b6ef6d90..b4f3957f5 100644
--- a/packages/compiler/src/frontend/input-tracker.test.ts
+++ b/packages/compiler/src/frontend/input-tracker.test.ts
@@ -60,6 +60,28 @@ test("semantic validation reads non-source dependencies once and still invalidat
expect(reads.mock.calls.filter(([path]) => path === dependency)).toHaveLength(1);
});
+test("an equivalent edit of a file whose raw text the frontend read does not match", async () => {
+ const dir = await mkdtemp(join(tmpdir(), "scriptc-inputs-"));
+ scratch.push(dir);
+ const source = join(dir, "entry.ts");
+ const previous = "export const answer = 1;\n";
+ await writeFile(source, previous);
+ const tracker = new FrontendInputTracker();
+ tracker.run(() => {
+ trackedReadFile(source);
+ tracker.record({ op: "text", path: source });
+ });
+ const snapshot = tracker.snapshot();
+ expect(validFrontendInputSnapshot(snapshot)).toBe(true);
+ expect(frontendInputsStillMatch(snapshot)).toBe(true);
+ await writeFile(source, `// comment\n${previous}`);
+ const equivalent = vi.fn(() => true);
+ expect(
+ frontendInputsSemanticallyMatch(snapshot, new Map([[source, previous]]), equivalent),
+ ).toBeNull();
+ expect(equivalent).not.toHaveBeenCalled();
+});
+
test("tracked frontend reads invalidate on byte edits", async () => {
const dir = await mkdtemp(join(tmpdir(), "scriptc-inputs-"));
scratch.push(dir);
diff --git a/packages/compiler/src/frontend/input-tracker.ts b/packages/compiler/src/frontend/input-tracker.ts
index fa2e92568..d27ab6285 100644
--- a/packages/compiler/src/frontend/input-tracker.ts
+++ b/packages/compiler/src/frontend/input-tracker.ts
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
import { AsyncLocalStorage } from "node:async_hooks";
import { readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
import { basename, dirname, resolve } from "node:path";
+import { observeSourceText } from "./ts7/ast-node.js";
/**
* Filesystem observations made while constructing and lowering one frontend
@@ -16,7 +17,10 @@ export type FrontendInputProbe =
| { op: "kind"; path: string; kind: "file" | "directory" | "other" | "missing" }
| { op: "entries"; path: string; files: string[]; directories: string[] }
| { op: "entries-error"; path: string }
- | { op: "realpath"; path: string; target: string | null };
+ | { op: "realpath"; path: string; target: string | null }
+ /** The frontend's output depends on the file's raw text or line
+ * positions, not only on its tokens (observeSourceText). */
+ | { op: "text"; path: string };
export interface FrontendInputSnapshot {
version: 1;
@@ -105,6 +109,7 @@ export class FrontendInputTracker {
record(probe: FrontendInputProbe): void {
const key = `${probe.op}\0${probe.path}`;
+ if (probe.op === "text" && this.probes.has(key)) return;
const previous = this.probes.get(key);
if (previous !== undefined && JSON.stringify(previous) !== JSON.stringify(probe)) {
// An input changed while this frontend was running. The build may
@@ -158,6 +163,13 @@ function record(probe: FrontendInputProbe): void {
(synchronousTracker ?? activeTracker.getStore())?.record(probe);
}
+// Source text the frontend reads raw (call-site text in messages, comptime
+// callbacks, line numbers) changes with edits that keep the tokens.
+observeSourceText((fileName) => {
+ const tracker = synchronousTracker ?? activeTracker.getStore();
+ if (tracker !== undefined) tracker.record({ op: "text", path: resolve(fileName) });
+});
+
/** Mark the active frontend as unsafe to persist because an exact host query
* bypassed the tracked filesystem wrappers. No-op outside a frontend run. */
export function markFrontendInputsUnstable(): void {
@@ -394,6 +406,9 @@ export function frontendInputsStillMatch(
return probe.target === null;
}
}
+ case "text":
+ // The file probe of the same path checks its content.
+ return true;
}
});
}
@@ -410,36 +425,44 @@ export function frontendInputsSemanticallyMatch(
exclusions: FrontendInputExclusions = {},
): FrontendSemanticMatch | null {
if (!validFrontendInputSnapshot(snapshot)) return null;
- const sourceProbes = new Map(
- snapshot.probes
- .filter((probe): probe is Extract => probe.op === "file")
- .map((probe) => [probe.path, probe]),
- );
+ const sourceProbes = new Map>();
+ const rawText = new Set();
+ for (const probe of snapshot.probes) {
+ if (probe.op === "file") sourceProbes.set(probe.path, probe);
+ else if (probe.op === "text") rawText.add(probe.path);
+ }
for (const [path, previous] of previousSources) {
if (sourceProbes.get(path)?.digest !== frontendSourceDigest(previous)) return null;
}
const changed: FrontendSemanticChange[] = [];
const currentSources = new Map();
- const adjusted: FrontendInputSnapshot = {
- ...snapshot,
- probes: snapshot.probes.map((probe) => {
- if (probe.op !== "file") return probe;
- const previous = previousSources.get(probe.path);
- if (previous === undefined || frontendSourceDigest(previous) !== probe.digest) return probe;
- let current: string;
- try {
- current = readFileSync(probe.path, "utf8");
- } catch {
- return probe;
- }
- const currentDigest = frontendSourceDigest(current);
- currentSources.set(probe.path, current);
- if (currentDigest === probe.digest) return probe;
- if (!isSemanticEquivalent(probe.path, previous, current)) return probe;
- changed.push({ path: probe.path, previous, current });
- return { ...probe, digest: currentDigest };
- }),
- };
+ const probes: FrontendInputProbe[] = [];
+ for (const probe of snapshot.probes) {
+ const previous = probe.op === "file" ? previousSources.get(probe.path) : undefined;
+ if (probe.op !== "file" || previous === undefined) {
+ probes.push(probe);
+ continue;
+ }
+ let current: string;
+ try {
+ current = readFileSync(probe.path, "utf8");
+ } catch {
+ return null;
+ }
+ const currentDigest = frontendSourceDigest(current);
+ currentSources.set(probe.path, current);
+ if (currentDigest === probe.digest) {
+ probes.push(probe);
+ continue;
+ }
+ // One edit that is not equivalent already rules out the match, and so
+ // does any edit of a file whose raw text the frontend read.
+ if (rawText.has(probe.path) || !isSemanticEquivalent(probe.path, previous, current))
+ return null;
+ changed.push({ path: probe.path, previous, current });
+ probes.push({ ...probe, digest: currentDigest });
+ }
+ const adjusted: FrontendInputSnapshot = { ...snapshot, probes };
if (!frontendInputsStillMatch(adjusted, exclusions)) return null;
for (const [path, previous] of previousSources) {
if (!currentSources.has(path)) {
@@ -486,6 +509,8 @@ export function validFrontendInputSnapshot(snapshot: unknown): snapshot is Front
return true;
case "realpath":
return value.target === null || typeof value.target === "string";
+ case "text":
+ return true;
default:
return false;
}
diff --git a/packages/compiler/src/frontend/lowering/builtins/global-namespace-keys.ts b/packages/compiler/src/frontend/lowering/builtins/global-namespace-keys.ts
new file mode 100644
index 000000000..bb364ee55
--- /dev/null
+++ b/packages/compiler/src/frontend/lowering/builtins/global-namespace-keys.ts
@@ -0,0 +1,123 @@
+/* The string-keyed properties Node 24 exposes on its builtin constructor
+ * and namespace globals, own and inherited, so `"key" in Error` (a feature
+ * probe such as `"captureStackTrace" in Error`) answers statically. The
+ * global must be the unshadowed builtin; compiled programs cannot add
+ * properties to these objects, so the answer is fixed. */
+
+const FUNCTION_PROTOTYPE = [
+ "length",
+ "name",
+ "arguments",
+ "caller",
+ "constructor",
+ "apply",
+ "bind",
+ "call",
+ "toString",
+];
+const OBJECT_PROTOTYPE = [
+ "constructor",
+ "__defineGetter__",
+ "__defineSetter__",
+ "hasOwnProperty",
+ "__lookupGetter__",
+ "__lookupSetter__",
+ "isPrototypeOf",
+ "propertyIsEnumerable",
+ "toString",
+ "valueOf",
+ "__proto__",
+ "toLocaleString",
+];
+const CONSTRUCTOR_OWN = ["length", "name", "prototype"];
+const ERROR_OWN = [
+ ...CONSTRUCTOR_OWN,
+ "captureStackTrace",
+ "prepareStackTrace",
+ "isError",
+ "stackTraceLimit",
+];
+
+/** Own string keys per global, beyond what the prototype chain adds. */
+const CONSTRUCTORS: Readonly> = {
+ Error: ERROR_OWN,
+ // The native error constructors inherit from Error.
+ TypeError: ERROR_OWN,
+ RangeError: ERROR_OWN,
+ SyntaxError: ERROR_OWN,
+ ReferenceError: ERROR_OWN,
+ EvalError: ERROR_OWN,
+ URIError: ERROR_OWN,
+ Object: [
+ ...CONSTRUCTOR_OWN,
+ "assign",
+ "getOwnPropertyDescriptor",
+ "getOwnPropertyDescriptors",
+ "getOwnPropertyNames",
+ "getOwnPropertySymbols",
+ "hasOwn",
+ "is",
+ "preventExtensions",
+ "seal",
+ "create",
+ "defineProperties",
+ "defineProperty",
+ "freeze",
+ "getPrototypeOf",
+ "setPrototypeOf",
+ "isExtensible",
+ "isFrozen",
+ "isSealed",
+ "keys",
+ "entries",
+ "fromEntries",
+ "values",
+ "groupBy",
+ ],
+ Array: [...CONSTRUCTOR_OWN, "isArray", "from", "fromAsync", "of"],
+ Promise: [
+ ...CONSTRUCTOR_OWN,
+ "all",
+ "allSettled",
+ "any",
+ "race",
+ "resolve",
+ "reject",
+ "withResolvers",
+ "try",
+ ],
+ Number: [
+ ...CONSTRUCTOR_OWN,
+ "isFinite",
+ "isInteger",
+ "isNaN",
+ "isSafeInteger",
+ "parseFloat",
+ "parseInt",
+ "MAX_VALUE",
+ "MIN_VALUE",
+ "NaN",
+ "NEGATIVE_INFINITY",
+ "POSITIVE_INFINITY",
+ "MAX_SAFE_INTEGER",
+ "MIN_SAFE_INTEGER",
+ "EPSILON",
+ ],
+ String: [...CONSTRUCTOR_OWN, "fromCharCode", "fromCodePoint", "raw"],
+ Map: [...CONSTRUCTOR_OWN, "groupBy"],
+ Set: CONSTRUCTOR_OWN,
+ WeakMap: CONSTRUCTOR_OWN,
+ WeakSet: CONSTRUCTOR_OWN,
+ Boolean: CONSTRUCTOR_OWN,
+ Function: CONSTRUCTOR_OWN,
+ Date: [...CONSTRUCTOR_OWN, "now", "parse", "UTC"],
+ BigInt: [...CONSTRUCTOR_OWN, "asUintN", "asIntN"],
+};
+
+/** Whether Node answers `key in ` with true, or null when `name` is
+ * not a tabled global. */
+export function builtinGlobalHasKey(name: string, key: string): boolean | null {
+ const own = CONSTRUCTORS[name];
+ if (own === undefined) return null;
+ return own.includes(key) || FUNCTION_PROTOTYPE.includes(key) || OBJECT_PROTOTYPE.includes(key);
+}
diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts
index 8a4a1e417..e394718d1 100644
--- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts
+++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts
@@ -908,6 +908,11 @@ export class ClassDynamicDispatch {
private readonly prototypeClasses = new Set();
private prototypeIdentityHelper: IrFunction | null = null;
private readonly prototypeIdentityClasses = new Set();
+ /** `value instanceof Error` through Error.prototype: boxed instances of
+ * classes with a heritage evaluated at run time test their prototype
+ * data's chain. */
+ private errorPrototypeHelper: IrFunction | null = null;
+ private readonly errorPrototypeClasses = new Set();
private readonly instancePrototypes = new Map }>();
private readonly checkedCasts = new Map }>();
/** The for-in key helpers, by libCall (dyn and native string[] keys). */
@@ -1114,6 +1119,49 @@ export class ClassDynamicDispatch {
});
changed = true;
}
+ if (
+ this.errorPrototypeHelper &&
+ !this.errorPrototypeClasses.has(className) &&
+ hasRuntimeHeritage(info)
+ ) {
+ this.errorPrototypeClasses.add(className);
+ const loc = this.errorPrototypeHelper.loc;
+ const type: IrType = { kind: "object", className };
+ const value = varRef("value", DYN, loc);
+ const prototype = classPrototypeData(lowerer, info, loc, {
+ kind: "dynCheck",
+ value,
+ type,
+ loc,
+ });
+ if (prototype)
+ this.errorPrototypeHelper.body.unshift({
+ kind: "if",
+ cond: {
+ kind: "libCall",
+ fn: "dyn.typedRefIs",
+ args: [value, { kind: "strLit", value: typeKey(type), type: STRING, loc }],
+ type: BOOL,
+ loc,
+ },
+ then: [
+ {
+ kind: "return",
+ value: {
+ kind: "libCall",
+ fn: "dyn.inheritsErrorPrototype",
+ args: [prototype],
+ type: BOOL,
+ loc,
+ },
+ loc,
+ },
+ ],
+ else_: null,
+ loc,
+ });
+ changed = true;
+ }
if (
this.serializesClasses &&
!this.jsonPrototypes.has(className) &&
@@ -1516,6 +1564,31 @@ export class ClassDynamicDispatch {
}
return { kind: "call", callee: cast.fn.name, args: [expr.value], type, loc: expr.loc };
}
+ if (expr.kind === "libCall" && expr.fn === "dyn.inheritsErrorPrototype") {
+ if (!this.errorPrototypeHelper) {
+ const loc = expr.loc;
+ this.errorPrototypeHelper = {
+ name: "%dyn.class.inheritsErrorPrototype",
+ params: [{ localId: "value", name: "value", type: DYN }],
+ locals: [{ id: "value", name: "value", type: DYN, mutable: false }],
+ returnType: BOOL,
+ loc,
+ body: [
+ { kind: "return", value: { ...expr, args: [varRef("value", DYN, loc)] }, loc },
+ ],
+ };
+ lowerer.liftedFns.push(this.errorPrototypeHelper);
+ this.generated.add(this.errorPrototypeHelper);
+ changed = true;
+ }
+ return {
+ kind: "call",
+ callee: this.errorPrototypeHelper.name,
+ args: expr.args,
+ type: BOOL,
+ loc: expr.loc,
+ };
+ }
if (
expr.kind === "libCall" &&
expr.fn === "dyn.getPrototype" &&
@@ -5942,3 +6015,11 @@ const INHERITED_OBJECT_METHODS: ReadonlySet = new Set([
"hasOwnProperty",
"propertyIsEnumerable",
]);
+
+/** Whether a class or one of its bases has a heritage evaluated at run
+ * time, whose prototype chain may reach Error.prototype. */
+function hasRuntimeHeritage(info: ClassInfo): boolean {
+ for (let current: ClassInfo | null = info; current; current = current.base)
+ if (current.callableBase) return true;
+ return false;
+}
diff --git a/packages/compiler/src/frontend/lowering/class-method-mutations.ts b/packages/compiler/src/frontend/lowering/class-method-mutations.ts
index abbaf3a75..fd23b6db8 100644
--- a/packages/compiler/src/frontend/lowering/class-method-mutations.ts
+++ b/packages/compiler/src/frontend/lowering/class-method-mutations.ts
@@ -380,6 +380,13 @@ export function collectClassMethodMutations(
markTarget(node.initializer, node);
if (ts.isPropertyAccessExpression(node) && isErrorPrototypeValue(lowerer, node))
lowerer.errorPrototypeValues = true;
+ // A heritage evaluated at run time may be Error, which links the
+ // class's instances to Error.prototype.
+ if (
+ (ts.isClassDeclaration(node) || ts.isClassExpression(node)) &&
+ runtimeHeritage(lowerer, node)
+ )
+ lowerer.errorPrototypeValues = true;
if (ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) {
const base = unwrap(node.expression);
const prototype =
@@ -621,3 +628,22 @@ export function collectClassMethodMutations(
if (!receiverFields.has(field)) lowerer.receiverFreeCallbackFields.add(field);
}
}
+
+/** Whether a class's `extends` expression is a value computed at run time:
+ * anything other than a name of a class declaration or of a builtin
+ * constructor (`extends Parent` over `const Parent = params.Parent ?? Object`,
+ * `extends make()`). */
+function runtimeHeritage(
+ lowerer: Lowerer,
+ node: ts.ClassDeclaration | ts.ClassExpression,
+): boolean {
+ const heritage = node.heritageClauses?.nodes.find(
+ (clause) => clause.token === ts.SyntaxKind.ExtendsKeyword,
+ )?.types?.nodes[0]?.expression;
+ if (heritage === undefined) return false;
+ if (!ts.isIdentifier(heritage) && !ts.isPropertyAccessExpression(heritage)) return true;
+ let symbol = lowerer.checker.getSymbolAtLocation(heritage);
+ if (symbol === undefined) return true;
+ if (symbol.flags & ts.SymbolFlags.Alias) symbol = lowerer.checker.getAliasedSymbol(symbol);
+ return !(symbol.flags & ts.SymbolFlags.Class) && !lowerer.isStdlibSymbol(symbol);
+}
diff --git a/packages/compiler/src/frontend/lowering/containers/array-callback-call.ts b/packages/compiler/src/frontend/lowering/containers/array-callback-call.ts
index a22a82bb1..b98000fa1 100644
--- a/packages/compiler/src/frontend/lowering/containers/array-callback-call.ts
+++ b/packages/compiler/src/frontend/lowering/containers/array-callback-call.ts
@@ -1,10 +1,10 @@
import * as ts from "../../ts7/adapter.js";
+import { lowerOptionalIndex } from "../undefined-index.js";
import { locOf } from "../../program.js";
import { varRef } from "../../../ir/build.js";
import { DYN, type IrExpr, type IrStmt, isUnitType } from "../../../ir/ir.js";
import type { Lowerer } from "../lowerer.js";
import { completeFuncValueArgs } from "../call-arguments.js";
-import { lowerOptionalNumber } from "../lower-exprs.js";
import { lowerSafeIndexRead } from "../array-values.js";
/** Retain native function slots during indexed calls. The array remains
@@ -37,7 +37,7 @@ export function lowerArrayCallbackCall(
return varRef(local.id, local.type, loc);
};
const receiver = store(array, "%callbackArray");
- const index = lowerOptionalNumber(
+ const index = lowerOptionalIndex(
lowerer,
lowerer.lowerExpr(access.argumentExpression),
locOf(access.argumentExpression),
diff --git a/packages/compiler/src/frontend/lowering/containers/array-methods.ts b/packages/compiler/src/frontend/lowering/containers/array-methods.ts
index 44182fbda..3ab2e84e2 100644
--- a/packages/compiler/src/frontend/lowering/containers/array-methods.ts
+++ b/packages/compiler/src/frontend/lowering/containers/array-methods.ts
@@ -361,6 +361,21 @@ export function lowerArrayMethodCall(
if (probe.type.kind === "array") {
receiverIr = probe.type;
probedUntyped = true;
+ } else if (
+ probe.type.kind === "dyn" &&
+ !isJsSourceFile(ts.getSourceFileOfNode(access)) &&
+ lowerer.isStdlibMember(access)
+ ) {
+ // An `unknown[]` (or `any[]`) array stored checked-dynamic — a
+ // generic body instantiated at `unknown`, a spread of unknown
+ // elements: the method runs on the runtime array (dynInvoke, JS
+ // exact), and a typed result is checked like any member read.
+ const dispatched = lowerDynDispatchMethodCall(lowerer, call, access, probe, true);
+ if (!dispatched) return null;
+ const result = lowerer.mapTypeOf(lowerer.typeOf(call));
+ return result && result.kind !== "void" && result.kind !== "dyn"
+ ? lowerer.coerceInto(call, dispatched, result)
+ : dispatched;
}
}
if (receiverIr?.kind === "union") {
diff --git a/packages/compiler/src/frontend/lowering/dispatch-sites.ts b/packages/compiler/src/frontend/lowering/dispatch-sites.ts
index c8a8d6ba6..f5b8ebdb2 100644
--- a/packages/compiler/src/frontend/lowering/dispatch-sites.ts
+++ b/packages/compiler/src/frontend/lowering/dispatch-sites.ts
@@ -12,10 +12,11 @@ import { toPrimitiveHint } from "./class-dynamic-dispatch.js";
import { GENERATOR_STEP_METHODS, HELPER_METHODS } from "./generator-dynamic-dispatch.js";
/** Runtime entry points that step a checked-dynamic iterable's iterator:
- * destructuring rest, and the collection constructors' and
- * Object.fromEntries' seeds. */
+ * destructuring rest, the collection constructors' and Object.fromEntries'
+ * seeds, and Math.min/Math.max over a spread. */
const RUNTIME_STEP_LIB_FNS: ReadonlySet = new Set([
"dyn.destructureRest",
+ "dyn.mathFold",
"dyn.nativeSetNew",
"dyn.mapSeedEntries",
"dyn.fromEntries",
@@ -80,6 +81,7 @@ const DISPATCH_LIB_CALLS = new Set([
"dyn.propertyIsEnumerableComputed",
"dyn.defineProperty",
"dyn.getPrototype",
+ "dyn.inheritsErrorPrototype",
"dyn.reflectGet",
"dyn.reflectSet",
]);
diff --git a/packages/compiler/src/frontend/lowering/divergences.ts b/packages/compiler/src/frontend/lowering/divergences.ts
index 1c362cb09..c971c672d 100644
--- a/packages/compiler/src/frontend/lowering/divergences.ts
+++ b/packages/compiler/src/frontend/lowering/divergences.ts
@@ -464,8 +464,11 @@ function identityUse(root: ts.Node, name: string, from = -1): IdentityUse | null
cur = cur.expression;
return cur;
};
+ // The text appears only in warnings. A build that reported warnings is
+ // not reused after a source edit, so the read is not recorded as a source
+ // text dependency (ts.getTextOfNode would record it).
const textOf = (node: ts.Node): string => {
- const text = ts.getTextOfNode(node);
+ const text = ts.getSourceFileOfNode(node).text.slice(ts.tokenStartOfNode(node), node.end);
return text.length > 60 ? `${text.slice(0, 57)}...` : text;
};
const visit = (node: ts.Node): "skip" | "stop" | undefined => {
diff --git a/packages/compiler/src/frontend/lowering/expressions/binary.ts b/packages/compiler/src/frontend/lowering/expressions/binary.ts
index 9702a6dd9..58f6e16b5 100644
--- a/packages/compiler/src/frontend/lowering/expressions/binary.ts
+++ b/packages/compiler/src/frontend/lowering/expressions/binary.ts
@@ -1,6 +1,11 @@
/* Binary operators: arithmetic, comparisons and equality, logical and nullish pairs, and string conversion for concatenation. */
import { keyedHelperName } from "../stable-names.js";
import * as ts from "../../ts7/adapter.js";
+import {
+ lowerOptionalIndex,
+ splitUndefinedIndex,
+ undefinedTypedArrayKeyFence,
+} from "../undefined-index.js";
import { cleanFuncAdaptable } from "../coercions/function-adapters.js";
import { type Lowerer, jsFuncNameOf } from "../lowerer.js";
import {
@@ -1160,12 +1165,26 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
}
if (recv?.type.kind === "array" || recv?.type.kind === "bytes") {
const receiver = lowerer.declareHiddenLocal("%setArray", recv.type);
- const index = lowerOptionalNumber(
- lowerer,
- lowerer.lowerExpr(expr.left.argumentExpression),
- locOf(expr.left.argumentExpression),
- expr.left.argumentExpression,
- );
+ // An undefined index names the "undefined" property
+ // (undefined-index.ts), which a typed array cannot hold.
+ const rawIndex = lowerer.lowerExpr(expr.left.argumentExpression);
+ const split =
+ recv.type.kind === "bytes"
+ ? splitUndefinedIndex(
+ lowerer,
+ rawIndex,
+ expr.left.argumentExpression,
+ locOf(expr.left.argumentExpression),
+ )
+ : null;
+ const index =
+ split?.index ??
+ lowerOptionalIndex(
+ lowerer,
+ rawIndex,
+ locOf(expr.left.argumentExpression),
+ expr.left.argumentExpression,
+ );
if (index.type.kind !== "f64")
lowerer.unsupported(
"SC1090",
@@ -1178,7 +1197,7 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
const result = varRef(value.id, raw.type, loc);
const arr = varRef(receiver.id, recv.type, loc);
const idx = varRef(key.id, F64, loc);
- const write: IrStmt =
+ const store: IrStmt =
recv.type.kind === "array"
? arrayValueStore(lowerer, arr, idx, result, recv.type.elem, loc)
: {
@@ -1188,10 +1207,20 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
value: lowerer.coerceInto(expr.right, result, F64),
loc,
};
+ const write: IrStmt = split
+ ? {
+ kind: "if",
+ cond: split.isUndefined,
+ then: [undefinedTypedArrayKeyFence(lowerer, expr.left, loc)],
+ else_: [store],
+ loc,
+ }
+ : store;
return {
kind: "seqExpr",
stmts: [
{ kind: "varDecl", localId: receiver.id, init: recv, loc },
+ ...(split?.stmts ?? []),
{ kind: "varDecl", localId: key.id, init: index, loc },
{ kind: "varDecl", localId: value.id, init: raw, loc },
write,
diff --git a/packages/compiler/src/frontend/lowering/expressions/element-access.ts b/packages/compiler/src/frontend/lowering/expressions/element-access.ts
index 045bebdf4..0be9be81a 100644
--- a/packages/compiler/src/frontend/lowering/expressions/element-access.ts
+++ b/packages/compiler/src/frontend/lowering/expressions/element-access.ts
@@ -1,5 +1,10 @@
/* Element reads and writes (`a[i]`, `a[i] = v`), compound element updates, and keyed record reads. */
import * as ts from "../../ts7/adapter.js";
+import {
+ lowerOptionalIndex,
+ splitUndefinedIndex,
+ undefinedTypedArrayKeyFence,
+} from "../undefined-index.js";
import { type Lowerer, jsFuncNameOf, neverTaintedJsType } from "../lowerer.js";
import {
BOOL,
@@ -65,6 +70,7 @@ import { lowerClassPrototypeComputedAssignment } from "../class-prototypes.js";
import { isClassCallback } from "../class-callbacks.js";
import { isKeyedFieldAccess } from "./keyed-field-targets.js";
import { lowerFieldCompoundValue } from "./updates.js";
+import { readsHeldEvolvingArray } from "../lower-stmts.js";
/** `a[i]` reads. Only f64 indices into array receivers are modeled; JS
* string-key element access (`a["length"]`) and string indexing (`s[0]`
@@ -751,12 +757,16 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre
: lowerer.maybeNarrow(arr, expr.expression);
}
+ // An alias or container of an evolving array whose reads disagree
+ // (`rows[0]` after `const rows = [xs]`) holds elements the checker's type
+ // does not list, so its element read stays checked-dynamic.
+ const heldEvolving = arr.type.kind === "dyn" && readsHeldEvolvingArray(lowerer, expr.expression);
// The checker sees an array but the VALUE lowered checked-dynamic (a
// jsdoc-typed default export from a .js module — signature 14): the
// read rides the dynCheck boundary when the array shape is one the checked-dynamic tree
// can validate, and fences by name when it is not — an arrayGet over a
// dyn receiver is never emitted (the validator ICE).
- if (arr.type.kind === "dyn") {
+ if (arr.type.kind === "dyn" && !heldEvolving) {
if (
canDynCheckTo(
receiverIr,
@@ -774,14 +784,22 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre
}
}
// Array reads can themselves supply an index. A missing numeric value
- // is undefined, whose property lookup misses just like NaN; keep that
- // value instead of rejecting a checker-number index with optional IR.
- const index = lowerOptionalNumber(
+ // is undefined, which names the array's "undefined" property
+ // (undefined-index.ts).
+ const index = lowerOptionalIndex(
lowerer,
lowerer.lowerExpr(expr.argumentExpression),
locOf(expr.argumentExpression),
expr.argumentExpression,
);
+ if (heldEvolving)
+ return {
+ kind: "dynKeyGet",
+ value: arr,
+ key: lowerer.coerceToExpected(index, DYN),
+ type: DYN,
+ loc: locOf(expr),
+ };
if (
arr.type.kind === "array" &&
(index.type.kind === "dyn" ||
@@ -1518,12 +1536,26 @@ export function lowerElementCompoundParts(
"compound assignment through a non-native array or byte view",
);
}
- const index = lowerOptionalNumber(
- lowerer,
- lowerer.lowerExpr(target.argumentExpression),
- locOf(target.argumentExpression),
- target.argumentExpression,
- );
+ // An undefined index names the "undefined" property (undefined-index.ts),
+ // which a typed array cannot hold.
+ const rawIndex = lowerer.lowerExpr(target.argumentExpression);
+ const split =
+ receiver.type.kind === "bytes"
+ ? splitUndefinedIndex(
+ lowerer,
+ rawIndex,
+ target.argumentExpression,
+ locOf(target.argumentExpression),
+ )
+ : null;
+ const index =
+ split?.index ??
+ lowerOptionalIndex(
+ lowerer,
+ rawIndex,
+ locOf(target.argumentExpression),
+ target.argumentExpression,
+ );
if (index.type.kind !== "f64")
lowerer.unsupported("SC1090", target.argumentExpression, "indexing with non-number keys");
const receiverLocal = lowerer.declareHiddenLocal("%compoundArray", receiver.type);
@@ -1632,15 +1664,25 @@ export function lowerElementCompoundParts(
}
const resultLocal = lowerer.declareHiddenLocal("%compoundResult", computed.type);
const resultRef = (): IrExpr => varRef(resultLocal.id, computed.type, loc);
- const write: IrStmt =
+ const store: IrStmt =
receiver.type.kind === "array"
? arrayValueStore(lowerer, receiverRef(), indexRef(), resultRef(), receiver.type.elem, loc)
: { kind: "bytesSet", arr: receiverRef(), index: indexRef(), value: resultRef(), loc };
+ const write: IrStmt = split
+ ? {
+ kind: "if",
+ cond: split.isUndefined,
+ then: [undefinedTypedArrayKeyFence(lowerer, target, loc)],
+ else_: [store],
+ loc,
+ }
+ : store;
return {
expr: {
kind: "seqExpr",
stmts: [
{ kind: "varDecl", localId: receiverLocal.id, init: receiver, loc },
+ ...(split?.stmts ?? []),
{ kind: "varDecl", localId: indexLocal.id, init: index, loc },
{ kind: "varDecl", localId: oldLocal.id, init: oldValue, loc },
{ kind: "varDecl", localId: resultLocal.id, init: computed, loc },
@@ -1867,13 +1909,23 @@ export function lowerElementWrite(lowerer: Lowerer, expr: ts.BinaryExpression):
// The checker's flow type is a typed array, but the storage is wider.
if (recv.type.kind !== "bytes") return lowerWiderBytesWrite(lowerer, expr, target, recv);
// An index the checker types as a number but whose storage may hold
- // undefined (a hole read) converts with ToNumber, as the reads do.
- const index = lowerOptionalNumber(
+ // undefined (a hole read) converts with ToNumber, as the reads do; an
+ // undefined index names a property the typed array cannot hold.
+ const rawIndex = lowerer.lowerExpr(target.argumentExpression);
+ const split = splitUndefinedIndex(
lowerer,
- lowerer.lowerExpr(target.argumentExpression),
- locOf(target.argumentExpression),
+ rawIndex,
target.argumentExpression,
+ locOf(target.argumentExpression),
);
+ const index =
+ split?.index ??
+ lowerOptionalNumber(
+ lowerer,
+ rawIndex,
+ locOf(target.argumentExpression),
+ target.argumentExpression,
+ );
if (index.type.kind === "dyn" && recv.type.kind === "bytes") {
return {
kind: "exprStmt",
@@ -1889,7 +1941,34 @@ export function lowerElementWrite(lowerer: Lowerer, expr: ts.BinaryExpression):
lowerer.unsupported("SC1090", target.argumentExpression, "indexing with non-number keys");
}
const value = lowerer.lowerExprExpecting(expr.right, F64);
- return { kind: "bytesSet", arr: recv, index, value, loc: locOf(expr) };
+ if (split === null) return { kind: "bytesSet", arr: recv, index, value, loc: locOf(expr) };
+ const loc = locOf(expr);
+ const receiver = lowerer.declareHiddenLocal("%indexedBytes", recv.type);
+ const valueLocal = lowerer.declareHiddenLocal("%elementValue", F64);
+ return {
+ kind: "block",
+ body: [
+ { kind: "varDecl", localId: receiver.id, init: recv, loc },
+ ...split.stmts,
+ { kind: "varDecl", localId: valueLocal.id, init: value, loc },
+ {
+ kind: "if",
+ cond: split.isUndefined,
+ then: [undefinedTypedArrayKeyFence(lowerer, target, loc)],
+ else_: [
+ {
+ kind: "bytesSet",
+ arr: varRef(receiver.id, recv.type, loc),
+ index,
+ value: varRef(valueLocal.id, F64, loc),
+ loc,
+ },
+ ],
+ loc,
+ },
+ ],
+ loc,
+ };
}
// Tuple element write `t[0] = v`: a positional-field write (recordSet)
// — literal indices only, like the read.
@@ -2184,7 +2263,7 @@ export function lowerElementWrite(lowerer: Lowerer, expr: ts.BinaryExpression):
"assignment through a possibly missing nested array receiver",
);
}
- const index = lowerOptionalNumber(
+ const index = lowerOptionalIndex(
lowerer,
lowerer.lowerExpr(target.argumentExpression),
locOf(target.argumentExpression),
diff --git a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts
index ff7848930..48106949e 100644
--- a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts
+++ b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts
@@ -1,4 +1,5 @@
/* Identifier references in expression position: lowerExprInner's identifier branch and its helpers. */
+import { predeclareForwardClass } from "../lower-stmts.js";
import { cjsMemberAliasValueOf } from "../cjs-exports-object.js";
import { builtinClassValue, builtinMemberClassKind } from "../builtins/builtin-class-dyn.js";
import * as ts from "../../ts7/adapter.js";
@@ -860,6 +861,12 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc:
{
const sym = lowerer.resolveValueSymbol(expr);
const classInfo = sym ? lowerer.classBySymbol.get(sym) : undefined;
+ // A local class referenced from a function hoisted above its statement:
+ // its TDZ box, holding the class as a checked-dynamic constructor.
+ if (!classInfo && sym && predeclareForwardClass(lowerer, sym)) {
+ const local = lowerer.resolveLocal(expr);
+ if (local) return { kind: "varRef", localId: local.id, type: local.type, loc };
+ }
// A GENERIC class name whose CONTEXTUAL type pins one instantiation
// (`const B: new (v: string) => Counter = Counter`): the
// value is that instantiation's class object — the generic-fn
diff --git a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts
index 4d04be30a..429abf793 100644
--- a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts
+++ b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts
@@ -37,6 +37,7 @@ 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";
+import { holdsEvolvingArray } from "../lower-stmts.js";
/** `{ a: 1, b: "x" }` → recordLit. The record type comes from the
* contextual type when tsc has one (annotated declarations, arguments,
@@ -397,7 +398,10 @@ export function lowerDynObjectLiteral(
? lowerer.lowerLambda(prop, true)
: ts.isShorthandPropertyAssignment(prop) && shorthandBuiltinValue(lowerer, prop)
? shorthandBuiltinValue(lowerer, prop)!
- : !boxValue && ts.isObjectLiteralExpression(valueExpr)
+ : !boxValue &&
+ (ts.isObjectLiteralExpression(valueExpr) ||
+ (ts.isArrayLiteralExpression(valueExpr) &&
+ holdsEvolvingArray(lowerer, valueExpr)))
? lowerer.lowerExprExpecting(valueExpr, DYN)
: lowerer.lowerExpr(valueExpr as ts.Expression);
raw = fenceClosureProbe(lowerer, valueExpr, undefined, lowerValue) ?? lowerValue();
diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts
index 2e6ab981a..55ce433d3 100644
--- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts
+++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts
@@ -553,6 +553,19 @@ export function lowerPropertyAccessExpr(
expr.name.text,
)
) {
+ // A name the program stores on the global object
+ // (`(_a = globalThis).__config ?? (_a.__config = {})`) reads back from
+ // the global object's own properties.
+ const globalType = lowerer.typeOf(staticallySelected(lowerer, expr.expression));
+ if (globalThisWrittenNames(lowerer, globalType).has(expr.name.text)) {
+ return {
+ kind: "dynKeyGet",
+ value: lowerer.coerceToExpected(lowerer.lowerExpr(expr.expression), DYN),
+ key: { kind: "strLit", value: expr.name.text, type: STRING, loc },
+ type: DYN,
+ loc,
+ };
+ }
return dynUndefinedExpr(loc);
}
// `Uint8Array.prototype.toBase64` and its siblings, the base64/hex
@@ -4491,3 +4504,29 @@ function contextualArgumentCount(lowerer: Lowerer, fn: ts.Node): number | null {
}
return count;
}
+
+/** The property names the program assigns on the global object, through
+ * `globalThis` or a binding of its type (`(_a = globalThis).x = v`). */
+function globalThisWrittenNames(lowerer: Lowerer, globalType: ts.Type): Set {
+ const known = lowerer.globalThisWrittenNames;
+ if (known) return known;
+ const names = new Set();
+ lowerer.globalThisWrittenNames = names;
+ for (const sf of lowerer.program.getImplementationSourceFiles()) {
+ if (sf.isDeclarationFile || !sf.text.includes("globalThis")) continue;
+ ts.walkPreorder(sf, (node) => {
+ if (
+ ts.isBinaryExpression(node) &&
+ (node.operatorToken.kind === ts.SyntaxKind.EqualsToken ||
+ node.operatorToken.kind === ts.SyntaxKind.QuestionQuestionEqualsToken ||
+ node.operatorToken.kind === ts.SyntaxKind.BarBarEqualsToken ||
+ node.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandEqualsToken) &&
+ ts.isPropertyAccessExpression(node.left) &&
+ lowerer.typeOf(node.left.expression) === globalType
+ )
+ names.add(node.left.name.text);
+ return undefined;
+ });
+ }
+ return names;
+}
diff --git a/packages/compiler/src/frontend/lowering/expressions/relational.ts b/packages/compiler/src/frontend/lowering/expressions/relational.ts
index 0eb8ae653..b731e4912 100644
--- a/packages/compiler/src/frontend/lowering/expressions/relational.ts
+++ b/packages/compiler/src/frontend/lowering/expressions/relational.ts
@@ -51,6 +51,7 @@ import { isJsSourceFile, locOf } from "../../program.js";
import { checkedClassInstanceOf } from "../class-construction.js";
import { unsupportedDiag } from "../../../diagnostics/diagnostic.js";
import { globalSymbolKey } from "./global-symbols.js";
+import { builtinGlobalHasKey } from "../builtins/global-namespace-keys.js";
import { foldedStringKeyOf } from "./object-literals.js";
import { NARROW_FIRST, sideEffectFreeOptionValue } from "../surfaces.js";
import { isExecExceptionType, refuseExecExceptionCode } from "../builtins/errors.js";
@@ -1430,6 +1431,16 @@ export function lowerInExpression(
"'in' with computed (non-literal) keys (a const whose type is one string literal folds; a string-typed key answers over index-signature record receivers)",
);
}
+ // A feature probe on a builtin constructor global (`"captureStackTrace"
+ // in Error`): the global is a fixed object, so the answer is static.
+ {
+ let recvNode = expr.right;
+ while (ts.isParenthesizedExpression(recvNode)) recvNode = recvNode.expression;
+ if (ts.isIdentifier(recvNode) && lowerer.isStdlibGlobal(recvNode, recvNode.text)) {
+ const has = builtinGlobalHasKey(recvNode.text, key);
+ if (has !== null) return { kind: "boolLit", value: has, type: BOOL, loc };
+ }
+ }
// A receiver re-read (the static folds below) is safe for plain reads
// AND for side-effect-free expressions (an object literal of literals
// — the `"a" in { a: true }` shape).
diff --git a/packages/compiler/src/frontend/lowering/expressions/updates.ts b/packages/compiler/src/frontend/lowering/expressions/updates.ts
index 128093dad..04ca23a96 100644
--- a/packages/compiler/src/frontend/lowering/expressions/updates.ts
+++ b/packages/compiler/src/frontend/lowering/expressions/updates.ts
@@ -1,5 +1,10 @@
/* Increment, decrement and prefix unary operators, and compound field updates. */
import * as ts from "../../ts7/adapter.js";
+import {
+ lowerOptionalIndex,
+ splitUndefinedIndex,
+ undefinedTypedArrayKeyFence,
+} from "../undefined-index.js";
import { isStrictCode } from "../strict-mode.js";
import { isPrivateWriteReceiver } from "../private-brands.js";
import type { Lowerer } from "../lowerer.js";
@@ -311,12 +316,27 @@ export function lowerIncDec(
(raw.type.kind === "array" || raw.type.kind === "bytes")
) {
const receiver = save(raw, "%incrementArray");
- const rawIndex = lowerOptionalNumber(
- lowerer,
- lowerer.lowerExpr(access.argumentExpression),
- locOf(access.argumentExpression),
- access.argumentExpression,
- );
+ // An undefined index names the "undefined" property
+ // (undefined-index.ts), which a typed array cannot hold.
+ const loweredIndex = lowerer.lowerExpr(access.argumentExpression);
+ const split =
+ raw.type.kind === "bytes"
+ ? splitUndefinedIndex(
+ lowerer,
+ loweredIndex,
+ access.argumentExpression,
+ locOf(access.argumentExpression),
+ )
+ : null;
+ if (split) stmts.push(...split.stmts);
+ const rawIndex =
+ split?.index ??
+ lowerOptionalIndex(
+ lowerer,
+ loweredIndex,
+ locOf(access.argumentExpression),
+ access.argumentExpression,
+ );
const index = save(
rawIndex.type.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(expr))
? { kind: "libCall", fn: "dyn.toNumberCoerce", args: [rawIndex], type: F64, loc }
@@ -340,7 +360,7 @@ export function lowerIncDec(
loc,
invalidNaN: true,
};
- return finish(read, (value) =>
+ const store = (value: IrExpr): IrStmt =>
type.kind === "array"
? arrayValueStore(
lowerer,
@@ -356,8 +376,15 @@ export function lowerIncDec(
index,
value: lowerer.coerceInto(access, value, F64),
loc,
- },
- );
+ };
+ if (!split) return finish(read, store);
+ return finish(read, (value) => ({
+ kind: "if",
+ cond: split.isUndefined,
+ then: [undefinedTypedArrayKeyFence(lowerer, access, loc)],
+ else_: [store(value)],
+ loc,
+ }));
}
// A TS member statically typed number on a checked-dynamic receiver (a
// class-backed protocol view): the expanded form `o.v = o.v + 1`, its
diff --git a/packages/compiler/src/frontend/lowering/function-returns.ts b/packages/compiler/src/frontend/lowering/function-returns.ts
index 5ede3efe3..dde109ea7 100644
--- a/packages/compiler/src/frontend/lowering/function-returns.ts
+++ b/packages/compiler/src/frontend/lowering/function-returns.ts
@@ -15,6 +15,7 @@ import {
import { isJsSourceFile } from "../program.js";
import { PoisonError, dynFallbackType } from "./lowerer.js";
import { functionCanReturnUndefined } from "../function-completion.js";
+import { jsBindingAppendsOnly, jsBindingHasOpenWrites } from "./lower-stmts.js";
import type { FnCtx } from "./function-context.js";
/** Keep the conservative completion rule shared by return construction
@@ -124,6 +125,32 @@ export function producesConstructor(lowerer: Lowerer, type: ts.Type): boolean {
return false;
}
+/** Whether a return of `decl` itself (not of a nested function) is a bare
+ * read of a JavaScript variable whose writes the checker does not
+ * constrain. A string that is only appended to stays a string. */
+function returnsOpenJsBinding(lowerer: Lowerer, decl: ts.SignatureDeclaration): boolean {
+ let open = false;
+ const opens = (value: ts.Expression | undefined): boolean => {
+ while (value && ts.isParenthesizedExpression(value)) value = value.expression;
+ if (!value || !ts.isIdentifier(value)) return false;
+ const binding = lowerer.resolveValueSymbol(value)?.valueDeclaration;
+ return (
+ binding !== undefined &&
+ ts.isVariableDeclaration(binding) &&
+ jsBindingHasOpenWrites(lowerer, binding) &&
+ !jsBindingAppendsOnly(lowerer, binding)
+ );
+ };
+ const scan = (node: ts.Node): void => {
+ if (open || (node !== decl && ts.isFunctionLike(node))) return;
+ if (ts.isReturnStatement(node) && opens(node.expression)) open = true;
+ ts.forEachChild(node, scan);
+ };
+ if (ts.isArrowFunction(decl) && decl.body && !ts.isBlock(decl.body)) open = opens(decl.body);
+ else scan(decl);
+ return open;
+}
+
/** Map a declaration's return contract while retaining JavaScript
* inference fallbacks. Diagnostics preserve the caller's blame node. */
export function declaredReturnType(
@@ -212,6 +239,23 @@ export function declaredReturnType(
lowerer.unions.get(mappedReturn.unionId)?.arms.some((arm) => arm.kind === "func")
)
return DYN;
+ // A JS function returning a local that holds whatever its writes store
+ // (jsBindingHasOpenWrites): the inferred result is the initializer's
+ // type, which the returned value need not have.
+ // An async function's promise fulfills with that value.
+ if (
+ isJsSourceFile(ts.getSourceFileOfNode(decl)) &&
+ !decl.type &&
+ !decl.asteriskToken &&
+ !hasExplicitJsDocReturn(decl) &&
+ mappedReturn !== null &&
+ mappedReturn.kind !== "dyn" &&
+ !(mappedReturn.kind === "promise" && mappedReturn.inner.kind === "dyn") &&
+ returnsOpenJsBinding(lowerer, decl)
+ )
+ return decl.modifiers?.nodes.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword)
+ ? { kind: "promise", inner: DYN }
+ : DYN;
// JS inference assigns Number to arithmetic over any, although the
// runtime operands can both be BigInts. Preserve that native result.
if (
diff --git a/packages/compiler/src/frontend/lowering/generic-functions.ts b/packages/compiler/src/frontend/lowering/generic-functions.ts
index e27ab7f5f..222cb89f9 100644
--- a/packages/compiler/src/frontend/lowering/generic-functions.ts
+++ b/packages/compiler/src/frontend/lowering/generic-functions.ts
@@ -342,10 +342,15 @@ export function genericCallInstance(
const local = localClassResultInstance(lowerer, expr, info, rsig, params, retTs);
if (local) return local;
// A result instantiated at `any` (a type argument inferred from an
- // untyped value, such as an untyped forwarding call) is checked-dynamic.
+ // untyped value, such as an untyped forwarding call) is checked-dynamic,
+ // and so is a JavaScript result whose JSDoc names a type the file never
+ // declares or imports (`@returns {MiddlewareHandler}` in a published
+ // bundle): the checker's error type is documentation it could not
+ // resolve, not a contract.
const mappedReturn =
lowerer.mapTypeOf(retTs) ??
- ((retTs.flags & ts.TypeFlags.Any) !== 0 && !isJsSourceFile(ts.getSourceFileOfNode(info.decl))
+ ((retTs.flags & ts.TypeFlags.Any) !== 0 &&
+ (!isJsSourceFile(ts.getSourceFileOfNode(info.decl)) || retTs.isErrorType())
? DYN
: null);
if (!mappedReturn) {
diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts
index c1bc044ae..14d94ad24 100644
--- a/packages/compiler/src/frontend/lowering/lower-calls.ts
+++ b/packages/compiler/src/frontend/lowering/lower-calls.ts
@@ -3797,18 +3797,22 @@ function lowerDynReceiverMethodCall(
}
if (access.name.text === "flatMap") return lowerDynArrayFlatMapCall(lowerer, call, access, recv);
// String methods claim only names NO other dyn-representable kind's
- // prototype declares (Array carries includes/indexOf/slice too): for
- // these, "the receiver is a string, or the call throws V8's TypeError"
- // IS Node's semantics for every possible dyn value. Shared names would
- // need a receiver-kind dispatch — they keep the fence.
+ // prototype declares (Array carries includes/indexOf/slice too), so no
+ // other kind inherits them. 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.
+ // A checked-dynamic object can still carry its OWN member of that
+ // name: zod's schemas assign `trim`, `toLowerCase` and `startsWith`
+ // in their constructors (`z.string().trim().min(1)`), an object's
+ // prototype can define `trim`, and a JavaScript module object can
+ // assign `repeat` in a function body (`common.repeat(" ", n)`, an
+ // object-typed receiver the member is missing from). An untyped
+ // receiver, or a member the checker cannot type, dispatches on the
+ // receiver's runtime kind: a string runs the string lowering, anything
+ // else calls its member as Node does.
if (
- !checkerUntyped &&
- (lowerer.typeOf(access).flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0 &&
+ (checkerUntyped ||
+ (lowerer.typeOf(access).flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0) &&
!call.questionDotToken &&
!access.questionDotToken &&
!call.arguments.nodes.some(ts.isSpreadElement)
@@ -4260,6 +4264,7 @@ const DYN_DISPATCH_METHODS = new Set([
"shift",
"unshift",
"slice",
+ "splice",
"at",
"indexOf",
"lastIndexOf",
@@ -8267,6 +8272,12 @@ function lowerObjectStaticCall(
...(fnName !== null ? { fnName } : {}),
};
}
+ // A class value (`Object.defineProperty(Definition, "name", { value })`)
+ // boxes to its constructor function, whose own-property table holds the
+ // definition for every alias of the class.
+ if (target?.type.kind === "classval" && lowerer.dynConvertible(target.type)) {
+ target = { kind: "dynFrom", value: target, type: DYN, loc: locOf(call.arguments.nodes[0]!) };
+ }
const descriptorPrimitive =
member === "getOwnPropertyDescriptor" &&
target &&
@@ -8662,11 +8673,15 @@ function lowerObjectStaticCall(
probed?.type.kind === "union" &&
isJsSourceFile(ts.getSourceFileOfNode(call)) &&
lowerer.dynConvertible(probed.type);
+ // An array walks its live view: the present indices, then its numeric
+ // and named properties.
+ const isArray = probed?.type.kind === "array" && lowerer.dynConvertible(probed.type);
if (
isDyn ||
isPrimitive ||
isFunction ||
isJsUnion ||
+ isArray ||
(probed && isDynTypedRefType(probed.type))
) {
const fn =
@@ -8676,7 +8691,14 @@ function lowerObjectStaticCall(
? "dyn.objValues"
: "dyn.objEntries";
let v = lowerer.lowerExpr(argNode);
- if (v.type.kind !== "dyn") v = { kind: "dynFrom", value: v, type: DYN, loc: locOf(call) };
+ if (v.type.kind !== "dyn")
+ v = {
+ kind: "dynFrom",
+ value: v,
+ ...(isArray ? { liveRef: true as const } : {}),
+ type: DYN,
+ loc: locOf(call),
+ };
// In TypeScript the keys come back as a native string[] and the
// entries as a native [string, any][]: the checker keeps every later
// write to the arrays well typed. JavaScript may store anything into
diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts
index e264e0a3a..6dd0124bd 100644
--- a/packages/compiler/src/frontend/lowering/lower-classes.ts
+++ b/packages/compiler/src/frontend/lowering/lower-classes.ts
@@ -5,6 +5,7 @@ import {
isDatePrototypeMethod,
lowerDateSuperMethodCall,
} from "./builtins/date-classes.js";
+import { predeclareForwardClass } from "./lower-stmts.js";
import { lowerDateStaticCall } from "./builtins/dates.js";
import { keyedHelperName } from "./stable-names.js";
import { lowerValueReturningConstructorNew } from "./js-dynamic-objects.js";
@@ -238,7 +239,7 @@ export function returnsOnlyThisValue(member: ts.MethodDeclaration): boolean {
return valid;
}
-function functionLocalClass(decl: ts.ClassLikeDeclaration): boolean {
+export function functionLocalClass(decl: ts.ClassLikeDeclaration): boolean {
for (
let parent: ts.Node | undefined = decl.parent;
parent && !ts.isSourceFile(parent);
@@ -2355,6 +2356,7 @@ export function collectClassShapeInner(
const checkedStatics =
isJsSourceFile(ts.getSourceFileOfNode(decl)) &&
((base && hasRuntimeStatics(base)) ||
+ definedThroughReflection(lowerer, decl) ||
decl.members.nodes.some(
(member) =>
ts.isAccessor(member) &&
@@ -5476,7 +5478,12 @@ export function classValueRef(lowerer: Lowerer, info: ClassInfo, blame: ts.Node)
const display = info.def.name.replace(/^%|^%m\d+\./, "");
fenceDecorationThrows(lowerer, info, blame);
if (info.localClass && ts.isIdentifier(blame)) {
- const stored = lowerer.resolveLocal(blame);
+ let stored = lowerer.resolveLocal(blame);
+ // A reference from a function hoisted above the class statement.
+ if (!stored) {
+ const symbol = lowerer.resolveValueSymbol(blame);
+ if (symbol && predeclareForwardClass(lowerer, symbol)) stored = lowerer.resolveLocal(blame);
+ }
if (stored)
return lowerer.coerceToExpected(
{ kind: "varRef", localId: stored.id, type: stored.type, loc: locOf(blame) },
@@ -6638,7 +6645,7 @@ function staticCallOn(
// functions, called directly as `%C.static:m%n` — with the same
// through-a-VALUE shadowing fence as plain statics.
const gfound = findGenericStaticOn(lowerer, info, access.name.text);
- if (!gfound) return null;
+ if (!gfound) return classConstructorCall(lowerer, call, access, info);
if (throughValue && staticShadowBelow(info, access.name.text)) {
lowerer.unsupported(
"SC1090",
@@ -6731,6 +6738,89 @@ function staticCallOn(
return { kind: "call", callee: fnName, args, type: found.method.ret, loc };
}
+/** `C.call(thisArg, ...)` / `C.apply(thisArg, args)` on a program class that
+ * declares no such static: Function.prototype.call/apply invoke the class
+ * constructor without `new`, which throws Node's TypeError "Class
+ * constructor C cannot be invoked without 'new'" after the arguments are
+ * evaluated (and, for apply, after the argument list is read). The boxed
+ * class value's call path throws exactly that. Neither expando statics nor
+ * writes through untyped values can give a class an own `call` or `apply`.
+ * A receiver that may hold a subclass at run time (a class value, `this` in
+ * a static method) needs every subclass to lack such a static too. The
+ * receiver must be one the caller does not evaluate again (a name, `this`,
+ * or a namespace member). Null otherwise. */
+function classConstructorCall(
+ lowerer: Lowerer,
+ call: ts.CallExpression,
+ access: ts.PropertyAccessExpression,
+ info: ClassInfo,
+): IrExpr | null {
+ const name = access.name.text;
+ const receiver = access.expression;
+ if (
+ (name !== "call" && name !== "apply") ||
+ !info.decl ||
+ info.def.runtime ||
+ info.builtinError ||
+ info.builtinEmitter ||
+ info.builtinStream ||
+ info.builtinEventTarget ||
+ info.runtimeStatics?.length ||
+ declaresStaticAbove(info, name) ||
+ (exactClassOfReceiver(lowerer, receiver) !== info &&
+ info.subclasses.some((subclass) => declaresStaticBelow(subclass, name))) ||
+ call.arguments.nodes.some(ts.isSpreadElement) ||
+ !(
+ ts.isIdentifier(receiver) ||
+ receiver.kind === ts.SyntaxKind.ThisKeyword ||
+ (ts.isPropertyAccessExpression(receiver) && nsMemberIdentOf(lowerer, receiver) !== null)
+ )
+ )
+ return null;
+ return {
+ kind: "dynInvoke",
+ recv: lowerer.lowerExprExpecting(receiver, DYN),
+ method: name,
+ calleeName: ts.getTextOfNode(access),
+ args: call.arguments.nodes.map((argument) => lowerer.lowerExprExpecting(argument, DYN)),
+ type: DYN,
+ loc: locOf(call),
+ };
+}
+
+/** Whether `info`'s declaration has a static member that may be named `name`
+ * (a method, field or accessor of that name, or a computed name), or `info`
+ * has no declaration to tell. */
+function declaresStatic(info: ClassInfo, name: string): boolean {
+ if (!info.decl) return true;
+ return info.decl.members.nodes.some(
+ (member) =>
+ member.name !== undefined &&
+ ts.canHaveModifiers(member) &&
+ ts.getModifiers(member)?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword) ===
+ true &&
+ (ts.isComputedPropertyName(member.name) ||
+ ((ts.isIdentifier(member.name) || ts.isStringLiteral(member.name)) &&
+ member.name.text === name)),
+ );
+}
+
+/** declaresStatic over `info` and its bases, which it inherits statics from. */
+function declaresStaticAbove(info: ClassInfo, name: string): boolean {
+ for (let c: ClassInfo | null = info; c; c = c.base) if (declaresStatic(c, name)) return true;
+ return false;
+}
+
+/** declaresStatic over `info` and its subclasses, or runtime statics, which a
+ * function-local JavaScript class defines at run time. */
+function declaresStaticBelow(info: ClassInfo, name: string): boolean {
+ return (
+ declaresStatic(info, name) ||
+ !!info.runtimeStatics?.length ||
+ info.subclasses.some((subclass) => declaresStaticBelow(subclass, name))
+ );
+}
+
/** `C.m(...)` where `m` is a static method that a function-local
* JavaScript class defines at runtime (a factory's `class extends base`),
* inherited by `C` through its statically known base chain. When the body
@@ -11189,3 +11279,37 @@ function genericFieldFnNodeOf(
}
return null;
}
+
+/** Whether a JavaScript class's own binding is the target of
+ * `Object.defineProperty`/`defineProperties` in its file
+ * (`Object.defineProperty(Definition, "name", { value })`): its constructor
+ * then needs a live own-property table. */
+function definedThroughReflection(lowerer: Lowerer, decl: ts.ClassLikeDeclaration): boolean {
+ const name = decl.name;
+ const sf = ts.getSourceFileOfNode(decl);
+ if (name === undefined || !sf.text.includes("definePropert")) return false;
+ const symbol = lowerer.checker.getSymbolAtLocation(name);
+ if (symbol === undefined) return false;
+ let found = false;
+ ts.walkPreorder(sf, (node) => {
+ if (found) return "stop";
+ if (
+ ts.isCallExpression(node) &&
+ ts.isPropertyAccessExpression(node.expression) &&
+ (node.expression.name.text === "defineProperty" ||
+ node.expression.name.text === "defineProperties") &&
+ lowerer.stdlibGlobalMember(node.expression, "Object") !== null
+ ) {
+ const target = node.arguments.nodes[0];
+ if (
+ target !== undefined &&
+ ts.isIdentifier(target) &&
+ target.text === name.text &&
+ lowerer.checker.getSymbolAtLocation(target) === symbol
+ )
+ found = true;
+ }
+ return undefined;
+ });
+ return found;
+}
diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts
index ac8b02b69..2bde5f2aa 100644
--- a/packages/compiler/src/frontend/lowering/lower-exprs.ts
+++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts
@@ -1,4 +1,10 @@
import { keyedHelperName } from "./stable-names.js";
+import {
+ isNewTarget,
+ lowerNewTarget,
+ lowerNewTargetComparison,
+ lowerNewTargetName,
+} from "./new-target.js";
import { lowerWebAssemblyValue, refuseWebAssemblyUse } from "./builtins/webassembly.js";
import { lowerAwaitedUniformPromiseAll } from "./builtins/promises.js";
import { dynUndefinedExpr, nodeThrowExpr, numLit, strLit, varRef } from "../../ir/build.js";
@@ -713,7 +719,8 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
if (ts.isPrefixUnaryExpression(expr)) return lowerer.lowerPrefixUnary(expr);
// `x++` / `x--` in expression position: yields the OLD value.
if (ts.isPostfixUnaryExpression(expr)) return lowerIncDec(lowerer, expr, false);
- if (ts.isBinaryExpression(expr)) return lowerer.lowerBinary(expr);
+ if (ts.isBinaryExpression(expr))
+ return lowerNewTargetComparison(lowerer, expr) ?? lowerer.lowerBinary(expr);
if (ts.isCallExpression(expr)) {
// `h.return?.()` on a helper object: the method exists, so the
// optional call is the call.
@@ -729,13 +736,15 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
if (ts.isElementAccessExpression(expr)) return lowerer.lowerElementAccess(expr);
if (ts.isConditionalExpression(expr)) return lowerTernary(lowerer, expr);
- if (ts.isPropertyAccessExpression(expr)) return lowerPropertyAccessExpr(lowerer, expr, loc);
+ if (ts.isPropertyAccessExpression(expr))
+ return lowerNewTargetName(lowerer, expr) ?? lowerPropertyAccessExpr(lowerer, expr, loc);
// Meta-properties, named: `new.target` reflects HOW a function was
// invoked (compiled functions are never constructors of themselves —
// no runtime invocation record exists), and `import.meta`/
// `import.defer` are module-loader surface a native binary does not
// carry.
+ if (isNewTarget(expr)) return lowerNewTarget(lowerer, expr);
if (ts.isMetaProperty(expr)) {
const name =
expr.keywordToken === ts.SyntaxKind.NewKeyword ? "new.target" : `import.${expr.name.text}`;
diff --git a/packages/compiler/src/frontend/lowering/lower-global-value.ts b/packages/compiler/src/frontend/lowering/lower-global-value.ts
index 4b8e7b10a..e7ab6b0f0 100644
--- a/packages/compiler/src/frontend/lowering/lower-global-value.ts
+++ b/packages/compiler/src/frontend/lowering/lower-global-value.ts
@@ -1,6 +1,5 @@
import * as ts from "../ts7/adapter.js";
import { DYN, STRING, arrayOf, type IrExpr } from "../../ir/ir.js";
-import { strLit } from "../../ir/build.js";
import { locOf } from "../program.js";
import type { Lowerer } from "./lowerer.js";
import { TEMPORAL_GLOBAL } from "../builtin-modules.js";
@@ -19,10 +18,14 @@ export function lowerGlobalValue(lowerer: Lowerer, expr: ts.Expression): IrExpr
return {
kind: "libCall",
fn: "global.native",
+ // One immortal name table per program (keyed by its contents): reading
+ // the global object in a hot path allocates nothing, and the runtime
+ // recognizes a table it has already registered.
args: [
{
- kind: "arrayLit",
- elems: names.map((name) => strLit(name, loc)),
+ kind: "templateStrings",
+ key: `%globalThis.names:${names.join(",")}`,
+ cooked: names,
type: arrayOf(STRING),
loc,
},
diff --git a/packages/compiler/src/frontend/lowering/lower-inspect.ts b/packages/compiler/src/frontend/lowering/lower-inspect.ts
index 47f490c7b..7c2ec4a2d 100644
--- a/packages/compiler/src/frontend/lowering/lower-inspect.ts
+++ b/packages/compiler/src/frontend/lowering/lower-inspect.ts
@@ -1918,13 +1918,21 @@ function inspectHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc): string {
// A closed record that kept the undeclared properties of its checked
// source renders through its live checked view, which holds them in the
- // source's own-key order beside the current field values.
+ // source's own-key order beside the current field values. An array with
+ // properties besides its elements does too: the view lists them after
+ // the elements.
let prependSourceGate: (() => void) | null = null;
- if (t.kind === "record" && recordMayKeepSource(lowerer, t)) {
+ const sourceTest: IrExpr | null =
+ t.kind === "record" && recordMayKeepSource(lowerer, t)
+ ? recordSourceTest(v(), loc)
+ : t.kind === "array" && lowerer.dynConvertible(t)
+ ? { kind: "libCall", fn: "array.hasProperties", args: [v()], type: BOOL, loc }
+ : null;
+ if (sourceTest !== null) {
prependSourceGate = (): void => {
body.unshift({
kind: "if",
- cond: recordSourceTest(v(), loc),
+ cond: sourceTest,
then: [
ret({
kind: "libCall",
diff --git a/packages/compiler/src/frontend/lowering/lower-stdlib.ts b/packages/compiler/src/frontend/lowering/lower-stdlib.ts
index 0beb24a0e..e671a0a08 100644
--- a/packages/compiler/src/frontend/lowering/lower-stdlib.ts
+++ b/packages/compiler/src/frontend/lowering/lower-stdlib.ts
@@ -2496,6 +2496,25 @@ export function lowerStdlibMethodCall(
// method, drains into the number[] the fold reads.
src = classGeneratorIterable(lowerer, src) ?? src;
if (src.type.kind === "generator") src = generatorDrain(lowerer, src, F64, loc) ?? src;
+ // An untyped iterable (`Math.min(...caps.map(f))` in JavaScript): the
+ // runtime packs its values and folds their ToNumber like Node. V8 names
+ // a null or undefined source by its text, calls as `f(...)`.
+ if (src.type.kind === "dyn") {
+ const what = ts.isCallExpression(spread.expression)
+ ? `${ts.getTextOfNode(spread.expression.expression)}(...)`
+ : ts.getTextOfNode(spread.expression);
+ return {
+ kind: "libCall",
+ fn: "dyn.mathFold",
+ args: [
+ src,
+ { kind: "boolLit", value: name === "max", type: BOOL, loc },
+ { kind: "strLit", value: what, type: STRING, loc },
+ ],
+ type: F64,
+ loc,
+ };
+ }
if (src.type.kind !== "array" || src.type.elem.kind !== "f64") {
lowerer.unsupported(
"SC1090",
diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts
index ea5ef72ac..66ecea602 100644
--- a/packages/compiler/src/frontend/lowering/lower-stmts.ts
+++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts
@@ -25,6 +25,7 @@ import { InternalCompilerError } from "../../errors.js";
* flow (if/while/for/for-of/do, switch, try/catch, abrupt completions),
* explicit resource management, and blocked-binding poisoning. */
import * as ts from "../ts7/adapter.js";
+import { lowerOptionalIndex } from "./undefined-index.js";
import { processDataMemberValue, programReadsExitCode } from "./builtins/process.js";
import { requireInitFlagStmt } from "./require-initialization.js";
import { isJsonRequireDecl } from "./json-require.js";
@@ -204,7 +205,6 @@ import {
lowerElementCompoundStatement,
lowerIncDec,
lowerIncDecTarget,
- lowerOptionalNumber,
lowerGroupsProjection,
matchResultNamedGroupsOf,
runtimeOptionalGuardIds,
@@ -1010,6 +1010,44 @@ export function predeclareForwardCapture(lowerer: Lowerer, symbol: ts.Symbol): b
return false;
}
+/** The CLASS-DECLARATION twin of predeclareForwardCapture: a function
+ * hoisted above a local `class D` (`function make() { return new D(); }`
+ * reached through an earlier function) captures the class binding, which
+ * is in its temporal dead zone until the class statement runs. The class
+ * cannot be collected yet (its heritage may name bindings declared between
+ * here and the statement), so the binding is pre-declared as a TDZ box
+ * holding the class as a checked-dynamic constructor; the class statement
+ * later initializes it, and every reference reads it from there. */
+export function predeclareForwardClass(lowerer: Lowerer, symbol: ts.Symbol): boolean {
+ const decl = symbol.valueDeclaration;
+ if (!decl || !ts.isClassDeclaration(decl) || decl.name === undefined) return false;
+ if (!isJsSourceFile(ts.getSourceFileOfNode(decl))) return false;
+ for (let i = lowerer.activeStmtLists.length - 1; i >= 0; i--) {
+ const entry = lowerer.activeStmtLists[i]!;
+ const idx = entry.stmts.indexOf(decl);
+ if (idx < 0) continue;
+ if (idx < entry.index || entry.ctx === lowerer.ctx || entry.ctx.tdzPredeclared.has(symbol))
+ return false;
+ const name = decl.name.text;
+ const count = entry.ctx.localCounters.get(name) ?? 0;
+ entry.ctx.localCounters.set(name, count + 1);
+ const local: IrLocal = {
+ id: `${name}.${count}`,
+ name,
+ type: DYN,
+ mutable: false,
+ boxed: true,
+ tdz: true,
+ };
+ entry.ctx.locals.push(local);
+ entry.frame.set(symbol, local);
+ entry.out.push({ kind: "varDecl", localId: local.id, init: null, loc: locOf(decl) });
+ entry.ctx.tdzPredeclared.set(symbol, local);
+ return true;
+ }
+ return false;
+}
+
/** The FUNCTION-DECLARATION twin of predeclareForwardCapture: JS hoists a
* nested `function f() {}` to scope entry — the binding is LIVE from the
* first statement, so a reference lexically above the declaration in the
@@ -1220,6 +1258,43 @@ function lowerJsVarFromExisting(
};
}
+/** Whether a JavaScript declaration's object value changes shape after its
+ * initializer (jsOpenObjectType, jsObjectBindingExpands) or carries
+ * methods, so only the checked-dynamic representation holds it.
+ * `inferred` is the declaration's mapped checker type. */
+function jsDeclarationExpandsObject(
+ lowerer: Lowerer,
+ decl: ts.VariableDeclaration,
+ inferred: IrType | null,
+): boolean {
+ const arms =
+ inferred?.kind === "union"
+ ? (lowerer.unions.get(inferred.unionId)?.arms ?? [])
+ : inferred
+ ? [inferred]
+ : [];
+ return (
+ (!decl.type &&
+ !hasJsTypeAnnotation(decl) &&
+ inferred !== null &&
+ jsOpenObjectType(decl, inferred, lowerer.shapes, lowerer.unions).kind === "dyn") ||
+ arms.some((arm) => arm.kind === "record" && jsObjectBindingExpands(lowerer, decl, arm)) ||
+ (isJsSourceFile(ts.getSourceFileOfNode(decl)) &&
+ !decl.type &&
+ !hasJsTypeAnnotation(decl) &&
+ decl.initializer !== undefined &&
+ ts.isObjectLiteralExpression(decl.initializer) &&
+ decl.initializer.properties.nodes.some(
+ (property) =>
+ ts.isMethodDeclaration(property) ||
+ ts.isAccessor(property) ||
+ (ts.isPropertyAssignment(property) &&
+ (ts.isFunctionExpression(property.initializer) ||
+ ts.isArrowFunction(property.initializer))),
+ ))
+ );
+}
+
/** The `var` symbol's function-scoped binding TYPE — the declared type at
* the binding name, with the JS-source fallbacks every mutable binding
* takes (an empty-object-literal type is checked-dynamic because tsc
@@ -1254,6 +1329,19 @@ function varBindingType(lowerer: Lowerer, nameNode: ts.Identifier): IrType | nul
type = staticImportNamespaceType(lowerer, nameNode.parent.initializer) ?? type;
}
if (!type || type.kind === "void") return null;
+ // A JavaScript `var` holds whatever its writes store, as a `let` does:
+ // the checker types it by its initializer and checks no later write
+ // (`var fraction = 0;`, later `fraction = match[7].slice(0, 3)`), and an
+ // object it holds may gain members (`mark.snippet = snippet(mark)`).
+ if (
+ type.kind !== "dyn" &&
+ ts.isVariableDeclaration(nameNode.parent) &&
+ nameNode.parent.name === nameNode &&
+ isJsSourceFile(ts.getSourceFileOfNode(nameNode)) &&
+ (jsBindingHasOpenWrites(lowerer, nameNode.parent) ||
+ jsDeclarationExpandsObject(lowerer, nameNode.parent, type))
+ )
+ return DYN;
// A binding readable before its first store holds undefined until then.
return lowerer.runtimeOptionalBindingType(nameNode, type);
}
@@ -1529,6 +1617,14 @@ export function lowerStmt(lowerer: Lowerer, stmt: ts.Statement): IrStmt | IrStmt
if (ts.isClassDeclaration(stmt)) {
if (!stmt.name) lowerer.unsupported("SC1090", stmt, "anonymous local class declarations");
const info = lowerer.lowerClassExpressionInfo(stmt);
+ // An earlier hoisted function captured the class: its TDZ box exists.
+ const symbol = lowerer.checker.getSymbolAtLocation(stmt.name);
+ const pre = symbol ? lowerer.ctx.tdzPredeclared.get(symbol) : undefined;
+ if (pre) {
+ lowerer.ctx.tdzPredeclared.delete(symbol!);
+ const value = lowerer.coerceToExpected(lowerer.lowerClassExpression(stmt), DYN);
+ return { kind: "assign", localId: pre.id, value, initializes: true, loc: locOf(stmt) };
+ }
const type: IrType = { kind: "classval", className: info.def.name };
const local = lowerer.declareLocal(stmt.name, stmt.name.text, type, true);
// Static members of a function-local class run while the class value is
@@ -4870,12 +4966,16 @@ export function bindPatternTarget(
}
// Preserve checked-value pattern bindings when the inferred type
// cannot represent them without a refusal or an alias-breaking copy.
+ // A name destructured from a container of an evolving array
+ // (evolvingArrayHold) may hold the array, which the checker types by
+ // the elements written before it.
const dynStays =
value.type.kind === "dyn" &&
- (() => {
+ ((() => {
const mapped = lowerer.mapTypeOf(lowerer.typeOf(name));
return mapped === null || jsBindingKeepsCheckedObject(lowerer, name, mapped);
- })();
+ })() ||
+ readsHeldEvolvingArray(lowerer, name));
let type = dynStays
? DYN
: (lowerer.runtimeOptionalBindingType(name, lowerer.irTypeOf(name)) ??
@@ -5004,13 +5104,16 @@ function evolvingArrayType(lowerer: Lowerer, decl: ts.VariableDeclaration): IrTy
// type once the reads settle it.
const written: ts.Expression[] = [];
// The file's identifiers are indexed by name once (identifiersNamed), so
- // each binding resolves only the uses spelled like it.
+ // each binding resolves only the uses spelled like it. A shorthand
+ // property (`{ xs }`) is a use too.
for (const node of identifiersNamed(lowerer, ts.getSourceFileOfNode(decl), decl.name.text)) {
if (
node === decl.name ||
node.pos < scope.pos ||
node.end > scope.end ||
- lowerer.checker.getSymbolAtLocation(node) !== symbol
+ (ts.isShorthandPropertyAssignment(node.parent) && node.parent.name === node
+ ? lowerer.checker.getShorthandAssignmentValueSymbol(node.parent)
+ : lowerer.checker.getSymbolAtLocation(node)) !== symbol
)
continue;
let target: ts.Node = node;
@@ -5068,28 +5171,105 @@ function evolvingArrayType(lowerer: Lowerer, decl: ts.VariableDeclaration): IrTy
return result;
}
-/** An unannotated alias of an evolving array binding whose reads disagree
- * (`const alias = xs` between `xs.push(1)` and `xs.push("two")`): the
- * checker types the alias by the elements written before it, but both names
- * hold one array, so the alias takes the binding's checked-dynamic type and
- * sees the later writes. Aliases of such an alias follow it. Null for other
- * bindings. */
-function evolvingArrayAliasType(lowerer: Lowerer, decl: ts.VariableDeclaration): IrType | null {
- let current = decl;
- for (let depth = 0; depth < 8; depth++) {
- let initializer = current.initializer;
- while (initializer !== undefined && ts.isParenthesizedExpression(initializer))
- initializer = initializer.expression;
- if (initializer === undefined || !ts.isIdentifier(initializer)) return null;
- const source = lowerer.checker.getSymbolAtLocation(initializer)?.valueDeclaration;
+/** How a TypeScript expression holds an evolving array binding whose reads
+ * disagree (evolvingArrayType answers checked-dynamic): "evolving" for the
+ * binding itself, "alias" for an unannotated binding initialized with it or
+ * with an alias of it (`const alias = xs`), and "container" for an object or
+ * array literal with it as a property, element or spread source at any
+ * depth (`{ items: xs }`, `[xs]`), an unannotated binding initialized with
+ * or destructured from such a literal, and a property or element read of a
+ * container (`box.items`). The checker types an alias or container by the
+ * elements written before it was taken, but it holds the same array. Null
+ * for other expressions. */
+function evolvingArrayHold(
+ lowerer: Lowerer,
+ expr: ts.Expression,
+ depth = 0,
+): "evolving" | "alias" | "container" | null {
+ while (ts.isParenthesizedExpression(expr)) expr = expr.expression;
+ if (depth >= 8) return null;
+ const binding = (symbol: ts.Symbol | undefined): "evolving" | "alias" | "container" | null => {
+ const source = symbol?.valueDeclaration;
+ // A name destructured from a container (`const { items } = box`) may
+ // be the array or another container.
+ if (source !== undefined && ts.isBindingElement(source) && ts.isIdentifier(source.name)) {
+ const host = hostVariableDeclarationOf(source.name);
+ return host !== null &&
+ !host.type &&
+ host.initializer !== undefined &&
+ evolvingArrayHold(lowerer, host.initializer, depth + 1) === "container"
+ ? "container"
+ : null;
+ }
if (source === undefined || !ts.isVariableDeclaration(source) || source.type) return null;
const evolved = evolvingArrayType(lowerer, source);
- if (evolved !== null) return evolved.kind === "dyn" ? evolved : null;
- current = source;
+ if (evolved !== null) return evolved.kind === "dyn" ? "evolving" : null;
+ if (source.initializer === undefined) return null;
+ const held = evolvingArrayHold(lowerer, source.initializer, depth + 1);
+ return held === "evolving" ? "alias" : held;
+ };
+ if (ts.isIdentifier(expr)) return binding(lowerer.checker.getSymbolAtLocation(expr));
+ if (ts.isPropertyAccessExpression(expr) || ts.isElementAccessExpression(expr))
+ return evolvingArrayHold(lowerer, expr.expression, depth + 1) === "container"
+ ? "container"
+ : null;
+ // A spread of the array itself (`{ ...xs }`, `[...xs]`) copies its
+ // elements, as in Node; a spread of a container copies its references.
+ if (ts.isObjectLiteralExpression(expr)) {
+ for (const property of expr.properties.nodes) {
+ const held = ts.isPropertyAssignment(property)
+ ? evolvingArrayHold(lowerer, property.initializer, depth + 1)
+ : ts.isShorthandPropertyAssignment(property)
+ ? binding(lowerer.checker.getShorthandAssignmentValueSymbol(property))
+ : ts.isSpreadAssignment(property) &&
+ evolvingArrayHold(lowerer, property.expression, depth + 1) === "container"
+ ? "container"
+ : null;
+ if (held !== null) return "container";
+ }
+ return null;
+ }
+ if (ts.isArrayLiteralExpression(expr)) {
+ for (const element of expr.elements.nodes) {
+ const held = ts.isSpreadElement(element)
+ ? evolvingArrayHold(lowerer, element.expression, depth + 1) === "container"
+ ? "container"
+ : null
+ : evolvingArrayHold(lowerer, element, depth + 1);
+ if (held !== null) return "container";
+ }
}
return null;
}
+/** Whether a TypeScript expression is an alias or container of an evolving
+ * array binding whose reads disagree (evolvingArrayHold). Its value is
+ * checked-dynamic, and the checker's type for it lists only the elements
+ * written before it was taken, so element reads through it stay
+ * checked-dynamic instead of checking against that type. */
+export function readsHeldEvolvingArray(lowerer: Lowerer, expr: ts.Expression): boolean {
+ if (isJsSourceFile(ts.getSourceFileOfNode(expr))) return false;
+ const held = evolvingArrayHold(lowerer, expr);
+ return held === "alias" || held === "container";
+}
+
+/** Whether a TypeScript object or array literal holds an evolving array
+ * binding whose reads disagree (evolvingArrayHold), so it builds as a
+ * checked-dynamic literal that keeps the array. */
+export function holdsEvolvingArray(lowerer: Lowerer, expr: ts.Expression): boolean {
+ if (isJsSourceFile(ts.getSourceFileOfNode(expr))) return false;
+ return evolvingArrayHold(lowerer, expr) === "container";
+}
+
+/** An unannotated binding initialized with an evolving array binding whose
+ * reads disagree, or with an alias or container of one (evolvingArrayHold):
+ * it takes the array's checked-dynamic type, so it sees the later writes.
+ * Null for other bindings. */
+function evolvingArrayHolderType(lowerer: Lowerer, decl: ts.VariableDeclaration): IrType | null {
+ if (decl.initializer === undefined) return null;
+ return evolvingArrayHold(lowerer, decl.initializer) === null ? null : DYN;
+}
+
/** A single static type for an initializer-less JavaScript `let` whose
* later plain writes all agree. An evolving JS local reads as `undefined`
* until control flow proves a write, so a scalar slot is sound only when
@@ -5597,26 +5777,7 @@ export function lowerVarDecl(
: inferredObjectType
? [inferredObjectType]
: [];
- const expandsObject =
- (!decl.type &&
- !hasJsTypeAnnotation(decl) &&
- inferredObjectType !== null &&
- jsOpenObjectType(decl, inferredObjectType, lowerer.shapes, lowerer.unions).kind === "dyn") ||
- inferredObjectArms.some(
- (arm) => arm.kind === "record" && jsObjectBindingExpands(lowerer, decl, arm),
- ) ||
- (isJsSourceFile(ts.getSourceFileOfNode(decl)) &&
- !decl.type &&
- !hasJsTypeAnnotation(decl) &&
- ts.isObjectLiteralExpression(decl.initializer) &&
- decl.initializer.properties.nodes.some(
- (property) =>
- ts.isMethodDeclaration(property) ||
- ts.isAccessor(property) ||
- (ts.isPropertyAssignment(property) &&
- (ts.isFunctionExpression(property.initializer) ||
- ts.isArrowFunction(property.initializer))),
- ));
+ const expandsObject = jsDeclarationExpandsObject(lowerer, decl, inferredObjectType);
let init: IrExpr;
try {
init =
@@ -5803,7 +5964,7 @@ export function lowerVarDecl(
!decl.type &&
!isJsSourceFile(ts.getSourceFileOfNode(decl))
) {
- const evolved = evolvingArrayType(lowerer, decl) ?? evolvingArrayAliasType(lowerer, decl);
+ const evolved = evolvingArrayType(lowerer, decl) ?? evolvingArrayHolderType(lowerer, decl);
if (evolved !== null) {
type = evolved;
init = lowerer.lowerExprExpecting(decl.initializer, evolved);
@@ -6327,18 +6488,15 @@ export function hasJsTypeAnnotation(decl: ts.VariableDeclaration): boolean {
);
}
-/** Inferred JS scalar initializers do not constrain later writes through
- * an any-valued callback. Keep those bindings open, including captured writes. */
-export function jsBindingHasOpenWrites(lowerer: Lowerer, decl: ts.VariableDeclaration): boolean {
- if (
- !isJsSourceFile(ts.getSourceFileOfNode(decl)) ||
- decl.type ||
- hasJsTypeAnnotation(decl) ||
- !ts.isIdentifier(decl.name)
- )
- return false;
- const symbol = lowerer.resolveValueSymbol(decl.name);
- if (!symbol) return false;
+/** The assignments to a declaration's binding in its function (or file),
+ * `=` and compound alike. Null when the name has no symbol. */
+function jsBindingWrites(
+ lowerer: Lowerer,
+ decl: ts.VariableDeclaration,
+ name: ts.Identifier,
+): ts.BinaryExpression[] | null {
+ const symbol = lowerer.resolveValueSymbol(name);
+ if (!symbol) return null;
let owner: ts.Node = decl;
while (owner.parent && !ts.isFunctionLike(owner) && !ts.isSourceFile(owner)) owner = owner.parent;
let writes = lowerer.jsBindingWritesByOwner.get(owner);
@@ -6359,9 +6517,36 @@ export function jsBindingHasOpenWrites(lowerer: Lowerer, decl: ts.VariableDeclar
writes = indexed;
lowerer.jsBindingWritesByOwner.set(owner, writes);
}
- const candidates = (writes.get(decl.name.text) ?? []).filter(
+ return (writes.get(name.text) ?? []).filter(
(node) => lowerer.resolveValueSymbol(node.left as ts.Identifier) === symbol,
);
+}
+
+/** Whether a JavaScript binding always holds a string: the checker types it
+ * as one, and every write appends to it (`result += part`), which yields a
+ * string whatever the part is. */
+export function jsBindingAppendsOnly(lowerer: Lowerer, decl: ts.VariableDeclaration): boolean {
+ if (!ts.isIdentifier(decl.name)) return false;
+ if (lowerer.mapTypeOf(lowerer.typeOf(decl.name))?.kind !== "string") return false;
+ const writes = jsBindingWrites(lowerer, decl, decl.name);
+ return (
+ writes !== null &&
+ writes.every((node) => node.operatorToken.kind === ts.SyntaxKind.PlusEqualsToken)
+ );
+}
+
+/** Inferred JS scalar initializers do not constrain later writes through
+ * an any-valued callback. Keep those bindings open, including captured writes. */
+export function jsBindingHasOpenWrites(lowerer: Lowerer, decl: ts.VariableDeclaration): boolean {
+ if (
+ !isJsSourceFile(ts.getSourceFileOfNode(decl)) ||
+ decl.type ||
+ hasJsTypeAnnotation(decl) ||
+ !ts.isIdentifier(decl.name)
+ )
+ return false;
+ const candidates = jsBindingWrites(lowerer, decl, decl.name);
+ if (candidates === null) return false;
if (
candidates.some(
(node) =>
@@ -6369,6 +6554,18 @@ export function jsBindingHasOpenWrites(lowerer: Lowerer, decl: ts.VariableDeclar
)
)
return true;
+ // A `+=` of a string turns a number or boolean binding into a string
+ // (`n += "px"`); the checker keeps the declared type.
+ if (
+ candidates.some(
+ (node) =>
+ node.operatorToken.kind === ts.SyntaxKind.PlusEqualsToken &&
+ (lowerer.typeOf(node.right).flags & ts.TypeFlags.StringLike) !== 0,
+ )
+ ) {
+ const declared = lowerer.mapTypeOf(lowerer.typeOf(decl.name));
+ if (declared?.kind === "f64" || declared?.kind === "bool") 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
@@ -9212,7 +9409,7 @@ function lowerAssignTargetInto(
if (arr.type.kind === "array") {
const recv = lowerer.declareHiddenLocal("%dtRecv", arr.type);
out.push({ kind: "varDecl", localId: recv.id, init: arr, loc });
- const index = lowerOptionalNumber(
+ const index = lowerOptionalIndex(
lowerer,
lowerer.lowerExpr(t.argumentExpression),
locOf(t.argumentExpression),
diff --git a/packages/compiler/src/frontend/lowering/lower-stream.ts b/packages/compiler/src/frontend/lowering/lower-stream.ts
index e35f5564f..c37ae9074 100644
--- a/packages/compiler/src/frontend/lowering/lower-stream.ts
+++ b/packages/compiler/src/frontend/lowering/lower-stream.ts
@@ -5683,7 +5683,10 @@ export function lowerStreamMethodCall(
// argument reaches an object-mode side's _write as passed, null when
// the callback took its place (a byte-mode side encodes a string
// chunk with it).
- const writeEnc = (encArg: ts.Expression | undefined, cbArg: ts.Expression | undefined) => {
+ const writeEnc = (
+ encArg: ts.Expression | undefined,
+ cbArg: ts.Expression | undefined,
+ ): IrExpr => {
const receiver = streamReceiverExpr(lowerer, access.expression);
const chunk = lowerer.lowerExprExpecting(args[0]!, DYN);
const tail: IrExpr[] = [];
@@ -5696,7 +5699,7 @@ export function lowerStreamMethodCall(
args: [receiver, numLit(flags, loc), chunk, ...tail],
type: BOOL,
loc,
- } satisfies IrExpr;
+ };
};
if (kind === "dyn") {
if (args.length === 3) return writeEnc(args[1], args[2]);
diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts
index c1b9a0ff8..7a4d57d6b 100644
--- a/packages/compiler/src/frontend/lowering/lowerer.ts
+++ b/packages/compiler/src/frontend/lowering/lowerer.ts
@@ -1,5 +1,6 @@
import { isDeclBoundaryFile } from "../decl-projection.js";
import { relieveHeapPressure } from "../../heap-pressure.js";
+import { moduleNamespaceObject } from "./module-namespace-elements.js";
import { keyedHelperName } from "./stable-names.js";
import { positionTag, StructuralPositions } from "./structural-positions.js";
import { onlyForwarded, stripTypeWrappers } from "./typed-arguments.js";
@@ -185,7 +186,7 @@ export { newFnCtx, type FnCtx } from "./function-context.js";
*/
import { dirname, relative, resolve } from "node:path";
import { bindingSource } from "../binding-source.js";
-import { tsgoPath } from "../dts-paths.js";
+import { compilerOwnedFileName, tsgoPath } from "../dts-paths.js";
import * as ts from "../ts7/adapter.js";
import { isBundledShimForwarder } from "../package-shims.js";
import { erasedTypeOnlyImport } from "../type-only-imports.js";
@@ -615,6 +616,7 @@ import {
lowerForStatement,
matchGroupsPatternSource,
isGuardedRecordRead,
+ holdsEvolvingArray,
} from "./lower-stmts.js";
import {
type FieldTarget,
@@ -2188,6 +2190,9 @@ export class Lowerer {
* and erased instances). */
erasingDepth = 0;
localClassInstantiations: { owner: ts.FunctionLikeDeclaration; name: string }[] = [];
+ /** The function-local classes of the program with an `extends` clause, whether lowering collected
+ * them yet or not (new-target.ts); found on first use. */
+ functionLocalSubclasses: ts.ClassLikeDeclaration[] | undefined = undefined;
/** Instance types (by checker type id) of the local classes generic
* factory calls return, and the per-instance class each names. */
readonly localClassInstanceTypes = new Map();
@@ -2972,9 +2977,14 @@ export class Lowerer {
* ABI and shared prototype descriptors instead of call-site specialization. */
readonly prototypeMethodAccesses = new Map();
/** 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`. */
+ * object an ES5 constructor function can inherit from, or has a class
+ * whose heritage is evaluated at run time (`extends (Base ?? Error)`):
+ * only then can a value that is not an Error instance be
+ * `instanceof Error`. */
errorPrototypeValues = false;
+ /** The property names the program assigns on the global object
+ * (property-access.ts), collected on first use. */
+ globalThisWrittenNames: Set | null = null;
/** Well-known symbol methods (`sym:iterator`) the program may replace
* through a symbol-keyed write: an instance or prototype property, or a
* reflection call. */
@@ -3201,13 +3211,14 @@ export class Lowerer {
/** Structural source positions (structural-positions.ts) for names that
* stay the same when unrelated code moves. */
/** Files are named by their path relative to the entry's directory, so
- * names do not depend on where the program sits, and default library
- * files by their base name: two checkers install them in different
- * directories. */
+ * names do not depend on where the program sits, default library files
+ * by their base name (two checkers install them in different
+ * directories), and the files scriptc ships by their name inside scriptc,
+ * so names do not depend on where scriptc is installed either. */
readonly positions = new StructuralPositions((sf) =>
this.program.isSourceFileDefaultLibrary(sf)
? `lib:${sf.fileName.slice(sf.fileName.lastIndexOf("/") + 1)}`
- : relative(dirname(this.entry.fileName), sf.fileName),
+ : (compilerOwnedFileName(sf.fileName) ?? relative(dirname(this.entry.fileName), sf.fileName)),
);
/** The outermost node whose token span is exactly `loc`, or null. */
@@ -3239,7 +3250,10 @@ export class Lowerer {
* before it. */
closureName(loc: SrcLoc, suffix = "", prefix = "%fn"): string {
const node = this.nodeAtLoc(loc);
- const where = node === null ? `${loc.file}:${loc.start}:${loc.end}` : this.positions.of(node);
+ const where =
+ node === null
+ ? `${compilerOwnedFileName(loc.file) ?? loc.file}:${loc.start}:${loc.end}`
+ : this.positions.of(node);
const key = `${prefix}|${this.closureRoot}|${where}|${suffix}`;
const first = keyedHelperName(this, prefix, key);
const seen = this.closureNameCounts.get(first) ?? 0;
@@ -3380,8 +3394,18 @@ export class Lowerer {
if (interned.has(fn.name)) this.sharedHelpers.add(fn);
if (this.sharedHelpers.size === 0) return;
const shared = new Map([...this.sharedHelpers].map((fn) => [fn.name, fn]));
- const earlier = (left: SrcLoc, right: SrcLoc): boolean =>
- left.file < right.file || (left.file === right.file && left.start < right.start);
+ // Files scriptc ships compare by their name inside scriptc, so the
+ // choice does not depend on where scriptc is installed.
+ const fileKeys = new Map();
+ const fileKey = (file: string): string => {
+ let key = fileKeys.get(file);
+ if (key === undefined) fileKeys.set(file, (key = compilerOwnedFileName(file) ?? file));
+ return key;
+ };
+ const earlier = (left: SrcLoc, right: SrcLoc): boolean => {
+ if (left.file === right.file) return left.start < right.start;
+ return fileKey(left.file) < fileKey(right.file);
+ };
// The earliest use of each shared helper outside the shared helpers, and
// which shared helpers each shared helper uses.
const uses = new Map();
@@ -9465,6 +9489,22 @@ export class Lowerer {
}
if (expr.type.kind === "void" && expected.kind !== "void" && this.neverValued.has(expr))
return this.divergentValue(expr, expected);
+ // A builtin error constructor global stored in a slot of its class value
+ // type (`{ Parent: Error }`): the class value itself, which a checked
+ // cast of the dynamic constructor value could not produce.
+ if (
+ expected.kind === "classval" &&
+ expr.kind === "libCall" &&
+ expr.fn === "dyn.globalConstructor" &&
+ expr.args[0]?.kind === "strLit" &&
+ `%${expr.args[0].value}` === expected.className
+ ) {
+ const info = this.classes.get(expected.className);
+ if (info?.builtinError && info.valueCtorParams !== undefined) {
+ this.defineClassValueConstruct(info);
+ return { kind: "classRef", className: info.def.name, type: expected, loc: expr.loc };
+ }
+ }
// process.stdout/stderr into a general Writable slot (general-streams.ts).
const stdioStream = stdioIntoGeneralStream(this, expr, expected);
if (stdioStream) return this.coerceToExpected(stdioStream, expected);
@@ -9571,6 +9611,13 @@ export class Lowerer {
// constructor. `require("events")` is the EventEmitter class itself,
// so its module binding is too (`ReleaseEmitter = EventEmitter` over
// `const EventEmitter = require("events")`).
+ if (expr.type.kind === "moduleNs" && isJsSourceFileName(expr.loc.file)) {
+ const object = moduleNamespaceObject(
+ this,
+ expr as IrExpr & { type: IrType & { kind: "moduleNs" } },
+ );
+ if (object) return object;
+ }
if (
expr.type.kind === "moduleNs" &&
expr.type.moduleId === "builtin:events" &&
@@ -10672,7 +10719,8 @@ export class Lowerer {
builtinOption("UrlObject") ||
builtinOption("URLFormatOptions") ||
!this.mapTypeOf(this.typeOf(x)) ||
- this.excessJsContextualProperty(x, parts)
+ this.excessJsContextualProperty(x, parts) ||
+ holdsEvolvingArray(this, x)
) {
return lowerDynObjectLiteral(this, x);
}
diff --git a/packages/compiler/src/frontend/lowering/module-namespace-elements.ts b/packages/compiler/src/frontend/lowering/module-namespace-elements.ts
index d2425750b..cf828c6f0 100644
--- a/packages/compiler/src/frontend/lowering/module-namespace-elements.ts
+++ b/packages/compiler/src/frontend/lowering/module-namespace-elements.ts
@@ -1,6 +1,13 @@
import { dynUndefinedExpr, strLit, varRef } from "../../ir/build.js";
import * as ts from "../ts7/adapter.js";
-import { DYN, STRING, type IrExpr, type IrFunction, type IrStmt } from "../../ir/ir.js";
+import {
+ DYN,
+ STRING,
+ type IrExpr,
+ type IrFunction,
+ type IrStmt,
+ type IrType,
+} from "../../ir/ir.js";
import { locOf } from "../program.js";
import { newFnCtx, type Lowerer } from "./lowerer.js";
@@ -25,12 +32,13 @@ export function lowerModuleNamespaceElement(
try {
const body: IrStmt[] = [];
const symbol = lowerer.checker.getSymbolAtLocation(source);
- symbol?.getExports().forEach((exported, key) => {
+ for (const exported of symbol ? lowerer.checker.getExportsOfModule(symbol) : []) {
+ const key = ts.symbolName(exported);
const target =
exported.flags & ts.SymbolFlags.Alias
? lowerer.checker.getAliasedSymbol(exported)
: exported;
- if (!(target.flags & ts.SymbolFlags.Value)) return;
+ if (!(target.flags & ts.SymbolFlags.Value)) continue;
const declaration = target.valueDeclaration;
const identifier = declaration?.name as ts.Node | undefined;
if (!identifier || !ts.isIdentifier(identifier)) {
@@ -43,7 +51,7 @@ export function lowerModuleNamespaceElement(
kind: "strEq",
negated: false,
left: varRef("key", STRING, loc),
- right: strLit(String(key), loc),
+ right: strLit(key, loc),
type: { kind: "bool" },
loc,
},
@@ -51,7 +59,7 @@ export function lowerModuleNamespaceElement(
else_: null,
loc,
});
- });
+ }
body.push({ kind: "return", value: dynUndefinedExpr(loc), loc });
const fn: IrFunction = {
name,
@@ -88,3 +96,78 @@ export function lowerModuleNamespaceElement(
};
return { kind: "call", callee: name, args: [receiver, text], type: DYN, loc };
}
+
+/** A module namespace taken as an untyped value (`{ ...ns }`, `{ iso: ns }`
+ * in JavaScript): an object holding each value export, keyed in the
+ * namespace's sorted key order. The copy is exact while every export is a
+ * binding that never changes after initialization (const, function or
+ * class); null for other namespaces, which keep their fence. */
+export function moduleNamespaceObject(
+ lowerer: Lowerer,
+ ns: IrExpr & { type: IrType & { kind: "moduleNs" } },
+): IrExpr | null {
+ const source = lowerer.sourceFileOfModuleNamespace(ns.type);
+ if (!source) return null;
+ const symbol = lowerer.checker.getSymbolAtLocation(source);
+ if (!symbol) return null;
+ const exports: { key: string; identifier: ts.Identifier }[] = [];
+ let exact = true;
+ // The module's resolved exports, which include `export *` members.
+ for (const exported of lowerer.checker.getExportsOfModule(symbol)) {
+ const key = ts.symbolName(exported);
+ const target =
+ exported.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(exported) : exported;
+ if (!(target.flags & ts.SymbolFlags.Value)) continue;
+ const declaration = target.valueDeclaration;
+ const identifier = declaration?.name as ts.Node | undefined;
+ const immutable =
+ declaration !== undefined &&
+ (ts.isFunctionDeclaration(declaration) ||
+ ts.isClassDeclaration(declaration) ||
+ (ts.isVariableDeclaration(declaration) &&
+ (ts.getCombinedNodeFlags(declaration) & ts.NodeFlags.Const) !== 0));
+ if (!identifier || !ts.isIdentifier(identifier) || !immutable) {
+ exact = false;
+ break;
+ }
+ exports.push({ key, identifier });
+ }
+ if (!exact) return null;
+ exports.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
+ const name = `%module.object:${ns.type.moduleId}`;
+ const loc = ns.loc;
+ if (!lowerer.liftedFns.some((fn) => fn.name === name)) {
+ const context = newFnCtx(false, null, null, DYN);
+ const previousClass = lowerer.currentClass;
+ lowerer.currentClass = null;
+ lowerer.fnStack.push(context);
+ try {
+ const fields = exports.map(({ key, identifier }) => ({
+ key: strLit(key, loc),
+ value: lowerer.lowerExprExpecting(identifier, DYN),
+ }));
+ if (fields.some((field) => field.value.type.kind !== "dyn")) return null;
+ const fn: IrFunction = {
+ name,
+ params: [],
+ returnType: DYN,
+ locals: [...context.locals],
+ body: [{ kind: "return", value: { kind: "dynObjLit", fields, type: DYN, loc }, loc }],
+ loc,
+ };
+ lowerer.liftedFns.push(fn);
+ } finally {
+ lowerer.fnStack.pop();
+ lowerer.currentClass = previousClass;
+ }
+ }
+ const call: IrExpr = { kind: "call", callee: name, args: [], type: DYN, loc };
+ if (ns.kind === "moduleNsRef") return call;
+ return {
+ kind: "seqExpr",
+ stmts: [{ kind: "exprStmt", expr: ns, loc }],
+ result: call,
+ type: DYN,
+ loc,
+ };
+}
diff --git a/packages/compiler/src/frontend/lowering/new-target.ts b/packages/compiler/src/frontend/lowering/new-target.ts
new file mode 100644
index 000000000..83994ca6f
--- /dev/null
+++ b/packages/compiler/src/frontend/lowering/new-target.ts
@@ -0,0 +1,357 @@
+/* `new.target`. Compiled code has no invocation record, but inside a class
+ * constructor `new.target` is always the class of the object under
+ * construction: a compiled `new` allocates the instance with its concrete
+ * class before any constructor of the chain runs, and every constructor of
+ * the chain receives that instance as `this`. The program's class
+ * hierarchy is closed, so the value is a static dispatch over the
+ * constructor's class and its descendants: the deepest class the instance
+ * is an instance of is its exact class.
+ *
+ * The checker rejects `new.target` in methods, accessors, static blocks and
+ * field initializers. Plain functions (constructible in JavaScript) keep
+ * the refusal. */
+import * as ts from "../ts7/adapter.js";
+import { BOOL, STRING, type IrExpr, type IrType, type SrcLoc } from "../../ir/ir.js";
+import { varRef } from "../../ir/build.js";
+import { locOf } from "../program.js";
+import type { Lowerer } from "./lowerer.js";
+import type { ClassInfo } from "./lower-classes.js";
+import { classValueRef, functionLocalClass } from "./lower-classes.js";
+
+export function isNewTarget(node: ts.Node): node is ts.MetaProperty {
+ return (
+ ts.isMetaProperty(node) &&
+ node.keywordToken === ts.SyntaxKind.NewKeyword &&
+ node.name.text === "target"
+ );
+}
+
+/** Where `new.target` is bound: a class constructor, or a plain function
+ * (refused). */
+type Binding =
+ | { kind: "constructor"; declaration: ts.ConstructorDeclaration }
+ | { kind: "function" };
+
+function bindingOf(node: ts.Node): Binding {
+ for (let p = node.parent; p !== undefined; p = p.parent) {
+ if (ts.isArrowFunction(p)) continue;
+ if (ts.isConstructorDeclaration(p)) return { kind: "constructor", declaration: p };
+ if (ts.isFunctionLike(p) || ts.isSourceFile(p)) return { kind: "function" };
+ }
+ return { kind: "function" };
+}
+
+interface Dispatch {
+ info: ClassInfo;
+ /** The constructor's descendants, deepest first, then its class. A
+ * generic family stands for its instantiations, also those that later
+ * bodies instantiate: its interval holds them all. */
+ classes: ClassInfo[];
+ self: IrExpr | null;
+}
+
+function depth(info: ClassInfo): number {
+ let d = 0;
+ for (let c = info.base; c; c = c.base) d++;
+ return d;
+}
+
+/** The function-local classes of the program that extend a class. */
+function functionLocalSubclasses(lowerer: Lowerer): ts.ClassLikeDeclaration[] {
+ let found = lowerer.functionLocalSubclasses;
+ if (found === undefined) {
+ const classes: ts.ClassLikeDeclaration[] = [];
+ for (const sf of lowerer.moduleOrder)
+ ts.walkPreorder(sf, (node) => {
+ if (
+ (ts.isClassDeclaration(node) || ts.isClassExpression(node)) &&
+ baseExpressionOf(node) !== undefined &&
+ functionLocalClass(node)
+ )
+ classes.push(node);
+ });
+ lowerer.functionLocalSubclasses = found = classes;
+ }
+ return found;
+}
+
+function baseExpressionOf(node: ts.ClassLikeDeclaration): ts.Expression | undefined {
+ let base = node.heritageClauses?.nodes.find(
+ (clause) => clause.token === ts.SyntaxKind.ExtendsKeyword,
+ )?.types?.nodes[0]?.expression;
+ while (base !== undefined && ts.isParenthesizedExpression(base)) base = base.expression;
+ return base;
+}
+
+/** Whether the class `node` extends the class of `info` (for an
+ * instantiation, its family), directly or through other classes, as far as
+ * its `extends` clauses name classes. */
+function derivesFrom(
+ lowerer: Lowerer,
+ node: ts.ClassLikeDeclaration,
+ info: ClassInfo,
+ seen: Set,
+): boolean {
+ if (seen.has(node)) return false;
+ seen.add(node);
+ const base = baseExpressionOf(node);
+ const name =
+ base === undefined
+ ? undefined
+ : ts.isIdentifier(base)
+ ? base
+ : ts.isPropertyAccessExpression(base)
+ ? base.name
+ : undefined;
+ let symbol = name === undefined ? undefined : lowerer.checker.getSymbolAtLocation(name);
+ if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Alias) !== 0)
+ symbol = lowerer.checker.getAliasedSymbol(symbol);
+ if (symbol === undefined) return false;
+ const named = lowerer.classBySymbol.get(symbol);
+ if (named !== undefined)
+ return (
+ named === info ||
+ named === info.genericInstance?.family ||
+ lowerer.isSubclassOf(named.def.name, info.def.name)
+ );
+ // A class lowering has not collected: through its declaration.
+ for (const declaration of symbol.declarations) {
+ let cls: ts.Node | undefined = declaration;
+ if (ts.isVariableDeclaration(cls)) {
+ cls = cls.initializer;
+ while (cls !== undefined && ts.isParenthesizedExpression(cls)) cls = cls.expression;
+ }
+ if (
+ cls !== undefined &&
+ (ts.isClassDeclaration(cls) || ts.isClassExpression(cls)) &&
+ derivesFrom(lowerer, cls, info, seen)
+ )
+ return true;
+ }
+ return false;
+}
+
+/** `node`'s name, quoted, for diagnostics. */
+function localClassName(node: ts.ClassLikeDeclaration): string {
+ if (node.name) return `'${node.name.text}'`;
+ const parent = node.parent;
+ return parent && ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name)
+ ? `'${parent.name.text}'`
+ : "a class expression";
+}
+
+/** The classes `new.target` can denote in the constructor being lowered,
+ * or null after a diagnostic. */
+function dispatchOf(lowerer: Lowerer, expr: ts.MetaProperty): Dispatch | null {
+ const binding = bindingOf(expr);
+ if (binding.kind !== "constructor") return null;
+ const info = lowerer.currentClass;
+ const cls = binding.declaration.parent;
+ if (!info || (info.decl !== null && info.decl !== cls)) {
+ lowerer.unsupported(
+ "SC1090",
+ expr,
+ "'new.target' in this constructor (its class has no lowering here)",
+ );
+ return null;
+ }
+ // An instantiation's subclasses can also be instantiations of generic
+ // subclasses of its family (`class G extends Box` under Box).
+ const family = info.genericInstance?.family;
+ const descendants: ClassInfo[] = [];
+ for (const other of lowerer.classes.values()) {
+ if (other === info) continue;
+ if (
+ lowerer.isSubclassOf(other.def.name, info.def.name) ||
+ (family !== undefined &&
+ other.generic !== undefined &&
+ other !== family &&
+ lowerer.isSubclassOf(other.def.name, family.def.name))
+ )
+ descendants.push(other);
+ }
+ descendants.sort((a, b) => depth(b) - depth(a));
+ const classes = [...descendants, info];
+ for (const c of classes) {
+ if (c.localClass) {
+ lowerer.unsupported(
+ "SC1090",
+ expr,
+ `'new.target' where '${c.def.jsName ?? c.def.name}' is a function-local class (each evaluation creates a new class)`,
+ );
+ return null;
+ }
+ }
+ // Lowering collects a function-local class with the body declaring it,
+ // which may come after this constructor.
+ for (const local of functionLocalSubclasses(lowerer)) {
+ if (!derivesFrom(lowerer, local, info, new Set())) continue;
+ lowerer.unsupported(
+ "SC1090",
+ expr,
+ `'new.target' where ${localClassName(local)} is a function-local class (each evaluation creates a new class)`,
+ );
+ }
+ let self: IrExpr | null = null;
+ if (classes.length > 1) {
+ const thisLocal = lowerer.resolveThis(expr);
+ if (!thisLocal || thisLocal.type.kind !== "object") {
+ lowerer.unsupported(
+ "SC1090",
+ expr,
+ "'new.target' where the constructor's 'this' has no class representation",
+ );
+ return null;
+ }
+ self = varRef(thisLocal.id, thisLocal.type, locOf(expr));
+ }
+ return { info, classes, self };
+}
+
+/** A conditional chain selecting `arm(D)` for the exact class D of the
+ * instance under construction. */
+function select(
+ dispatch: Dispatch,
+ type: IrType,
+ loc: SrcLoc,
+ arm: (info: ClassInfo) => IrExpr,
+): IrExpr {
+ const { classes, self } = dispatch;
+ let result = arm(classes[classes.length - 1]!);
+ for (let i = classes.length - 2; i >= 0; i--) {
+ const c = classes[i]!;
+ result = {
+ kind: "ternary",
+ cond: { kind: "instanceOf", value: self!, className: c.def.name, type: BOOL, loc },
+ then: arm(c),
+ else_: result,
+ type,
+ loc,
+ };
+ }
+ return result;
+}
+
+function refuseFunction(lowerer: Lowerer, expr: ts.MetaProperty): never {
+ return lowerer.unsupported(
+ "SC1090",
+ expr,
+ "'new.target' in a plain function (a constructible function's invocation record does not exist in compiled code; use a class constructor)",
+ );
+}
+
+/** `new.target` as a value. */
+export function lowerNewTarget(lowerer: Lowerer, expr: ts.MetaProperty): IrExpr {
+ const loc = locOf(expr);
+ if (bindingOf(expr).kind === "function") refuseFunction(lowerer, expr);
+ const dispatch = dispatchOf(lowerer, expr)!;
+ const type: IrType = { kind: "classval", className: dispatch.info.def.name };
+ for (const c of dispatch.classes) {
+ if (c.generic) {
+ lowerer.unsupported(
+ "SC1090",
+ expr,
+ `'new.target' as a value where '${c.def.jsName ?? c.def.name}' is a generic subclass (its instantiations have no common class value; read 'new.target.name' or compare 'new.target' with a class instead)`,
+ );
+ }
+ if (c !== dispatch.info && !lowerer.ctorAbiEquals(c, dispatch.info)) {
+ lowerer.unsupported(
+ "SC1090",
+ expr,
+ `'new.target' as a value where '${c.def.jsName ?? c.def.name}' constructs with a different signature than '${dispatch.info.def.jsName ?? dispatch.info.def.name}' (read 'new.target.name' or compare 'new.target' with a class instead)`,
+ );
+ }
+ }
+ return select(dispatch, type, loc, (c) =>
+ lowerer.coerceToExpected(classValueRef(lowerer, c, expr), type),
+ );
+}
+
+/** `new.target.name`: the exact class's name, without materializing a
+ * class value (subclasses may construct with other signatures). */
+export function lowerNewTargetName(
+ lowerer: Lowerer,
+ access: ts.PropertyAccessExpression,
+): IrExpr | null {
+ if (access.questionDotToken || access.name.text !== "name") return null;
+ let target: ts.Expression = access.expression;
+ while (ts.isParenthesizedExpression(target)) target = target.expression;
+ if (!isNewTarget(target) || bindingOf(target).kind !== "constructor") return null;
+ const dispatch = dispatchOf(lowerer, target);
+ if (!dispatch) return null;
+ const loc = locOf(access);
+ return select(dispatch, STRING, loc, (c) => {
+ // An own static `name` field replaces the class's name; a subclass has
+ // its own name. An instantiation's statics are its family's.
+ const owner = c.genericInstance?.family ?? c;
+ const field = owner.staticFields.find((f) => f.name === "name");
+ if (field !== undefined) {
+ if (field.type.kind !== "string")
+ lowerer.unsupported(
+ "SC1090",
+ access,
+ `'new.target.name' where '${c.def.jsName ?? c.def.name}' declares a static 'name' that is not a string`,
+ );
+ return { kind: "varRef", localId: field.globalId, type: field.type, loc };
+ }
+ if (owner.staticMethods?.has("name") || owner.genericStatics?.has("name"))
+ lowerer.unsupported(
+ "SC1090",
+ access,
+ `'new.target.name' where '${c.def.jsName ?? c.def.name}' declares a static method 'name'`,
+ );
+ // A generic family's instantiations share its name.
+ if (c.generic)
+ return { kind: "strLit", value: c.def.jsName ?? c.generic.baseName, type: STRING, loc };
+ return {
+ kind: "libCall",
+ fn: "class.name",
+ args: [classValueRef(lowerer, c, target)],
+ type: STRING,
+ loc,
+ };
+ });
+}
+
+/** `new.target === C` (and `!==`, `==`, `!=`, either operand order) with a
+ * program class named by the other operand: an exact-class test. */
+export function lowerNewTargetComparison(
+ lowerer: Lowerer,
+ expr: ts.BinaryExpression,
+): IrExpr | null {
+ const op = expr.operatorToken.kind;
+ const negated =
+ op === ts.SyntaxKind.ExclamationEqualsEqualsToken ||
+ op === ts.SyntaxKind.ExclamationEqualsToken;
+ if (
+ !negated &&
+ op !== ts.SyntaxKind.EqualsEqualsEqualsToken &&
+ op !== ts.SyntaxKind.EqualsEqualsToken
+ )
+ return null;
+ const strip = (e: ts.Expression): ts.Expression => {
+ while (ts.isParenthesizedExpression(e)) e = e.expression;
+ return e;
+ };
+ const left = strip(expr.left);
+ const right = strip(expr.right);
+ const target = isNewTarget(left) ? left : isNewTarget(right) ? right : null;
+ if (!target || bindingOf(target).kind !== "constructor") return null;
+ const other = target === left ? right : left;
+ if (!ts.isIdentifier(other)) return null;
+ // A constructor's new.target is never undefined.
+ if (other.text === "undefined" && (lowerer.typeOf(other).flags & ts.TypeFlags.Undefined) !== 0)
+ return { kind: "boolLit", value: negated, type: BOOL, loc: locOf(expr) };
+ const symbol = lowerer.resolveValueSymbol(other);
+ const named = symbol ? lowerer.classBySymbol.get(symbol) : undefined;
+ if (!named || named.generic || named.localClass) return null;
+ const dispatch = dispatchOf(lowerer, target);
+ if (!dispatch) return null;
+ const loc = locOf(expr);
+ return select(dispatch, BOOL, loc, (c) => ({
+ kind: "boolLit",
+ value: (c === named) !== negated,
+ type: BOOL,
+ loc,
+ }));
+}
diff --git a/packages/compiler/src/frontend/lowering/undefined-index.ts b/packages/compiler/src/frontend/lowering/undefined-index.ts
new file mode 100644
index 000000000..5226d7008
--- /dev/null
+++ b/packages/compiler/src/frontend/lowering/undefined-index.ts
@@ -0,0 +1,101 @@
+import * as ts from "../ts7/adapter.js";
+import { BOOL, DYN, F64, UNDEFINED_T, type IrExpr, type IrStmt, type SrcLoc } from "../../ir/ir.js";
+import { varRef } from "../../ir/build.js";
+import type { Lowerer } from "./lowerer.js";
+import { lowerOptionalNumber } from "./lower-exprs.js";
+import { keyedHelperName } from "./stable-names.js";
+
+/** An element index the checker types as a number can be undefined at run
+ * time (an element read from a holey array). JavaScript then names the
+ * property "undefined", not "NaN". */
+
+/** The array element index `operand` (the lowered index of `node`) as a
+ * number. An undefined index is the runtime's "undefined" key
+ * (`array.undefinedKey`, a NaN the array's property table spells
+ * "undefined"); other values convert with ToNumber, as lowerOptionalNumber
+ * does. */
+export function lowerOptionalIndex(
+ lowerer: Lowerer,
+ operand: IrExpr,
+ loc: SrcLoc,
+ node: ts.Expression,
+): IrExpr {
+ const split = splitUndefinedIndex(lowerer, operand, node, loc);
+ if (split === null) return lowerOptionalNumber(lowerer, operand, loc, node);
+ return {
+ kind: "seqExpr",
+ stmts: split.stmts,
+ result: {
+ kind: "ternary",
+ cond: split.isUndefined,
+ then: { kind: "libCall", fn: "array.undefinedKey", args: [], type: F64, loc },
+ else_: split.index,
+ type: F64,
+ loc,
+ },
+ type: F64,
+ loc,
+ };
+}
+
+/** A typed-array element index that can be undefined: `stmts` evaluate it
+ * once, `isUndefined` tests it, and `index` is the number a defined index
+ * converts to. */
+export interface SplitIndex {
+ stmts: IrStmt[];
+ isUndefined: IrExpr;
+ index: IrExpr;
+}
+
+/** Splits `raw`, the lowered index of `node`, when it is a number that can
+ * be undefined; null otherwise. */
+export function splitUndefinedIndex(
+ lowerer: Lowerer,
+ raw: IrExpr,
+ node: ts.Expression,
+ loc: SrcLoc,
+): SplitIndex | null {
+ if (raw.type.kind !== "union") return null;
+ const undefinedTag = lowerer.armTag(raw.type.unionId, UNDEFINED_T);
+ if (undefinedTag < 0) return null;
+ const local = lowerer.declareHiddenLocal("%elementIndex", raw.type);
+ const stable = varRef(local.id, raw.type, loc);
+ const index = lowerOptionalNumber(lowerer, stable, loc, node);
+ if (index.type.kind !== "f64") return null;
+ return {
+ stmts: [{ kind: "varDecl", localId: local.id, init: raw, loc }],
+ isUndefined: {
+ kind: "unionIsTag",
+ unionId: raw.type.unionId,
+ tag: undefinedTag,
+ negated: false,
+ value: stable,
+ type: BOOL,
+ loc,
+ },
+ index,
+ };
+}
+
+/** A typed array has no property named "undefined" to store into: the
+ * store throws the catchable SC1090 error when it runs. A call, so that it
+ * can sit among an expression's statements. */
+export function undefinedTypedArrayKeyFence(lowerer: Lowerer, node: ts.Node, loc: SrcLoc): IrStmt {
+ const line = ts.getLineAndCharacterOfPosition(ts.getSourceFileOfNode(node), loc.start).line + 1;
+ const message = `typed array properties named 'undefined' (an element index that is undefined) are not supported yet [SC1090 at ${loc.file}:${line}]`;
+ const key = `index.undefinedFence:${message}`;
+ let name = lowerer.arrHofHelpers.get(key);
+ if (name === undefined) {
+ name = keyedHelperName(lowerer, "%index.undefinedFence", key);
+ lowerer.arrHofHelpers.set(key, name);
+ lowerer.liftedFns.push({
+ name,
+ params: [],
+ locals: [],
+ returnType: DYN,
+ body: [{ kind: "runtimeFence", code: "SC1090", message, loc }],
+ loc,
+ });
+ }
+ return { kind: "exprStmt", expr: { kind: "call", callee: name, args: [], type: DYN, loc }, loc };
+}
diff --git a/packages/compiler/src/frontend/token-equivalence.test.ts b/packages/compiler/src/frontend/token-equivalence.test.ts
new file mode 100644
index 000000000..880a07221
--- /dev/null
+++ b/packages/compiler/src/frontend/token-equivalence.test.ts
@@ -0,0 +1,107 @@
+import { expect, test } from "vitest";
+import { tokenEquivalentSource } from "./token-equivalence.js";
+
+const equivalent = (previous: string, current: string, path = "/src/a.ts") =>
+ tokenEquivalentSource(path, previous, current);
+
+test("whitespace and plain comments are trivia", () => {
+ expect(equivalent("const a = 1;\n", "const a = 1; // one\n")).toBe(true);
+ expect(equivalent("f(a,b);\n", "f( a, /* first */ b );\n")).toBe(true);
+ expect(equivalent("const s = `x${a}y`;\n", "const s = `x${ a /* the value */ }y`;\n")).toBe(true);
+});
+
+test("tokens, literals and line breaks that change semicolon insertion are not", () => {
+ expect(equivalent("const a = 1;\n", "const a = 2;\n")).toBe(false);
+ expect(equivalent('const s = "a // b";\n', 'const s = "a // c";\n')).toBe(false);
+ expect(equivalent("const s = `a // ${b} /* c */`;\n", "const s = `a // ${b} /* d */`;\n")).toBe(
+ false,
+ );
+ // `return` followed by a line break returns undefined.
+ expect(equivalent("return a;\n", "return\na;\n")).toBe(false);
+ expect(equivalent("return a;\n", "return /*\n*/ a;\n")).toBe(false);
+});
+
+test("comment-like text inside regular expressions is code", () => {
+ expect(equivalent("const r = /[//]/; x = 1;\n", "const r = /[//]/; x = 2;\n")).toBe(false);
+ expect(equivalent("if (a) /\\/*/.test(b); c = 1;\n", "if (a) /\\/*/.test(b); c = 2;\n")).toBe(
+ false,
+ );
+ expect(equivalent("const q = a / b; // ratio\n", "const q = a / b; // the ratio\n")).toBe(true);
+ expect(equivalent("return /a/; // one\n", "return /a/; // two\n")).toBe(true);
+ // A `for await` header closes like a `for` header.
+ expect(
+ equivalent(
+ "for await (const x of xs) /[//]/.test(x); c = 1;\n",
+ "for await (const x of xs) /[//]/.test(x); c = 2;\n",
+ ),
+ ).toBe(false);
+});
+
+test("a / after a line break is never equivalent", () => {
+ const loop = (body: string) => `a: for (;;) {\n${body}\n}\n`;
+ // Automatic semicolon insertion decides whether these start a regular
+ // expression.
+ expect(
+ equivalent(loop("break\n/[//]/.test(x); c = 1;"), loop("break\n/[//]/.test(x); c = 2;")),
+ ).toBe(false);
+ expect(
+ equivalent(loop("break a\n/[//]/.test(x); c = 1;"), loop("break a\n/[//]/.test(x); c = 2;")),
+ ).toBe(false);
+ expect(
+ equivalent("type A = B\n/[//]/.test(x); c = 1;\n", "type A = B\n/[//]/.test(x); c = 2;\n"),
+ ).toBe(false);
+ // A comment spanning lines separates them too; JavaScript keeps its comments as tokens.
+ expect(
+ equivalent(
+ "x\n/* a\n */ /[//]/.test(y); c = 1;\n",
+ "x\n/* a\n */ /[//]/.test(y); c = 2;\n",
+ "/src/a.js",
+ ),
+ ).toBe(false);
+ // A / on the line of the token before it still divides.
+ expect(equivalent(loop("continue\nx / 2; // one"), loop("continue\nx / 2; // two"))).toBe(true);
+});
+
+test("code units outside ASCII are equivalent only inside strings, templates and comments", () => {
+ expect(equivalent('const s = "é"; // one\n', 'const s = "é"; // two\n')).toBe(true);
+ expect(equivalent("const s = `é${a}`; // one\n", "const s = `é${a}`; // two\n")).toBe(true);
+ expect(equivalent("const a = 1; // é\n", "const a = 1; // è\n")).toBe(true);
+ expect(equivalent("const café = 1; // one\n", "const café = 1; // two\n")).toBe(false);
+ expect(
+ equivalent(
+ "ä: for (;;) { break ä\n/[//]/.test(x); c = 1; }\n",
+ "ä: for (;;) { break ä\n/[//]/.test(x); c = 2; }\n",
+ ),
+ ).toBe(false);
+ expect(equivalent("const r = /é/; // one\n", "const r = /é/; // two\n")).toBe(false);
+});
+
+test("whitespace outside ASCII is never equivalent", () => {
+ expect(
+ equivalent("return\u3000/[//]/.test(x); c = 1;\n", "return\u3000/[//]/.test(x); c = 2;\n"),
+ ).toBe(false);
+ expect(equivalent("const a = 1;\u00a0// one\n", "const a = 1;\u00a0// two\n")).toBe(false);
+});
+
+test("U+2028 and U+2029 end a line comment, so their files are never equivalent", () => {
+ expect(equivalent("// a\u2028c = 1;\n", "// a\u2028c = 2;\n")).toBe(false);
+ expect(equivalent("/* a\u2029*/ c = 1;\n", "/* b\u2029*/ c = 1;\n")).toBe(false);
+});
+
+test("comments that can affect compilation are kept", () => {
+ expect(equivalent("/** @type {number} */ let a;\n", "/** @type {string} */ let a;\n")).toBe(
+ false,
+ );
+ expect(equivalent("// @ts-ignore\nf();\n", "f();\n")).toBe(false);
+ expect(equivalent('/// \n', "\n")).toBe(false);
+ // Every comment is kept in JavaScript, whose types come from JSDoc.
+ expect(equivalent("let a; // one\n", "let a; // two\n", "/src/a.js")).toBe(false);
+});
+
+test("text the scanner cannot classify is never equivalent", () => {
+ expect(equivalent("x = {} / 2; // a\n", "x = {} / 2; // b\n")).toBe(false);
+ expect(equivalent("++/a/.lastIndex; // a\n", "++/a/.lastIndex; // b\n")).toBe(false);
+ expect(equivalent("const \\u0061 = 1; // a\n", "const \\u0061 = 1; // b\n")).toBe(false);
+ expect(equivalent("const a = ; // a\n", "const a = ; // b\n", "/src/a.tsx")).toBe(false);
+ expect(equivalent('const s = "open\n', 'const s = "open // \n')).toBe(false);
+});
diff --git a/packages/compiler/src/frontend/token-equivalence.ts b/packages/compiler/src/frontend/token-equivalence.ts
new file mode 100644
index 000000000..7a0f6cc79
--- /dev/null
+++ b/packages/compiler/src/frontend/token-equivalence.ts
@@ -0,0 +1,391 @@
+/* Token equivalence of two versions of a source file, for frontend cache
+ * reuse after edits that touch only comments and whitespace.
+ *
+ * Two versions are equivalent when they have the same tokens, in the same
+ * order, with line breaks between the same pairs of tokens (automatic
+ * semicolon insertion depends on them), and differ only in whitespace and
+ * in comments that cannot affect compilation. The scanner answers "not
+ * equivalent" whenever it cannot classify text with certainty, so a wrong
+ * answer can only cost a cache hit:
+ *
+ * - Every comment in a file that is not TypeScript (`.ts`, `.mts`, `.cts`)
+ * is kept as a token: JavaScript types come from JSDoc. TypeScript
+ * comments are kept when they start with `///` (references and
+ * directives) or contain `@` or `#` (checker directives, JSDoc tags, and
+ * annotations lowering reads).
+ * - A `/` starts a regular expression or divides according to the token
+ * before it. After `)` it divides unless the parenthesis closes an `if`,
+ * `while`, `for`, `for await`, or `with` header. After `}`, `++` or
+ * `--` the scanner cannot tell which, so it gives up, as it does for
+ * escaped identifiers, which can spell keywords.
+ * - A `/` with a line break (or a comment that spans lines) since the
+ * previous token gives up: whether automatic semicolon insertion ends the
+ * statement before it decides how it reads.
+ * - Template literals track their `${` substitutions on a brace stack.
+ * - Any code unit outside ASCII gives up, except inside strings, templates
+ * and comments. So do files that contain whitespace or a line terminator
+ * outside ASCII anywhere (U+2028 and U+2029 end a line comment), JSX
+ * files, and unterminated strings, comments, templates and regular
+ * expressions.
+ *
+ * The scanner models only what these rules name; anything else makes the
+ * versions not equivalent, which rebuilds. */
+
+interface Token {
+ text: string;
+ comment: boolean;
+ lineBreakBefore: boolean;
+}
+
+/** Keywords after which `/` starts a regular expression. */
+const EXPRESSION_KEYWORDS = new Set([
+ "return",
+ "typeof",
+ "instanceof",
+ "in",
+ "of",
+ "new",
+ "delete",
+ "void",
+ "throw",
+ "case",
+ "do",
+ "else",
+ "yield",
+ "await",
+ "default",
+ "extends",
+]);
+
+/** Whitespace and line terminators outside ASCII. */
+const NON_ASCII_SPACE = /[\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]/;
+
+/** Keywords whose parenthesized header is followed by a statement. */
+const HEADER_KEYWORDS = new Set(["if", "while", "for", "with"]);
+
+const PUNCTUATORS = [
+ ">>>=",
+ "...",
+ "===",
+ "!==",
+ "**=",
+ "<<=",
+ ">>=",
+ ">>>",
+ "&&=",
+ "||=",
+ "??=",
+ "=>",
+ "==",
+ "!=",
+ "<=",
+ ">=",
+ "&&",
+ "||",
+ "??",
+ "?.",
+ "++",
+ "--",
+ "+=",
+ "-=",
+ "*=",
+ "%=",
+ "&=",
+ "|=",
+ "^=",
+ "**",
+ "<<",
+ ">>",
+];
+
+function isTypeScript(path: string): boolean {
+ return path.endsWith(".ts") || path.endsWith(".mts") || path.endsWith(".cts");
+}
+
+function isIdentifierCode(code: number): boolean {
+ return (
+ (code >= 97 && code <= 122) ||
+ (code >= 65 && code <= 90) ||
+ (code >= 48 && code <= 57) ||
+ code === 36 ||
+ code === 95 ||
+ code === 92
+ );
+}
+
+function isLineBreak(code: number): boolean {
+ return code === 10 || code === 13;
+}
+
+function isSpace(code: number): boolean {
+ return code === 32 || code === 9 || code === 11 || code === 12 || isLineBreak(code);
+}
+
+/** The tokens of `source`, or null when the scanner cannot classify it. */
+function scan(path: string, source: string): Token[] | null {
+ if (path.endsWith(".tsx") || path.endsWith(".jsx")) return null;
+ if (NON_ASCII_SPACE.test(source)) return null;
+ const typeScript = isTypeScript(path);
+ const tokens: Token[] = [];
+ // Brace kinds: true for a template substitution, false otherwise.
+ const braces: boolean[] = [];
+ // Paren kinds: true for a statement header.
+ const parens: boolean[] = [];
+ let lineBreakBefore = false;
+ // A line break, or a comment spanning lines, since the last token other
+ // than a comment.
+ let breakSinceCode = false;
+ // The previous token and the one before it, and whether a `)` closed a
+ // statement header.
+ const last: { token: string | null; before: string | null } = { token: null, before: null };
+ let closedHeader = false;
+ const push = (text: string, comment = false): void => {
+ tokens.push({ text, comment, lineBreakBefore });
+ lineBreakBefore = false;
+ if (!comment) {
+ breakSinceCode = false;
+ last.before = last.token;
+ last.token = text;
+ }
+ };
+ let index = 0;
+ const length = source.length;
+ // A shebang is one token.
+ if (source.startsWith("#!")) {
+ while (index < length && !isLineBreak(source.charCodeAt(index))) index++;
+ push(source.slice(0, index));
+ }
+ /** Scans template text from `index` (after a backtick or a closing
+ * substitution brace) through the closing backtick or `${`. */
+ const template = (start: number): boolean => {
+ let position = index;
+ while (position < length) {
+ const code = source.charCodeAt(position);
+ if (code === 92) {
+ position += 2;
+ continue;
+ }
+ if (code === 96) {
+ position++;
+ index = position;
+ push(source.slice(start, position));
+ closedHeader = false;
+ return true;
+ }
+ if (code === 36 && source.charCodeAt(position + 1) === 123) {
+ position += 2;
+ index = position;
+ push(source.slice(start, position));
+ braces.push(true);
+ last.before = last.token;
+ last.token = "${";
+ return true;
+ }
+ position++;
+ }
+ return false;
+ };
+ while (index < length) {
+ const code = source.charCodeAt(index);
+ if (isSpace(code)) {
+ if (isLineBreak(code)) lineBreakBefore = breakSinceCode = true;
+ index++;
+ continue;
+ }
+ // Outside strings, templates and comments, the scanner models ASCII only.
+ if (code > 127) return null;
+ const next = source.charCodeAt(index + 1);
+ // Comments.
+ if (code === 47 && (next === 47 || next === 42)) {
+ const start = index;
+ if (next === 47) {
+ while (index < length && !isLineBreak(source.charCodeAt(index))) index++;
+ } else {
+ const end = source.indexOf("*/", index + 2);
+ if (end < 0) return null;
+ index = end + 2;
+ }
+ const text = source.slice(start, index);
+ const semantic =
+ !typeScript || text.startsWith("///") || text.includes("@") || text.includes("#");
+ const multiline = /[\r\n]/.test(text);
+ if (multiline) breakSinceCode = true;
+ if (semantic) push(text, true);
+ else if (multiline) lineBreakBefore = true;
+ continue;
+ }
+ // Strings.
+ if (code === 34 || code === 39) {
+ const start = index;
+ index++;
+ while (index < length) {
+ const inner = source.charCodeAt(index);
+ if (inner === 92) {
+ index += 2;
+ continue;
+ }
+ if (isLineBreak(inner)) return null;
+ index++;
+ if (inner === code) break;
+ if (index >= length) return null;
+ }
+ if (source.charCodeAt(index - 1) !== code || index - start < 2) return null;
+ push(source.slice(start, index));
+ closedHeader = false;
+ continue;
+ }
+ // Templates.
+ if (code === 96) {
+ const start = index;
+ index++;
+ if (!template(start)) return null;
+ continue;
+ }
+ // Identifiers, keywords and numbers.
+ if (isIdentifierCode(code) || (code === 46 && next >= 48 && next <= 57)) {
+ const start = index;
+ index++;
+ while (index < length) {
+ const inner = source.charCodeAt(index);
+ if (isIdentifierCode(inner) || inner === 46) {
+ // A dot continues only a number (`1.5`), not member access.
+ if (inner === 46) {
+ const first = source.charCodeAt(start);
+ if (!((first >= 48 && first <= 57) || first === 46)) break;
+ }
+ index++;
+ continue;
+ }
+ // Exponent signs (`1e-5`).
+ if (
+ (inner === 43 || inner === 45) &&
+ (source.charCodeAt(index - 1) === 101 || source.charCodeAt(index - 1) === 69)
+ ) {
+ const first = source.charCodeAt(start);
+ if ((first >= 48 && first <= 57) || first === 46) {
+ index++;
+ continue;
+ }
+ }
+ break;
+ }
+ const word = source.slice(start, index);
+ // An escaped identifier can spell a keyword (`\u0072eturn`).
+ if (word.includes("\\")) return null;
+ push(word);
+ closedHeader = false;
+ continue;
+ }
+ // Division or a regular expression.
+ if (code === 47) {
+ if (breakSinceCode) return null;
+ const before = last.token;
+ let regex: boolean;
+ if (before === null) regex = true;
+ else if (before === "}") return null;
+ else if (before === ")") regex = closedHeader;
+ // `++/re/.lastIndex` is valid, `x++ / 2` too.
+ else if (before === "++" || before === "--") return null;
+ else if (before === "]") regex = false;
+ else if (/^[A-Za-z0-9_$\\]/.test(before)) regex = EXPRESSION_KEYWORDS.has(before);
+ else if (before.startsWith("'") || before.startsWith('"') || before.endsWith("`"))
+ regex = false;
+ else if (before.startsWith("/") && before.length > 1 && before !== "/=") regex = false;
+ else regex = true;
+ if (regex) {
+ const start = index;
+ index++;
+ let inClass = false;
+ while (index < length) {
+ const inner = source.charCodeAt(index);
+ if (isLineBreak(inner) || inner > 127) return null;
+ if (inner === 92) {
+ index += 2;
+ continue;
+ }
+ if (inner === 91) inClass = true;
+ else if (inner === 93) inClass = false;
+ else if (inner === 47 && !inClass) break;
+ index++;
+ }
+ if (index >= length) return null;
+ index++;
+ while (index < length && isIdentifierCode(source.charCodeAt(index))) index++;
+ push(source.slice(start, index));
+ closedHeader = false;
+ continue;
+ }
+ const text = next === 61 ? "/=" : "/";
+ index += text.length;
+ push(text);
+ closedHeader = false;
+ continue;
+ }
+ // Brackets.
+ if (code === 40) {
+ parens.push(
+ last.token !== null &&
+ (HEADER_KEYWORDS.has(last.token) || (last.token === "await" && last.before === "for")),
+ );
+ index++;
+ push("(");
+ closedHeader = false;
+ continue;
+ }
+ if (code === 41) {
+ const header = parens.pop() ?? false;
+ index++;
+ push(")");
+ closedHeader = header;
+ continue;
+ }
+ if (code === 123) {
+ braces.push(false);
+ index++;
+ push("{");
+ closedHeader = false;
+ continue;
+ }
+ if (code === 125) {
+ if (braces.pop() === true) {
+ const start = index;
+ index++;
+ if (!template(start)) return null;
+ continue;
+ }
+ index++;
+ push("}");
+ closedHeader = false;
+ continue;
+ }
+ // Other punctuators.
+ let text = source[index]!;
+ for (const punctuator of PUNCTUATORS)
+ if (source.startsWith(punctuator, index)) {
+ text = punctuator;
+ break;
+ }
+ index += text.length;
+ push(text);
+ closedHeader = false;
+ }
+ if (braces.length !== 0 || parens.length !== 0) return null;
+ return tokens;
+}
+
+/** Whether `current` differs from `previous` only in whitespace and in
+ * comments that cannot affect compilation. */
+export function tokenEquivalentSource(path: string, previous: string, current: string): boolean {
+ if (previous === current) return true;
+ const left = scan(path, previous);
+ if (left === null) return false;
+ const right = scan(path, current);
+ if (right === null || left.length !== right.length) return false;
+ for (let index = 0; index < left.length; index++) {
+ const a = left[index]!;
+ const b = right[index]!;
+ if (a.text !== b.text || a.comment !== b.comment) return false;
+ if (index > 0 && a.lineBreakBefore !== b.lineBreakBefore) return false;
+ }
+ return true;
+}
diff --git a/packages/compiler/src/frontend/ts7/ast-node.ts b/packages/compiler/src/frontend/ts7/ast-node.ts
index 92c71aeba..4c7726c00 100644
--- a/packages/compiler/src/frontend/ts7/ast-node.ts
+++ b/packages/compiler/src/frontend/ts7/ast-node.ts
@@ -17,6 +17,16 @@ import {
import type { SourceFile } from "./ast-types.js";
import { SyntaxKind, NodeFlags } from "./enums.js";
+/** Receives the file name of a source file whose raw text or line
+ * positions a reader takes: the text of a node other than a token
+ * (getText), a node's text with its trivia (getFullText), or line starts. */
+let sourceTextObserver: ((fileName: string) => void) | undefined;
+
+/** Installs the observer of raw source text reads (sourceTextObserver). */
+export function observeSourceText(observer: ((fileName: string) => void) | undefined): void {
+ sourceTextObserver = observer;
+}
+
/** A list of child nodes with the list's own position, the shape of tsc-ts's
* NodeList. The file keeps the nodes array, not this object: each read of a
* list field returns a new AstNodeList over the same array, so `nodes` keeps
@@ -210,6 +220,12 @@ export class AstFile {
return result;
}
+ /** Reports a read of this file's raw text or line positions. */
+ noteSourceText(): void {
+ if (sourceTextObserver !== undefined && this.root.kind === SyntaxKind.SourceFile)
+ sourceTextObserver(this.root.fileName);
+ }
+
/** The file name of the source file node at `index`; the root's is
* read once. */
sourceFileName(index: number): string {
@@ -220,6 +236,7 @@ export class AstFile {
}
lineStarts(): number[] {
+ this.noteSourceText();
if (this.lines === undefined) this.lines = astLineStarts(this.root.text ?? "");
return this.lines;
}
@@ -495,10 +512,14 @@ export class AstNode {
return this.getStart(sourceFile) - this.pos;
}
getFullText(sourceFile?: AstNode): string {
- return ((sourceFile ?? this.file.root).text ?? "").substring(this.pos, this.end);
+ const source = sourceFile ?? this.file.root;
+ source.file.noteSourceText();
+ return (source.text ?? "").substring(this.pos, this.end);
}
getText(sourceFile?: AstNode): string {
const source = sourceFile ?? this.file.root;
+ // A token's text is the same in every token-equivalent version.
+ if (this.kind > SyntaxKind.LastToken) source.file.noteSourceText();
return (source.text ?? "").substring(this.getStart(source), this.end);
}
diff --git a/packages/compiler/src/frontend/ts7/checker-backend.test.ts b/packages/compiler/src/frontend/ts7/checker-backend.test.ts
index e37357afa..cd37bafff 100644
--- a/packages/compiler/src/frontend/ts7/checker-backend.test.ts
+++ b/packages/compiler/src/frontend/ts7/checker-backend.test.ts
@@ -24,15 +24,11 @@ function writePackage(directory: string, name: string, executable?: string): voi
writeFileSync(join(directory, "lib", executable), "");
}
-test("tsc-ts is the default checker except on Windows, and SCRIPTC_CHECKER selects one", () => {
- expect(checkerBackend({}, "linux")).toBe("tsc-ts");
- expect(checkerBackend({ SCRIPTC_CHECKER: "" }, "linux")).toBe("tsc-ts");
- expect(checkerBackend({}, "darwin")).toBe("tsc-ts");
- expect(checkerBackend({}, "win32")).toBe("tsgo");
- expect(checkerBackend({ SCRIPTC_CHECKER: "" }, "win32")).toBe("tsgo");
- expect(checkerBackend({ SCRIPTC_CHECKER: "tsc-ts" }, "linux")).toBe("tsc-ts");
- expect(checkerBackend({ SCRIPTC_CHECKER: "tsc-ts" }, "win32")).toBe("tsc-ts");
- expect(checkerBackend({ SCRIPTC_CHECKER: "tsgo" }, "linux")).toBe("tsgo");
+test("tsc-ts is the default checker, and SCRIPTC_CHECKER selects one", () => {
+ expect(checkerBackend({})).toBe("tsc-ts");
+ expect(checkerBackend({ SCRIPTC_CHECKER: "" })).toBe("tsc-ts");
+ expect(checkerBackend({ SCRIPTC_CHECKER: "tsc-ts" })).toBe("tsc-ts");
+ expect(checkerBackend({ SCRIPTC_CHECKER: "tsgo" })).toBe("tsgo");
expect(() => checkerBackend({ SCRIPTC_CHECKER: "tsc" })).toThrow(
'SCRIPTC_CHECKER must be tsgo or tsc-ts, not "tsc"',
);
diff --git a/packages/compiler/src/frontend/ts7/checker-backend.ts b/packages/compiler/src/frontend/ts7/checker-backend.ts
index 14418cc94..0b32f24f6 100644
--- a/packages/compiler/src/frontend/ts7/checker-backend.ts
+++ b/packages/compiler/src/frontend/ts7/checker-backend.ts
@@ -4,17 +4,13 @@ import { fileURLToPath } from "node:url";
/** The TypeScript 7 API servers a compile can check with: tsc-ts, the default,
* and tsgo from the `typescript` package, selected with `SCRIPTC_CHECKER=tsgo`.
- * Both serve the same API and give the same results. On Windows the default is
- * tsgo: the compiler connects to the server over a named pipe there, and tsc-ts's
- * API server has no named-pipe transport yet. */
+ * Both serve the same API and give the same results. On Windows the Node host
+ * connects to either over a named pipe the server creates (rpc-process.ts). */
export type CheckerBackend = "tsgo" | "tsc-ts";
-export function checkerBackend(
- env: NodeJS.ProcessEnv = process.env,
- platform: string = process.platform,
-): CheckerBackend {
+export function checkerBackend(env: NodeJS.ProcessEnv = process.env): CheckerBackend {
const value = env["SCRIPTC_CHECKER"];
- if (value === undefined || value === "") return platform === "win32" ? "tsgo" : "tsc-ts";
+ if (value === undefined || value === "") return "tsc-ts";
if (value === "tsc-ts") return "tsc-ts";
if (value === "tsgo") return "tsgo";
throw new Error(`SCRIPTC_CHECKER must be tsgo or tsc-ts, not ${JSON.stringify(value)}`);
diff --git a/packages/compiler/src/frontend/ts7/checker.ts b/packages/compiler/src/frontend/ts7/checker.ts
index 9550d1dc2..eb2b9e20b 100644
--- a/packages/compiler/src/frontend/ts7/checker.ts
+++ b/packages/compiler/src/frontend/ts7/checker.ts
@@ -631,6 +631,14 @@ export class CheckerFacade {
return aliased;
}
+ /** A module symbol's exports, with the members of its `export *`
+ * declarations resolved (a symbol's own export table lists only the
+ * module's own declarations). */
+ getExportsOfModule(symbol: Ts7Symbol): readonly Ts7Symbol[] {
+ this.ensureActive();
+ return this.raw.getExportsOfModule(symbol);
+ }
+
getDeclaredTypeOfSymbol(symbol: Ts7Symbol): Type {
this.ensureActive();
let type = this.cache.declaredTypeOfSymbol.get(symbol);
diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts
index 26ed4defb..4f7b93cfd 100644
--- a/packages/compiler/src/index.ts
+++ b/packages/compiler/src/index.ts
@@ -126,6 +126,7 @@ import { moduleRuntimeFeatures } from "./ir/runtime-features.js";
import { serializeModuleParts } from "./ir/serialize.js";
import { readTextParts, writeTextParts } from "./text-parts.js";
import { validateModule } from "./ir/validate.js";
+import { compilerOwnedFileRoots } from "./frontend/dts-paths.js";
import { loadProgram } from "./frontend/program-node.js";
import { runFrontend, type FrontendFactory } from "./frontend/pipeline.js";
import {
@@ -786,7 +787,7 @@ async function prepareExecutableInput(
if (outputKind === "ir") {
await mkdir(dirname(opts.outPath), { recursive: true });
- await writeTextParts(opts.outPath, serializeModuleParts(mod));
+ await writeTextParts(opts.outPath, serializeModuleParts(mod, compilerOwnedFileRoots()));
return { ok: true, artifact: { kind: "ir", path: opts.outPath } };
}
@@ -886,7 +887,7 @@ async function prepareExecutableInput(
if (opts.emitIr) {
irPath = defaultSourcePaths.ir;
irSourcePath = privateSiblingPath(irPath, "ir");
- await writeTextParts(irSourcePath, serializeModuleParts(mod));
+ await writeTextParts(irSourcePath, serializeModuleParts(mod, compilerOwnedFileRoots()));
}
const nativeFeatures = executableNativeFeatures(mod, backend, opts.optimization ?? "release");
@@ -1987,7 +1988,7 @@ async function emitSemanticLibraryHit(
let irPath: string | undefined;
if (opts.emitIr) {
irPath = join(opts.outDir, `${stem}.lib.ir.json`);
- await writeTextParts(irPath, serializeModuleParts(mod));
+ await writeTextParts(irPath, serializeModuleParts(mod, compilerOwnedFileRoots()));
}
await compileLibraryNative(profile, llvmPath, archivePath, opts.sanitize ?? false, hit.native);
timing("native-archive");
@@ -2273,7 +2274,7 @@ async function compileLibraryTracked(
let irPath: string | undefined;
if (opts.emitIr) {
irPath = join(opts.outDir, `${stem}.lib.ir.json`);
- await writeTextParts(irPath, serializeModuleParts(mod));
+ await writeTextParts(irPath, serializeModuleParts(mod, compilerOwnedFileRoots()));
}
const nativeFeatures = libraryNativeFeatures(mod, profile.emission);
diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts
index 2d5e87ffb..9be819dd3 100644
--- a/packages/compiler/src/ir/builtin-effects.ts
+++ b/packages/compiler/src/ir/builtin-effects.ts
@@ -672,6 +672,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([
"dyn.propertyIsEnumerableComputed",
// the destructuring pack throws V8's TypeError on non-iterable dyn kinds
"dyn.iterPack",
+ // ToNumber of each element can run valueOf
+ "dyn.mathFold",
"dyn.destructureRest",
"dyn.arrayFromIterator",
"dyn.iterator",
diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts
index d4b2cf2d2..e60a6600b 100644
--- a/packages/compiler/src/ir/builtin-signatures.ts
+++ b/packages/compiler/src/ir/builtin-signatures.ts
@@ -219,6 +219,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
* not iterable (cannot read property Symbol(Symbol.iterator))"). In the
* may-throw seed set. */
"dyn.iterPack": { argTypes: [DYN, STRING], result: DYN },
+ "dyn.mathFold": { argTypes: [DYN, BOOL, STRING], result: F64 },
"dyn.destructureRest": { argTypes: [DYN, STRING], result: DYN },
"dyn.arrayFromIterator": { argTypes: [DYN], result: DYN },
"dyn.iterator": { argTypes: [DYN, STRING], result: DYN },
@@ -3821,6 +3822,13 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
// Whether the array (args[0]) still has the key (args[1]) from its
// namedKeys list. Borrowed; never throws.
"array.hasNamedKey": { argTypes: [null, STRING], result: BOOL },
+ // Whether an array (args[0], any element type) has properties besides its
+ // elements: noncanonical numeric keys, match metadata or named
+ // properties. Borrowed; never throws.
+ "array.hasProperties": { argTypes: [null], result: BOOL },
+ // The key of an element access whose index is undefined (the property
+ // named "undefined"; frontend/lowering/undefined-index.ts).
+ "array.undefinedKey": { argTypes: [], result: F64 },
// node:events EventEmitter: receivers are emitter-hierarchy objects and
// the chaining forms (on/off/removeAll/setMax) return the receiver's
// own class — program-dependent object types, checked in the libCall
diff --git a/packages/compiler/src/ir/char-unit-locals.ts b/packages/compiler/src/ir/char-unit-locals.ts
index 4008f4e1e..beeeffaf3 100644
--- a/packages/compiler/src/ir/char-unit-locals.ts
+++ b/packages/compiler/src/ir/char-unit-locals.ts
@@ -161,6 +161,10 @@ function unitLocal(local: IrLocal): IrLocal {
* declaration from a one-argument charAt, a one-unit test, or a switch
* over one-unit cases. */
function understoodMention(node: Record): string | null {
+ // count() visits every object in the body (types, locations, cases), so
+ // only the node kinds handled here are read as IR nodes
+ const kind = node.kind;
+ if (kind !== "varDecl" && kind !== "strEq" && kind !== "strCmp" && kind !== "switch") return null;
const n = node as unknown as IrExpr | IrStmt;
if (n.kind === "varDecl") return n.init && isCharAt(n.init) ? n.localId : null;
if (n.kind === "strEq" || n.kind === "strCmp") return unitUse(n)?.id ?? null;
diff --git a/packages/compiler/src/ir/function-references.test.ts b/packages/compiler/src/ir/function-references.test.ts
index 7bef40aa6..45d046db9 100644
--- a/packages/compiler/src/ir/function-references.test.ts
+++ b/packages/compiler/src/ir/function-references.test.ts
@@ -208,6 +208,7 @@ const MODULE_FIELDS: Record>, Role> = {
"contracts.int32Returns[]": "fact",
"contracts.int32Locals[]": "other",
"contracts.int32Elements[]": "other",
+ "int32ArrayCandidates[]": "other",
"contracts.noThrow[]": "fact",
"contracts.acyclic[]": "other",
"contracts.immortalGlobals[]": "other",
diff --git a/packages/compiler/src/ir/int32-arrays.ts b/packages/compiler/src/ir/int32-arrays.ts
new file mode 100644
index 000000000..02de7268f
--- /dev/null
+++ b/packages/compiler/src/ir/int32-arrays.ts
@@ -0,0 +1,515 @@
+import { F64, type IrExpr, type IrFunction, type IrModule, type IrStmt } from "./ir.js";
+
+type SwitchCase = Extract["cases"][number];
+import { reparseExpr } from "./serialize.js";
+import {
+ everyExprChild,
+ everyStmtChild,
+ everyStmtList,
+ transformExpr,
+ transformStmt,
+ transformStmtList,
+ type IrTransform,
+} from "./traverse.js";
+
+/** Local number arrays the backend may store as int32 slots.
+ *
+ * A candidate is a local `number[]` of a synchronous function that no
+ * other binding can hold and only compiled element operations reach:
+ * every write of the local stores a fresh array (an array literal, or
+ * `new Array(n).fill(v)`), and every read of it is the receiver of an
+ * element read, write, state test, `length`, `push` or `pop` (a discarded
+ * `pop` included), or the array of an element-reader helper call. The
+ * array never reaches a runtime function that reads its slots, a library
+ * call, a closure, a return or untyped code.
+ *
+ * Candidates are only proposed here. The int32 slot analysis
+ * (int32-slots.ts) keeps the ones whose every stored element is a proven
+ * int32 (never -0), assuming the candidates' own elements are int32s, at
+ * a fixpoint, and the LLVM backend stores those as SCR_ELEM_I32. It keeps
+ * the ones whose stores are exact integers but not all proven int32s as
+ * int32 arrays with checked stores: SCR_ELEM_I32 until a store that is not
+ * an int32 widens them to doubles in place. Their element-reader helper
+ * calls are inlined first (rewriteCandidates), so no shared helper ever
+ * receives an int32 array. */
+
+/** Array methods a candidate may be the receiver of: the runtime paths
+ * behind them read and write int32 slots through the double-valued slot
+ * accessors. */
+export const INT32_ARRAY_METHODS: ReadonlySet = new Set([
+ "length",
+ "push",
+ "pop",
+ "getNumber",
+]);
+
+/** An element-reader helper (`%arr.idxOr.*`): `(a, i) => a's state at i is
+ * a value ? the element : undefined`, as one returned ternary. */
+export function isElementReader(fn: IrFunction): boolean {
+ if (fn.params.length !== 2 || fn.body.length !== 1) return false;
+ const [array, index] = fn.params;
+ const ret = fn.body[0]!;
+ if (array!.type.kind !== "array" || array!.type.elem.kind !== "f64") return false;
+ if (index!.type.kind !== "f64" || ret.kind !== "return" || ret.value?.kind !== "ternary")
+ return false;
+ const isParam = (e: IrExpr, id: string): boolean => e.kind === "varRef" && e.localId === id;
+ const { cond, then, else_ } = ret.value;
+ return (
+ cond.kind === "bin" &&
+ cond.op === "===" &&
+ cond.left.kind === "arrayState" &&
+ isParam(cond.left.arr, array!.localId) &&
+ isParam(cond.left.index, index!.localId) &&
+ cond.right.kind === "numLit" &&
+ cond.right.value === 1 &&
+ then.kind === "unionWrap" &&
+ then.value.kind === "arrayGet" &&
+ isParam(then.value.arr, array!.localId) &&
+ isParam(then.value.index, index!.localId) &&
+ else_.kind === "unionWrap" &&
+ else_.value.kind === "unitLit"
+ );
+}
+
+/** `new Array(n).fill(v)` as lowering spells it: `fill` over a sequence
+ * that declares a temporary empty array, sets its length and yields it. */
+function fillAllocation(e: IrExpr): (IrExpr & { kind: "arrIntrinsic" }) | null {
+ if (e.kind !== "arrIntrinsic" || e.method !== "fill" || e.receiver.kind !== "seqExpr")
+ return null;
+ const seq = e.receiver;
+ const [decl, length] = seq.stmts;
+ if (
+ seq.stmts.length !== 2 ||
+ decl?.kind !== "varDecl" ||
+ decl.init?.kind !== "arrayLit" ||
+ decl.init.elems.length !== 0 ||
+ length?.kind !== "arraySetLength" ||
+ length.arr.kind !== "varRef" ||
+ length.arr.localId !== decl.localId ||
+ seq.result.kind !== "varRef" ||
+ seq.result.localId !== decl.localId
+ )
+ return null;
+ return e;
+}
+
+/** Fill-form arrays shorter than this hold doubles even when they are int32
+ * arrays with checked stores: their doubles fit a typical 32 KiB L1 data
+ * cache, so 4-byte slots save no memory traffic. */
+export const CHECKED_INT32_MIN_LENGTH = 4096;
+
+/** The length `n` of `new Array(n).fill(v)`, or null for any other
+ * allocation. */
+export function fillAllocationLength(e: IrExpr): IrExpr | null {
+ const fill = fillAllocation(e);
+ if (!fill || fill.receiver.kind !== "seqExpr") return null;
+ const set = fill.receiver.stmts[1]!;
+ return set.kind === "arraySetLength" ? set.length : null;
+}
+
+/** A number expression whose evaluation has no effect: locals, literals and
+ * arithmetic. */
+function isPureLength(e: IrExpr): boolean {
+ if (e.type.kind !== "f64") return false;
+ switch (e.kind) {
+ case "numLit":
+ case "varRef":
+ return true;
+ case "bin":
+ return e.left.type.kind === "f64" && isPureLength(e.left) && isPureLength(e.right);
+ case "unary":
+ return isPureLength(e.operand);
+ default:
+ return false;
+ }
+}
+
+/** Whether the candidate `id` of `fn` may hold int32 slots with checked
+ * stores: every array it is assigned is the fill form, with a length that
+ * may reach CHECKED_INT32_MIN_LENGTH and evaluates without effects (the
+ * backend evaluates it once more to pick the new array's kind). */
+export function checkableInt32Allocations(fn: IrFunction, id: string): boolean {
+ let ok = true;
+ everyStmtList(fn.body, {
+ stmt: (s) => {
+ if ((s.kind === "varDecl" || s.kind === "assign") && s.localId === id) {
+ const value = s.kind === "varDecl" ? s.init : s.value;
+ const length = value ? fillAllocationLength(value) : null;
+ if (
+ length === null ||
+ !isPureLength(length) ||
+ (length.kind === "numLit" && length.value < CHECKED_INT32_MIN_LENGTH)
+ )
+ ok = false;
+ }
+ return ok;
+ },
+ expr: () => ok,
+ });
+ return ok;
+}
+
+/** A fresh number array: a literal without spreads, or the fill form. */
+export function int32ArrayAllocation(e: IrExpr): boolean {
+ return (e.kind === "arrayLit" && (e.spreads?.length ?? 0) === 0) || fillAllocation(e) !== null;
+}
+
+/** The array literal a candidate's allocation builds (the literal itself,
+ * or the fill form's temporary's), and the fill form's temporary. */
+export function allocatedLiteral(
+ e: IrExpr,
+): { literal: IrExpr & { kind: "arrayLit" }; temporary: string | null } | null {
+ if (e.kind === "arrayLit") return { literal: e, temporary: null };
+ const fill = fillAllocation(e);
+ if (!fill || fill.receiver.kind !== "seqExpr") return null;
+ const decl = fill.receiver.stmts[0]!;
+ return decl.kind === "varDecl" && decl.init?.kind === "arrayLit"
+ ? { literal: decl.init, temporary: decl.localId }
+ : null;
+}
+
+/** `xs.pop()` whose value is unused, as lowering spells it: a sequence
+ * that binds the array to a hidden local, then only reads that local's
+ * length and truncates it with pop stores. Returns the hidden local and the
+ * array read it binds. */
+function discardedPop(e: IrExpr): { local: string; array: IrExpr & { kind: "varRef" } } | null {
+ if (e.kind !== "seqExpr") return null;
+ const [bind, ...rest] = e.stmts;
+ if (bind?.kind !== "varDecl" || bind.init?.kind !== "varRef") return null;
+ const local = bind.localId;
+ const isLocal = (x: IrExpr): boolean => x.kind === "varRef" && x.localId === local;
+ const expr = (x: IrExpr): boolean => {
+ if (x.kind === "arrIntrinsic" && x.method === "length" && isLocal(x.receiver)) return true;
+ return !isLocal(x) && everyExprChild(x, expr, stmt);
+ };
+ const stmt = (x: IrStmt): boolean => {
+ if (x.kind === "arraySetLength" && x.pop === true && isLocal(x.arr)) return expr(x.length);
+ return (
+ !((x.kind === "varDecl" || x.kind === "assign") && x.localId === local) &&
+ everyStmtChild(x, expr, stmt)
+ );
+ };
+ return rest.every(stmt) && expr(e.result) ? { local, array: bind.init } : null;
+}
+
+/** Whether evaluating `e` may assign the local `id`. */
+function writesLocal(e: IrExpr, id: string): boolean {
+ const expr = (x: IrExpr): boolean =>
+ !(x.kind === "assignExpr" && x.localId === id) && everyExprChild(x, expr, stmt);
+ const stmt = (x: IrStmt): boolean =>
+ !((x.kind === "varDecl" || x.kind === "assign") && x.localId === id) &&
+ everyStmtChild(x, expr, stmt);
+ return !expr(e);
+}
+
+/** The candidate locals of `fn` (see the module comment). */
+export function int32ArrayCandidates(fn: IrFunction, readers: ReadonlySet): Set {
+ const result = new Set();
+ if (fn.async || fn.generator) return result;
+ const captured = new Set(
+ [...(fn.captures ?? []), ...(fn.classCaptures ?? [])].map((c) => c.localId),
+ );
+ const params = new Set(fn.params.map((p) => p.localId));
+ for (const local of fn.locals)
+ if (
+ local.type.kind === "array" &&
+ local.type.elem.kind === "f64" &&
+ !local.boxed &&
+ !local.tdz &&
+ !captured.has(local.id) &&
+ !params.has(local.id)
+ )
+ result.add(local.id);
+ if (result.size === 0) return result;
+ const written = new Set();
+ // A receiver position: the candidate is read there without escaping.
+ const receiver = (e: IrExpr): void => {
+ if (e.kind !== "varRef") expr(e);
+ };
+ const expr = (e: IrExpr): boolean => {
+ switch (e.kind) {
+ case "varRef":
+ result.delete(e.localId);
+ return true;
+ case "assignExpr":
+ result.delete(e.localId);
+ break;
+ case "arrayGet":
+ case "arrayState":
+ case "arrayHas":
+ receiver(e.arr);
+ expr(e.index);
+ return true;
+ case "arrIntrinsic":
+ if (!INT32_ARRAY_METHODS.has(e.method)) break;
+ receiver(e.receiver);
+ e.args.forEach(expr);
+ return true;
+ case "seqExpr": {
+ // The pop's hidden local is the array itself; every use of it is a
+ // length read or a pop store.
+ const pop = discardedPop(e);
+ if (!pop) break;
+ e.stmts.slice(1).every(stmt);
+ expr(e.result);
+ return true;
+ }
+ case "call": {
+ if (!readers.has(e.callee) || e.args.length !== 2) break;
+ const [array, index] = e.args;
+ // Inlining evaluates the index before the array read.
+ if (array!.kind === "varRef" && writesLocal(index!, array!.localId))
+ result.delete(array!.localId);
+ receiver(array!);
+ expr(index!);
+ return true;
+ }
+ }
+ return everyExprChild(e, expr, stmt);
+ };
+ const stmt = (s: IrStmt): boolean => {
+ switch (s.kind) {
+ case "varDecl":
+ case "assign": {
+ const value = s.kind === "varDecl" ? s.init : s.value;
+ if (value && int32ArrayAllocation(value)) written.add(s.localId);
+ else if (value) result.delete(s.localId);
+ if (value) expr(value);
+ return true;
+ }
+ case "arraySet":
+ receiver(s.arr);
+ expr(s.index);
+ expr(s.value);
+ return true;
+ case "arraySetUndefined":
+ // Stores a present undefined: a state, and a cleared (+0) slot.
+ receiver(s.arr);
+ expr(s.index);
+ return true;
+ case "forOf":
+ result.delete(s.localId);
+ break;
+ case "tryCatch":
+ if (s.catchLocalId) result.delete(s.catchLocalId);
+ break;
+ }
+ return everyStmtChild(s, expr, stmt);
+ };
+ fn.body.every(stmt);
+ for (const id of result) if (!written.has(id)) result.delete(id);
+ return result;
+}
+
+/** The element values stored into each candidate: element writes, pushed
+ * values, literal elements and fill values. */
+export function int32ArrayStores(
+ fn: IrFunction,
+ candidates: ReadonlySet,
+): Map {
+ const stores = new Map();
+ const add = (id: string, value: IrExpr): void => {
+ const list = stores.get(id);
+ if (list) list.push(value);
+ else stores.set(id, [value]);
+ };
+ const target = (e: IrExpr): string | null =>
+ e.kind === "varRef" && candidates.has(e.localId) ? e.localId : null;
+ const expr = (e: IrExpr): boolean => {
+ if (e.kind === "arrIntrinsic" && e.method === "push") {
+ const id = target(e.receiver);
+ if (id !== null) for (const arg of e.args) add(id, arg);
+ }
+ return everyExprChild(e, expr, stmt);
+ };
+ const stmt = (s: IrStmt): boolean => {
+ if ((s.kind === "varDecl" || s.kind === "assign") && candidates.has(s.localId)) {
+ const value = s.kind === "varDecl" ? s.init : s.value;
+ if (value?.kind === "arrayLit") for (const el of value.elems) add(s.localId, el);
+ else if (value) {
+ const fill = fillAllocation(value);
+ if (fill) add(s.localId, fill.args[0]!);
+ }
+ }
+ if (s.kind === "arraySet") {
+ const id = target(s.arr);
+ if (id !== null) add(id, s.value);
+ }
+ return everyStmtChild(s, expr, stmt);
+ };
+ fn.body.every(stmt);
+ return stores;
+}
+
+/** Rewrite a function's candidates for the backend. A discarded pop uses
+ * the candidate in place of its hidden local. An element-reader helper call
+ * whose array is a candidate is inlined: the helper's returned ternary, with
+ * the candidate for its array and the index for its index, so the read means
+ * the same. An index that is not a plain read or literal is bound to a fresh
+ * local first: in its own declaration just before the statement when the
+ * call is the whole value of a declaration or assignment (the form lowering
+ * gives `a[i]!`, and the one the backend reads without a box), otherwise in a
+ * sequence around the ternary. Selection keeps out indexes that write the
+ * array, so evaluating the index before the array read is unobservable. */
+function rewriteCandidates(
+ fn: IrFunction,
+ candidates: ReadonlySet,
+ readers: ReadonlyMap,
+): IrFunction {
+ const locals = [...fn.locals];
+ let fresh = 0;
+ const bind = (index: IrExpr): { decl: IrStmt; ref: IrExpr } => {
+ const id = `%int32.read.${fresh++}`;
+ locals.push({ id, name: id, type: F64, mutable: false });
+ return {
+ decl: { kind: "varDecl", localId: id, init: index, loc: index.loc },
+ ref: { kind: "varRef", localId: id, type: F64, loc: index.loc },
+ };
+ };
+ const readerCall = (e: IrExpr | null): (IrExpr & { kind: "call" }) | null =>
+ e?.kind === "call" &&
+ e.args.length === 2 &&
+ readers.has(e.callee) &&
+ e.args[0]!.kind === "varRef" &&
+ candidates.has(e.args[0]!.localId)
+ ? e
+ : null;
+ const simple = (e: IrExpr): boolean => e.kind === "varRef" || e.kind === "numLit";
+ // The statements of a list, with complex read indexes declared first.
+ const hoist = (list: IrStmt[]): IrStmt[] =>
+ list.flatMap((s): IrStmt[] => {
+ if (s.kind !== "varDecl" && s.kind !== "assign") return [s];
+ const value = s.kind === "varDecl" ? s.init : s.value;
+ const call = readerCall(value);
+ if (!call || simple(call.args[1]!)) return [s];
+ const { decl, ref } = bind(call.args[1]!);
+ const read: IrExpr = { ...call, args: [call.args[0]!, ref] };
+ if (s.kind === "varDecl") return [decl, { ...s, init: read }];
+ return [decl, { ...s, value: read }];
+ });
+ const hoistCase = (c: SwitchCase): SwitchCase => ({ ...c, body: hoist(c.body) });
+ const lists = (s: IrStmt): IrStmt => {
+ switch (s.kind) {
+ case "if":
+ return { ...s, then: hoist(s.then), else_: s.else_ === null ? null : hoist(s.else_) };
+ case "block":
+ return { ...s, body: hoist(s.body) };
+ case "while":
+ return { ...s, body: hoist(s.body) };
+ case "doWhile":
+ return { ...s, body: hoist(s.body) };
+ case "for":
+ return { ...s, body: hoist(s.body) };
+ case "forOf":
+ return { ...s, body: hoist(s.body) };
+ case "switch":
+ return { ...s, cases: s.cases.map((c) => hoistCase(c)) };
+ case "tryCatch":
+ return {
+ ...s,
+ tryBody: hoist(s.tryBody),
+ catchBody: s.catchBody === null ? null : hoist(s.catchBody),
+ finallyBody: s.finallyBody === null ? null : hoist(s.finallyBody),
+ };
+ }
+ return s;
+ };
+ // Hidden pop locals replaced by their candidate array.
+ const popLocals = new Set();
+ const rewrite = (e: IrExpr): IrExpr => {
+ const pop = discardedPop(e);
+ if (e.kind === "seqExpr" && pop && candidates.has(pop.array.localId)) {
+ popLocals.add(pop.local);
+ const array = pop.array;
+ const substitute: IrTransform = {
+ expr: (x) =>
+ x.kind === "varRef" && x.localId === pop.local ? { ...array, loc: x.loc } : x,
+ stmt: (x) => x,
+ };
+ return {
+ ...e,
+ stmts: e.stmts.slice(1).map((x) => transformStmt(x, substitute)),
+ result: transformExpr(e.result, substitute),
+ };
+ }
+ const call = readerCall(e);
+ if (!call) {
+ if (e.kind === "seqExpr") return { ...e, stmts: hoist(e.stmts) };
+ return e;
+ }
+ const helper = readers.get(call.callee)!;
+ const array = call.args[0]!;
+ const [arrayParam, indexParam] = helper.params;
+ const ret = helper.body[0]!;
+ if (ret.kind !== "return" || !ret.value) return e;
+ let index = call.args[1]!;
+ const stmts: IrStmt[] = [];
+ if (!simple(index)) {
+ const { decl, ref } = bind(index);
+ stmts.push(decl);
+ index = ref;
+ }
+ const operand = index;
+ const body = transformExpr(reparseExpr(ret.value), {
+ expr: (x) => {
+ if (x.kind !== "varRef") return x;
+ if (x.localId === arrayParam!.localId) return reparseExpr(array);
+ if (x.localId === indexParam!.localId) return reparseExpr(operand);
+ return x;
+ },
+ stmt: (x) => x,
+ });
+ // The ternary is a fresh copy (reparseExpr): it takes the call's location.
+ const typed = body;
+ typed.loc = e.loc;
+ return stmts.length === 0
+ ? typed
+ : { kind: "seqExpr", stmts, result: typed, type: typed.type, loc: e.loc };
+ };
+ const body = transformStmtList(hoist(fn.body), { expr: rewrite, stmt: lists });
+ if (fresh === 0 && popLocals.size === 0) return { ...fn, body };
+ return { ...fn, body, locals: locals.filter((local) => !popLocals.has(local.id)) };
+}
+
+/** Propose the module's int32 array candidates (`function\0local` keys in
+ * `int32ArrayCandidates`), only those in `keep` when given, and rewrite
+ * their functions. */
+export function markInt32ArrayCandidates(
+ mod: IrModule,
+ keep: ReadonlySet | null = null,
+): IrModule {
+ if (mod.lib !== undefined) return mod;
+ const readers = new Map();
+ for (const fn of mod.functions) if (isElementReader(fn)) readers.set(fn.name, fn);
+ const names = new Set(readers.keys());
+ const keys: string[] = [];
+ const functions = mod.functions.map((fn) => {
+ const candidates = int32ArrayCandidates(fn, names);
+ for (const id of candidates)
+ if (keep === null || keep.has(`${fn.name}\0${id}`)) keys.push(`${fn.name}\0${id}`);
+ else candidates.delete(id);
+ return candidates.size === 0 ? fn : rewriteCandidates(fn, candidates, readers);
+ });
+ return keys.length === 0 ? mod : { ...mod, functions, int32ArrayCandidates: keys };
+}
+
+/** The module the backend emits: rewritten for exactly the candidates the
+ * int32 slot analysis (`prove`, returning its int32Arrays) keeps, so a
+ * module without int32 arrays is `mod` itself. A dropped candidate's
+ * rewrite is undone by marking again without it, until every candidate
+ * marked is kept. */
+export function selectInt32Arrays(
+ mod: IrModule,
+ prove: (marked: IrModule) => ReadonlySet,
+): IrModule {
+ let keep: ReadonlySet | null = null;
+ for (let round = 0; round < 4; round++) {
+ const marked = markInt32ArrayCandidates(mod, keep);
+ if (marked === mod) return mod;
+ const kept = prove(marked);
+ const proposed = marked.int32ArrayCandidates ?? [];
+ if (proposed.every((key) => kept.has(key))) return marked;
+ keep = new Set(proposed.filter((key) => kept.has(key)));
+ }
+ return mod;
+}
diff --git a/packages/compiler/src/ir/int32-slots.ts b/packages/compiler/src/ir/int32-slots.ts
index ac0747276..4a2abc5b0 100644
--- a/packages/compiler/src/ir/int32-slots.ts
+++ b/packages/compiler/src/ir/int32-slots.ts
@@ -10,6 +10,7 @@ import {
} from "./integer-ranges.js";
import { exprFunctionReference, moduleFunctionReferences } from "./function-references.js";
import { findInitializerBindings, withInitializerBindings } from "./initializer-bindings.js";
+import { checkableInt32Allocations, int32ArrayStores } from "./int32-arrays.js";
import { everyExprChild, everyStmtChild, everyStmtList, everyTypeChild } from "./traverse.js";
/** The storage slot an array-valued expression reads, as an
@@ -169,7 +170,10 @@ export class Int32Slots {
this.int32Locals.has(`${fnName}\0${localId}`) || this.int32Locals.has(`\0${localId}`)
? INT32_RANGE
: null;
- if (this.int32Elements.size > 0) result.elements = (arr) => this.hasInt32Elements(fnName, arr);
+ if (this.int32Elements.size > 0) {
+ result.elements = (arr) => this.hasInt32Elements(fnName, arr);
+ if (this.narrowedReads) result.narrowedReads = true;
+ }
const params = this.paramFacts.get(fnName);
if (params)
result.params =
@@ -234,6 +238,28 @@ export class Int32Slots {
addInt32Elements(key: string): void {
this.int32Elements.add(key);
}
+ dropInt32Elements(key: string): boolean {
+ return this.int32Elements.delete(key);
+ }
+ hasInt32ElementSlot(key: string): boolean {
+ return this.int32Elements.has(key);
+ }
+ /** Whether the module has int32 array candidates (the narrowedReads
+ * range fact). */
+ narrowedReads = false;
+ /** Local number arrays (`function\0local`) stored as int32 slots: the
+ * module's int32 array candidates whose every stored element the
+ * fixpoint proved an int32. */
+ readonly int32Arrays = new Set();
+ /** Local number arrays that may hold int32 slots with checked stores: the
+ * candidates whose element stores the fixpoint proved exact integers but
+ * not int32s, allocated only by fills that may be long. A fill of at least
+ * CHECKED_INT32_MIN_LENGTH elements creates one with int32 slots and a
+ * shorter one with doubles. Their elements give no int32 facts; compiled
+ * accesses test the array's kind (once per innermost loop where they can,
+ * backend/llvm/int32-kind-loops.ts), and a store that is not an int32
+ * widens it first (scr_arr_widen_i32). */
+ readonly checkedInt32Arrays = new Set();
/** Whether every present element of the number array `arr` evaluates to
* (read inside `fnName`) is an int32. */
hasInt32Elements(fnName: string, arr: IrExpr): boolean {
@@ -752,6 +778,18 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}):
}
for (const key of contracts?.int32Locals ?? []) slots.addInt32Local(key);
for (const key of contracts?.int32Elements ?? []) slots.addInt32Elements(key);
+ // Int32 array candidates (ir/int32-arrays.ts): assumed to hold int32
+ // elements until a store the fixpoint cannot prove drops one.
+ const arrayCandidates = new Map>();
+ slots.narrowedReads = (mod.int32ArrayCandidates?.length ?? 0) > 0;
+ for (const key of mod.int32ArrayCandidates ?? []) {
+ const cut = key.indexOf("\0");
+ const fnName = key.slice(0, cut);
+ const set = arrayCandidates.get(fnName) ?? new Set();
+ set.add(key.slice(cut + 1));
+ arrayCandidates.set(fnName, set);
+ slots.addInt32Elements(`l:${key}`);
+ }
/* ── candidate fields ── */
const children = new Map();
@@ -1031,6 +1069,7 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}):
return false;
};
const hasCandidateWrite = (fn: IrFunction): boolean => {
+ if (arrayCandidates.has(fn.name)) return true;
const box = (localId: string): boolean => slots.hasBox(slots.boxGroup(fn.name, localId));
const returns = slots.hasReturn(fn.name);
return !everyStmtList(fn.body, {
@@ -1076,6 +1115,36 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}):
continue;
}
const int32 = (e: IrExpr): boolean => withinInt32(ranges.get(e));
+ // Every element stored into an int32 array candidate must be an int32
+ // under the facts that assume the candidates' own elements are; a
+ // failing store drops its array's element facts, and the function is
+ // analyzed again without them. When every failing store is still an
+ // exact integer under those facts (a counter, `a[i]! + 1`) and the array
+ // is only ever allocated by a fill that may be long
+ // (checkableInt32Allocations), it keeps int32 slots with checked stores
+ // (checkedInt32Arrays), which widen it to doubles in place at the first
+ // store that is not an int32.
+ const arrays = arrayCandidates.get(fn.name);
+ if (arrays) {
+ let dropped = false;
+ for (const [id, values] of int32ArrayStores(fn, arrays)) {
+ const key = `l:${fn.name}\0${id}`;
+ if (!slots.hasInt32ElementSlot(key) || values.every(int32)) continue;
+ slots.dropInt32Elements(key);
+ const checkable = (value: IrExpr): boolean => {
+ const range = ranges.get(value);
+ return int32(value) || range != null;
+ };
+ if (values.every(checkable) && checkableInt32Allocations(fn, id))
+ slots.checkedInt32Arrays.add(`${fn.name}\0${id}`);
+ else reason(`array:${fn.name}\0${id}`, "an element store is not int32");
+ dropped = true;
+ }
+ if (dropped && !queued.has(fn.name)) {
+ queued.add(fn.name);
+ queue.push(fn.name);
+ }
+ }
// An int32 written into a slot still assumed non-negative: widen the
// assumption when the value may be negative, and reanalyze its readers.
const settle = (slot: string, e: IrExpr): void => {
@@ -1144,6 +1213,10 @@ export function analyzeInt32Slots(mod: IrModule, inputs: Int32SlotInputs = {}):
}
for (const proven of provenByFunction.values())
for (const value of proven) slots.provenWrites.add(value);
+ for (const [fnName, ids] of arrayCandidates)
+ for (const id of ids)
+ if (slots.hasInt32ElementSlot(`l:${fnName}\0${id}`))
+ slots.int32Arrays.add(`${fnName}\0${id}`);
const ranged = rangedParameters(slots, functions, retry, numeric, (callee) =>
readers.get(`ret:${callee}`),
);
diff --git a/packages/compiler/src/ir/integer-ranges.ts b/packages/compiler/src/ir/integer-ranges.ts
index 1d295ca24..0e5ca426d 100644
--- a/packages/compiler/src/ir/integer-ranges.ts
+++ b/packages/compiler/src/ir/integer-ranges.ts
@@ -220,6 +220,11 @@ export interface IntegerSlotFacts {
/** Number arrays whose every present element is an int32 (Int32[]
* contracts): an element read that found a value reads an int32. */
elements?: (arr: IrExpr) => boolean;
+ /** Element reads narrowed out of a union (`xs[i]!`, also when a union
+ * local holds the read) keep the element's range, and a ternary arm that
+ * always throws yields no value. Only modules with int32 array candidates
+ * (ir/int32-arrays.ts) set it, so no other program's ranges change. */
+ narrowedReads?: boolean;
}
/** A local read, or a numeric local plus or minus an integer literal
@@ -240,6 +245,50 @@ function offsetRef(e: IrExpr): { localId: string; offset: number } | null {
return null;
}
+/** Library calls that always throw. */
+const THROWING_LIB = /^error\.\w*Throw$/;
+
+function throws(e: IrExpr): boolean {
+ return e.kind === "libCall" && THROWING_LIB.test(e.fn);
+}
+
+/** The values written to each union-typed local of `fn` by declarations
+ * and assignments; null for a local that another write reaches or that a
+ * closure captures. */
+function unionLocalWrites(
+ fn: IrFunction,
+ captures: ReadonlySet,
+): Map {
+ const unionLocals = new Set(
+ fn.locals
+ .filter((l) => l.type.kind === "union" && !l.boxed && !captures.has(l.id))
+ .map((l) => l.id),
+ );
+ const result = new Map();
+ const add = (id: string, value: IrExpr | null): void => {
+ if (!unionLocals.has(id)) return;
+ const list = result.get(id);
+ if (list === null) return;
+ if (value === null) result.set(id, null);
+ else if (list) list.push(value);
+ else result.set(id, [value]);
+ };
+ everyStmtList(fn.body, {
+ stmt: (s) => {
+ if (s.kind === "varDecl" && s.init) add(s.localId, s.init);
+ else if (s.kind === "assign") add(s.localId, s.value);
+ else if (s.kind === "forOf") add(s.localId, null);
+ else if (s.kind === "tryCatch" && s.catchLocalId) add(s.catchLocalId, null);
+ return true;
+ },
+ expr: (e) => {
+ if (e.kind === "assignExpr") add(e.localId, null);
+ return true;
+ },
+ });
+ return result;
+}
+
/** Structured local dataflow. Calls cannot write uncaptured local slots,
* but argument expressions can; unknown evaluation forms invalidate all
* syntactically written slots before their children are inspected. Loops
@@ -272,6 +321,10 @@ export function analyzeIntegerRangeFacts(
.map((l) => l.id),
);
const loopLocals = new Map(fn.locals.filter((l) => !captures.has(l.id)).map((l) => [l.id, l]));
+ // With narrowed reads, the values written to union locals (an element
+ // read held before it is narrowed), or null for a local some other write
+ // (assignExpr, for-of, catch, capture) reaches.
+ const unionWrites = slots.narrowedReads ? unionLocalWrites(fn, captures) : null;
// A captured binding has no local dataflow: only a proof that covers
// every write of the shared box anywhere applies to its reads.
const boxedRange = (localId: string): IntegerRange | null => {
@@ -490,6 +543,45 @@ export function analyzeIntegerRangeFacts(
return range;
}
+ /** narrowedRange through a union local: every value written to it. */
+ function narrowedValue(
+ value: IrExpr,
+ unionId: string,
+ tag: number,
+ ): IntegerRange | null | "never" {
+ if (value.kind !== "varRef") return narrowedRange(value, unionId, tag);
+ const written = unionWrites?.get(value.localId);
+ if (!written || written.length === 0) return null;
+ let result: IntegerRange | null | "never" = "never";
+ for (const w of written) {
+ const arm = narrowedRange(w, unionId, tag);
+ if (arm === "never") continue;
+ if (arm === null) return null;
+ result = result === "never" ? arm : union(result, arm);
+ }
+ return result;
+ }
+ /** The range of the values of union tag `tag` that `value` (already
+ * analyzed) can hold: "never" when no arm holds that tag. */
+ function narrowedRange(
+ value: IrExpr,
+ unionId: string,
+ tag: number,
+ ): IntegerRange | null | "never" {
+ if (value.kind === "seqExpr") return narrowedRange(value.result, unionId, tag);
+ if (value.kind === "ternary") {
+ const yes = narrowedRange(value.then, unionId, tag),
+ no = narrowedRange(value.else_, unionId, tag);
+ if (yes === "never") return no;
+ if (no === "never") return yes;
+ return yes && no ? union(yes, no) : null;
+ }
+ if (value.kind === "unionWrap" && value.unionId === unionId) {
+ if (value.tag !== tag) return "never";
+ return value.value.kind === "arrayGet" ? (ranges.get(value.value) ?? null) : null;
+ }
+ return null;
+ }
function remember(e: IrExpr, range: IntegerRange | null): IntegerRange | null {
// Shared IR objects have to satisfy the proof at every occurrence.
const previous = ranges.get(e);
@@ -683,6 +775,10 @@ export function analyzeIntegerRangeFacts(
right = expr(e.else_, no);
merge(facts, yes, no);
if (left && right) range = union(left, right);
+ // With narrowed reads: an arm that always throws yields no value,
+ // so the other arm's range is the result's.
+ else if (slots.narrowedReads && throws(e.then)) range = right;
+ else if (slots.narrowedReads && throws(e.else_)) range = left;
break;
}
case "logical": {
@@ -777,27 +873,45 @@ export function analyzeIntegerRangeFacts(
expr(e.obj, facts);
range = slotRange(e);
break;
+ case "unionNarrow":
+ // Only modules with narrowed reads take this rule: the narrow
+ // evaluates its operand first, and throws for every arm of another
+ // tag, so an element read `xs[i]!` (a ternary of the wrapped
+ // element and a wrapped undefined) has the element's range.
+ if (!slots.narrowedReads || e.type.kind !== "f64") {
+ range = conservative(e, facts);
+ break;
+ }
+ expr(e.value, facts);
+ {
+ const arm = narrowedValue(e.value, e.unionId, e.tag);
+ range = arm === "never" ? null : arm;
+ }
+ break;
default:
- // Do not assume the traversal order is an evaluation order for an
- // unknown node. Every child gets the same conservative entry state.
- havoc(facts, [e], facts);
- everyExprChild(
- e,
- (child) => {
- expr(child, new Map(facts));
- return true;
- },
- (child) => {
- body([child], new Map(facts));
- return true;
- },
- );
- // A direct call's result carries its whole-program return proof.
- range = slotRange(e);
+ range = conservative(e, facts);
break;
}
return remember(e, range);
}
+ /** An unknown node: do not assume the traversal order is an evaluation
+ * order. Every child gets the same conservative entry state. */
+ function conservative(e: IrExpr, facts: Facts): IntegerRange | null {
+ havoc(facts, [e], facts);
+ everyExprChild(
+ e,
+ (child) => {
+ expr(child, new Map(facts));
+ return true;
+ },
+ (child) => {
+ body([child], new Map(facts));
+ return true;
+ },
+ );
+ // A direct call's result carries its whole-program return proof.
+ return slotRange(e);
+ }
function induction(
s: IrStmt & { kind: "for" },
facts: Facts,
diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts
index 067f79a26..360a41c5f 100644
--- a/packages/compiler/src/ir/ir.ts
+++ b/packages/compiler/src/ir/ir.ts
@@ -1077,6 +1077,12 @@ export interface IrModule {
* write in the program (ir/contract-check.ts). Backends may rely on them
* unconditionally. Absent when the program writes no contract. */
contracts?: IrContracts;
+ /** Local number arrays (`function\0local`) the backend may store as
+ * int32 slots, proposed by ir/int32-arrays.ts after their element-reader
+ * calls were inlined; the int32 slot analysis keeps the ones whose every
+ * stored element it proves an int32, and those whose stores are exact
+ * integers with checked stores. Absent when there is none. */
+ int32ArrayCandidates?: string[];
/** The WebAssembly modules translated into this program (wasm/
* translate.ts), present exactly when the program uses the WebAssembly
* API (possibly empty). The backend appends each module's translation
@@ -2098,8 +2104,9 @@ export type IrStmt =
* and whose `code` carries the SC diagnostic code; unwinds exactly like
* `throw`.
* TypeScript sources produce it only as the tsc-unreachable fallthrough
- * trap (appendImplicitUndefinedReturn's SC9002) — their construct fences
- * stay compile errors. */
+ * trap (appendImplicitUndefinedReturn's SC9002) and for a typed-array
+ * store through an element index that is undefined at run time
+ * (undefined-index.ts); their construct fences stay compile errors. */
| { kind: "runtimeFence"; code: string; message: string; loc: SrcLoc }
/** Rethrow of a catch binding (`throw e` where e is the binding):
* re-raises the SAVED exception exactly — kind and payload preserved,
diff --git a/packages/compiler/src/ir/serialize.test.ts b/packages/compiler/src/ir/serialize.test.ts
index 8345ab10a..c24e1f198 100644
--- a/packages/compiler/src/ir/serialize.test.ts
+++ b/packages/compiler/src/ir/serialize.test.ts
@@ -198,3 +198,24 @@ test("byte decoding keeps the whole-document errors and version refusal", () =>
const sentinel = text.replace('"nan"', '"future"');
expect(() => deserializeModuleBytes(Buffer.from(sentinel))).toThrow("Invalid IR number sentinel");
});
+
+test("file roots respell the source locations under them and nothing else", () => {
+ const mod = numbers([1, 2]);
+ const shim = { file: '/opt/scriptc "a"/shims/pkg/index.ts', start: 3, end: 4 };
+ const other = { file: "/opt/elsewhere/shims/pkg/index.ts", start: 5, end: 6 };
+ const body = mod.functions[0]!.body as IrStmt[];
+ body.push(
+ { kind: "exprStmt", expr: { kind: "numLit", value: 3, type: F64, loc: shim }, loc: shim },
+ { kind: "exprStmt", expr: { kind: "numLit", value: 4, type: F64, loc: other }, loc: other },
+ );
+ const roots = [{ from: '/opt/scriptc "a"/shims/', to: "scriptc:shims/" }];
+ const relocated = deserializeModule(serializeModuleParts(mod, roots).join(""));
+ const files = (relocated.functions[0]!.body as IrStmt[]).map((stmt) => stmt.loc.file);
+ expect(files).toEqual([
+ "numbers.ts",
+ "numbers.ts",
+ "scriptc:shims/pkg/index.ts",
+ "/opt/elsewhere/shims/pkg/index.ts",
+ ]);
+ expect(serializeModuleParts(mod).join("")).toBe(serializeModule(mod, true));
+});
diff --git a/packages/compiler/src/ir/serialize.ts b/packages/compiler/src/ir/serialize.ts
index ca5eb955e..314f258d6 100644
--- a/packages/compiler/src/ir/serialize.ts
+++ b/packages/compiler/src/ir/serialize.ts
@@ -46,11 +46,41 @@ function encodeFunction(fn: IrFunction): string | undefined {
return ordinaryNumbers(fn) ? JSON.stringify(fn) : JSON.stringify(fn, encodeNumber);
}
+/** A directory whose source location files an IR artifact names by a
+ * prefix of its own: a location file `from + rest` serializes as
+ * `to + rest`. */
+export interface SerializedFileRoot {
+ from: string;
+ to: string;
+}
+
+/** Respells the source location files under `roots` in serialized parts.
+ * A location's file is the first member of its JSON object, and the
+ * escaped spelling of a path prefix is the prefix of the escaped path. */
+function relocateFiles(parts: string[], roots: readonly SerializedFileRoot[]): string[] {
+ const needles = roots.map(({ from, to }) => ({
+ from: `"file":${JSON.stringify(from).slice(0, -1)}`,
+ to: `"file":${JSON.stringify(to).slice(0, -1)}`,
+ }));
+ return parts.map((part) =>
+ needles.reduce(
+ (text, { from, to }) => (text.includes(from) ? text.replaceAll(from, to) : text),
+ part,
+ ),
+ );
+}
+
/** The compact encoding as ordered parts: the module header up to the
* function array, one part per function (with separate comma parts), and the
* header's remainder. Concatenating the parts yields exactly
- * `serializeModule(mod, true)`; writers stream them to disk in batches. */
-export function serializeModuleParts(mod: IrModule): string[] {
+ * `serializeModule(mod, true)`; writers stream them to disk in batches.
+ * `fileRoots` respells the source locations in those directories, so
+ * artifacts do not depend on where the compiler that wrote them is
+ * installed. */
+export function serializeModuleParts(
+ mod: IrModule,
+ fileRoots: readonly SerializedFileRoot[] = [],
+): string[] {
// A replacer reads through live native views. Serializing the complete
// function array through one view refreshes every function capsule for
// each element, making large compiler artifacts quadratic. The sentinel
@@ -65,7 +95,7 @@ export function serializeModuleParts(mod: IrModule): string[] {
parts.push(encodeFunction(fn) ?? "null");
}
parts.push("]" + header.slice(offset + slot.length));
- return parts;
+ return fileRoots.length === 0 ? parts : relocateFiles(parts, fileRoots);
}
/** Compiler artifacts use compact JSON; API consumers can retain the
diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts
index adab9628f..0e990918b 100644
--- a/packages/compiler/src/ir/validate.ts
+++ b/packages/compiler/src/ir/validate.ts
@@ -4269,7 +4269,11 @@ function validateFunction(
if (!ok) err(`libCall dyn.objEntryPairs must return a [string, any] tuple array`, e.loc);
return true;
}
- if (e.fn === "array.namedKeys" || e.fn === "array.hasNamedKey") {
+ if (
+ e.fn === "array.namedKeys" ||
+ e.fn === "array.hasNamedKey" ||
+ e.fn === "array.hasProperties"
+ ) {
if (e.args[0]?.type.kind !== "array") {
err(`libCall ${e.fn} arg 0: expected an array, got ${e.args[0]?.type.kind}`, e.loc);
}
diff --git a/packages/compiler/src/native/cache.ts b/packages/compiler/src/native/cache.ts
index 566df308a..3c9bdf411 100644
--- a/packages/compiler/src/native/cache.ts
+++ b/packages/compiler/src/native/cache.ts
@@ -14,7 +14,7 @@ import {
} from "node:fs";
import { join } from "node:path";
import { resolveBuildCacheRoot } from "../backend/cache-root.js";
-import { writeTextGeneratedSync } from "./text-file.js";
+import { joinedParts, writeTextGeneratedSync } from "./text-file.js";
/** UTF-8 bytes one native compiler string can hold (the runtime's
* SCR_STR_MAX_CAP). Exceeding it traps rather than throwing, so payloads
@@ -28,6 +28,13 @@ export function contentDigest(value: string | Uint8Array): string {
return hash.digest("hex");
}
+/** contentDigest of the text the parts concatenate to. */
+export function partsDigest(parts: readonly string[]): string {
+ const hash = createHash("sha256");
+ for (const part of joinedParts(parts)) hash.update(part);
+ return hash.digest("hex");
+}
+
/** contentDigest of a file's bytes, read in 1 MiB blocks so a large file
* (a module's LLVM text) is never held whole. */
export function fileDigest(path: string): string {
diff --git a/packages/compiler/src/native/compiler.ts b/packages/compiler/src/native/compiler.ts
index 2c850cb14..a37d76f80 100644
--- a/packages/compiler/src/native/compiler.ts
+++ b/packages/compiler/src/native/compiler.ts
@@ -362,7 +362,7 @@ export class NativeCompiler {
mkdirSync(inputDirectory);
const llvmInput = outputKind === "llvm" ? stagedOutput : join(inputDirectory, "program.ll");
// In slices: the module text is never encoded whole.
- writeTextPartsSync(llvmInput, [llvm]);
+ writeTextPartsSync(llvmInput, llvm);
timing("llvm-write");
if (outputKind === "llvm") {
renameSync(stagedOutput, output);
@@ -496,7 +496,7 @@ export class NativeCompiler {
renameSync(stagedOutput + ".dSYM", output + ".dSYM");
}
mkdirSync(options.outDir, { recursive: true });
- writeTextPartsSync(llvmPath, [llvm]);
+ writeTextPartsSync(llvmPath, llvm);
let irPath: string | undefined;
if (options.emitIr) {
irPath = join(options.outDir, `${stem}.ir.json`);
@@ -585,7 +585,7 @@ export class NativeCompiler {
const llvm = prepared.llvm;
const llvmInput = join(stage, "program.ll");
// In slices: the module text is never encoded whole.
- writeTextPartsSync(llvmInput, [llvm]);
+ writeTextPartsSync(llvmInput, llvm);
timing("llvm-write");
const features = prepared.features;
const pack = selectNativeRuntimePack(
@@ -648,7 +648,7 @@ export class NativeCompiler {
});
}
timing("native-archive");
- writeTextPartsSync(llvmPath, [llvm]);
+ writeTextPartsSync(llvmPath, llvm);
let irPath: string | undefined;
if (options.emitIr) {
irPath = join(options.outDir, `${stem}.lib.ir.json`);
diff --git a/packages/compiler/src/native/executable-cache.ts b/packages/compiler/src/native/executable-cache.ts
index a122fb759..c0ac755b9 100644
--- a/packages/compiler/src/native/executable-cache.ts
+++ b/packages/compiler/src/native/executable-cache.ts
@@ -11,7 +11,7 @@ import {
import { runNativeTool } from "../backend/native-tools.js";
import type { NativeRuntimeSelection } from "../backend/runtime-pack-native.js";
import type { NativeToolchain } from "./toolchain.js";
-import { contentDigest, NativeCache } from "./cache.js";
+import { contentDigest, NativeCache, partsDigest } from "./cache.js";
interface InputIdentity {
path: string;
@@ -254,7 +254,7 @@ export class NativeExecutableCache {
export function openNativeExecutableCache(
cache: NativeCache | null,
toolchain: NativeToolchain,
- llvm: string,
+ llvm: readonly string[],
options: CompileRequestOptions,
ffi: FfiProfile | null,
pack: NativeRuntimeSelection,
@@ -299,7 +299,7 @@ export function openNativeExecutableCache(
const key = contentDigest(
JSON.stringify({
schema: 1,
- llvm: contentDigest(llvm),
+ llvm: partsDigest(llvm),
options,
toolchain,
runtimeIdentity: pack.packageText,
diff --git a/packages/compiler/src/native/prepare.test.ts b/packages/compiler/src/native/prepare.test.ts
index 7e22450da..112484f4a 100644
--- a/packages/compiler/src/native/prepare.test.ts
+++ b/packages/compiler/src/native/prepare.test.ts
@@ -11,11 +11,33 @@ import { NativeCache } from "./cache.js";
import { prepareNativeExecutable } from "./prepare.js";
import type { NativeToolchain } from "./toolchain.js";
+/** A prepared input with its LLVM parts joined: a cache hit reads the text
+ * back as one part. */
+function text(input: ReturnType["prepare"]>) {
+ return input.ok ? { ...input, llvm: input.llvm.join("") } : input;
+}
+
const scratch: string[] = [];
afterEach(() => {
for (const path of scratch.splice(0)) rmSync(path, { recursive: true, force: true });
});
+function fixtureToolchain(): NativeToolchain {
+ const target = nativeCodegenTarget()!;
+ return {
+ compilerVersion: "test",
+ target,
+ helper: target.helper,
+ helperExecutable: "unused",
+ helperPackageRoot: "unused",
+ runtimePackRoot: "unused",
+ linker: "unused",
+ linkerArgs: [],
+ dsymutil: "unused",
+ ts7Executable: ts7Executable(),
+ };
+}
+
function fixture() {
const temporary = mkdtempSync(join(tmpdir(), "scriptc-native-frontend-"));
scratch.push(temporary);
@@ -32,19 +54,7 @@ function fixture() {
}),
);
writeFileSync(join(root, "package.json"), '{"type":"module"}');
- const target = nativeCodegenTarget()!;
- const toolchain: NativeToolchain = {
- compilerVersion: "test",
- target,
- helper: target.helper,
- helperExecutable: "unused",
- helperPackageRoot: "unused",
- runtimePackRoot: "unused",
- linker: "unused",
- linkerArgs: [],
- dsymutil: "unused",
- ts7Executable: ts7Executable(),
- };
+ const toolchain = fixtureToolchain();
const cache = new NativeCache(join(temporary, "cache"));
const options = {
outPath: join(root, ".scriptc", "main"),
@@ -68,17 +78,77 @@ test("warm frontend reuse survives output creation and invalidates imported sour
if (!first.ok) return;
mkdirSync(f.options.outDir);
writeFileSync(f.options.outPath, "binary placeholder");
- writeFileSync(join(f.options.outDir, "main.ll"), first.llvm);
+ writeFileSync(join(f.options.outDir, "main.ll"), first.llvm.join(""));
const warm = f.prepare();
- expect(warm).toEqual(first);
+ expect(text(warm)).toEqual(text(first));
expect(f.loads()).toBe(1);
writeFileSync(f.dependency, 'export const value = "second";\n');
const edited = f.prepare();
expect(edited.ok).toBe(true);
- if (edited.ok) expect(edited.llvm).not.toBe(first.llvm);
+ if (edited.ok) expect(edited.llvm.join("")).not.toBe(first.llvm.join(""));
expect(f.loads()).toBe(2);
});
+test("comment and whitespace edits reuse the frontend output a fresh build reproduces", () => {
+ const f = fixture();
+ const first = f.prepare();
+ expect(first.ok, JSON.stringify(first)).toBe(true);
+ if (!first.ok) return;
+ const commented = '// The exported value.\nexport const value = /* unchanged */ "first";\n';
+ writeFileSync(f.dependency, commented);
+ const reused = f.prepare();
+ expect(f.loads()).toBe(1);
+ expect(reused.ok).toBe(true);
+ if (!reused.ok) return;
+ expect(reused.llvm.join("")).toBe(first.llvm.join(""));
+ expect(new Map(reused.sources).get(f.dependency)).toBe(commented);
+ // The reused output is the one the edited sources lower to.
+ const fresh = prepareNativeExecutable(
+ f.entry,
+ f.options,
+ null,
+ fixtureToolchain(),
+ (path, npmStatic, externalTypes) => runFrontend(path, loadProgram, npmStatic, externalTypes),
+ new NativeCache(join(f.root, "fresh-cache")),
+ );
+ expect(fresh.ok && fresh.llvm.join("")).toBe(first.llvm.join(""));
+ // The refreshed entry still notices a later code edit.
+ writeFileSync(f.dependency, 'export const value = "second";\n');
+ const edited = f.prepare();
+ expect(f.loads()).toBe(2);
+ expect(edited.ok && edited.llvm.join("")).not.toBe(first.llvm.join(""));
+});
+
+test("a comment edit inside source text the output carries is rebuilt", () => {
+ const f = fixture();
+ // A failing assert(value) reports its call's source text.
+ const source = (call: string) =>
+ `import assert from "node:assert";\nexport const value = "first";\n${call};\n`;
+ writeFileSync(f.dependency, source("assert(value.length > 0)"));
+ const first = f.prepare();
+ expect(first.ok, JSON.stringify(first)).toBe(true);
+ if (!first.ok) return;
+ writeFileSync(f.dependency, source("assert(value.length > 0 /* checked */)"));
+ const edited = f.prepare();
+ expect(f.loads()).toBe(2);
+ expect(edited.ok && edited.llvm.join("")).not.toBe(first.llvm.join(""));
+});
+
+test("a whitespace edit that moves a reported source line is rebuilt", () => {
+ const f = fixture();
+ // node:test reports the file, line and column of each test.
+ const source = (gap: string) =>
+ `import { test } from "node:test";\nexport const value = "first";\n${gap}test("value", () => {});\n`;
+ writeFileSync(f.dependency, source(""));
+ const first = f.prepare();
+ expect(first.ok, JSON.stringify(first)).toBe(true);
+ if (!first.ok) return;
+ writeFileSync(f.dependency, source("\n"));
+ const edited = f.prepare();
+ expect(f.loads()).toBe(2);
+ expect(edited.ok && edited.llvm.join("")).not.toBe(first.llvm.join(""));
+});
+
test("structurally invalid cached features cause a rebuild even with a valid digest", () => {
const f = fixture();
expect(f.prepare().ok).toBe(true);
@@ -101,13 +171,13 @@ test("serialized IR travels as bounded parts through the frontend cache", () =>
expect(mod.functions.length).toBeGreaterThan(0);
const warm = f.prepare({ emitIr: true });
expect(f.loads()).toBe(1);
- expect(warm).toEqual(first);
+ expect(text(warm)).toEqual(text(first));
const family = join(f.cache.root, "frontend");
const key = readdirSync(family).find((name) => /^[0-9a-f]{64}$/.test(name))!;
const payload = JSON.parse(readFileSync(join(family, key), "utf8"));
expect(payload.schema).toBe("scriptc.native-frontend.v3");
expect(payload.input.llvm).toBe("");
expect(payload.input.ir).toBeNull();
- expect(payload.llvm).toBe(first.llvm);
+ expect(payload.llvm).toBe(first.llvm.join(""));
expect(payload.ir).toEqual(first.ir);
});
diff --git a/packages/compiler/src/native/prepare.ts b/packages/compiler/src/native/prepare.ts
index edc90c449..f7f399f80 100644
--- a/packages/compiler/src/native/prepare.ts
+++ b/packages/compiler/src/native/prepare.ts
@@ -7,7 +7,11 @@ import type {
CompileLibraryOptions,
CompileRequestOptions,
} from "../compile-types.js";
-import { emitLlvmModule, type ForkPrimitives, lowRegionTarget } from "../backend/llvm/emitter.js";
+import {
+ emitLlvmModuleParts,
+ type ForkPrimitives,
+ lowRegionTarget,
+} from "../backend/llvm/emitter.js";
import { inlineRcOption } from "../backend/llvm/shapes.js";
import { executableLinkFeatures } from "../backend/executable-features.js";
import type { NativeLinkFeatures } from "../backend/native-link-info-core.js";
@@ -16,26 +20,32 @@ import type { FfiProfile } from "../ffi/ffi-manifest.js";
import type { FrontendFactory } from "../frontend/pipeline.js";
import {
FrontendInputTracker,
+ frontendInputsSemanticallyMatch,
frontendInputsStillMatch,
validFrontendInputSnapshot,
type FrontendInputSnapshot,
type FrontendInputExclusions,
trackedReadFile,
} from "../frontend/input-tracker.js";
+import { compilerOwnedFileRoots } from "../frontend/dts-paths.js";
import { provenanceSources } from "../frontend/provenance-registry.js";
import { processFork } from "../frontend/process-fork.js";
+import { tokenEquivalentSource } from "../frontend/token-equivalence.js";
+import { llvmBytes, wtf8Bytes } from "../backend/literals.js";
import { prepareLibrary } from "../library/prepare.js";
import type { LibraryProfile } from "../library/library-profile.js";
import type { IrModule } from "../ir/ir.js";
import { deserializeModuleBytes, serializeModuleParts } from "../ir/serialize.js";
import { contentDigest, NATIVE_STRING_BYTES, type NativeCache } from "./cache.js";
-import { batchTextParts, textSlices } from "./text-file.js";
+import { batchTextParts, joinedParts, textSlices } from "./text-file.js";
import type { NativeToolchain } from "./toolchain.js";
import { nativeFileIdentity } from "./file-identity.js";
export interface NativeExecutableInput {
ok: true;
- llvm: string;
+ /** The module's LLVM text as parts that concatenate to it
+ * (emitLlvmModuleParts), never joined: a large program's is 150 MB. */
+ llvm: readonly string[];
/** Compact serialized IR as bounded parts (`batchTextParts`): compiler-scale
* modules exceed one native string. */
ir: string[] | null;
@@ -71,7 +81,8 @@ function* frontendCacheParts(
input: { ...input, llvm: "", ir: null },
});
yield metadata.slice(0, metadata.length - 1) + ',"llvm":"';
- for (const slice of textSlices(input.llvm)) yield JSON.stringify(slice).slice(1, -1);
+ for (const part of joinedParts(input.llvm))
+ for (const slice of textSlices(part)) yield JSON.stringify(slice).slice(1, -1);
yield '"';
if (input.ir === null) yield ',"ir":null}';
else {
@@ -111,18 +122,71 @@ function publishFrontendCache(
);
}
+/** The cached input when every frontend input still matches, else the
+ * entry the inputs no longer match, for reuse after equivalent edits. */
function readFrontendCache(
cache: NativeCache,
key: string,
exclusions: FrontendInputExclusions,
-): NativeExecutableInput | null {
+): { input: NativeExecutableInput } | { stale: FrontendCacheEntry } | null {
// The entry is decoded as one string; larger entries are misses.
const bytes = cache.read("frontend", key, NATIVE_STRING_BYTES);
if (bytes === null) return null;
const cached: unknown = JSON.parse(bytes.toString("utf8"));
- if (!validCachedInput(cached) || !frontendInputsStillMatch(cached.probes, exclusions))
+ if (!validCachedInput(cached)) return null;
+ if (!frontendInputsStillMatch(cached.probes, exclusions)) return { stale: cached };
+ return { input: { ...cached.input, llvm: [cached.llvm], ir: cached.ir } };
+}
+
+/** The stale entry's input when every changed source is token-equivalent to
+ * the text the entry was built from (edits to whitespace and to comments
+ * that cannot affect compilation; see token-equivalence.ts). Equivalent
+ * sources lower to the same IR apart from source locations, which only dev
+ * builds' debug information and the serialized IR carry. Diagnostics carry
+ * locations too, so an entry that reported warnings is rebuilt. A changed
+ * source whose raw text or line positions the frontend read (call-site text
+ * in messages, comptime callbacks, test locations) is rebuilt as well:
+ * frontendInputsSemanticallyMatch refuses its `text` probe. */
+function equivalentFrontendInput(
+ stale: FrontendCacheEntry,
+ options: CompileRequestOptions,
+ exclusions: FrontendInputExclusions,
+): { input: NativeExecutableInput; probes: FrontendInputSnapshot } | null {
+ if (
+ options.optimization === "dev" ||
+ options.sanitize === true ||
+ options.emitIr === true ||
+ stale.ir !== null ||
+ (stale.input.warnings?.length ?? 0) > 0
+ )
return null;
- return { ...cached.input, llvm: cached.llvm, ir: cached.ir };
+ const match = frontendInputsSemanticallyMatch(
+ stale.probes,
+ new Map(stale.input.sources),
+ tokenEquivalentSource,
+ exclusions,
+ );
+ if (match === null || match.changed.length === 0) return null;
+ // Runtime fences and unsupported-construct messages name their source as
+ // `[ at :]`, and an equivalent edit can move the line.
+ if (
+ match.changed.some(({ path }) =>
+ stale.llvm.includes(` at ${llvmBytes(wtf8Bytes(path), false)}:`),
+ )
+ )
+ return null;
+ return {
+ input: {
+ ...stale.input,
+ llvm: [stale.llvm],
+ ir: null,
+ sources: stale.input.sources.map(([path, text]) => [
+ path,
+ match.currentSources.get(path) ?? text,
+ ]),
+ },
+ probes: match.snapshot,
+ };
}
function implementationIdentity(toolchain: NativeToolchain): string {
@@ -284,10 +348,19 @@ export function prepareNativeExecutable(
.sort(([a], [b]) => a.localeCompare(b)),
}),
);
- const input = readFrontendCache(cache, key, exclusions);
- if (input !== null) {
+ const cached = readFrontendCache(cache, key, exclusions);
+ if (cached !== null && "input" in cached) {
timing("frontend-cache-hit");
- return input;
+ return cached.input;
+ }
+ const equivalent =
+ cached === null || outputKind === "ir"
+ ? null
+ : equivalentFrontendInput(cached.stale, options, exclusions);
+ if (equivalent !== null) {
+ publishFrontendCache(cache, key, equivalent.probes, equivalent.input);
+ timing("frontend-cache-equivalent");
+ return equivalent.input;
}
} catch {
key = null;
@@ -305,13 +378,13 @@ export function prepareNativeExecutable(
if (irInput !== "") timing("ir-input");
const ir =
outputKind === "ir" || options.emitIr
- ? batchTextParts(serializeModuleParts(prepared.mod))
+ ? batchTextParts(serializeModuleParts(prepared.mod, compilerOwnedFileRoots()))
: null;
timing("ir-serialize");
const llvm =
outputKind === "ir"
- ? ""
- : emitLlvmModule(prepared.mod, {
+ ? []
+ : emitLlvmModuleParts(prepared.mod, {
targetTriple: toolchain.target.llvmTriple,
pointerBits: toolchain.target.pointerBits,
wasi: toolchain.target.platform === "wasi",
@@ -399,10 +472,10 @@ export function prepareNativeLibrary(
.sort(([a], [b]) => a.localeCompare(b)),
}),
);
- const input = readFrontendCache(cache, key, exclusions);
- if (input !== null) {
+ const cached = readFrontendCache(cache, key, exclusions);
+ if (cached !== null && "input" in cached) {
timing("frontend-cache-hit");
- return input;
+ return cached.input;
}
} catch {
key = null;
@@ -427,7 +500,7 @@ export function prepareNativeLibrary(
);
});
if (!prepared.ok) return prepared;
- const llvm = emitLlvmModule(prepared.mod, {
+ const llvm = emitLlvmModuleParts(prepared.mod, {
targetTriple: toolchain.target.llvmTriple,
pointerBits: toolchain.target.pointerBits,
wasi: toolchain.target.platform === "wasi",
@@ -438,7 +511,9 @@ export function prepareNativeLibrary(
const input: NativeExecutableInput = {
ok: true,
llvm,
- ir: options.emitIr ? batchTextParts(serializeModuleParts(prepared.mod)) : null,
+ ir: options.emitIr
+ ? batchTextParts(serializeModuleParts(prepared.mod, compilerOwnedFileRoots()))
+ : null,
sidecarJson: prepared.sidecarJson,
features: executableLinkFeatures(prepared.mod),
sources: [...prepared.sourceTexts],
diff --git a/packages/compiler/src/native/text-file.test.ts b/packages/compiler/src/native/text-file.test.ts
index b497bc4a2..7534b5058 100644
--- a/packages/compiler/src/native/text-file.test.ts
+++ b/packages/compiler/src/native/text-file.test.ts
@@ -2,7 +2,7 @@ import { mkdtempSync, readFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, expect, test } from "vitest";
-import { textSlices, writeTextPartsSync } from "./text-file.js";
+import { joinedParts, textSlices, writeTextPartsSync } from "./text-file.js";
const directories: string[] = [];
afterEach(() => {
@@ -40,3 +40,13 @@ test("a part larger than one slice is written as its UTF-8 bytes", () => {
expect(seen.join("")).toBe("head," + large + "tail");
expect(Math.max(...seen.map((slice) => slice.length))).toBeLessThanOrEqual(1024 * 1024);
});
+
+test("small parts join into batches and a long part passes through whole", () => {
+ const small = Array.from({ length: 3000 }, (_, index) => `line ${index}\n`);
+ const long = "x".repeat(2 * 1024 * 1024);
+ const parts = [...small, long, "tail"];
+ const joined = [...joinedParts(parts)];
+ expect(joined.join("")).toBe(parts.join(""));
+ expect(joined.length).toBeLessThan(5);
+ expect(joined).toContain("tail");
+});
diff --git a/packages/compiler/src/native/text-file.ts b/packages/compiler/src/native/text-file.ts
index d377c5273..8b0953120 100644
--- a/packages/compiler/src/native/text-file.ts
+++ b/packages/compiler/src/native/text-file.ts
@@ -42,6 +42,24 @@ export function* textSlices(text: string, length = TEXT_WRITE_LENGTH): Generator
}
}
+/** `parts` with consecutive parts joined into strings of about
+ * TEXT_WRITE_LENGTH UTF-16 units (a longer part passes through whole), so
+ * writing or escaping many small parts takes few calls. */
+export function* joinedParts(parts: readonly string[]): Generator {
+ let batch: string[] = [];
+ let length = 0;
+ for (const part of parts) {
+ batch.push(part);
+ length += part.length;
+ if (length >= TEXT_WRITE_LENGTH) {
+ yield batch.length === 1 ? batch[0]! : batch.join("");
+ batch = [];
+ length = 0;
+ }
+ }
+ if (batch.length > 0) yield batch.join("");
+}
+
/** Encodes and writes `part` to `fd` in slices (`textSlices`), so a large
* part is never encoded whole. `written` sees each slice after it is
* written. */
@@ -55,8 +73,8 @@ function writeTextPart(fd: number, part: string, written?: (slice: string) => vo
}
}
-/** Write text parts to `path` in order without joining them first; pass
- * batched parts (`batchTextParts`) for many small ones. */
+/** Write text parts to `path` in order without joining them whole: small
+ * parts are written in batches (`joinedParts`). */
export function writeTextPartsSync(
path: string,
parts: readonly string[],
@@ -64,7 +82,7 @@ export function writeTextPartsSync(
): void {
const fd = openSync(path, "w");
try {
- for (const part of parts) writeTextPart(fd, part, written);
+ for (const part of joinedParts(parts)) writeTextPart(fd, part, written);
} finally {
closeSync(fd);
}
diff --git a/packages/compiler/src/native/toolchain.test.ts b/packages/compiler/src/native/toolchain.test.ts
index 0ee434300..ad4570c0c 100644
--- a/packages/compiler/src/native/toolchain.test.ts
+++ b/packages/compiler/src/native/toolchain.test.ts
@@ -99,11 +99,11 @@ test("native toolchain assets resolve from the physical manifest through package
const direct = loadNativeToolchain(path);
const viaLink = loadNativeToolchain(join(linked, "bin", "scriptc.exe.json"));
expect(viaLink).toEqual(direct);
- expect(viaLink.ts7Executable).toBe(join(assets, "typescript/lib/tsc.exe"));
+ expect(viaLink.ts7Executable).toBe(join(assets, "tsc-ts/lib/tsc-ts.exe"));
expect(
- loadNativeToolchain(join(linked, "bin", "scriptc.exe.json"), { SCRIPTC_CHECKER: "tsc-ts" })
+ loadNativeToolchain(join(linked, "bin", "scriptc.exe.json"), { SCRIPTC_CHECKER: "tsgo" })
.ts7Executable,
- ).toBe(join(assets, "tsc-ts/lib/tsc-ts.exe"));
+ ).toBe(join(assets, "typescript/lib/tsc.exe"));
expect(viaLink.helperExecutable).toBe(join(assets, "llvm/bin/scriptc-llvm-codegen.exe"));
expect(
loadNativeToolchain(join(linked, "bin", "scriptc.exe.json"), {
diff --git a/packages/compiler/src/native/toolchain.ts b/packages/compiler/src/native/toolchain.ts
index fbcd94251..5abbb329e 100644
--- a/packages/compiler/src/native/toolchain.ts
+++ b/packages/compiler/src/native/toolchain.ts
@@ -177,11 +177,11 @@ export function loadNativeToolchain(
: [...target.defaultLinkerArgs];
return {
compilerVersion: manifest.compiler_version,
- // tsc-ts, the default checker except on Windows, comes with the distribution;
- // a development build takes it from the tsc-ts package installed with the
- // compiler or for the working directory. SCRIPTC_CHECKER selects a checker.
+ // tsc-ts, the default checker, comes with the distribution; a development
+ // build takes it from the tsc-ts package installed with the compiler or for
+ // the working directory. SCRIPTC_CHECKER selects a checker.
ts7Executable:
- checkerBackend(env, host.platform) === "tsgo"
+ checkerBackend(env) === "tsgo"
? pathFrom(root, manifest.ts7)
: manifest.tsc_ts !== undefined
? pathFrom(root, manifest.tsc_ts)
diff --git a/packages/compiler/test/array-index-product-emission.test.ts b/packages/compiler/test/array-index-product-emission.test.ts
new file mode 100644
index 000000000..bfd891398
--- /dev/null
+++ b/packages/compiler/test/array-index-product-emission.test.ts
@@ -0,0 +1,55 @@
+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 } from "../src/index.js";
+
+async function emitLlvm(source: string): Promise {
+ const dir = await mkdtemp(join(tmpdir(), "scriptc-index-product-"));
+ try {
+ const entry = join(dir, "main.ts");
+ const outPath = join(dir, "main.ll");
+ await writeFile(entry, source);
+ const result = await compile(entry, { outDir: dir, outPath, outputKind: "llvm" });
+ expect(result.ok, JSON.stringify(result.diagnostics)).toBe(true);
+ return await readFile(outPath, "utf8");
+ } finally {
+ await rm(dir, { recursive: true, force: true });
+ }
+}
+
+const source = `
+function at(a: number[], x: number, y: number, k: number): number | undefined {
+ return a[x * y + k];
+}
+function cancel(a: number[], p: number, q: number, r: number, s: number): number | undefined {
+ return a[p * q - r * s];
+}
+const a = [1, 2, 3];
+const v = process.argv.length;
+console.log(at(a, v | 0, (v + 1) | 0, -v | 0), cancel(a, v | 0, v | 0, v | 0, v | 0));
+`;
+
+function body(llvm: string, name: string): string {
+ return new RegExp(`^define [^@]*@sc_\\w+?_${name}(\\.i32)?\\([^]*?^}`, "m").exec(llvm)?.[0] ?? "";
+}
+
+test("an int32 product plus an int32 indexes as an exact i64 without a round trip", async () => {
+ const llvm = await emitLlvm(source);
+ // The int32 entry: the index is the i64 product and sum, compared with
+ // the array's counts; the double goes only to the slow path's runtime call.
+ const at = /^define [^@]*@sc_bf_at\.i32\([^]*?^}/m.exec(llvm)?.[0] ?? "";
+ const product = /(%\S+) = mul nsw i64 %\S+, %\S+/.exec(at)?.[1];
+ expect(product).toBeDefined();
+ expect(at).toContain(`= add nsw i64 ${product}, `);
+ expect(at).not.toMatch(/fptosi double %\S+ to i64/);
+});
+
+test("a difference of two int32 products keeps the double's round trip", async () => {
+ const llvm = await emitLlvm(source);
+ // Each product may pass 2^53, where its double rounds before the
+ // difference: the index keeps the exactness test of the double.
+ const cancel = body(llvm, "cancel");
+ expect(cancel).toMatch(/fptosi double %\S+ to i64/);
+ expect(cancel).toMatch(/fcmp oeq double/);
+});
diff --git a/packages/compiler/test/int32-element-array-emission.test.ts b/packages/compiler/test/int32-element-array-emission.test.ts
new file mode 100644
index 000000000..0b68a5d3e
--- /dev/null
+++ b/packages/compiler/test/int32-element-array-emission.test.ts
@@ -0,0 +1,198 @@
+import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { expect, test } from "vitest";
+import { compile, deserializeModule } from "../src/index.js";
+import type { IrModule } from "../src/ir/ir.js";
+import { markInt32ArrayCandidates, selectInt32Arrays } from "../src/ir/int32-arrays.js";
+
+async function emitLlvm(source: string): Promise {
+ const dir = await mkdtemp(join(tmpdir(), "scriptc-int32-elements-"));
+ try {
+ const entry = join(dir, "main.ts");
+ const outPath = join(dir, "main.ll");
+ await writeFile(entry, source);
+ const result = await compile(entry, { outDir: dir, outPath, outputKind: "llvm" });
+ expect(result.ok, JSON.stringify(result.diagnostics)).toBe(true);
+ return await readFile(outPath, "utf8");
+ } finally {
+ await rm(dir, { recursive: true, force: true });
+ }
+}
+
+async function lowerIr(source: string): Promise {
+ const dir = await mkdtemp(join(tmpdir(), "scriptc-int32-elements-"));
+ 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" });
+ expect(result.ok, JSON.stringify(result.diagnostics)).toBe(true);
+ return deserializeModule(await readFile(outPath, "utf8"));
+ } finally {
+ await rm(dir, { recursive: true, force: true });
+ }
+}
+
+const source = `
+function compose(n: number, rounds: number): number {
+ const p: number[] = [];
+ const q: number[] = [];
+ for (let i = 0; i < n; i++) p.push((i * 7919 + 3) & (n - 1));
+ for (let i = 0; i < n; i++) q.push(i);
+ let check = 0;
+ for (let r = 0; r < rounds; r++) {
+ for (let i = 0; i < n; i++) q[i] = p[q[i]!]!;
+ check = (check + q[r % n]!) | 0;
+ }
+ return check;
+}
+function counts(n: number): number {
+ const c = new Array(n).fill(0) as number[];
+ for (let i = 0; i < n; i++) c[(i * 5) % n] = i;
+ while (c.length > 1) c.pop();
+ return c[0]!;
+}
+function fraction(n: number): number {
+ const a = [1, 2, 3];
+ a[1] = n / 3;
+ return a[1]!;
+}
+function negativeZero(): number {
+ const a = [1, -0];
+ return 1 / a[1]!;
+}
+function counter(n: number): number {
+ const a = new Array(n).fill(0) as number[];
+ for (let i = 0; i < 1000; i++) a[i % n] = a[i % n]! + 1;
+ return a[0]!;
+}
+function shortCounter(): number {
+ const a = new Array(16).fill(0) as number[];
+ for (let i = 0; i < 1000; i++) a[i % 16] = a[i % 16]! + 1;
+ return a[0]!;
+}
+function literalCounter(n: number): number {
+ const a = [0];
+ for (let i = 0; i < n; i++) a[0] = a[0]! + 1;
+ return a[0]!;
+}
+function joined(n: number): string {
+ const a = [n | 0, 2];
+ return a.join(",");
+}
+const { n } = JSON.parse('{"n": 8}') as { n: number };
+console.log(
+ compose(n | 0, 3),
+ counts(n | 0),
+ fraction(n),
+ negativeZero(),
+ counter(n),
+ shortCounter(),
+ literalCounter(n),
+ joined(n),
+);
+`;
+
+/** Every function emitted for the source function \`name\` (its int32 ABI
+ * body and its wrapper included). */
+function functions(llvm: string, name: string): string {
+ const pattern = new RegExp(`^define [^@]*@sc_b?f_${name}[.\\w]*\\([^]*?^}`, "gm");
+ return [...llvm.matchAll(pattern)].map((m) => m[0]).join("\n");
+}
+
+test("arrays storing only int32 elements get 4-byte slots", async () => {
+ const llvm = await emitLlvm(source);
+ const compose = functions(llvm, "compose");
+ expect(compose).not.toBe("");
+ // Both arrays are allocated as SCR_ELEM_I32 (7).
+ expect(compose.match(/@scr_arr_new_small\(i32 7\)/g)?.length).toBe(2);
+ // Reads load an i32 slot; writes store one.
+ expect(compose).toMatch(/getelementptr inbounds i32, ptr %\w+, i64 %\w+\n\s+%\w+ = load i32/);
+ expect(compose).toMatch(/store i32 %\w+, ptr %\w+, !tbaa/);
+ // The element read through the helper's ternary stays in a stack box.
+ expect(compose).not.toContain("@scr_union_new_f64");
+ expect(compose).not.toMatch(/call [^\n]*@sc_b?f__x25_arr_idxOr/);
+ // The fill form and a discarded pop.
+ const counts = functions(llvm, "counts");
+ expect(counts).toMatch(/@scr_arr_new\(i32 7, i64 0\)/);
+ expect(counts).not.toContain("popArray");
+});
+
+test("arrays with a store that has no exact integer range, or that escape, keep doubles", async () => {
+ const llvm = await emitLlvm(source);
+ for (const name of ["fraction", "negativeZero", "joined"]) {
+ const fn = functions(llvm, name);
+ expect(fn, name).not.toBe("");
+ expect(fn, name).not.toMatch(/@scr_arr_new(_small)?\(i32 7/);
+ expect(fn, name).not.toContain("arr.kind.");
+ }
+});
+
+test("a counter array keeps int32 slots with checked stores", async () => {
+ const llvm = await emitLlvm(source);
+ const counter = functions(llvm, "counter");
+ // `a[k] = a[k]! + 1` is an exact integer but no int32 proof: a fill of at
+ // least 4096 elements creates SCR_ELEM_I32 storage (SCR_ELEM_F64 for a
+ // shorter one), accesses test the kind, and a store widens the array
+ // before a value that is not an int32.
+ expect(counter).toMatch(
+ /fcmp oge double %\w+, [^\n]+\n\s+%\w+ = select i1 %\w+, i32 7, i32 0\n\s+%\w+ = call ptr @scr_arr_new(_small)?\(i32 %\w+/,
+ );
+ expect(counter).toMatch(/load i8, ptr %\w+[^\n]*\n\s+%\w+ = icmp eq i8 %\w+, 7/);
+ expect(counter).toContain("@llvm.fptosi.sat.i32.f64");
+ expect(counter).toContain("call void @scr_arr_widen_i32(");
+ // Arrays with proven stores take no kind test, and a fill of a constant
+ // short length or an array literal keeps doubles.
+ for (const name of ["compose", "counts", "shortCounter", "literalCounter"]) {
+ const fn = functions(llvm, name);
+ expect(fn, name).not.toContain("arr.kind.");
+ expect(fn, name).not.toContain("kind.fast");
+ expect(fn, name).not.toContain("scr_arr_widen_i32");
+ }
+ for (const name of ["shortCounter", "literalCounter"])
+ expect(functions(llvm, name), name).not.toMatch(/@scr_arr_new(_small)?\(i32 7/);
+});
+
+test("a loop over a counter array tests its kind once on entry", async () => {
+ const llvm = await emitLlvm(source);
+ const counter = functions(llvm, "counter");
+ // The loop's fast copy runs while the array holds int32 slots, with no
+ // kind test of its own: the sum is an integer, and a sum past the int32
+ // range leaves for the third copy, which re-executes the store with kind
+ // tests and widens the array.
+ expect(counter).toMatch(
+ /icmp eq i8 %\w+, 7\n\s+br i1 %\w+, label %kind\.fast\d+, label %kind\.wide/,
+ );
+ const rest = counter.slice(counter.search(/^kind\.fast\d+:/m));
+ // The fast copy is the last one, before the loop's exit.
+ const fast = rest.slice(0, rest.search(/^loop\.e\d+:/m));
+ expect(fast).not.toBe("");
+ expect(fast).not.toMatch(/icmp eq i8 %\w+, 7/);
+ expect(fast).toMatch(/add nsw i64 %\w+, 1/);
+ expect(fast).toMatch(
+ /icmp sle i64 %\w+, 2147483647\n[^\n]*\n\s+br i1 %\w+, label %arr\.store\.i32\d+, label %kind\.resume\d+/,
+ );
+ expect(fast).toMatch(/store i32 %\w+, ptr %\w+, !tbaa/);
+ expect(counter).toMatch(/^kind\.resume\d+:/m);
+});
+
+test("only the candidates the proof keeps are rewritten", async () => {
+ const mod = await lowerIr(source);
+ const proposed = markInt32ArrayCandidates(mod).int32ArrayCandidates ?? [];
+ expect(proposed).toEqual(
+ expect.arrayContaining(["compose\0p.0", "compose\0q.0", "counts\0c.0", "counter\0a.0"]),
+ );
+ // An array that reaches `join` is never proposed.
+ expect(proposed.some((key) => key.startsWith("joined\0"))).toBe(false);
+ // Without a kept candidate, the module is the input itself.
+ expect(selectInt32Arrays(mod, () => new Set())).toBe(mod);
+ const kept = new Set(["compose\0p.0", "compose\0q.0"]);
+ const selected = selectInt32Arrays(
+ mod,
+ (marked) => new Set((marked.int32ArrayCandidates ?? []).filter((key) => kept.has(key))),
+ );
+ expect(selected.int32ArrayCandidates).toEqual([...kept]);
+ for (const fn of selected.functions)
+ if (fn.name !== "compose") expect(fn).toBe(mod.functions.find((f) => f.name === fn.name));
+});
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7037-js-var-open-writes/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7037-js-var-open-writes/main.ts.json
new file mode 100644
index 000000000..a1bdb91b8
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7037-js-var-open-writes/main.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/7037-js-var-open-writes/main.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7038-js-var-later-writes.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7038-js-var-later-writes.mjs.json
new file mode 100644
index 000000000..1e51a088f
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7038-js-var-later-writes.mjs.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/7038-js-var-later-writes.mjs"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7060-array-numeric-keys.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7060-array-numeric-keys.cjs.json
new file mode 100644
index 000000000..5fff9651a
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7060-array-numeric-keys.cjs.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/7060-array-numeric-keys.cjs"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7061-array-object-keys.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7061-array-object-keys.ts.json
new file mode 100644
index 000000000..b2830e983
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7061-array-object-keys.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/7061-array-object-keys.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7062-inspect-array-properties.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7062-inspect-array-properties.ts.json
new file mode 100644
index 000000000..891c4ffe0
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7062-inspect-array-properties.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/7062-inspect-array-properties.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7325-array-index-products-near-2-53.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7325-array-index-products-near-2-53.ts.json
new file mode 100644
index 000000000..3239c26bf
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7325-array-index-products-near-2-53.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/7325-array-index-products-near-2-53.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7326-int32-element-arrays.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7326-int32-element-arrays.ts.json
new file mode 100644
index 000000000..f00823a6e
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7326-int32-element-arrays.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/7326-int32-element-arrays.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7327-int32-array-widening.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7327-int32-array-widening.ts.json
new file mode 100644
index 000000000..050b6a9ac
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7327-int32-array-widening.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/7327-int32-array-widening.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7364-new-target-late-instantiations.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7364-new-target-late-instantiations.ts.json
new file mode 100644
index 000000000..20d761c6c
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7364-new-target-late-instantiations.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/7364-new-target-late-instantiations.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7402-unknown-array-methods.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7402-unknown-array-methods.ts.json
new file mode 100644
index 000000000..db33da675
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7402-unknown-array-methods.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/7402-unknown-array-methods.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7441-untyped-receiver-own-string-method-names.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7441-untyped-receiver-own-string-method-names.ts.json
new file mode 100644
index 000000000..7477193ef
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7441-untyped-receiver-own-string-method-names.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/7441-untyped-receiver-own-string-method-names.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7480-builtin-constructor-in.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7480-builtin-constructor-in.js.json
new file mode 100644
index 000000000..efb9dd725
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7480-builtin-constructor-in.js.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/7480-builtin-constructor-in.js"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7480-class-call-without-new.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7480-class-call-without-new.ts.json
new file mode 100644
index 000000000..8e5ed83bc
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7480-class-call-without-new.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/7480-class-call-without-new.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7481-class-call-without-new.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7481-class-call-without-new.mjs.json
new file mode 100644
index 000000000..7f55a233d
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7481-class-call-without-new.mjs.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/7481-class-call-without-new.mjs"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7481-class-runtime-heritage-object-error.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7481-class-runtime-heritage-object-error.js.json
new file mode 100644
index 000000000..d0d390662
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7481-class-runtime-heritage-object-error.js.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/7481-class-runtime-heritage-object-error.js"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7482-define-property-function-class-values.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7482-define-property-function-class-values.js.json
new file mode 100644
index 000000000..d1d97ba4f
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7482-define-property-function-class-values.js.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/7482-define-property-function-class-values.js"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7483-constructor-factory-traits.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7483-constructor-factory-traits.js.json
new file mode 100644
index 000000000..c3fa9964f
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7483-constructor-factory-traits.js.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/7483-constructor-factory-traits.js"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7484-hoisted-function-captures-later-class.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7484-hoisted-function-captures-later-class.js.json
new file mode 100644
index 000000000..515b9bf97
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7484-hoisted-function-captures-later-class.js.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/7484-hoisted-function-captures-later-class.js"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7485-global-object-own-properties.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7485-global-object-own-properties.js.json
new file mode 100644
index 000000000..aa1affb3c
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7485-global-object-own-properties.js.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/7485-global-object-own-properties.js"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7486-prototype-accessor-string-method-names.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7486-prototype-accessor-string-method-names.js.json
new file mode 100644
index 000000000..0cf87c443
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7486-prototype-accessor-string-method-names.js.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/7486-prototype-accessor-string-method-names.js"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7487-namespace-spread-untyped/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7487-namespace-spread-untyped/main.js.json
new file mode 100644
index 000000000..b8a66c735
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7487-namespace-spread-untyped/main.js.json
@@ -0,0 +1,9 @@
+{
+ "order": [
+ "/tests/corpus/7487-namespace-spread-untyped/shapes.js",
+ "/tests/corpus/7487-namespace-spread-untyped/extra.js",
+ "/tests/corpus/7487-namespace-spread-untyped/all.js",
+ "/tests/corpus/7487-namespace-spread-untyped/main.js"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7488-math-spread-untyped.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7488-math-spread-untyped.js.json
new file mode 100644
index 000000000..4d1376a12
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7488-math-spread-untyped.js.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/7488-math-spread-untyped.js"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7489-math-spread-generator-only.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7489-math-spread-generator-only.js.json
new file mode 100644
index 000000000..5277102eb
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7489-math-spread-generator-only.js.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/7489-math-spread-generator-only.js"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7540-undefined-element-index-keys.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7540-undefined-element-index-keys.ts.json
new file mode 100644
index 000000000..528758c4e
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7540-undefined-element-index-keys.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/7540-undefined-element-index-keys.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7560-new-target-constructors.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7560-new-target-constructors.ts.json
new file mode 100644
index 000000000..b4426af2c
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7560-new-target-constructors.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/7560-new-target-constructors.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7561-new-target-generic-registry.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7561-new-target-generic-registry.ts.json
new file mode 100644
index 000000000..dc4eab819
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7561-new-target-generic-registry.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/7561-new-target-generic-registry.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7602-error-stack-limit-value.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7602-error-stack-limit-value.js.json
new file mode 100644
index 000000000..99e1b8904
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7602-error-stack-limit-value.js.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/7602-error-stack-limit-value.js"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7603-error-stack-limit-own.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7603-error-stack-limit-own.js.json
new file mode 100644
index 000000000..5ed656fa9
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7603-error-stack-limit-own.js.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/7603-error-stack-limit-own.js"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7604-error-stack-limit-frozen.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7604-error-stack-limit-frozen.js.json
new file mode 100644
index 000000000..a437ff57e
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7604-error-stack-limit-frozen.js.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/7604-error-stack-limit-frozen.js"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/class-call-shadowed-static.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/class-call-shadowed-static.ts.json
new file mode 100644
index 000000000..6c87e5f66
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/class-call-shadowed-static.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/diagnostics/class-call-shadowed-static.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-late-local-subclass.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-late-local-subclass.ts.json
new file mode 100644
index 000000000..7046e28d5
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-late-local-subclass.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/diagnostics/new-target-late-local-subclass.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-local-subclass.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-local-subclass.ts.json
new file mode 100644
index 000000000..4f597a8a2
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-local-subclass.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/diagnostics/new-target-local-subclass.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-plain-function.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-plain-function.ts.json
new file mode 100644
index 000000000..9f424457c
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-plain-function.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/diagnostics/new-target-plain-function.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-value-generic-subclass.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-value-generic-subclass.ts.json
new file mode 100644
index 000000000..1abcce3ad
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-value-generic-subclass.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/diagnostics/new-target-value-generic-subclass.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-value-signatures.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-value-signatures.ts.json
new file mode 100644
index 000000000..249f8f4bd
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-value-signatures.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/diagnostics/new-target-value-signatures.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/jsdoc-unresolved-generic-return/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/jsdoc-unresolved-generic-return/main.ts.json
new file mode 100644
index 000000000..5395e20be
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/jsdoc-unresolved-generic-return/main.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/fixtures/npm/cases/jsdoc-unresolved-generic-return/main.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/runtime/src/scr_array.c b/packages/runtime/src/scr_array.c
index f3e216a36..91328a1e8 100644
--- a/packages/runtime/src/scr_array.c
+++ b/packages/runtime/src/scr_array.c
@@ -176,7 +176,22 @@ static size_t scr_arr_prop_index(const ScrArr *a, const char *key,
return SIZE_MAX;
}
+#define SCR_ARR_UNDEFINED_KEY_BITS UINT64_C(0x7ff8000000756e64)
+
+double scr_arr_undefined_key(void) {
+ uint64_t bits = SCR_ARR_UNDEFINED_KEY_BITS;
+ double key;
+ memcpy(&key, &bits, sizeof key);
+ return key;
+}
+
static size_t scr_arr_prop_key(double i, char *buf) {
+ uint64_t bits;
+ memcpy(&bits, &i, sizeof bits);
+ if (bits == SCR_ARR_UNDEFINED_KEY_BITS) {
+ memcpy(buf, "undefined", sizeof "undefined");
+ return sizeof "undefined" - 1;
+ }
return scr_f64_to_str(i, buf);
}
@@ -277,6 +292,7 @@ typedef struct {
static uint64_t scr_arr_storage_get(const ScrArrStorage *s, size_t index) {
if (s->elem == SCR_ELEM_BOOL) return ((const uint8_t *)s->data)[index];
if (s->elem == SCR_ELEM_CREF) return scr_cref_decode(((const uint32_t *)s->data)[index]);
+ if (s->elem == SCR_ELEM_I32) return scr_arr_f64_slot((double)((const int32_t *)s->data)[index]);
return s->data[index];
}
@@ -578,6 +594,36 @@ static void scr_arr_move_inline(ScrArr *a, size_t cap) {
a->spare &= (uint16_t)~SCR_ARR_INLINE_SLOTS;
}
+/* Widen an int32 array (SCR_ELEM_I32) to doubles in place, one way: its
+ * dense values move to 8-byte slots, with their states when it has them, and
+ * its kind becomes SCR_ELEM_F64. Compiled code calls it before storing a
+ * value that is not an int32 into an int32 array with checked stores; `data`
+ * may move. Sparse entries already hold doubles. */
+void scr_arr_widen_i32(ScrArr *a) {
+ if (a->elem != SCR_ELEM_I32) return;
+ size_t cap = a->cap;
+ if (cap > 0) {
+ /* Int32 arrays never shift, so they have no head room; inline slots
+ * have no ext prefix. */
+ bool inline_slots = scr_arr_inline(a);
+ bool states = scr_arr_has_states(a);
+ size_t unit = sizeof(uint64_t) + (states ? 1 : 0);
+ size_t prefix = inline_slots ? 0 : scr_arr_prefix(a);
+ if (cap > (SIZE_MAX - prefix) / unit) scr_arr_oom();
+ char *start = scr_mem_alloc(prefix + cap * unit);
+ if (!start) scr_arr_oom();
+ if (prefix) memcpy(start, scr_arr_alloc_start(a), prefix);
+ uint64_t *data = (uint64_t *)(start + prefix);
+ const int32_t *old = (const int32_t *)a->data;
+ for (size_t i = 0; i < cap; i++) data[i] = scr_arr_f64_slot((double)old[i]);
+ if (states) memcpy((uint8_t *)data + cap * sizeof(uint64_t), scr_arr_states_end(a) - cap, cap);
+ if (!inline_slots && a->rc != SIZE_MAX) scr_mem_free(scr_arr_alloc_start(a));
+ a->data = data;
+ a->spare &= (uint16_t)~SCR_ARR_INLINE_SLOTS;
+ }
+ a->elem = SCR_ELEM_F64;
+}
+
/* How a dense allocation is sized. */
typedef enum {
SCR_ARR_GROW_APPEND, /* geometric headroom for further appends */
@@ -621,7 +667,7 @@ static void scr_arr_grow_dense_slow(ScrArr *a, size_t need, ScrArrGrow mode);
* paths pick the same capacity. */
static inline void scr_arr_grow_dense_to(ScrArr *a, size_t need, ScrArrGrow mode) {
if (need <= a->cap) return;
- if (a->elem == SCR_ELEM_BOOL || a->elem == SCR_ELEM_CREF ||
+ if (a->elem == SCR_ELEM_BOOL || a->elem == SCR_ELEM_CREF || a->elem == SCR_ELEM_I32 ||
(a->spare & (SCR_ARR_INLINE_SLOTS | SCR_ARR_HEAD_ROOM | SCR_ARR_HAS_EXT)) || need > SCR_ARR_DENSE_LIMIT) {
scr_arr_grow_dense_slow(a, need, mode);
return;
@@ -1904,6 +1950,10 @@ static ScrArr *scr_arr_fill_slot(ScrArr *a, uint64_t slot, uint8_t state,
if (state != SCR_ARR_VALUE) scr_arr_cut(a, from);
if (a->elem == SCR_ELEM_BOOL) memset(scr_arr_dense_at(a, from), (int)slot, until - from);
else if (a->elem == SCR_ELEM_CREF) for (size_t i = from; i < until; i++) scr_arr_dense_put(a, i, slot);
+ else if (a->elem == SCR_ELEM_I32) {
+ int32_t v = scr_arr_i32_of(slot);
+ for (size_t i = from; i < until; i++) ((int32_t *)a->data)[i] = v;
+ }
else for (size_t i = from; i < until; i++) a->data[i] = slot;
if (scr_arr_has_states(a)) memset(scr_arr_state_run(a, from, until - from), state, until - from);
} else {
@@ -2007,7 +2057,7 @@ static uint64_t scr_arr_require_slot(ScrArr *a, double i) {
if (scr_arr_prop_get_state(a, i, &prop, &state)) {
if (state == SCR_ARR_UNDEFINED) {
char buf[32];
- scr_f64_to_str(i, buf);
+ scr_arr_prop_key(i, buf);
scr_trap_fmt("scriptc: RangeError: numeric property %s is undefined\n", buf);
}
return prop;
@@ -5116,7 +5166,15 @@ ScrStr *scr_str_raw(ScrArr *raw, ScrArr *subs) {
bool scr_arr_numeric_key(const char *key, size_t len, double *out) {
char text[64];
+ if (len == sizeof "undefined" - 1 && memcmp(key, "undefined", len) == 0) {
+ *out = scr_arr_undefined_key();
+ return true;
+ }
if (len == 0 || len >= sizeof text) return false;
+ /* A number's spelling starts with a digit, '-', "NaN" or "Infinity":
+ * other names answer without parsing. */
+ char first = key[0];
+ if (!(first >= '0' && first <= '9') && first != '-' && first != 'N' && first != 'I') return false;
memcpy(text, key, len);
text[len] = '\0';
char *end;
@@ -5130,6 +5188,11 @@ bool scr_arr_numeric_key(const char *key, size_t len, double *out) {
return true;
}
+bool scr_arr_has_properties(const ScrArr *a) {
+ const ScrArrExt *x = scr_arr_ext_of(a);
+ return x && (x->prop_len || x->metadata);
+}
+
void scr_arr_copy_metadata(ScrArr *a, ScrDyn *target) {
const ScrDyn *metadata = SCR_AX(a)->metadata;
if (metadata && metadata->kind == SCR_DYN_HANDLE && metadata->v.handle.tag == SCR_DYNH_MATCH_SOURCE &&
@@ -5137,21 +5200,7 @@ void scr_arr_copy_metadata(ScrArr *a, ScrDyn *target) {
return;
metadata = SCR_AX(a)->metadata;
if (!metadata) return;
- for (size_t i = 0; i < metadata->v.obj.len; i++) {
- const ScrDynEntry *entry = &metadata->v.obj.entries[i];
- ScrStr *key = scr_str_new(entry->key, entry->key_len);
- if (!entry->accessor && entry->writable && entry->enumerable && entry->configurable) {
- scr_dyn_key_set(target, key, entry->value);
- } else {
- /* Properties defined through a view keep their attributes. */
- ScrDyn *name = scr_dyn_new_str(key);
- ScrDyn *descriptor = scr_dyn_own_descriptor(metadata, key);
- if (descriptor) scr_dyn_release(scr_dyn_define_property(target, name, descriptor));
- scr_dyn_release(descriptor);
- scr_dyn_release(name);
- }
- scr_str_release(key);
- }
+ for (size_t i = 0; i < metadata->v.obj.len; i++) scr_dyn_copy_entry(target, metadata, &metadata->v.obj.entries[i]);
}
ScrArr *scr_arr_named_keys(ScrArr *a) {
@@ -5209,8 +5258,27 @@ void scr_arr_adopt_view_properties(ScrArr *a, ScrDyn *view, ScrArr *target) {
}
ScrDyn *named = view && view->kind == SCR_DYN_ARR ? view->v.arr.properties : NULL;
if (!named || named->v.obj.len == 0) return;
+ /* The view's numeric properties are copies of the native ones (every
+ * write to them goes to the native array), so only the others move. */
+ bool numeric = false;
+ for (size_t i = 0; i < named->v.obj.len && !numeric; i++) {
+ double number;
+ numeric = scr_arr_numeric_key(named->v.obj.entries[i].key, named->v.obj.entries[i].key_len, &number);
+ }
+ if (numeric) {
+ ScrDyn *rest = scr_dyn_new_obj();
+ for (size_t i = 0; i < named->v.obj.len; i++) {
+ const ScrDynEntry *entry = &named->v.obj.entries[i];
+ double number;
+ if (!scr_arr_numeric_key(entry->key, entry->key_len, &number)) scr_dyn_copy_entry(rest, named, entry);
+ }
+ named = rest;
+ } else {
+ scr_dyn_retain(named);
+ }
ScrArrExt *x = scr_arr_ext(a);
ScrDyn *old = x->metadata;
- x->metadata = scr_dyn_retain(named);
+ x->metadata = named->v.obj.len ? named : NULL;
+ if (!x->metadata) scr_dyn_release(named);
scr_dyn_release(old);
}
diff --git a/packages/runtime/src/scr_array.h b/packages/runtime/src/scr_array.h
index e13acb233..4666750ea 100644
--- a/packages/runtime/src/scr_array.h
+++ b/packages/runtime/src/scr_array.h
@@ -48,6 +48,12 @@ typedef enum {
* address over 8, 0 for NULL. Compact instances are never counted or
* traced, so these arrays retain, release and trace nothing. */
SCR_ELEM_CREF,
+ /* Numbers that the compiler proves are always int32 (never -0), in
+ * 4-byte slots: arrays that only compiled element accesses ever reach
+ * (backend/llvm int32 arrays). Every runtime slot is the value's double
+ * bits, as for SCR_ELEM_F64: scr_arr_dense_get and scr_arr_dense_put
+ * convert, so the paths compiled code calls need no case of their own. */
+ SCR_ELEM_I32,
} ScrElemKind;
typedef enum {
@@ -134,11 +140,12 @@ typedef struct ScrArr {
* 0 is always valid. */
uint32_t filled;
/* Dense values: `cap` 8-byte slots, `cap` bytes for SCR_ELEM_BOOL or
- * `cap` 4-byte slots for SCR_ELEM_CREF (scr_arr_slot_width), followed in
- * the same allocation, when the array has states (SCR_ARR_STATES), by one
- * state byte per slot in reverse slot order (scr_arr_states_end). Sparse
- * entries and properties keep 8-byte slots for every kind; a bool slot is
- * 0 or 1 wherever it lives, and a CREF slot's 8-byte form is the pointer
+ * `cap` 4-byte slots for SCR_ELEM_CREF and SCR_ELEM_I32
+ * (scr_arr_slot_width), followed in the same allocation, when the array
+ * has states (SCR_ARR_STATES), by one state byte per slot in reverse slot
+ * order (scr_arr_states_end). Sparse entries and properties keep 8-byte
+ * slots for every kind; a bool slot is 0 or 1 wherever it lives, a CREF
+ * slot's 8-byte form is the pointer and an I32 slot's the double's bits
* (scr_arr_dense_get). Rarely
* used storage (sparse indices, noncanonical numeric properties,
* match-result metadata) lives out of line, and an array that has it
@@ -260,7 +267,7 @@ static inline const ScrArrOps *scr_arr_ops(const ScrArr *a) {
/* Bytes per dense value slot of element kind `elem`. */
static inline size_t scr_elem_slot_width(uint8_t elem) {
if (elem == SCR_ELEM_BOOL) return 1;
- if (__builtin_expect(elem == SCR_ELEM_CREF, 0)) return sizeof(uint32_t);
+ if (__builtin_expect(elem == SCR_ELEM_CREF || elem == SCR_ELEM_I32, 0)) return sizeof(uint32_t);
return sizeof(uint64_t);
}
@@ -325,11 +332,38 @@ static inline void scr_arr_need_states(ScrArr *a) {
static inline uint64_t scr_cref_decode(uint32_t ref) { return (uint64_t)ref << 3; }
static inline uint32_t scr_cref_encode(uint64_t slot) { return (uint32_t)(slot >> 3); }
+/* The bits of `d` as a slot, and back. */
+static inline uint64_t scr_arr_f64_slot(double d) {
+ uint64_t slot;
+ memcpy(&slot, &d, sizeof slot);
+ return slot;
+}
+
+static inline double scr_arr_slot_f64(uint64_t slot) {
+ double d;
+ memcpy(&d, &slot, sizeof d);
+ return d;
+}
+
+/* An int32 array's slot for a double: its stores are proven int32s that
+ * are not -0 (and a cleared slot is +0), so anything else is a compiler
+ * bug. */
+static inline int32_t scr_arr_i32_of(uint64_t slot) {
+ double d = scr_arr_slot_f64(slot);
+ if (__builtin_expect(!(d >= -2147483648.0 && d <= 2147483647.0 && d == (double)(int32_t)d) ||
+ slot == UINT64_C(0x8000000000000000),
+ 0))
+ __builtin_trap();
+ return (int32_t)d;
+}
+
/* The dense value at `index` (< cap) as an 8-byte slot, and its store. */
static inline uint64_t scr_arr_dense_get(const ScrArr *a, size_t index) {
if (a->elem == SCR_ELEM_BOOL) return ((const uint8_t *)a->data)[index];
if (__builtin_expect(a->elem == SCR_ELEM_CREF, 0))
return scr_cref_decode(((const uint32_t *)a->data)[index]);
+ if (__builtin_expect(a->elem == SCR_ELEM_I32, 0))
+ return scr_arr_f64_slot((double)((const int32_t *)a->data)[index]);
return a->data[index];
}
@@ -337,6 +371,8 @@ static inline void scr_arr_dense_put(ScrArr *a, size_t index, uint64_t slot) {
if (a->elem == SCR_ELEM_BOOL) ((uint8_t *)a->data)[index] = (uint8_t)slot;
else if (__builtin_expect(a->elem == SCR_ELEM_CREF, 0))
((uint32_t *)a->data)[index] = scr_cref_encode(slot);
+ else if (__builtin_expect(a->elem == SCR_ELEM_I32, 0))
+ ((int32_t *)a->data)[index] = scr_arr_i32_of(slot);
else a->data[index] = slot;
}
@@ -397,6 +433,9 @@ ScrArr *scr_arr_new_ops(const ScrArrOps *ops, size_t initial_cap);
* array plus five 8-byte slots). Otherwise exactly scr_arr_new(elem, 0) and
* scr_arr_new_ops(ops, 0). Returns +1. */
ScrArr *scr_arr_new_small(ScrElemKind elem);
+/* An int32 array's storage as doubles, in place (SCR_ELEM_I32 to
+ * SCR_ELEM_F64); no-op for any other kind. */
+void scr_arr_widen_i32(ScrArr *a);
ScrArr *scr_arr_new_ops_small(const ScrArrOps *ops);
/* scr_arr_new(elem, cap) for a known final length: the `cap` slots share
* the array's own block (one allocation). With `states`, the array starts
@@ -421,9 +460,15 @@ void scr_arr_copy_metadata(ScrArr *a, struct ScrDyn *target);
* properties of `target`, so neither is lost when the storage is swapped. */
void scr_arr_adopt_view_properties(ScrArr *a, struct ScrDyn *view, ScrArr *target);
/* True when `key` is the string spelling of a number that is not an array
- * index ("-1", "1.5", "NaN", "Infinity"), with the number in *out. Arrays
- * keep such properties in their noncanonical property table. */
+ * index ("-1", "1.5", "NaN", "Infinity"), or "undefined", with the number in
+ * *out (scr_arr_undefined_key for "undefined"). Arrays keep such properties
+ * in their noncanonical property table. */
bool scr_arr_numeric_key(const char *key, size_t len, double *out);
+/* The number compiled code passes as the key of an element access whose
+ * index is undefined at run time: a NaN with its own payload, which the
+ * property table spells "undefined" (JavaScript names that property, not
+ * "NaN"). */
+double scr_arr_undefined_key(void);
/* Whether the array still has the named or noncanonical numeric property
* `key`: `for...in` skips a key deleted before its turn. Borrows both. */
bool scr_arr_has_named_key(ScrArr *a, ScrStr *key);
@@ -431,6 +476,10 @@ bool scr_arr_has_named_key(ScrArr *a, ScrStr *key);
* noncanonical numeric properties, then the named properties in insertion
* order. +1. */
ScrArr *scr_arr_named_keys(ScrArr *a);
+/* Whether the array has properties besides its elements (noncanonical
+ * numeric keys, match metadata, named properties): util.inspect lists
+ * them after the elements. */
+bool scr_arr_has_properties(const ScrArr *a);
/* Installed by scr_regex.c when it records a pending match source; this
* unit links without the regex runtime. Writes the pending metadata of
* `a` into its dynamic copy or materializes it, answering whether it
diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h
index 28c2accce..cf50194c8 100644
--- a/packages/runtime/src/scr_dyn.h
+++ b/packages/runtime/src/scr_dyn.h
@@ -624,6 +624,9 @@ static inline const ScrDynTypedArrayOps *scr_dyn_typed_ref_array_ops(const ScrDy
ScrDyn *scr_dyn_obj_get(const ScrDyn *d, const char *key, size_t key_len);
/* Own-enumerable check on a SCR_DYN_OBJ. */
bool scr_dyn_obj_enumerable(const ScrDyn *d, const char *key, size_t key_len);
+/* Stores own property `entry` of `source` on `target` with its attributes:
+ * plain data properties by an ordinary write, others by their descriptor. */
+void scr_dyn_copy_entry(ScrDyn *target, const ScrDyn *source, const ScrDynEntry *entry);
/* Literal discriminator tests used before selecting a typed record layout.
* Missing fields and different primitive kinds never match. Borrowed args. */
bool scr_dyn_field_eq_str(const ScrDyn *d, const ScrStr *key, const ScrStr *value);
@@ -864,6 +867,7 @@ ScrDyn *scr_dyn_open_iterator(const ScrDyn *value, int *kind, bool generators);
* for-of protocol reads it. +1, or NULL with the error pending. */
ScrDyn *scr_dyn_iterator_result_get(const ScrDyn *result, const char *key, size_t len);
void scr_dyn_install_iterator_symbol(ScrDyn *key);
+void scr_dyn_install_has_instance_symbol(ScrDyn *key);
void scr_dyn_install_async_iterator_symbol(ScrDyn *key);
void scr_dyn_install_async_dispose_symbol(ScrDyn *key);
/* True when key is Symbol.asyncDispose (registered by the symbol unit when
@@ -1273,6 +1277,10 @@ void scr_error_set_prop(ScrError *e, const char *key, size_t key_len, ScrDyn *va
ScrDyn *scr_errdyn_view_of(const ScrError *e); /* borrowed or NULL */
/* ToBoolean over the dyn kind (JS-exact); borrowed, never throws. */
bool scr_dyn_truthy(const ScrDyn *d);
+/* Math.max/Math.min(...src) over an untyped iterable; `what` names the
+ * source in V8's null/undefined spread TypeError. NaN with it pending. */
+double scr_dyn_math_fold(const ScrDyn *src, bool max, const ScrStr *what);
+int scr_dyn_has_instance_hook(const ScrDyn *value, const ScrDyn *callee);
/* JS String() over a dyn value (arrays join, [object Object], the error
* encoding renders "name: message"); borrowed, result +1, never throws. */
ScrStr *scr_dyn_display(const ScrDyn *d);
diff --git a/packages/runtime/src/scr_dyn_builtin_class.c b/packages/runtime/src/scr_dyn_builtin_class.c
index 2f947048a..0647a65c0 100644
--- a/packages/runtime/src/scr_dyn_builtin_class.c
+++ b/packages/runtime/src/scr_dyn_builtin_class.c
@@ -1581,6 +1581,8 @@ bool scr_dyn_function_instanceof(ScrDyn *value, ScrDyn *callee) {
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
return false;
}
+ int hooked = scr_dyn_has_instance_hook(value, callee);
+ if (hooked >= 0) return hooked == 1;
ScrDyn *prototype = bc_get(callee, "prototype");
if (!prototype) return false;
bool found = false;
diff --git a/packages/runtime/src/scr_inspect.c b/packages/runtime/src/scr_inspect.c
index a4d124ac3..1dce8d3c2 100644
--- a/packages/runtime/src/scr_inspect.c
+++ b/packages/runtime/src/scr_inspect.c
@@ -992,6 +992,15 @@ ScrStr *scr_insp_error(ScrError *e, double recurse, double depth) {
/* Property-name rendering: bare when the key matches Node's keyStrRegExp
* (/^[a-zA-Z_][a-zA-Z_0-9]*$/, '__proto__' excepted), quoted otherwise. */
+/* util.inspect's text for an accessor property: [Getter], [Setter] or
+ * [Getter/Setter], by which of the two are functions. +1. */
+static ScrStr *insp_accessor_tag(const ScrDynEntry *ent) {
+ bool get = ent->getter && ent->getter->kind == SCR_DYN_FUNC;
+ bool set = ent->setter && ent->setter->kind == SCR_DYN_FUNC;
+ const char *text = get && set ? "[Getter/Setter]" : get ? "[Getter]" : set ? "[Setter]" : "undefined";
+ return scr_str_new(text, strlen(text));
+}
+
static void insp_key_into(InspBuf *b, const char *key, size_t len) {
if (len == 9 && memcmp(key, "__proto__", 9) == 0) {
ib_cstr(b, "['__proto__']");
@@ -1499,14 +1508,20 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) {
return scr_insp_str(d->v.str);
case SCR_DYN_ARR: {
size_t len = d->v.arr.len;
- if (len == 0) return scr_str_new("[]", 2);
+ /* Named and noncanonical numeric properties follow the elements as
+ * `key: value` entries (Node's array extras). */
+ const ScrDyn *props = d->v.arr.properties;
+ size_t nprops = 0;
+ for (size_t i = 0; props && i < props->v.obj.len; i++) nprops += props->v.obj.entries[i].enumerable;
+ if (len == 0 && nprops == 0) return scr_str_new("[]", 2);
if (recurse > depth) return scr_str_new("[Array]", 7);
scr_insp_begin(recurse + 1);
/* Node's maxArrayLength (100) counts output entries; a run of holes
* is one `` entry (formatSpecialArray). */
const unsigned char *presence = d->v.arr.presence;
size_t i = 0;
- for (size_t shown = 0; i < len && shown < 100; shown++) {
+ size_t shown = 0;
+ for (; i < len && shown < 100; shown++) {
if (presence && !presence[i]) {
size_t next = i + 1;
while (next < len && !presence[next]) next++;
@@ -1524,15 +1539,38 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) {
scr_str_release(s);
}
bool more = i < len;
+ size_t entries = shown + (more ? 1 : 0);
if (more) {
ScrStr *m = scr_insp_more_items((double)(len - i));
scr_insp_entry(m, d->v.arr.items[i]->kind == SCR_DYN_NUM);
scr_str_release(m);
}
+ for (size_t k = 0; props && k < props->v.obj.len; k++) {
+ const ScrDynEntry *ent = &props->v.obj.entries[k];
+ if (!ent->enumerable) continue;
+ ScrStr *val = ent->accessor ? insp_accessor_tag(ent) : scr_insp_dyn(ent->value, recurse + 1, depth);
+ InspBuf eb = {0};
+ insp_key_into(&eb, ent->key, ent->key_len);
+ ib_cstr(&eb, ": ");
+ ib_bytes(&eb, val->data, val->len);
+ scr_str_release(val);
+ ScrStr *entry = ib_take(&eb);
+ /* Node's grouping pads by the array element at the entry's output
+ * position (value[i] in groupArrayElements), not by the entry. */
+ bool number = entries < len && (!presence || presence[entries]) &&
+ d->v.arr.items[entries]->kind == SCR_DYN_NUM;
+ scr_insp_entry(entry, number);
+ scr_str_release(entry);
+ entries++;
+ }
+ /* Node's grouping leaves out the last entry when there are more
+ * entries than maxArrayLength (the `... more items` entry when the
+ * array has no properties). */
+ bool last_out = entries > 100;
ScrStr *base = scr_str_new("", 0);
ScrStr *b0 = scr_str_new("[", 1);
ScrStr *b1 = scr_str_new("]", 1);
- ScrStr *out = scr_insp_end(base, b0, b1, recurse + 1, true, more);
+ ScrStr *out = scr_insp_end(base, b0, b1, recurse + 1, true, last_out);
scr_str_release(base);
scr_str_release(b0);
scr_str_release(b1);
diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c
index dea3426e6..465448858 100644
--- a/packages/runtime/src/scr_json.c
+++ b/packages/runtime/src/scr_json.c
@@ -189,6 +189,19 @@ void scr_dyn_install_async_iterator_symbol(ScrDyn *key) {
scr_atexit(scr_async_iterator_symbol_cleanup);
}
+/* Symbol.hasInstance, installed when the program first evaluates it: a
+ * function can only define a hook after reading the symbol. */
+static SCR_TL ScrDyn *scr_has_instance_symbol;
+static void scr_has_instance_symbol_cleanup(void) {
+ scr_dyn_release(scr_has_instance_symbol);
+ scr_has_instance_symbol = NULL;
+}
+void scr_dyn_install_has_instance_symbol(ScrDyn *key) {
+ if (scr_has_instance_symbol) return;
+ scr_has_instance_symbol = scr_dyn_retain(key);
+ scr_atexit(scr_has_instance_symbol_cleanup);
+}
+
void scr_dyn_install_iterator_symbol(ScrDyn *key) {
if (scr_iterator_symbol) return;
scr_iterator_symbol = scr_dyn_retain(key);
@@ -1557,6 +1570,50 @@ static ScrDyn *scr_dyn_not_iterable(const ScrDyn *src, const ScrStr *msg) {
return NULL;
}
+static ScrDyn *scr_native_iterator_drain(const ScrDyn *src);
+
+/* Math.max(...src) / Math.min(...src) over an untyped iterable: every
+ * value's ToNumber runs in order, then NaN poisons and ±0 orders as in
+ * Node. A non-iterable source throws V8's call-spread TypeError: `what`
+ * names the source for null and undefined, every other kind gets the
+ * Symbol.iterator wording. NaN with the exception pending when packing or
+ * a coercion throws. */
+double scr_dyn_math_fold(const ScrDyn *src, bool max, const ScrStr *what) {
+ if (src->kind == SCR_DYN_UNDEF || src->kind == SCR_DYN_NULL) {
+ ScrJsonBuf b;
+ scr_jb_init(&b);
+ scr_jb_write(&b, what->data, what->len);
+ scr_jb_puts(&b, src->kind == SCR_DYN_NULL ? " is not iterable (cannot read property null)"
+ : " is not iterable (cannot read property undefined)");
+ scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&b));
+ return NAN;
+ }
+ static const char spread_msg[] = "Spread syntax requires ...iterable[Symbol.iterator] to be a function";
+ ScrStr *msg = scr_str_new(spread_msg, sizeof spread_msg - 1);
+ /* The spread takes every value before the call: arrays, strings, bytes
+ * and collections pack, a native iterator (a Map's keys()) drains, and
+ * other iterables (a generator, an object with a Symbol.iterator method)
+ * step their iterator to the end. */
+ ScrDyn *pack = src->kind == SCR_DYN_HANDLE && (src->v.handle.tag == SCR_DYNH_ITERATOR ||
+ scr_dyn_handle_ops_of(src)->iter_step)
+ ? scr_native_iterator_drain(src)
+ : scr_dyn_destructure_rest(src, msg);
+ scr_str_release(msg);
+ if (!pack) return NAN;
+ double best = max ? -INFINITY : INFINITY;
+ bool nan = false;
+ for (size_t i = 0; i < pack->v.arr.len; i++) {
+ double v = scr_dyn_number_coerce(pack->v.arr.items[i]);
+ if (scr_exc_pending()) break;
+ if (isnan(v)) nan = true;
+ else if (max ? (v > best || (v == 0.0 && best == 0.0 && !signbit(v)))
+ : (v < best || (v == 0.0 && best == 0.0 && signbit(v))))
+ best = v;
+ }
+ scr_dyn_release(pack);
+ return nan ? NAN : best;
+}
+
ScrDyn *scr_dyn_iter_pack(const ScrDyn *src, const ScrStr *msg) {
if (src->kind == SCR_DYN_HANDLE && scr_dyn_handle_ops_of(src)->iter_pack)
return scr_dyn_handle_ops_of(src)->iter_pack(src->v.handle.ptr);
@@ -1870,6 +1927,25 @@ ScrDyn *scr_dyn_error_constructor(const char *name) {
return NULL;
}
+/* `Error.stackTraceLimit` read or written through the Error constructor
+ * held as a value (zod's `const E = Error; E.stackTraceLimit = 0`): the
+ * same limit the static member reads and stack capture honors. */
+static bool scr_dyn_is_stack_limit(const ScrDyn *d, const char *key, size_t key_len) {
+ return key_len == 15 && memcmp(key, "stackTraceLimit", 15) == 0 && d->kind == SCR_DYN_FUNC &&
+ scr_error_ctor_values[0] && d->v.fn.clo == scr_error_ctor_values[0]->v.fn.clo;
+}
+
+/* An own `stackTraceLimit` on the Error value, assigned a non-number or
+ * defined with Object.defineProperty, replaces the limit for reads and
+ * writes through the value; writes to it keep its descriptor. */
+static bool scr_dyn_stack_limit_shadowed(const ScrDyn *d) {
+ ScrClosure *owner = scr_closure_identity(d->v.fn.clo);
+ ScrDyn *table = owner->props ? (ScrDyn *)scr_box_get_ref(owner->props) : NULL;
+ bool own = table && scr_dyn_find_entry(table, "stackTraceLimit", 15);
+ scr_dyn_release(table);
+ return own;
+}
+
ScrDyn *scr_dyn_object_prototype(void) {
if (!scr_builtin_object_prototype) {
scr_builtin_object_prototype = scr_dyn_alloc(SCR_DYN_OBJ);
@@ -3454,6 +3530,43 @@ ScrDyn *scr_dyn_class_view_unavailable(void *ptr) {
return scr_dyn_retain(scr_dyn_undefined());
}
+void scr_dyn_copy_entry(ScrDyn *target, const ScrDyn *source, const ScrDynEntry *entry) {
+ ScrStr *key = scr_str_new(entry->key, entry->key_len);
+ if (!entry->accessor && entry->writable && entry->enumerable && entry->configurable) {
+ scr_dyn_key_set(target, key, entry->value);
+ } else {
+ /* Properties defined through a view keep their attributes. */
+ ScrDyn *name = scr_dyn_new_str(key);
+ ScrDyn *descriptor = scr_dyn_own_descriptor(source, key);
+ if (descriptor) scr_dyn_release(scr_dyn_define_property(target, name, descriptor));
+ scr_dyn_release(descriptor);
+ scr_dyn_release(name);
+ }
+ scr_str_release(key);
+}
+
+static ScrDyn *scr_typed_ref_numeric_property(const ScrDyn *d, double key);
+
+/* A live array's view lists the native numeric properties first, then the
+ * named properties the snapshot copied from the array's metadata. */
+static void scr_typed_ref_view_numeric_properties(const ScrDyn *capsule, ScrDyn *view) {
+ const ScrDynTypedArrayOps *ops = scr_dyn_typed_ref_array_ops(capsule);
+ ScrArr *arr = capsule->v.typed_ref.ptr;
+ const ScrArrExt *x = ops ? scr_arr_ext_of(arr) : NULL;
+ if (!x || x->prop_len == 0 || view->kind != SCR_DYN_ARR) return;
+ ScrDyn *named = view->v.arr.properties;
+ view->v.arr.properties = NULL;
+ for (size_t i = 0; i < x->prop_len; i++) {
+ double number;
+ if (!scr_arr_numeric_key(x->props[i].key, x->props[i].key_len, &number)) continue;
+ ScrDyn *value = scr_typed_ref_numeric_property(capsule, number);
+ if (value) scr_dyn_arr_named_put(view, x->props[i].key, x->props[i].key_len, value);
+ }
+ if (!named) return;
+ for (size_t i = 0; i < named->v.obj.len; i++) scr_dyn_copy_entry(view, named, &named->v.obj.entries[i]);
+ scr_dyn_release(named);
+}
+
ScrDyn *scr_dyn_typed_ref_materialize(const ScrDyn *d) {
ScrDyn *capsule = (ScrDyn *)d;
ScrDyn *fresh = capsule->v.typed_ref.materialize(capsule->v.typed_ref.ptr);
@@ -3461,6 +3574,7 @@ ScrDyn *scr_dyn_typed_ref_materialize(const ScrDyn *d) {
scr_dyn_release(fresh);
return scr_dyn_retain(scr_dyn_undefined());
}
+ scr_typed_ref_view_numeric_properties(capsule, fresh);
if (!capsule->v.typed_ref.materialized) {
capsule->v.typed_ref.materialized = fresh;
} else {
@@ -3697,6 +3811,16 @@ ScrDyn *scr_dyn_typed_ref_string_view(const ScrDyn *d) {
return out;
}
+/* A numeric property of a live array ("-1", "1.5"): +1, or NULL when the
+ * native array has none. */
+static ScrDyn *scr_typed_ref_numeric_property(const ScrDyn *d, double key) {
+ ScrArr *arr = d->v.typed_ref.ptr;
+ double state = scr_arr_state(arr, key);
+ if (state == (double)SCR_ARR_VALUE) return scr_dyn_typed_ref_array_ops(d)->get(arr, key);
+ if (state == (double)SCR_ARR_UNDEFINED) return scr_dyn_retain(scr_dyn_undefined());
+ return NULL;
+}
+
/* 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. */
@@ -3721,6 +3845,8 @@ ScrDyn *scr_dyn_typed_ref_key_get(const ScrDyn *d, const ScrStr *key) {
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);
+ double number;
+ if (scr_arr_numeric_key(key->data, key->len, &number)) return scr_typed_ref_numeric_property(d, number);
size_t index;
if (!scr_typed_ref_canonical_index(key->data, key->len, &index)) return NULL;
return scr_dyn_typed_ref_element(d, index);
@@ -3730,6 +3856,9 @@ ScrDyn *scr_dyn_typed_ref_key_get(const ScrDyn *d, const ScrStr *key) {
static bool scr_typed_ref_array_has_own(const ScrDyn *d, const ScrStr *key) {
if (!scr_dyn_typed_ref_array_ops(d)) return false;
if (scr_typed_ref_is_length(key)) return true;
+ double number;
+ if (scr_arr_numeric_key(key->data, key->len, &number))
+ return scr_arr_state(d->v.typed_ref.ptr, number) != (double)SCR_ARR_HOLE;
size_t index;
if (!scr_typed_ref_canonical_index(key->data, key->len, &index)) return false;
ScrArr *arr = d->v.typed_ref.ptr;
@@ -3742,6 +3871,22 @@ static bool scr_typed_ref_array_has_own(const ScrDyn *d, const ScrStr *key) {
static bool scr_typed_ref_array_set(ScrDyn *d, const ScrStr *key, const ScrDyn *value) {
const ScrDynTypedArrayOps *ops = scr_dyn_typed_ref_array_ops(d);
if (!ops || !ops->set) return false;
+ /* Numeric properties live in the native array's property table, which
+ * holds values of the element type. */
+ double number;
+ if (scr_arr_numeric_key(key->data, key->len, &number)) {
+ if (ops->set(d->v.typed_ref.ptr, number, value)) return true;
+ ScrStr *kind = scr_dyn_typeof(value);
+ ScrJsonBuf message;
+ scr_jb_init(&message);
+ scr_jb_puts(&message, "expected the array's element type at $[");
+ scr_jb_write(&message, key->data, key->len);
+ scr_jb_puts(&message, "], got ");
+ scr_jb_write(&message, kind->data, kind->len);
+ scr_str_release(kind);
+ scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&message));
+ return true;
+ }
size_t index;
if (!scr_typed_ref_canonical_index(key->data, key->len, &index)) return false;
ScrArr *arr = d->v.typed_ref.ptr;
@@ -3984,12 +4129,49 @@ static int scr_dyn_global_constructor_instanceof(const ScrDyn *value, const ScrD
/* `value instanceof callee` for an untyped constructor value. `refusal`, when
* given, is the site's SC1090 message for a runtime constructor with no
* instance test. */
+/* A function whose own-property table defines Symbol.hasInstance
+ * (`Object.defineProperty(F, Symbol.hasInstance, { value })`): 1 or 0 by the
+ * truthiness of the method's answer with `this` bound to F, -1 when F
+ * defines none, or 0 with the exception pending. Functions without a table
+ * use the ordinary prototype test. */
+int scr_dyn_has_instance_hook(const ScrDyn *value, const ScrDyn *callee) {
+ if (!scr_has_instance_symbol || callee->kind != SCR_DYN_FUNC || !scr_closure_identity(callee->v.fn.clo)->props)
+ return -1;
+ ScrDyn *hook = scr_dyn_symbol_key_get(callee, scr_has_instance_symbol, true);
+ if (scr_exc_pending()) {
+ scr_dyn_release(hook);
+ return 0;
+ }
+ if (!hook || hook->kind != SCR_DYN_FUNC) {
+ scr_dyn_release(hook);
+ return -1;
+ }
+ scr_dyn_this_push_dyn((ScrDyn *)callee);
+ ScrDyn *argument = (ScrDyn *)value;
+ ScrDyn *result = scr_dyn_call(hook, &argument, 1, "Symbol.hasInstance");
+ scr_dyn_this_pop();
+ scr_dyn_release(hook);
+ if (!result) return 0;
+ bool truthy = scr_dyn_truthy(result);
+ scr_dyn_release(result);
+ return truthy ? 1 : 0;
+}
+
bool scr_bytes_instanceof_at(const ScrDyn *value, const ScrDyn *callee, const ScrStr *refusal) {
if (!callee || callee->kind != SCR_DYN_FUNC) {
static const char message[] = "Right-hand side of 'instanceof' is not callable";
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
return false;
}
+ int hooked = scr_dyn_has_instance_hook(value, callee);
+ if (hooked >= 0) return hooked == 1;
+ /* An error constructor held as a value (`inst instanceof params.Parent`
+ * with Parent === Error): the error kind's interval, and for Error itself
+ * objects inheriting Error.prototype. */
+ for (size_t i = 0; i < SCR_ERROR_CTORS; i++) {
+ if (!scr_error_ctor_values[i] || callee->v.fn.clo != scr_error_ctor_values[i]->v.fn.clo) continue;
+ return scr_dyn_err_instanceof(value, (double)i) || (i == 0 && scr_dyn_inherits_error_prototype(value));
+ }
if (scr_array_buffer_ctor && callee->v.fn.clo == scr_array_buffer_ctor->v.fn.clo) return scr_array_buffer_is(value);
if (scr_array_ctor && callee->v.fn.clo == scr_array_ctor->v.fn.clo) return value->kind == SCR_DYN_ARR || scr_dyn_ref_is_array(value);
for (size_t i = 0; i < SCR_BYTES_ELEM_COUNT; i++) {
@@ -5022,7 +5204,43 @@ ScrDyn *scr_dyn_class_inherit(ScrDyn *constructor, ScrDyn *base) {
return scr_exc_pending() ? NULL : scr_dyn_retain(constructor);
}
+/* Defines `key` on target as a writable, configurable, non-enumerable data
+ * property holding value (borrowed), as Error's constructor installs its
+ * message and cause. */
+static void scr_dyn_define_hidden(ScrDyn *target, const ScrStr *key, ScrDyn *value) {
+ ScrDyn *descriptor = scr_dyn_new_obj();
+ scr_dyn_obj_set(descriptor, "value", 5, scr_dyn_retain(value));
+ scr_dyn_obj_set(descriptor, "writable", 8, scr_dyn_new_bool(true));
+ scr_dyn_obj_set(descriptor, "enumerable", 10, scr_dyn_new_bool(false));
+ scr_dyn_obj_set(descriptor, "configurable", 12, scr_dyn_new_bool(true));
+ ScrDyn *name = scr_dyn_new_str((ScrStr *)key);
+ ScrDyn *defined = scr_dyn_define_property(target, name, descriptor);
+ scr_dyn_release(name);
+ scr_dyn_release(descriptor);
+ scr_dyn_release(defined);
+}
+
+/* A builtin constructor a class expression's runtime heritage can name:
+ * 1 for Object, 2 for Error, 0 otherwise. */
+static int scr_dyn_builtin_base_kind(const ScrDyn *constructor) {
+ if (constructor->kind != SCR_DYN_FUNC || constructor->v.fn.class_obj || !constructor->v.fn.sig ||
+ !constructor->v.fn.name || strcmp(constructor->v.fn.sig, "native:constructor") != 0)
+ return 0;
+ if (strcmp(constructor->v.fn.name, "Object") == 0) return 1;
+ if (strcmp(constructor->v.fn.name, "Error") == 0 && constructor->v.fn.clo->fn == (void *)scr_error_ctor_names[0])
+ return 2;
+ return 0;
+}
+
ScrDyn *scr_dyn_class_base_prototype(ScrDyn *constructor) {
+ /* `class extends (Base ?? Object)`: Object's instances are ordinary
+ * objects, laid out like a class without heritage (undefined); Error's
+ * inherit Error.prototype. */
+ switch (scr_dyn_builtin_base_kind(constructor)) {
+ case 1: return scr_dyn_retain(scr_dyn_undefined());
+ case 2: return scr_dyn_error_prototype();
+ default: break;
+ }
if (constructor->kind != SCR_DYN_FUNC || constructor->v.fn.class_obj ||
scr_closure_identity(constructor->v.fn.clo)->function_kind != 4) {
static const char message[] = "Class extends value is not a native ordinary constructor";
@@ -5039,6 +5257,55 @@ ScrDyn *scr_dyn_class_base_prototype(ScrDyn *constructor) {
}
void scr_dyn_class_super(ScrDyn *constructor, ScrDyn *receiver, ScrDyn *arguments) {
+ switch (scr_dyn_builtin_base_kind(constructor)) {
+ case 1:
+ /* super() through Object keeps the receiver the derived constructor
+ * created (OrdinaryCreateFromConstructor). */
+ return;
+ case 2: {
+ /* super(message, options) through Error: the message and cause become
+ * non-enumerable own properties, then the stack (its text alone, as for
+ * scriptc's other errors). */
+ size_t argc = arguments->v.arr.len;
+ ScrDyn *const *argv = arguments->v.arr.items;
+ if (argc > 0 && argv[0]->kind != SCR_DYN_UNDEF) {
+ ScrStr *text = scr_dyn_string_coerce_js(argv[0]);
+ if (!text) return;
+ ScrDyn *value = scr_dyn_new_str(text);
+ scr_str_release(text);
+ ScrStr *key = scr_str_new("message", 7);
+ scr_dyn_define_hidden(receiver, key, value);
+ scr_str_release(key);
+ scr_dyn_release(value);
+ if (scr_exc_pending()) return;
+ }
+ /* InstallErrorCause: an options object with a `cause` property, own or
+ * inherited, defines the value its getter or data property reads. */
+ ScrDyn *options = argc > 1 ? argv[1] : NULL;
+ if (options && options->kind != SCR_DYN_NULL &&
+ (options->kind == SCR_DYN_FUNC || scr_dyn_is_object(options))) {
+ ScrStr *key = scr_str_new("cause", 5);
+ if (scr_dyn_in(options, key) && !scr_exc_pending()) {
+ ScrDyn *cause = scr_dyn_obj_read(options, "cause", 5);
+ if (cause && !scr_exc_pending()) scr_dyn_define_hidden(receiver, key, cause);
+ scr_dyn_release(cause);
+ }
+ scr_str_release(key);
+ if (scr_exc_pending()) return;
+ }
+ ScrStr *text = scr_dyn_error_string(receiver);
+ if (!text) return;
+ ScrDyn *stack = scr_dyn_new_str(text);
+ scr_str_release(text);
+ ScrStr *key = scr_str_new("stack", 5);
+ scr_dyn_define_hidden(receiver, key, stack);
+ scr_str_release(key);
+ scr_dyn_release(stack);
+ return;
+ }
+ default:
+ break;
+ }
scr_dyn_this_push_dyn(receiver);
ScrDyn *result = scr_dyn_call(constructor, arguments->v.arr.items, arguments->v.arr.len, "super");
scr_dyn_this_pop();
@@ -6690,6 +6957,21 @@ 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 && scr_dyn_typed_ref_array_ops(target) &&
+ (key->kind == SCR_DYN_STR || key->kind == SCR_DYN_NUM)) {
+ /* A native array's numeric properties ("-1", "1.5") live in its
+ * property table, which holds values without attributes. A number key
+ * names one unless it is an array index. */
+ double number = key->kind == SCR_DYN_NUM ? key->v.num : 0;
+ bool numeric = key->kind == SCR_DYN_NUM
+ ? !(number >= 0 && number < 4294967295.0 && number == trunc(number))
+ : scr_arr_numeric_key(key->v.str->data, key->v.str->len, &number);
+ if (numeric) {
+ static const char message[] = "Object.defineProperty on a numeric property of a native array is not supported yet";
+ scr_throw_error_msg(SCR_ERR_ERROR, message, sizeof message - 1);
+ 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);
@@ -8992,8 +9274,19 @@ void scr_dyn_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value) {
}
if (recv->kind == SCR_DYN_FUNC) {
if (scr_dyn_fn_property_fence(key)) return;
+ bool limit = scr_dyn_is_stack_limit(recv, key->data, key->len);
+ /* A frozen Error keeps the limit read-only. */
+ if (limit && value->kind == SCR_DYN_NUM && !scr_dyn_stack_limit_shadowed(recv) && !scr_dyn_is_frozen(recv)) {
+ scr_stack_limit_set(value->v.num);
+ return;
+ }
ScrDyn *table = scr_dyn_fn_properties(recv);
scr_dyn_object_key_set(table, key, value, recv);
+ /* A number the own data property takes also sets the limit. */
+ ScrDynEntry *own = limit && value->kind == SCR_DYN_NUM && !scr_exc_pending()
+ ? scr_dyn_find_entry(table, key->data, key->len)
+ : NULL;
+ if (own && own->value == value) scr_stack_limit_set(value->v.num);
scr_dyn_release(table);
return;
}
@@ -11773,6 +12066,8 @@ static ScrDyn *scr_function_constructor(uint32_t kind) {
/* Keyed read on a FUNC node (see scr_runtime.h): own props first, then
* the function-instance built-ins name/length. +1 or NULL. */
ScrDyn *scr_dyn_fn_get(const ScrDyn *d, const char *key, size_t key_len) {
+ if (scr_dyn_is_stack_limit(d, key, key_len) && !scr_dyn_stack_limit_shadowed(d))
+ return scr_dyn_new_num(scr_stack_limit_get());
ScrClosure *owner = scr_closure_identity(d->v.fn.clo);
if (d->v.fn.class_obj && d->v.fn.class_obj->static_data && !owner->props)
owner->props = scr_box_retain(d->v.fn.class_obj->static_data);
diff --git a/packages/runtime/src/scr_node_builtin.c b/packages/runtime/src/scr_node_builtin.c
index 233617092..c0031e67b 100644
--- a/packages/runtime/src/scr_node_builtin.c
+++ b/packages/runtime/src/scr_node_builtin.c
@@ -90,6 +90,10 @@ static SCR_TL ScrDyn *scr_global_own;
* live on the box (string-keyed ones in scr_global_own), so every spelling
* of the global object answers this box. */
static SCR_TL ScrDyn *scr_global_box;
+/* The compiler's immortal name table (one per program) whose names
+ * scr_global_known holds; cleared with the tables, so a library re-init
+ * registers the names again. */
+static SCR_TL const ScrArr *scr_global_registered;
static SCR_TL bool scr_global_cleanup_registered;
static SCR_TL ScrDyn *(*scr_global_console_get)(void);
static SCR_TL ScrDyn *(*scr_global_fetch_get)(void);
@@ -190,6 +194,7 @@ static void scr_global_cleanup(void) {
scr_dyn_release(scr_global_own);
scr_global_known = NULL;
scr_global_own = NULL;
+ scr_global_registered = NULL;
}
static void *scr_global_retain(void *handle) { return handle; }
@@ -246,8 +251,12 @@ ScrDyn *scr_global_object(void) {
}
ScrDyn *scr_global_native_init(ScrArr *known, ScrDyn *(*console_get)(void)) {
+ /* The compiler passes one immortal name table per program: once it is
+ * registered, later reads of the global object only take a reference. */
+ if (known == scr_global_registered && scr_global_box) return scr_dyn_retain(scr_global_box);
ScrDyn *global = scr_global_object();
scr_global_console_get = console_get;
+ if (known->rc == SIZE_MAX) scr_global_registered = known;
for (size_t i = 0; i < known->len; i++) {
ScrStr *key = (ScrStr *)scr_arr_get_ref(known, (double)i);
if (!scr_dyn_obj_get(scr_global_known, key->data, key->len))
diff --git a/packages/runtime/src/scr_symbol.c b/packages/runtime/src/scr_symbol.c
index 304f2544f..647c31dc3 100644
--- a/packages/runtime/src/scr_symbol.c
+++ b/packages/runtime/src/scr_symbol.c
@@ -85,6 +85,11 @@ ScrSym *scr_sym_well_known(ScrStr *name) {
scr_dyn_install_iterator_symbol(key);
scr_dyn_release(key);
}
+ if (name->len == 11 && !memcmp(name->data, "hasInstance", 11)) {
+ ScrDyn *key = scr_dyn_new_symbol(symbol);
+ scr_dyn_install_has_instance_symbol(key);
+ scr_dyn_release(key);
+ }
if (name->len == 13 && !memcmp(name->data, "asyncIterator", 13)) {
ScrDyn *key = scr_dyn_new_symbol(symbol);
scr_dyn_install_async_iterator_symbol(key);
diff --git a/packages/runtime/src/scr_zlib_stream.c b/packages/runtime/src/scr_zlib_stream.c
index 54ffacf02..40daa6c76 100644
--- a/packages/runtime/src/scr_zlib_stream.c
+++ b/packages/runtime/src/scr_zlib_stream.c
@@ -549,7 +549,12 @@ void scr_zlib_stream_flush(ScrStream *s, ScrDyn *kind, ScrClosure *cb) {
return;
}
int ki = (int)k;
- if (zs->flush_bufs[ki] == NULL) zs->flush_bufs[ki] = scr_bytes_new(SCR_BYTES_U8, 0);
+ if (zs->flush_bufs[ki] == NULL) {
+ /* a Buffer, as Node's kFlushBuffers are: write() passes a Buffer
+ * through unchanged, so _transform recognizes the sentinel */
+ zs->flush_bufs[ki] = scr_bytes_new(SCR_BYTES_U8, 0);
+ zs->flush_bufs[ki]->is_buffer = true;
+ }
scr_stream_write(s, zs->flush_bufs[ki], cb);
}
diff --git a/packages/runtime/test/test_array.c b/packages/runtime/test/test_array.c
index 769b8ef3a..7ca0ebd1a 100644
--- a/packages/runtime/test/test_array.c
+++ b/packages/runtime/test/test_array.c
@@ -1614,6 +1614,102 @@ static bool inline_slots(const ScrArr *a) {
return a->data == (const uint64_t *)((const char *)a + ((sizeof(ScrArr) + 7) & ~(size_t)7));
}
+/* Int32 slots: 4-byte values, then the state bytes when the array has them. */
+static bool i32_layout_ok(const ScrArr *a) {
+ return a->elem == SCR_ELEM_I32 && scr_arr_slot_width(a) == sizeof(int32_t);
+}
+
+/* SCR_ELEM_I32: 4-byte int32 slots behind the double-valued entry points. */
+static void test_i32_slots(void) {
+ ScrArr *a = scr_arr_new_small(SCR_ELEM_I32);
+ for (int i = 0; i < 1000; i++) scr_arr_push_f64(a, (double)(i * 7 - 3000));
+ check(i32_layout_ok(a) && !scr_arr_has_states(a) && a->filled == 1000,
+ "int32 pushes grow four-byte storage without states");
+ bool values_ok = true;
+ for (int i = 0; i < 1000; i++) {
+ values_ok &= ((const int32_t *)a->data)[i] == i * 7 - 3000;
+ values_ok &= scr_arr_get_f64(a, i) == (double)(i * 7 - 3000);
+ values_ok &= scr_arr_get_number(a, i) == (double)(i * 7 - 3000);
+ }
+ check(values_ok, "int32 values are stored as 4-byte integers and read as doubles");
+ scr_arr_set_f64(a, 10, 2147483647);
+ scr_arr_set_f64(a, 11, -2147483648.0);
+ check(scr_arr_get_f64(a, 10) == 2147483647 && scr_arr_get_f64(a, 11) == -2147483648.0,
+ "int32 extremes round-trip");
+ check(scr_arr_pop_f64(a) == 999 * 7 - 3000 && scr_arr_len(a) == 999, "pop reads the last int32");
+ scr_arr_set_len(a, 5);
+ scr_arr_set_len(a, 8);
+ check(i32_layout_ok(a) && scr_arr_has_states(a) && scr_arr_state(a, 6) == SCR_ARR_HOLE &&
+ isnan(scr_arr_get_number(a, 6)) && scr_arr_get_f64(a, 4) == 4 * 7 - 3000,
+ "int32 truncation clears slots and extension adds holes");
+ scr_arr_set_f64(a, 20, 5);
+ check(scr_arr_len(a) == 21 && scr_arr_state(a, 15) == SCR_ARR_HOLE && scr_arr_get_f64(a, 20) == 5,
+ "an int32 store past the length leaves holes");
+ scr_arr_release(scr_arr_fill_f64(a, -9, 0, 21));
+ bool filled_ok = a->filled == 21;
+ for (int i = 0; i < 21; i++) filled_ok &= scr_arr_get_f64(a, i) == -9;
+ check(filled_ok, "int32 fill writes every slot");
+ scr_arr_release(a);
+
+ ScrArr *s = scr_arr_new(SCR_ELEM_I32, 0);
+ scr_arr_set_f64(s, 0, 1);
+ scr_arr_set_f64(s, 3000000, 3);
+ scr_arr_set_f64(s, 2000000, -2);
+ check(arr_sparse_len(s) == 2 && scr_arr_get_f64(s, 2000000) == -2 && scr_arr_get_f64(s, 3000000) == 3,
+ "int32 sparse entries keep double slots");
+ scr_arr_set_len(s, 2000001);
+ scr_arr_release(scr_arr_fill_f64(s, 4, 1, 2000000));
+ check(arr_sparse_len(s) == 0 && i32_layout_ok(s) && scr_arr_get_f64(s, 2000000) == -2 &&
+ scr_arr_get_f64(s, 1999999) == 4 && scr_arr_get_f64(s, 0) == 1,
+ "densifying absorbs sparse entries into int32 slots");
+ scr_arr_release(s);
+
+ ScrArr *k = scr_arr_new(SCR_ELEM_I32, 6);
+ for (int i = 0; i < 6; i++) scr_arr_push_f64(k, i);
+ scr_arr_push_f64(k, 6);
+ check(i32_layout_ok(k) && scr_arr_get_f64(k, 6) == 6 && scr_arr_get_f64(k, 0) == 0,
+ "a known-count int32 array grows out of its block");
+ scr_arr_release(k);
+
+ /* Widening: in place, to 8-byte double slots, keeping states. */
+ ScrArr *w = scr_arr_new(SCR_ELEM_I32, 3);
+ for (int i = 0; i < 3; i++) scr_arr_push_f64(w, i - 1);
+ check(inline_slots(w), "a three-element int32 construction keeps inline slots");
+ scr_arr_widen_i32(w);
+ check(w->elem == SCR_ELEM_F64 && !inline_slots(w) && scr_arr_len(w) == 3 && w->filled == 3 &&
+ scr_arr_get_f64(w, 0) == -1 && scr_arr_get_f64(w, 2) == 1,
+ "widening moves inline int32 slots out as doubles");
+ scr_arr_set_f64(w, 1, 2.5);
+ check(scr_arr_get_f64(w, 1) == 2.5, "a widened array stores fractions");
+ scr_arr_widen_i32(w);
+ check(w->elem == SCR_ELEM_F64 && scr_arr_get_f64(w, 1) == 2.5, "widening twice is a no-op");
+ scr_arr_release(w);
+
+ ScrArr *h = scr_arr_new_small(SCR_ELEM_I32);
+ for (int i = 0; i < 100; i++) scr_arr_push_f64(h, i * 3);
+ scr_arr_set_len(h, 120);
+ scr_arr_set_undefined(h, 5);
+ scr_arr_set_f64(h, 4000000, -7);
+ size_t cap = h->cap;
+ scr_arr_widen_i32(h);
+ bool widened = h->elem == SCR_ELEM_F64 && h->cap == cap && scr_arr_len(h) == 4000001 &&
+ scr_arr_states_end(h) == (const uint8_t *)h->data + 9 * (size_t)h->cap;
+ for (int i = 0; i < 100; i++)
+ if (i != 5) widened &= scr_arr_get_f64(h, i) == i * 3;
+ check(widened && scr_arr_state(h, 5) == SCR_ARR_UNDEFINED && scr_arr_state(h, 110) == SCR_ARR_HOLE &&
+ scr_arr_get_f64(h, 4000000) == -7 && arr_sparse_len(h) == 1,
+ "widening keeps values, present undefined, holes and sparse entries");
+ scr_arr_push_f64(h, 0.5);
+ check(scr_arr_get_f64(h, 4000001) == 0.5, "a widened array keeps growing as doubles");
+ scr_arr_release(h);
+
+ ScrArr *e = scr_arr_new_small(SCR_ELEM_I32);
+ scr_arr_widen_i32(e);
+ scr_arr_push_f64(e, -0.0);
+ check(e->elem == SCR_ELEM_F64 && signbit(scr_arr_get_f64(e, 0)), "an empty int32 array widens too");
+ scr_arr_release(e);
+}
+
static void test_inline_slots(void) {
#ifdef SCR_RC_AUDIT
long strings0 = scr_str_live_count();
@@ -2472,6 +2568,7 @@ int main(int argc, char **argv) {
test_splice_drop();
test_filled_prefix();
test_bool_bytes();
+ test_i32_slots();
test_inline_slots();
test_head_room();
test_states_on_demand();
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 589de2a51..525994ba1 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -110,8 +110,8 @@ importers:
specifier: workspace:*
version: link:../runtime
tsc-ts:
- specifier: 0.0.7
- version: 0.0.7
+ specifier: 0.0.8
+ version: 0.0.8
typescript:
specifier: 7.0.2
version: 7.0.2
@@ -1360,8 +1360,8 @@ packages:
cpu: [x64]
os: [win32]
- '@scriptc/threads@0.4.1':
- resolution: {integrity: sha512-wKgEBiVc07ZIGNFGwJpV5JbTXJN1N42qG6/g7ipnwt3klsU0nuWH2ToKV9vYH+RQstF7+twq6zMCf2qhUloTHQ==}
+ '@scriptc/threads@0.4.2':
+ resolution: {integrity: sha512-nR9FUxpOpgtTmJu6dejttElBL+JJTAZzJ2DZM+eNoujmLx9VpdlAVdgj3cu/MDsTUg/1sIUBq1ME1csC/Aa6yg==}
engines: {node: '>=24.0.0'}
'@sinclair/typebox@0.25.24':
@@ -1377,42 +1377,42 @@ packages:
'@ts-morph/common@0.11.1':
resolution: {integrity: sha512-7hWZS0NRpEsNV8vWJzg7FEz6V8MaLNeJOmwmghqUXTpzk16V1LLZhdo+4QvE/+zv4cVci0OviuJFnqhEfoV3+g==}
- '@tsc-ts/darwin-arm64@0.0.7':
- resolution: {integrity: sha512-nIaTJKBpG6n60xxRYfBlh02G6aSO2punIvj4qPy8/CXZu0Kw4ra/J3PYJtrtYMIh/eIGo4+f1Q01v5dFPPwZbw==}
+ '@tsc-ts/darwin-arm64@0.0.8':
+ resolution: {integrity: sha512-+hB9yWk58lwZHGrVEzqEC4D7MQW7Vd2z7blV3TULIKZPTMwdf8aT7XFAmfP48PoGff0H9yuQKyRXJl9I+sQv9w==}
cpu: [arm64]
os: [darwin]
- '@tsc-ts/darwin-x64@0.0.7':
- resolution: {integrity: sha512-8ITDyF3FG4SYeS1cV4+oRpbbk0jVsfd23Ww138wbQymbOLYjWPafzj4bymxnIE5kEWjZPVqTFDbaYqgWwNStag==}
+ '@tsc-ts/darwin-x64@0.0.8':
+ resolution: {integrity: sha512-yz4UddicqMXe+T7nV7ftpf155lkiK6gIPNwSB8rH9dlwiV6a17CO/vpSk627QLMqnfGNicB6YwLMk/SJ9moPdw==}
cpu: [x64]
os: [darwin]
- '@tsc-ts/linux-arm64-gnu@0.0.7':
- resolution: {integrity: sha512-nXGjtbM3YrARWOb+nkDaFdhKGSzPf3ofl/gV4x8JnVQ8SgK4IWJ5J0u93sk3mTYgdLj/Gcobp/D3lZ6WIzjKvA==}
+ '@tsc-ts/linux-arm64-gnu@0.0.8':
+ resolution: {integrity: sha512-LHqfT9mQ4tusPdWmnanCvC8/YBOF5bHM5h5863a3t2HTgFVdps1Qwb0yzrW5DDbojIhQ0Z+GupxtXVUQcV0Q/Q==}
cpu: [arm64]
os: [linux]
libc: [glibc]
- '@tsc-ts/linux-arm64-musl@0.0.7':
- resolution: {integrity: sha512-hSjpQsOhrLaDsbvEO5EH9nC3Vy0WAt3aB+GejytEh9fl4F3kT0BasbBP4wJqu6S5SG9upLW7ZClXiNVnGYDGlQ==}
+ '@tsc-ts/linux-arm64-musl@0.0.8':
+ resolution: {integrity: sha512-I0CC3xZMwoQM6OiVsubRHb0VI/RrJpneb33kDQhadYHq3taPos71KwP+CbCXNu88rNsDsyn7D1oCVfJb73g4Mw==}
cpu: [arm64]
os: [linux]
libc: [musl]
- '@tsc-ts/linux-x64-gnu@0.0.7':
- resolution: {integrity: sha512-MCU2MFefQGG/++nepg09BtYnsN2mVzeYfIS6GW30ChxUt2nJUMkEvzbDim9czDevW4LRqHgfnwEt+hs2ud1SDQ==}
+ '@tsc-ts/linux-x64-gnu@0.0.8':
+ resolution: {integrity: sha512-PLERuwUuxT92x0BZAMQubiQP5jhlqoLmswaZWqcvfKAZyBp0TbWiVOmuNnDbJozvBaNUrfdGPb9suiQ+Me7M6A==}
cpu: [x64]
os: [linux]
libc: [glibc]
- '@tsc-ts/linux-x64-musl@0.0.7':
- resolution: {integrity: sha512-abTZZkn74772MPABStuXWJ1GHI07nbEf4a3Nwl7NYI5mLFYxeLSbq6+cGh0BUJeBeMhFNsjtQ5/j5sGmsrJ2tg==}
+ '@tsc-ts/linux-x64-musl@0.0.8':
+ resolution: {integrity: sha512-zDl36l1uyWc0mQniOAMZmkVD9TrlFExHlDT0fL8JGD4XLgx+bRe+To38yP0KSBehXBzjDNn8xrEoJ+3RP6mYwQ==}
cpu: [x64]
os: [linux]
libc: [musl]
- '@tsc-ts/win32-x64-msvc@0.0.7':
- resolution: {integrity: sha512-EOhLTkuWYytJYklR5BI1jRtTpEIPh1FQJKD8R37qqlcnPrELlRO6Nc2ncDpdCoYbpN+KrewIVPB3xD4o3VGUaw==}
+ '@tsc-ts/win32-x64-msvc@0.0.8':
+ resolution: {integrity: sha512-9gO+pFoLiIxITlAE2P6JL8ihZwns7z/2Raw5+38RqLzHmnSsRN47+H4p/4EqQsLW5/TTsAo9uSeSkf3EuRg+XA==}
cpu: [x64]
os: [win32]
@@ -2577,8 +2577,8 @@ packages:
ts-toolbelt@6.15.5:
resolution: {integrity: sha512-FZIXf1ksVyLcfr7M317jbB67XFJhOO1YqdTcuGaq9q5jLUoTikukZ+98TPjKiP2jC5CgmYdWWYs0s2nLSU0/1A==}
- tsc-ts@0.0.7:
- resolution: {integrity: sha512-KRWTXRUvwMXi4At6xOWPDa/0BreY3MR2tPHooEMNWLZUA2eLphNe7PJiHclPLLn9RCnHY4ffDTTeF5hqgyBvtA==}
+ tsc-ts@0.0.8:
+ resolution: {integrity: sha512-EiyDziYOryBg/YEvev/mKCn+hD4M6VFjH7VP9nv2WadDKYaPrdb+a3hP+wYr/UsDWNE7AdL7YGnjZppeXlXy6g==}
engines: {node: '>=20'}
hasBin: true
@@ -3439,7 +3439,7 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.62.2':
optional: true
- '@scriptc/threads@0.4.1': {}
+ '@scriptc/threads@0.4.2': {}
'@sinclair/typebox@0.25.24': {}
@@ -3454,25 +3454,25 @@ snapshots:
mkdirp: 1.0.4
path-browserify: 1.0.1
- '@tsc-ts/darwin-arm64@0.0.7':
+ '@tsc-ts/darwin-arm64@0.0.8':
optional: true
- '@tsc-ts/darwin-x64@0.0.7':
+ '@tsc-ts/darwin-x64@0.0.8':
optional: true
- '@tsc-ts/linux-arm64-gnu@0.0.7':
+ '@tsc-ts/linux-arm64-gnu@0.0.8':
optional: true
- '@tsc-ts/linux-arm64-musl@0.0.7':
+ '@tsc-ts/linux-arm64-musl@0.0.8':
optional: true
- '@tsc-ts/linux-x64-gnu@0.0.7':
+ '@tsc-ts/linux-x64-gnu@0.0.8':
optional: true
- '@tsc-ts/linux-x64-musl@0.0.7':
+ '@tsc-ts/linux-x64-musl@0.0.8':
optional: true
- '@tsc-ts/win32-x64-msvc@0.0.7':
+ '@tsc-ts/win32-x64-msvc@0.0.8':
optional: true
'@tybys/wasm-util@0.10.3':
@@ -4813,17 +4813,17 @@ snapshots:
ts-toolbelt@6.15.5: {}
- tsc-ts@0.0.7:
+ tsc-ts@0.0.8:
dependencies:
- '@scriptc/threads': 0.4.1
+ '@scriptc/threads': 0.4.2
optionalDependencies:
- '@tsc-ts/darwin-arm64': 0.0.7
- '@tsc-ts/darwin-x64': 0.0.7
- '@tsc-ts/linux-arm64-gnu': 0.0.7
- '@tsc-ts/linux-arm64-musl': 0.0.7
- '@tsc-ts/linux-x64-gnu': 0.0.7
- '@tsc-ts/linux-x64-musl': 0.0.7
- '@tsc-ts/win32-x64-msvc': 0.0.7
+ '@tsc-ts/darwin-arm64': 0.0.8
+ '@tsc-ts/darwin-x64': 0.0.8
+ '@tsc-ts/linux-arm64-gnu': 0.0.8
+ '@tsc-ts/linux-arm64-musl': 0.0.8
+ '@tsc-ts/linux-x64-gnu': 0.0.8
+ '@tsc-ts/linux-x64-musl': 0.0.8
+ '@tsc-ts/win32-x64-msvc': 0.0.8
tslib@2.8.1:
optional: true
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index ceb1506e9..e74d40442 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -7,15 +7,15 @@ minimumReleaseAgeExclude:
- "effect@4.0.0"
- "@effect/platform-node@4.0.0"
- "@effect/platform-node-shared@4.0.0"
- - "@tsc-ts/darwin-arm64@0.0.7"
- - "@tsc-ts/darwin-x64@0.0.7"
- - "@tsc-ts/linux-arm64-gnu@0.0.7"
- - "@tsc-ts/linux-arm64-musl@0.0.7"
- - "@tsc-ts/linux-x64-gnu@0.0.7"
- - "@tsc-ts/linux-x64-musl@0.0.7"
- - "@tsc-ts/win32-x64-msvc@0.0.7"
- - "tsc-ts@0.0.7"
- - "@scriptc/threads@0.4.1"
+ - "@tsc-ts/darwin-arm64@0.0.8"
+ - "@tsc-ts/darwin-x64@0.0.8"
+ - "@tsc-ts/linux-arm64-gnu@0.0.8"
+ - "@tsc-ts/linux-arm64-musl@0.0.8"
+ - "@tsc-ts/linux-x64-gnu@0.0.8"
+ - "@tsc-ts/linux-x64-musl@0.0.8"
+ - "@tsc-ts/win32-x64-msvc@0.0.8"
+ - "tsc-ts@0.0.8"
+ - "@scriptc/threads@0.4.2"
allowBuilds:
esbuild: true
overrides:
diff --git a/scripts/verify-native-cli.mjs b/scripts/verify-native-cli.mjs
index 115573b5c..92de57938 100644
--- a/scripts/verify-native-cli.mjs
+++ b/scripts/verify-native-cli.mjs
@@ -94,5 +94,15 @@ if (process.argv.includes("--run")) {
}).trim();
if (version !== identity.version)
throw new Error(`native command reported ${version}, expected ${identity.version}`);
+ // tsc-ts, the default checker, runs on the distribution's own platform.
+ const tscTs = resolve(dirname(binary), hostPath(toolchain.tsc_ts));
+ if (windows && !tscTs.endsWith("tsc-ts.exe"))
+ throw new Error(`tsc_ts is not tsc-ts.exe: ${tscTs}`);
+ const tscTsVersion = execFileSync(tscTs, ["--version"], {
+ encoding: "utf8",
+ env: { ...process.env, PATH: "" },
+ }).trim();
+ if (!tscTsVersion.startsWith("Version "))
+ throw new Error(`the distribution's tsc-ts reported ${JSON.stringify(tscTsVersion)}`);
}
console.log(`${identity.name}@${identity.version}: native distribution verified`);
diff --git a/tests/corpus/6987-array-view-named-properties.ts b/tests/corpus/6987-array-view-named-properties.ts
index ee357d072..faba7dc90 100644
--- a/tests/corpus/6987-array-view-named-properties.ts
+++ b/tests/corpus/6987-array-view-named-properties.ts
@@ -102,13 +102,3 @@ for (const key in visited) {
console.log("visited key", key);
if (key === "first") delete (visited as unknown as Bag).second;
}
-
-// A non-configurable numeric-looking property refuses the delete and stays.
-const fixed: string[] = ["a"];
-Object.defineProperty(fixed as unknown as Bag, "-3", { value: "kept", enumerable: true });
-try {
- delete (fixed as unknown as Bag)["-3"];
-} catch (e) {
- console.log((e as Error).name);
-}
-console.log((fixed as unknown as Bag)["-3"], "-3" in (fixed as unknown as Bag));
diff --git a/tests/corpus/7006-evolving-array-aliases.ts b/tests/corpus/7006-evolving-array-aliases.ts
index 215b3f3c5..fce1d0946 100644
--- a/tests/corpus/7006-evolving-array-aliases.ts
+++ b/tests/corpus/7006-evolving-array-aliases.ts
@@ -77,3 +77,51 @@ function spreadSame(): number {
return xs.reduce((sum, v) => sum + v, 0);
}
console.log(spreadSame());
+
+// An object or array literal that holds the array between the writes
+// (`{ items: xs }`, `[xs]`, a shorthand property, a nested literal), a
+// binding of such a literal, and a name destructured from one hold the
+// same array, so they see the later writes.
+function objectHolder(): string {
+ const xs = [];
+ xs.push(1);
+ const box = { items: xs, label: "box" };
+ xs.push("two");
+ box.items.push(3);
+ return `${box.items.length} ${box.items === xs} ${box.items[1]} ${box.label} ${xs.join(",")}`;
+}
+function arrayHolder(): string {
+ const xs = [];
+ xs.push(1);
+ const rows = [xs, [5]];
+ xs.push("two");
+ let total = 0;
+ for (const row of rows) total += row.length;
+ return `${rows[0].length} ${rows[0] === xs} ${rows[0][1]} ${total}`;
+}
+function shorthandHolder(): string {
+ const xs = [];
+ xs.push(1);
+ const box = { xs, count: 3 };
+ xs.push("two");
+ return `${box.xs.length} ${box.xs === xs} ${box.count}`;
+}
+function nestedHolder(): string {
+ const xs = [];
+ xs.push(1);
+ const box = { inner: [xs], pair: [{ items: xs }] };
+ const outer = { box };
+ xs.push("two");
+ return `${box.inner[0].length} ${box.pair[0].items === xs} ${outer.box.inner[0][1]}`;
+}
+function holderAliases(): string {
+ const xs = [];
+ xs.push(1);
+ const box = { items: xs };
+ const other = box;
+ const { items } = box;
+ xs.push("two");
+ return `${other.items.length} ${items.length} ${items === xs} ${JSON.stringify(other)}`;
+}
+console.log(objectHolder(), "|", arrayHolder(), "|", shorthandHolder());
+console.log(nestedHolder(), "|", holderAliases());
diff --git a/tests/corpus/7037-js-var-open-writes/main.ts b/tests/corpus/7037-js-var-open-writes/main.ts
new file mode 100644
index 000000000..cc0612974
--- /dev/null
+++ b/tests/corpus/7037-js-var-open-writes/main.ts
@@ -0,0 +1,18 @@
+// A JavaScript package whose `var` bindings start as one type and later
+// hold another, as js-yaml's timestamp and error code does: a number that
+// becomes a string and back (`fraction = match[7].slice(0, 3)`), an object
+// literal that gains a member (`mark.snippet = snippet(mark)`), and `+=` of
+// a string onto a number. Node runs every write; each binding holds it.
+import { errorAt, millis, label, total } from "scalar-marks";
+
+console.log(millis("2001-12-14t21:59:43.10"), millis("2001-12-14t21:59:43"), millis("1970-01-01t00:00:00.12345"));
+for (const [input, at] of [["a: [1", 5], ["key: 'open", 10], ["a:\n [1", 7], ["ok", 1]] as const) {
+ try {
+ console.log(errorAt(input, at, "unexpected end"));
+ } catch (e) {
+ const err = e as { name: string; message: string; mark: { line: number; column: number; snippet: string } };
+ console.log(err.name, "|", err.message, "|", err.mark.line, err.mark.column, JSON.stringify(err.mark.snippet));
+ }
+}
+console.log(label(3), label(0));
+console.log(total([1, 2, 3]), total(["a", "b"]), total([1, "b", 2]));
diff --git a/tests/corpus/7037-js-var-open-writes/node_modules/scalar-marks/index.d.ts b/tests/corpus/7037-js-var-open-writes/node_modules/scalar-marks/index.d.ts
new file mode 100644
index 000000000..0e0f8252c
--- /dev/null
+++ b/tests/corpus/7037-js-var-open-writes/node_modules/scalar-marks/index.d.ts
@@ -0,0 +1,4 @@
+export declare function millis(data: string): number;
+export declare function errorAt(input: string, position: number, message: string): string;
+export declare function label(n: number): string;
+export declare function total(values: (number | string)[]): number | string;
diff --git a/tests/corpus/7037-js-var-open-writes/node_modules/scalar-marks/index.js b/tests/corpus/7037-js-var-open-writes/node_modules/scalar-marks/index.js
new file mode 100644
index 000000000..7d6b6116c
--- /dev/null
+++ b/tests/corpus/7037-js-var-open-writes/node_modules/scalar-marks/index.js
@@ -0,0 +1,81 @@
+"use strict";
+var TIMESTAMP = new RegExp(
+ '^([0-9][0-9][0-9][0-9])-([0-9][0-9]?)-([0-9][0-9]?)' +
+ '[Tt]([0-9][0-9]?):([0-9][0-9]):([0-9][0-9])(?:\\.([0-9]*))?$');
+
+// The milliseconds of a timestamp: the fraction starts as the number 0,
+// holds its digits as a string while it is padded, and ends as a number.
+function millis(data) {
+ var match, fraction = 0, delta = null;
+ match = TIMESTAMP.exec(data);
+ if (match === null) throw new Error('Date resolve error');
+ if (match[7]) {
+ fraction = match[7].slice(0, 3);
+ while (fraction.length < 3) {
+ fraction += '0';
+ }
+ fraction = +fraction;
+ }
+ if (delta) fraction -= delta;
+ return Date.UTC(+match[1], +match[2] - 1, +match[3], +match[4], +match[5], +match[6], fraction);
+}
+
+function State(input) {
+ this.input = input;
+ this.position = 0;
+ this.line = 0;
+ this.lineStart = 0;
+}
+
+function snippet(mark) {
+ var start = mark.position, text = mark.buffer;
+ return text.slice(0, start) + '<' + text.slice(start) + '>';
+}
+
+function MarkError(reason, mark) {
+ Error.call(this);
+ this.name = 'MarkError';
+ this.reason = reason;
+ this.mark = mark;
+ this.message = reason + ' (' + (mark.line + 1) + ':' + (mark.column + 1) + ')';
+}
+MarkError.prototype = Object.create(Error.prototype);
+MarkError.prototype.constructor = MarkError;
+
+// The error's mark is an object literal that gains its snippet afterwards.
+function generateError(state, message) {
+ var mark = {
+ buffer: state.input,
+ position: state.position,
+ line: state.line,
+ column: state.position - state.lineStart
+ };
+ mark.snippet = snippet(mark);
+ return new MarkError(message, mark);
+}
+
+function errorAt(input, position, message) {
+ var state = new State(input);
+ for (var i = 0; i < position && i < input.length; i++) {
+ state.position++;
+ if (input[i] === '\n') { state.line++; state.lineStart = state.position; }
+ }
+ if (state.position >= input.length) throw generateError(state, message);
+ return 'no error at ' + state.position;
+}
+
+// A number binding that `+=` turns into a string.
+function label(n) {
+ var width = 10;
+ width += 'px';
+ return n + ' ' + width;
+}
+
+// A sum that becomes a string concatenation once a string arrives.
+function total(values) {
+ var sum = 0;
+ for (var i = 0; i < values.length; i++) sum += values[i];
+ return sum;
+}
+
+module.exports = { millis: millis, errorAt: errorAt, label: label, total: total };
diff --git a/tests/corpus/7037-js-var-open-writes/node_modules/scalar-marks/package.json b/tests/corpus/7037-js-var-open-writes/node_modules/scalar-marks/package.json
new file mode 100644
index 000000000..ade0a9891
--- /dev/null
+++ b/tests/corpus/7037-js-var-open-writes/node_modules/scalar-marks/package.json
@@ -0,0 +1 @@
+{ "name": "scalar-marks", "version": "1.0.0", "main": "./index.js", "types": "./index.d.ts" }
diff --git a/tests/corpus/7038-js-var-later-writes.mjs b/tests/corpus/7038-js-var-later-writes.mjs
new file mode 100644
index 000000000..48b85d77a
--- /dev/null
+++ b/tests/corpus/7038-js-var-later-writes.mjs
@@ -0,0 +1,59 @@
+// JavaScript `var` and `let` bindings that later hold another type than
+// their initializer's, which nothing checks in a JavaScript file: Node
+// runs each write, and a function returning the binding returns what it
+// holds.
+function retyped(input) {
+ var value = 0;
+ value = input.slice(0, 2);
+ return value;
+}
+
+function padded(digits) {
+ var fraction = 0;
+ if (digits) {
+ fraction = digits.slice(0, 3);
+ while (fraction.length < 3) fraction += "0";
+ fraction = +fraction;
+ }
+ return fraction + 1;
+}
+
+function suffixed(n) {
+ var width = n * 2;
+ width += "px";
+ return width;
+}
+
+function letRetyped(record) {
+ let current = 1;
+ current = record.name;
+ return current;
+}
+
+function grown(line) {
+ var mark = { line: line, column: line - 1 };
+ mark.text = "at " + (mark.line + 1);
+ return mark;
+}
+
+async function awaitedTotal(values) {
+ var total = 0;
+ for (var i = 0; i < values.length; i++) total += await values[i];
+ console.log("awaited", total, typeof total);
+ return total;
+}
+
+function counted(values) {
+ var count = 0;
+ for (var i = 0; i < values.length; i++) count += values[i];
+ return count;
+}
+
+console.log(retyped("abc"), typeof retyped("abc"));
+console.log(padded("5"), padded("12345"), padded(""));
+console.log(suffixed(4), typeof suffixed(4));
+console.log(letRetyped({ name: "x" }), letRetyped({ name: 2 }));
+console.log(JSON.stringify(grown(3)), Object.keys(grown(0)).join(","));
+console.log(counted([1, 2]), counted([1, "a", 2]));
+await awaitedTotal([1, Promise.resolve(2)]);
+await awaitedTotal([1, Promise.resolve("b")]);
diff --git a/tests/corpus/7060-array-numeric-keys.cjs b/tests/corpus/7060-array-numeric-keys.cjs
new file mode 100644
index 000000000..5a1978cd9
--- /dev/null
+++ b/tests/corpus/7060-array-numeric-keys.cjs
@@ -0,0 +1,50 @@
+// Number keys that are not array indices ("-1", "1.5", "NaN", "Infinity")
+// are ordinary properties of an array. Writes and reads through an untyped
+// function, through a string key, and through the array itself all reach
+// the same property, and key walks list them after the indices.
+
+function put(target, key, value) {
+ target[key] = value;
+}
+function get(target, key) {
+ return target[key];
+}
+function has(target, key) {
+ return key in target;
+}
+function drop(target, key) {
+ delete target[key];
+}
+
+const empty = [];
+empty[-1] = "x";
+empty[1.5] = "y";
+empty[NaN] = "z";
+console.log(empty[-1], empty[1.5], empty[NaN], empty.length);
+console.log(empty["-1"], empty["1.5"], empty["NaN"]);
+
+const words = ["p", "q"];
+put(words, -1, "x");
+put(words, 1.5, "y");
+put(words, NaN, "z");
+put(words, Infinity, "inf");
+console.log(get(words, -1), get(words, 1.5), get(words, NaN), get(words, Infinity));
+console.log(words[-1], words[1.5], words[NaN], words[Infinity], words["-1"], words.length);
+console.log(get(words, "-1"), get(words, "1.50"), get(words, "-0"), has(words, "1.5"));
+console.log(Object.keys(words), JSON.stringify(words));
+for (const key in words) console.log("key", key);
+
+words[-1] = "again";
+console.log(get(words, -1), words["-1"]);
+put(words, "label", "named");
+console.log(get(words, "label"), get(words, -1));
+drop(words, -1);
+console.log(words[-1], has(words, -1), get(words, 1.5));
+console.log(Object.keys(words));
+
+const numbers = [1, 2];
+put(numbers, -1, 10);
+put(numbers, 2.5, 20);
+console.log(numbers[-1], get(numbers, -1), numbers[2.5], numbers.length);
+numbers.push(3);
+console.log(get(numbers, -1), numbers.join(","));
diff --git a/tests/corpus/7061-array-object-keys.ts b/tests/corpus/7061-array-object-keys.ts
new file mode 100644
index 000000000..93691a78f
--- /dev/null
+++ b/tests/corpus/7061-array-object-keys.ts
@@ -0,0 +1,28 @@
+// Object.keys, Object.values and Object.entries of a typed array list the
+// present indices, then its numeric properties ("-1", "1.5"), then its
+// named properties, as in Node. Holes are skipped.
+
+type Bag = Record;
+
+const words: string[] = ["p", "q"];
+words[-1] = "x";
+(words as unknown as Bag)["1.5"] = "y";
+(words as unknown as Bag).label = "named";
+console.log(Object.keys(words), Object.values(words), Object.entries(words));
+console.log((words as unknown as Bag)["-1"], words[1.5], "1.5" in (words as unknown as Bag));
+for (const key of Object.keys(words)) console.log(key, key.length);
+
+const holes: (number | undefined)[] = [1, , 3, undefined];
+console.log(Object.keys(holes), Object.values(holes), Object.entries(holes));
+
+const empty: number[] = [];
+console.log(Object.keys(empty).length, Object.entries(empty));
+
+interface Point {
+ x: number;
+}
+const points: Point[] = [{ x: 1 }, { x: 2 }];
+console.log(Object.keys(points), Object.values(points));
+const keys: string[] = Object.keys(points);
+keys.push("extra");
+console.log(keys.join("|"), Object.keys(points).length);
diff --git a/tests/corpus/7062-inspect-array-properties.ts b/tests/corpus/7062-inspect-array-properties.ts
new file mode 100644
index 000000000..7cd5ca984
--- /dev/null
+++ b/tests/corpus/7062-inspect-array-properties.ts
@@ -0,0 +1,61 @@
+// console.log of an array lists its named and numeric properties after the
+// elements, as Node's util.inspect does: typed arrays written through an
+// unknown view, regex match results, arrays from JSON.parse, holes, Node's
+// column grouping and the 100-entry limit.
+
+type Bag = Record;
+
+const named: number[] = [1, 2, 3];
+(named as unknown as Bag).extra = "named";
+console.log(named);
+
+const words: string[] = ["a"];
+(words as unknown as Bag)["my-key"] = { deep: [1, { x: 1 }] };
+console.log(words, [words]);
+
+const empty: number[] = [];
+(empty as unknown as Bag).x = 1;
+console.log(empty);
+
+const numeric: number[] = [1, 2];
+numeric[-1] = 5;
+numeric[1.5] = 6;
+console.log(numeric);
+
+const holes: (number | undefined)[] = [1, , 3];
+(holes as unknown as Bag).tail = true;
+console.log(holes);
+
+const grid: number[] = [];
+for (let i = 0; i < 30; i++) grid.push(i * 7);
+(grid as unknown as Bag).label = "grid";
+console.log(grid);
+
+const long: number[] = [];
+for (let i = 0; i < 120; i++) long.push(i);
+(long as unknown as Bag).label = "long";
+console.log(long);
+
+const full: number[] = [];
+for (let i = 0; i < 100; i++) full.push(i);
+(full as unknown as Bag).label = "full";
+console.log(full);
+
+const match = /b(?c)?/.exec("abc");
+console.log(match);
+const plainMatch = /x/.exec("axb");
+console.log(plainMatch);
+
+const parsed = JSON.parse("[1,2,3,4,5,6,7,8]") as unknown as Bag;
+parsed.extra = "named";
+console.log(parsed);
+
+const plain: number[] = [1, 2, 3];
+console.log(plain, [] as number[]);
+
+const accessed: number[] = [1, 2];
+const accessedBag = accessed as unknown as Bag;
+Object.defineProperty(accessedBag, "both", { get: () => 1, set: (_v: unknown) => {}, enumerable: true });
+Object.defineProperty(accessedBag, "getter", { get: () => 2, enumerable: true });
+Object.defineProperty(accessedBag, "setter", { set: (_v: unknown) => {}, enumerable: true });
+console.log(accessed);
diff --git a/tests/corpus/7325-array-index-products-near-2-53.ts b/tests/corpus/7325-array-index-products-near-2-53.ts
new file mode 100644
index 000000000..41e60b09d
--- /dev/null
+++ b/tests/corpus/7325-array-index-products-near-2-53.ts
@@ -0,0 +1,82 @@
+// Element reads and writes indexed by products of int32 values plus or
+// minus int32 terms. The products can pass the int32 range and 2^53, where
+// a double product rounds: an index that lands in an array's range reads
+// and writes that element, and every other one (negative, past the length,
+// rounded past 2^53) keeps the property key Node computes from the double.
+
+function show(label: string, value: unknown): void {
+ console.log(label, Object.is(value, -0) ? "-0" : String(value));
+}
+
+function read(a: number[], x: number, y: number, k: number): number | undefined {
+ return a[x * y + k];
+}
+
+function readBack(a: number[], x: number, y: number, k: number): number | undefined {
+ return a[x * y - k];
+}
+
+function write(a: number[], x: number, y: number, k: number, v: number): void {
+ a[x * y + k] = v;
+}
+
+function rows(n: number, cols: number, a: number[]): number {
+ let s = 0;
+ for (let i = 0; i < n; i++) for (let j = 0; j < cols; j++) s += a[i * cols + j]! * (j + 1);
+ return s;
+}
+
+const a: number[] = [];
+for (let i = 0; i < 64; i++) a.push(i * 10);
+
+const big = 2147483647;
+const half = 65536;
+
+// In range.
+show("3*5+2", read(a, 3 | 0, 5 | 0, 2 | 0));
+show("7*-1+9", read(a, 7 | 0, -1 | 0, 9 | 0));
+show("-0 index", read(a, -1 | 0, 0 | 0, 0 | 0));
+// The product passes the int32 range; the difference lands in range.
+show("2^16*2^15-2^31+12", readBack(a, half | 0, 32768 | 0, (-2147483648 * -1 - 12) | 0));
+show("(2^31-1)*1-(2^31-1)+5", read(a, big | 0, 1 | 0, -2147483642 | 0));
+show("2^16*2^16-big+20", readBack(a, half | 0, half | 0, (big - 20) | 0));
+// Out of range or negative: holes and named keys.
+show("past end", read(a, 8 | 0, 8 | 0, 1 | 0));
+show("negative", read(a, -3 | 0, 5 | 0, 0 | 0));
+show("2^31*2^31", read(a, -2147483648 | 0, -2147483648 | 0, 7 | 0));
+
+// Products around 2^53, where the double product rounds.
+const keys: number[] = [];
+const x = 134217727; // 2^27 - 1
+const y = 67108865; // 2^26 + 1: x * y = 2^53 + 2^26 - 1
+const near: [number, number, number][] = [
+ [x, y, 0],
+ [x, y, 1],
+ [x, y, -67108864],
+ [x, y, -67108863],
+ [94906265, 94906265, 0],
+ [94906267, 94906267, -3],
+ [-2147483648, 2147483647, 2147483647],
+];
+for (const [p, q, k] of near) {
+ const sparse: number[] = [];
+ write(sparse, p | 0, q | 0, k | 0, 1);
+ // The key the double computes; a local holds only the double.
+ const key: number = p * q + k;
+ keys.push(key);
+ show(`write ${p}*${q}+${k}`, `${sparse[key]} ${sparse[key + 1]} ${sparse[key - 1]} ${sparse.length}`);
+ show(`read ${p}*${q}+${k}`, read(sparse, p | 0, q | 0, k | 0));
+}
+show("keys", keys.join(","));
+
+// A difference of two products above 2^53 keeps the double's rounding.
+function cancel(sparse: number[], p: number, q: number, r: number, s: number): number | undefined {
+ return sparse[p * q - r * s];
+}
+const cancelled: number[] = [7, 8, 9];
+show("cancel", cancel(cancelled, x | 0, y | 0, x | 0, y | 0));
+show("cancel+1", cancel(cancelled, x | 0, y | 0, (9007199254740992 / 67108864) | 0, 67108864 | 0));
+
+// Row-major sums with a width from JSON.
+const dims = JSON.parse("[[8, 8], [4, 16], [16, 4], [3, 7]]") as [number, number][];
+for (const [n, cols] of dims) show(`rows ${n}x${cols}`, rows(n | 0, cols | 0, a));
diff --git a/tests/corpus/7326-int32-element-arrays.ts b/tests/corpus/7326-int32-element-arrays.ts
new file mode 100644
index 000000000..f5dc0d787
--- /dev/null
+++ b/tests/corpus/7326-int32-element-arrays.ts
@@ -0,0 +1,165 @@
+// Local number arrays whose every stored element is an int32: reads, writes,
+// holes, present undefined, out-of-range and noncanonical indexes, growth,
+// pop, the fill form, reassignment, and arrays that do not qualify.
+
+function compose(n: number, rounds: number): number {
+ const p: number[] = [];
+ const q: number[] = [];
+ for (let i = 0; i < n; i++) p.push((i * 7919 + 3) & (n - 1));
+ for (let i = 0; i < n; i++) q.push(i);
+ let check = 0;
+ for (let r = 0; r < rounds; r++) {
+ for (let i = 0; i < n; i++) q[i] = p[q[i]!]!;
+ check = (check + q[r % n]!) | 0;
+ }
+ return check;
+}
+
+function insertionSort(n: number): string {
+ const a: number[] = [];
+ let seed = 12345;
+ for (let i = 0; i < n; i++) {
+ seed = (seed * 1103515245 + 12345) & 0x7fffffff;
+ a.push((seed % 2001) - 1000);
+ }
+ for (let i = 1; i < n; i++) {
+ for (let j = i; j > 0 && a[j - 1]! > a[j]!; j--) {
+ const t = a[j]!;
+ a[j] = a[j - 1]!;
+ a[j - 1] = t;
+ }
+ }
+ let sorted = true;
+ for (let i = 1; i < n; i++) if (a[i - 1]! > a[i]!) sorted = false;
+ return `${sorted} ${a[0]} ${a[n >> 1]} ${a[n - 1]} ${a.length}`;
+}
+
+function reads(): string[] {
+ const a = [7, -3, 2147483647, -2147483648, 0];
+ const out: string[] = [];
+ out.push(`${a[0]} ${a[1]} ${a[2]} ${a[3]} ${a[4]} ${a[5]}`);
+ out.push(`${a[2]! + 1} ${a[3]! - 1} ${a[1]! / 2} ${a[2]! * 2}`);
+ const idx = [0, 4, 5, -1, 1.5, NaN, -0, 2 ** 32, Infinity];
+ for (const i of idx) out.push(`${i}: ${a[i]} ${a[i] === undefined} ${i in a}`);
+ out.push(`len ${a.length}`);
+ return out;
+}
+
+function holes(): string[] {
+ const a = [1, 2, 3];
+ const out: string[] = [];
+ a[6] = 9;
+ out.push(`len ${a.length} ${a[3]} ${a[6]} ${4 in a} ${6 in a}`);
+ a[4] = -5;
+ let sum = 0,
+ present = 0;
+ for (let i = 0; i < a.length; i++) {
+ if (a[i] !== undefined) {
+ sum += a[i]!;
+ present++;
+ }
+ }
+ out.push(`sum ${sum} present ${present}`);
+ // A hole read through a non-null assertion stores a present undefined.
+ const q = [10, 20, 30];
+ q[1] = a[3]!;
+ out.push(`q ${q[0]} ${q[1]} ${q[2]} ${1 in q} ${q.length}`);
+ q[1] = 21;
+ out.push(`q ${q[1]} ${(q[0]! + q[1]!) | 0}`);
+ return out;
+}
+
+function sparse(): string[] {
+ const a = [1, 2];
+ const out: string[] = [];
+ a[100000] = 42;
+ a[3] = 4;
+ out.push(`len ${a.length} ${a[100000]} ${a[3]} ${a[2]} ${a[99999]}`);
+ a[100000] = -42;
+ out.push(`${a[100000]} ${a[100001]}`);
+ return out;
+}
+
+function growth(n: number): string[] {
+ const a: number[] = [];
+ const out: string[] = [];
+ for (let i = 0; i < n; i++) a.push((i * i) | 0);
+ out.push(`len ${a.length} ${a[0]} ${a[n - 1]} ${a[n]}`);
+ let sum = 0;
+ while (a.length > n / 2) sum = (sum + a.pop()!) | 0;
+ out.push(`popped ${sum} len ${a.length} last ${a[a.length - 1]}`);
+ for (let i = 0; i < 3; i++) a.push(i - 3);
+ out.push(`len ${a.length} ${a[a.length - 3]} ${a[a.length - 1]}`);
+ while (a.length > 0) a.pop();
+ out.push(`empty ${a.length} ${a.pop()} ${a[0]}`);
+ a.push(5);
+ out.push(`again ${a.length} ${a[0]}`);
+ return out;
+}
+
+function filled(n: number): string[] {
+ const counts = new Array(n).fill(0) as number[];
+ const marks: number[] = new Array(n).fill(-1);
+ for (let i = 0; i < 3 * n; i++) {
+ const k = ((i * 37) % n) | 0;
+ counts[k] = (i * 3 - 7) | 0;
+ marks[(k + 1) % n] = k;
+ }
+ let a = 0,
+ b = 0;
+ for (let i = 0; i < n; i++) {
+ a = (a + counts[i]!) | 0;
+ b = (b + marks[i]!) | 0;
+ }
+ return [`fill ${counts.length} ${a} ${b} ${counts[n]} ${marks[0]}`];
+}
+
+function reassigned(rounds: number): string {
+ let a = [1, 2, 3];
+ let total = 0;
+ for (let r = 0; r < rounds; r++) {
+ if (r % 3 === 0) a = [r, r + 1];
+ else a.push(r);
+ total = (total + a[a.length - 1]! + a.length) | 0;
+ }
+ return `reassigned ${total} ${a.length} ${a[0]}`;
+}
+
+// These arrays do not qualify; they print the same values as before.
+function disqualified(): string[] {
+ const out: string[] = [];
+ const negZero = [0, -0];
+ out.push(`${Object.is(negZero[1], -0)} ${1 / negZero[1]!}`);
+ const fraction = [1, 2];
+ fraction[1] = 2.5;
+ out.push(`${fraction[1]}`);
+ const big = [1];
+ big[0] = big[0]! + 2147483647;
+ out.push(`${big[0]}`);
+ const joined = [3, 1, 2];
+ joined.sort((x, y) => x - y);
+ out.push(joined.join(","));
+ const escaped = [4, 5];
+ const alias = escaped;
+ alias[0] = 6;
+ out.push(`${escaped[0]} ${alias[1]}`);
+ const counter = [0];
+ for (let i = 0; i < 10; i++) counter[0] = counter[0]! + 1;
+ out.push(`${counter[0]}`);
+ return out;
+}
+
+const config = JSON.parse('{"n": 256, "rounds": 50, "sort": 300}') as {
+ n: number;
+ rounds: number;
+ sort: number;
+};
+console.log(compose(config.n | 0, config.rounds | 0));
+console.log(insertionSort(config.sort | 0));
+for (const line of reads()) console.log(line);
+for (const line of holes()) console.log(line);
+for (const line of sparse()) console.log(line);
+for (const line of growth(config.n | 0)) console.log(line);
+for (const line of filled(17)) console.log(line);
+console.log(reassigned(config.rounds | 0));
+for (const line of disqualified()) console.log(line);
diff --git a/tests/corpus/7327-int32-array-widening.ts b/tests/corpus/7327-int32-array-widening.ts
new file mode 100644
index 000000000..798292c11
--- /dev/null
+++ b/tests/corpus/7327-int32-array-widening.ts
@@ -0,0 +1,186 @@
+// Local number arrays with checked int32 stores: `new Array(n).fill(v)`
+// arrays whose stores are exact integers but not proven int32s. A fill of
+// at least 4096 elements creates int32 slots, and the first store past the
+// int32 range widens the array to doubles in place; a shorter fill creates
+// doubles. Each case runs at both sizes. Arrays with a store that has no
+// exact integer range (a product of two int32s, a fraction, a value that may
+// be -0), and arrays allocated as literals, keep doubles throughout.
+
+function counters(n: number, rounds: number): string {
+ const c: number[] = new Array(n).fill(0);
+ for (let r = 0; r < rounds; r++)
+ for (let i = 0; i < c.length; i++) c[i] = c[i]! + ((i & 3) + 1) * 1000000;
+ return `counters ${n} ${c[0]} ${c[1]} ${c[2]} ${c[3]} ${c[n - 1]} ${c.length}`;
+}
+
+// The store at i = 45 passes 2^31 and widens the array partway through a
+// loop; the remaining passes store doubles.
+function denseLoop(n: number, k: number): string {
+ const a: number[] = new Array(n).fill(0);
+ for (let i = 0; i < a.length; i++) a[i] = i + k + 2147483600;
+ let s = 0;
+ for (let i = 0; i < a.length; i++) s = (s + a[i]!) % 1000003;
+ return `dense ${n} ${a[44]} ${a[45]} ${a[n - 1]} ${s}`;
+}
+
+// Checked stores whose values stay int32: no widening.
+function bfs(n: number): string {
+ const dist: number[] = new Array(n).fill(-1);
+ const queue: number[] = [0];
+ dist[0] = 0;
+ for (let h = 0; h < queue.length; h++) {
+ const u = queue[h]!;
+ const edges = [(u + 1) & (n - 1), (u * 5 + 1) & (n - 1)];
+ for (let j = 0; j < edges.length; j++) {
+ const v = edges[j]!;
+ if (dist[v]! < 0) {
+ dist[v] = dist[u]! + 1;
+ queue.push(v);
+ }
+ }
+ }
+ let far = 0,
+ sum = 0;
+ for (let i = 0; i < n; i++) {
+ if (dist[i]! > far) far = dist[i]!;
+ sum += dist[i]!;
+ }
+ return `bfs ${n} ${far} ${sum} ${queue.length}`;
+}
+
+// A fill value that is not an int32 widens the new array before the fill.
+function filledPastInt32(n: number, k: number): string {
+ const a: number[] = new Array(n).fill(k * 1000000000);
+ a[1] = a[1]! + 1;
+ const b: number[] = new Array(n).fill(k);
+ b[2] = b[2]! * 2;
+ return `filled ${n} ${a[0]} ${a[1]} ${a.length} ${b[0]} ${b[2]}`;
+}
+
+// A chain of sums crosses 2^31 partway through; the second loop runs after
+// the array widened.
+function chain(n: number, step: number): string {
+ const a: number[] = new Array(n).fill(0);
+ for (let i = 1; i < n; i++) a[i] = a[i - 1]! + (i < 16 ? step : 1);
+ let s = 0;
+ for (let i = 0; i < n; i++) s = (s + (a[i]! % 1000003)) % 1000003;
+ for (let i = 1; i < n; i++) a[i] = a[i]! - a[i - 1]!;
+ return `chain ${n} ${step} ${a[1]} ${a[5]} ${a[6]} ${a[n - 1]} ${s}`;
+}
+
+// Two arrays in one loop: one widens partway through, the other keeps
+// int32 slots; then both are read in another loop.
+function twoArrays(n: number, m: number, k: number): string {
+ const small: number[] = new Array(n).fill(1);
+ const big: number[] = new Array(m).fill(k);
+ const count = Math.min(n, m);
+ for (let i = 1; i < count; i++) {
+ small[i] = small[i - 1]! + 1;
+ big[i] = big[i - 1]! + (i < 8 ? 700000000 : 1);
+ }
+ let s = 0,
+ t = 0;
+ for (let i = 0; i < count; i++) {
+ s += small[i]!;
+ t = (t + (big[i]! % 997)) % 997;
+ }
+ return `two ${n} ${m} ${small[count - 1]} ${big[2]} ${big[3]} ${big[count - 1]} ${s} ${t}`;
+}
+
+// Pushes that pass 2^31 widen the array before they append.
+function pushes(n: number, k: number): string {
+ const a: number[] = new Array(n).fill(k);
+ for (let i = 0; i < 8; i++) a.push(a[a.length - 1]! + 900000000);
+ let s = 0;
+ for (let i = 0; i < a.length; i++) s = (s + (a[i]! % 10007)) % 10007;
+ return `pushes ${n} ${a.length} ${a[n]} ${a[n + 2]} ${a[n + 7]} ${s}`;
+}
+
+function controlFlow(n: number, k: number): string {
+ const a: number[] = new Array(n).fill(k);
+ let stores = 0;
+ for (let i = 1; i < n; i++) {
+ if (i % 2 === 0) continue;
+ a[i] = a[i - 1]! + 1000000000 * (i % 4);
+ stores++;
+ if (a[i]! > 2500000000) break;
+ }
+ let i = 0;
+ while (i < n && a[i]! < 2000000000) i++;
+ let j = n;
+ do j--;
+ while (j > 0 && a[j] === k);
+ return `flow ${n} ${stores} ${a[1]} ${a[2]} ${a[3]} ${i} ${j}`;
+}
+
+function readOnly(n: number, wide: boolean): string {
+ const a: number[] = new Array(n).fill(2);
+ if (wide) a[0] = a[0]! + 3000000000;
+ let s = 0,
+ present = 0;
+ for (let i = 0; i < n + 2; i++) {
+ s += a[i] ?? 0;
+ if (i in a) present++;
+ }
+ return `read ${n} ${wide} ${s} ${present}`;
+}
+
+function holes(n: number): string {
+ // Holes, present undefined and sparse entries survive widening.
+ const a: number[] = new Array(n).fill(3);
+ a[n + 3] = a[1]! + 1;
+ a[n + 2] = a[n + 3]! - 1;
+ a[2 * n + 100000] = a[0]! + 1;
+ a[n + 1] = a[n + 2]! + 3000000000;
+ return `holes ${n} ${a.length} ${a[n]} ${n in a} ${a[n + 1]} ${a[n + 2]} ${a[2 * n + 100000]}`;
+}
+
+// These arrays never take int32 slots: a fractional store, a product of
+// two int32s, -0, an escape, and arrays allocated as literals.
+function neverInt32(n: number, x: number, y: number): string[] {
+ const f: number[] = new Array(n).fill(1);
+ f[0] = f[0]! / x;
+ const m: number[] = new Array(n).fill(3);
+ m[1] = m[1]! * y;
+ const z: number[] = new Array(n).fill(4);
+ z[1] = z[1]! % 2;
+ z[2] = -z[0]! % 2;
+ const j: number[] = new Array(n).fill(3);
+ j[0] = j[0]! + 1;
+ const lit = [1, 2, 3];
+ lit[0] = lit[0]! + 3000000000;
+ const pushed: number[] = [];
+ for (let i = 0; i < 4; i++) pushed.push(i * 1000000000);
+ pushed[0] = pushed[3]! + pushed[2]!;
+ return [
+ `never ${f[0]} ${m[1]} ${z[1]} ${Object.is(z[2], -0)} ${j.slice(0, 2).join("|")}`,
+ `literals ${lit[0]} ${lit[2]} ${pushed[0]} ${pushed.length}`,
+ ];
+}
+
+const config = JSON.parse('{"small": 64, "large": 8192, "k": 3, "y": 2000000000}') as {
+ small: number;
+ large: number;
+ k: number;
+ y: number;
+};
+const lines: string[] = [];
+for (const n of [config.small | 0, config.large | 0]) {
+ lines.push(counters(n, 1000));
+ lines.push(denseLoop(n, config.k | 0));
+ lines.push(bfs(n));
+ lines.push(filledPastInt32(n, config.k | 0));
+ lines.push(chain(n, 400000000));
+ lines.push(chain(n, 100000000));
+ lines.push(twoArrays(n, n, config.k | 0));
+ lines.push(pushes(n, config.k | 0));
+ lines.push(controlFlow(n, config.k | 0));
+ lines.push(readOnly(n, false));
+ lines.push(readOnly(n, true));
+ lines.push(holes(n));
+}
+// One long array and one short: their kinds differ on entry to the loop.
+lines.push(twoArrays(config.large | 0, config.small | 0, config.k | 0));
+lines.push(twoArrays(config.small | 0, config.large | 0, config.k | 0));
+lines.push(...neverInt32(config.small | 0, config.k, config.y | 0));
+for (const line of lines) console.log(line);
diff --git a/tests/corpus/7364-new-target-late-instantiations.ts b/tests/corpus/7364-new-target-late-instantiations.ts
new file mode 100644
index 000000000..3fa7ff94a
--- /dev/null
+++ b/tests/corpus/7364-new-target-late-instantiations.ts
@@ -0,0 +1,71 @@
+// new.target in a constructor whose subclasses are generic classes
+// instantiated only in bodies lowered later, and classes whose own static
+// `name` field replaces their name.
+class Plain {
+ readonly label: string;
+ readonly plain: boolean;
+ constructor() {
+ this.label = new.target.name;
+ this.plain = new.target === Plain;
+ }
+}
+class Sub extends Plain {
+ readonly v: T;
+ constructor(v: T) {
+ super();
+ this.v = v;
+ }
+}
+
+class Box {
+ readonly label: string;
+ readonly exact: boolean;
+ readonly value: T;
+ constructor(value: T) {
+ this.value = value;
+ this.label = new.target.name;
+ this.exact = new.target === Leaf;
+ }
+}
+class Tagged extends Box {}
+class Leaf extends Tagged {}
+
+class Named {
+ static name = "custom";
+ readonly label: string;
+ constructor() {
+ this.label = new.target.name;
+ }
+}
+class Child extends Named {}
+class Other extends Named {
+ static name = "other";
+}
+class Family {
+ static name = "family";
+ readonly label: string;
+ readonly v: T;
+ constructor(v: T) {
+ this.label = new.target.name;
+ this.v = v;
+ }
+}
+
+function later(): void {
+ const n = new Sub(1);
+ const p = new Plain();
+ console.log(n.label, new Sub("s").label, p.label, n.plain, p.plain);
+ const t = new Tagged(1);
+ const s = new Tagged("a");
+ const b = new Box(2);
+ const l = new Leaf(3);
+ console.log(t.label, s.label, b.label, l.label, t.exact, l.exact, b.exact);
+ console.log(new Family(true).label);
+}
+
+// The bases construct here first; their subclasses only in later().
+console.log(new Plain().label, new Box(false).label);
+console.log(new Named().label, new Child().label, new Other().label);
+Named.name = "changed";
+console.log(new Named().label, new Child().label, new Other().label);
+later();
diff --git a/tests/corpus/7402-unknown-array-methods.ts b/tests/corpus/7402-unknown-array-methods.ts
new file mode 100644
index 000000000..e374dd40f
--- /dev/null
+++ b/tests/corpus/7402-unknown-array-methods.ts
@@ -0,0 +1,14 @@
+// Methods on arrays of unknown elements, which are stored checked-dynamic:
+// direct `unknown[]` bindings and generic bodies instantiated at `unknown`.
+const values: unknown[] = [1, "two", 3, { four: 4 }];
+const removed = values.splice(1, 2);
+console.log(values, removed, removed.length);
+values.splice(1, 0, "inserted", null);
+console.log(values, values.indexOf(null), values.includes("inserted"));
+console.log(values.slice(1).reverse(), values.join("|"));
+
+function takeAt(items: readonly T[], index: number): T[] {
+ const pool = [...items];
+ return pool.splice(index, 1);
+}
+console.log(takeAt([10, 20, 30], 1), takeAt([true, "x", 7], 2));
diff --git a/tests/corpus/7441-untyped-receiver-own-string-method-names.ts b/tests/corpus/7441-untyped-receiver-own-string-method-names.ts
new file mode 100644
index 000000000..632ceb16f
--- /dev/null
+++ b/tests/corpus/7441-untyped-receiver-own-string-method-names.ts
@@ -0,0 +1,38 @@
+// An untyped receiver calling a String.prototype method name holds a
+// string or a checked-dynamic object with its own method of that name
+// (zod's schemas assign `trim`, `toLowerCase` and `startsWith` in their
+// constructors, so `z.string().trim().min(1)` chains schema calls).
+// Each call dispatches on the receiver's runtime kind.
+interface Field {
+ value: string;
+ checks: string[];
+}
+
+function field(value: string, checks: string[]): any {
+ const self: Field & Record = { value, checks };
+ self["trim"] = () => field(value.trim(), [...checks, "trim"]);
+ self["toLowerCase"] = () => field(value.toLowerCase(), [...checks, "lower"]);
+ self["startsWith"] = (prefix: string) => checks.length > 0 && value.startsWith(prefix);
+ self["min"] = (n: number) => field(value, [...checks, `min(${n})`]);
+ return self;
+}
+
+const chained = field(" Hello World ", []).trim().toLowerCase().min(1);
+console.log(JSON.stringify(chained.value), chained.checks.join(","));
+console.log(field("abc", ["x"]).startsWith("ab"), field("abc", []).startsWith("ab"));
+
+const text: any = " Mixed Case ";
+console.log(JSON.stringify(text.trim()), text.toLowerCase(), text.startsWith(" M"));
+
+const parsed: any = JSON.parse('{"name":" padded "}');
+console.log(JSON.stringify(parsed.name.trim()));
+
+for (const value of [field(" a ", []), " b ", 42, null]) {
+ const v: any = value;
+ try {
+ const out = v.trim();
+ console.log(typeof out === "string" ? JSON.stringify(out) : `field ${JSON.stringify(out.value)}`);
+ } catch (error) {
+ console.log(error instanceof TypeError, (error as Error).message);
+ }
+}
diff --git a/tests/corpus/7480-builtin-constructor-in.js b/tests/corpus/7480-builtin-constructor-in.js
new file mode 100644
index 000000000..781a00105
--- /dev/null
+++ b/tests/corpus/7480-builtin-constructor-in.js
@@ -0,0 +1,9 @@
+// Feature probes on builtin constructor globals answer like Node.
+const E = "captureStackTrace" in Error ? Error : null;
+console.log(E === Error, "stackTraceLimit" in Error, "nope" in Error);
+console.log("captureStackTrace" in TypeError, "keys" in Object, "fromAsync" in Array, "withResolvers" in Promise);
+console.log("call" in Map, "hasOwnProperty" in Set, "groupBy" in Map, "groupBy" in Set, "now" in Date);
+const capture = "captureStackTrace" in Error ? Error.captureStackTrace : () => {};
+const target = {};
+capture(target);
+console.log(typeof target.stack);
diff --git a/tests/corpus/7480-class-call-without-new.ts b/tests/corpus/7480-class-call-without-new.ts
new file mode 100644
index 000000000..82b3b8452
--- /dev/null
+++ b/tests/corpus/7480-class-call-without-new.ts
@@ -0,0 +1,60 @@
+// Function.prototype.call and apply invoke a class constructor without
+// `new`, which throws a TypeError naming the class after the arguments are
+// evaluated. A declared static `call` answers instead.
+class Base {
+ n = 1;
+}
+class Derived extends Base {}
+class Pair {
+ left: string;
+ right: string;
+ constructor(left: string, right: string) {
+ this.left = left;
+ this.right = right;
+ }
+}
+class WithCall {
+ static call(value: number): number {
+ return value + 1;
+ }
+}
+
+function report(label: string, run: () => unknown): void {
+ try {
+ run();
+ console.log(label, "returned");
+ } catch (error) {
+ console.log(label, error instanceof TypeError, (error as Error).message);
+ }
+}
+
+const receiver = new Base();
+report("call", () => Base.call(receiver));
+report("apply", () => Base.apply(receiver, []));
+report("derived", () => Derived.call(new Derived()));
+report("arguments", () => Pair.call(new Pair("a", "b"), "c", "d"));
+
+const order: string[] = [];
+function effect(label: string, value: T): T {
+ order.push(label);
+ return value;
+}
+report("ordered", () => Pair.call(effect("this", new Pair("a", "b")), effect("left", "x"), effect("right", "y")));
+console.log(order.join(","));
+
+function local(): void {
+ class Inner {
+ x = 2;
+ }
+ report("local", () => Inner.call(new Inner()));
+}
+local();
+
+class Statics {
+ static run(): void {
+ report("static this", () => this.call(new Statics()));
+ }
+}
+Statics.run();
+
+console.log(WithCall.call(4));
diff --git a/tests/corpus/7481-class-call-without-new.mjs b/tests/corpus/7481-class-call-without-new.mjs
new file mode 100644
index 000000000..4734f0491
--- /dev/null
+++ b/tests/corpus/7481-class-call-without-new.mjs
@@ -0,0 +1,63 @@
+// An ES5 constructor that calls a class constructor as its base, and the
+// other ways of calling a class without `new`, throw Node's TypeError.
+class Base {
+ constructor() {
+ this.n = 1;
+ }
+}
+
+function report(label, run) {
+ try {
+ run();
+ console.log(label, "returned");
+ } catch (error) {
+ console.log(label, error instanceof TypeError, error.message);
+ }
+}
+
+function Sub() {
+ Base.call(this);
+ this.m = 2;
+}
+report("es5 subclass", () => new Sub());
+
+function Applied() {
+ Base.apply(this, arguments);
+}
+report("es5 apply", () => new Applied(1, 2));
+
+report("apply null", () => Base.apply(null, null));
+report("apply non-object", () => Base.apply(null, 5));
+report("apply array-like", () => Base.apply({}, { length: 1, 0: "a" }));
+
+const order = [];
+function effect(label, value) {
+ order.push(label);
+ return value;
+}
+report("ordered", () => Base.call(effect("this", {}), effect("a", 1), effect("b", 2)));
+console.log(order.join(","));
+
+function viaValue(C) {
+ report("value " + C.name, () => C.call({}));
+}
+class Derived extends Base {}
+viaValue(Base);
+viaValue(Derived);
+
+class Configured {}
+Configured.defaults = { size: 1 };
+function Wrapped() {
+ Configured.call(this, Configured.defaults);
+}
+report("expando statics", () => new Wrapped());
+
+const Anonymous = class {};
+report("anonymous", () => Anonymous.call({}));
+
+class WithCall {
+ static call(value) {
+ return "own " + value;
+ }
+}
+console.log(WithCall.call(4));
diff --git a/tests/corpus/7481-class-runtime-heritage-object-error.js b/tests/corpus/7481-class-runtime-heritage-object-error.js
new file mode 100644
index 000000000..32e17c640
--- /dev/null
+++ b/tests/corpus/7481-class-runtime-heritage-object-error.js
@@ -0,0 +1,51 @@
+function make(name, params) {
+ const Parent = (params && params.Parent) || Object;
+ class Definition extends Parent {
+ describe() {
+ return `${name}:${this.kind}`;
+ }
+ }
+ return Definition;
+}
+const A = make("A");
+const a = new A();
+a.kind = "plain";
+console.log(typeof a, a instanceof A, a.describe(), JSON.stringify(a), Object.keys(a));
+const E = make("E", { Parent: Error });
+const e = new E("boom");
+e.kind = "error";
+console.log(e instanceof E, e.message, typeof e.stack, e.describe(), Object.keys(e), String(e));
+const Parent = E;
+console.log(e instanceof Parent, new E() .message === "", Object.prototype.hasOwnProperty.call(new E(), "message"));
+try {
+ throw new E("thrown");
+} catch (err) {
+ console.log(err instanceof E, err.message);
+}
+console.log(e instanceof Error, new A() instanceof Error);
+try {
+ throw new E("caught");
+} catch (err) {
+ console.log(err instanceof Error, err.message);
+}
+// The cause option is read like Error's: inherited and accessor causes
+// count, and a throwing getter's exception propagates.
+const inherited = new E("inherited", Object.create({ cause: "from prototype" }));
+console.log(inherited.cause, Object.prototype.hasOwnProperty.call(inherited, "cause"));
+let reads = 0;
+const accessor = new E("accessor", {
+ get cause() {
+ reads++;
+ return "from getter";
+ },
+});
+console.log(accessor.cause, reads, "cause" in new E("none", {}), "cause" in new E("undef", { cause: undefined }));
+try {
+ new E("throws", {
+ get cause() {
+ throw new Error("getter failed");
+ },
+ });
+} catch (err) {
+ console.log(err.message);
+}
diff --git a/tests/corpus/7482-define-property-function-class-values.js b/tests/corpus/7482-define-property-function-class-values.js
new file mode 100644
index 000000000..7a682adab
--- /dev/null
+++ b/tests/corpus/7482-define-property-function-class-values.js
@@ -0,0 +1,35 @@
+const desc = { value: undefined, enumerable: false };
+function factory(name) {
+ class Definition {}
+ Object.defineProperty(Definition, "name", { value: name });
+ function init(inst, def) {
+ if (!inst._meta) {
+ desc.value = { def, traits: new Set() };
+ try {
+ Object.defineProperty(inst, "_meta", desc);
+ } finally {
+ desc.value = undefined;
+ }
+ }
+ inst._meta.traits.add(name);
+ }
+ function _(def) {
+ const inst = this;
+ init(inst, def);
+ return inst;
+ }
+ Object.defineProperty(_, "init", { value: init });
+ Object.defineProperty(_, Symbol.hasInstance, {
+ value: (inst) => !!(inst && inst._meta && inst._meta.traits.has(name)),
+ });
+ Object.defineProperty(_, "name", { value: name });
+ return { ctor: _, Definition };
+}
+const { ctor: Thing, Definition } = factory("Thing");
+console.log(Definition.name, Thing.name, typeof Thing.init);
+const t = new Thing({ a: 1 });
+console.log(Object.keys(t), JSON.stringify(t), t._meta.def.a, [...t._meta.traits]);
+console.log(t instanceof Thing, {} instanceof Thing);
+const plain = {};
+Thing.init(plain, { b: 2 });
+console.log(plain instanceof Thing, Object.keys(plain), plain._meta.def.b);
diff --git a/tests/corpus/7483-constructor-factory-traits.js b/tests/corpus/7483-constructor-factory-traits.js
new file mode 100644
index 000000000..e022f0672
--- /dev/null
+++ b/tests/corpus/7483-constructor-factory-traits.js
@@ -0,0 +1,58 @@
+const _desc = { value: undefined, enumerable: false };
+function $constructor(name, initializer, params) {
+ const proto = {};
+ function Internals(def) {
+ this.def = def;
+ this.constr = _;
+ this.traits = new Set();
+ }
+ Internals.prototype = proto;
+ function init(inst, def) {
+ if (!inst._zod) {
+ _desc.value = new Internals(def);
+ try {
+ Object.defineProperty(inst, "_zod", _desc);
+ } finally {
+ _desc.value = undefined;
+ }
+ } else if (inst._zod.traits.has(name)) {
+ return;
+ }
+ inst._zod.traits.add(name);
+ initializer(inst, def);
+ }
+ const Parent = params?.Parent ?? Object;
+ class Definition extends Parent {}
+ Object.defineProperty(Definition, "name", { value: name });
+ function _(def) {
+ const inst = params?.Parent ? new Definition() : this;
+ init(inst, def);
+ return inst;
+ }
+ Object.defineProperty(_, "init", { value: init });
+ Object.defineProperty(_, Symbol.hasInstance, {
+ value: (inst) => {
+ if (params?.Parent && inst instanceof params.Parent) return true;
+ return inst?._zod?.traits?.has(name);
+ },
+ });
+ Object.defineProperty(_, "name", { value: name });
+ return _;
+}
+const $Base = $constructor("$Base", (inst, def) => {
+ inst.kind = def.kind;
+ inst._zod.parse = (v) => (typeof v === def.kind ? { value: v } : { issues: [`expected ${def.kind}`] });
+});
+const $Str = $constructor("$Str", (inst, def) => {
+ $Base.init(inst, def);
+ inst.min = def.min;
+});
+const $Err = $constructor("$Err", (inst, def) => {
+ inst.issues = def;
+ inst.message = JSON.stringify(def);
+}, { Parent: Error });
+const s = new $Str({ kind: "string", min: 2 });
+console.log(s.kind, s.min, [...s._zod.traits], s instanceof $Str, s instanceof $Base, Object.keys(s));
+console.log(JSON.stringify(s._zod.parse("ab")), JSON.stringify(s._zod.parse(3)));
+const e = new $Err(["bad"]);
+console.log(e instanceof $Err, e.issues, e.message, $Str.name, $Err.name);
diff --git a/tests/corpus/7484-hoisted-function-captures-later-class.js b/tests/corpus/7484-hoisted-function-captures-later-class.js
new file mode 100644
index 000000000..2272882af
--- /dev/null
+++ b/tests/corpus/7484-hoisted-function-captures-later-class.js
@@ -0,0 +1,23 @@
+function f(params) {
+ function early() {
+ return make;
+ }
+ const Parent = params?.Parent ?? Object;
+ class D extends Parent {}
+ function make() {
+ return new D();
+ }
+ return early()();
+}
+console.log(typeof f(), typeof f({ Parent: Error }));
+function g() {
+ function early() {
+ return make;
+ }
+ class E {}
+ function make() {
+ return new E();
+ }
+ return early()();
+}
+console.log(typeof g());
diff --git a/tests/corpus/7485-global-object-own-properties.js b/tests/corpus/7485-global-object-own-properties.js
new file mode 100644
index 000000000..a9a72be18
--- /dev/null
+++ b/tests/corpus/7485-global-object-own-properties.js
@@ -0,0 +1,22 @@
+var _a;
+globalThis.__direct = { n: 1 };
+console.log(typeof globalThis.__direct, globalThis.__direct.n);
+
+(_a = globalThis).__alias ?? (_a.__alias = { n: 2 });
+console.log(typeof globalThis.__alias);
+const g = globalThis;
+g.__viaConst = { n: 3 };
+console.log(typeof globalThis.__viaConst);
+globalThis.__nullish ??= { n: 4 };
+console.log(typeof globalThis.__nullish);
+
+(_a = globalThis).__demo_config ?? (_a.__demo_config = {});
+const globalConfig = globalThis.__demo_config;
+function config(next) {
+ if (next) Object.assign(globalConfig, next);
+ return globalConfig;
+}
+config({ localeError: (issue) => `bad ${issue}` });
+console.log(typeof globalConfig, typeof globalConfig.localeError, config().localeError("x"));
+const pp = globalThis.__demo_config?.postProcessor;
+console.log(pp === undefined, globalThis.__demo_config === globalConfig);
diff --git a/tests/corpus/7486-prototype-accessor-string-method-names.js b/tests/corpus/7486-prototype-accessor-string-method-names.js
new file mode 100644
index 000000000..9920717ea
--- /dev/null
+++ b/tests/corpus/7486-prototype-accessor-string-method-names.js
@@ -0,0 +1,53 @@
+function own(inst, key, value, enumerable = true) {
+ Object.defineProperty(inst, key, { configurable: true, writable: true, enumerable, value });
+ return value;
+}
+function defineBound(proto, key, fn) {
+ Object.defineProperty(proto, key, {
+ configurable: true,
+ get() {
+ return this == null ? fn : own(this, key, fn.bind(this));
+ },
+ set(value) {
+ own(this, key, value);
+ },
+ });
+}
+function members(proto, table) {
+ for (const key in table) {
+ const desc = Object.getOwnPropertyDescriptor(table, key);
+ if (desc.get) Object.defineProperty(proto, key, { ...desc, enumerable: false });
+ else defineBound(proto, key, desc.value);
+ }
+}
+function Schema(def) {
+ this.def = def;
+}
+let installed = false;
+function make(def) {
+ const inst = new Schema(def);
+ if (!installed) {
+ installed = true;
+ members(Object.getPrototypeOf(inst), {
+ min(n) { return make({ ...this.def, min: n }); },
+ max(n) { return make({ ...this.def, max: n }); },
+ trim() { return make({ ...this.def, trim: true }); },
+ get size() { return Object.keys(this.def).length; },
+ });
+ }
+ return inst;
+}
+const s = make({ kind: "string" }).min(1).max(200).trim();
+console.log(JSON.stringify(s.def), s.size, Object.keys(s), typeof s.trim);
+const detached = make({ kind: "x" }).max;
+console.log(JSON.stringify(detached(3).def));
+const proto2 = {};
+Object.defineProperty(proto2, "aaa", { configurable: true, get() { return () => "custom-aaa"; } });
+Object.defineProperty(proto2, "trim", { configurable: true, get() { return () => "custom-trim"; } });
+Object.defineProperty(proto2, "trimx", { configurable: true, get() { return () => "custom-trimx"; } });
+Object.defineProperty(proto2, "slice", { configurable: true, get() { return () => "custom-slice"; } });
+const o2 = Object.create(proto2);
+console.log(o2.aaa());
+console.log(o2.trimx());
+console.log(o2.slice());
+console.log(o2.trim());
diff --git a/tests/corpus/7487-namespace-spread-untyped/all.js b/tests/corpus/7487-namespace-spread-untyped/all.js
new file mode 100644
index 000000000..e317afdab
--- /dev/null
+++ b/tests/corpus/7487-namespace-spread-untyped/all.js
@@ -0,0 +1,2 @@
+export * from "./shapes.js";
+export const own = "own";
diff --git a/tests/corpus/7487-namespace-spread-untyped/extra.js b/tests/corpus/7487-namespace-spread-untyped/extra.js
new file mode 100644
index 000000000..8c62c2dd0
--- /dev/null
+++ b/tests/corpus/7487-namespace-spread-untyped/extra.js
@@ -0,0 +1,2 @@
+export const tag = (s) => `<${s}>`;
+export function zeta() { return "z"; }
diff --git a/tests/corpus/7487-namespace-spread-untyped/main.js b/tests/corpus/7487-namespace-spread-untyped/main.js
new file mode 100644
index 000000000..2352e844d
--- /dev/null
+++ b/tests/corpus/7487-namespace-spread-untyped/main.js
@@ -0,0 +1,16 @@
+import * as shapes from "./shapes.js";
+import * as extra from "./extra.js";
+// A namespace spread into an untyped object (zod's local
+// `const z = { ..._schemas, ..._checks, iso: _iso }`).
+function untyped(value) {
+ return value;
+}
+const z = untyped({ ...shapes, ...extra, extra });
+console.log(Object.keys(z).join(","));
+for (const key of ["circle", "square"]) console.log(key, z[key](2).area.toFixed(2));
+console.log(z.VERSION, typeof z.extra.tag, z.extra.tag("x"), new z.Box(4).size);
+// A namespace with star re-exports holds the re-exported members too.
+import * as all from "./all.js";
+const w = untyped({ ...all });
+console.log(Object.keys(w).join(","), w.circle(1).area.toFixed(2), w.own, new w.Box(2).size);
+for (const key of ["circle", "own", "VERSION", "missing"]) console.log(key, typeof all[key]);
diff --git a/tests/corpus/7487-namespace-spread-untyped/shapes.js b/tests/corpus/7487-namespace-spread-untyped/shapes.js
new file mode 100644
index 000000000..a8b5cbfc2
--- /dev/null
+++ b/tests/corpus/7487-namespace-spread-untyped/shapes.js
@@ -0,0 +1,4 @@
+export const VERSION = "1.2";
+export function circle(r) { return { kind: "circle", area: Math.PI * r * r }; }
+export function square(s) { return { kind: "square", area: s * s }; }
+export class Box { constructor(size) { this.size = size; } }
diff --git a/tests/corpus/7488-math-spread-untyped.js b/tests/corpus/7488-math-spread-untyped.js
new file mode 100644
index 000000000..028135d41
--- /dev/null
+++ b/tests/corpus/7488-math-spread-untyped.js
@@ -0,0 +1,69 @@
+// Math.max/Math.min spreading an untyped iterable (zod's
+// `Math.min(...caps.map((c) => c[0]))`).
+function caps(list) {
+ return list.map((c) => c[0]);
+}
+console.log(Math.min(...caps([[5], [2], [9]])), Math.max(...caps([[5], [2], [9]])));
+console.log(Math.min(...caps([])), Math.max(...caps([])));
+console.log(Math.min(...caps([["3"], [true], [null]])), Math.max(...caps([["7"], [undefined]])));
+console.log(Object.is(Math.max(...caps([[-0], [0]])), 0), Object.is(Math.min(...caps([[0], [-0]])), -0));
+const order = [];
+const valued = (n) => ({ valueOf() { order.push(n); return n; } });
+console.log(Math.max(...caps([[valued(1)], [NaN], [valued(3)]])), order.join(","));
+try {
+ Math.min(...caps([[{ valueOf() { throw new Error("boom"); } }]]));
+} catch (e) {
+ console.log(e.message);
+}
+const sources = { none: undefined, nil: null, num: 5, obj: {}, str: "48" };
+function pick(k) {
+ return sources[k];
+}
+for (const k of Object.keys(sources)) {
+ try {
+ console.log(Math.max(...sources[k]));
+ } catch (e) {
+ console.log(e.constructor.name, e.message);
+ }
+ try {
+ console.log(Math.min(...pick(k)));
+ } catch (e) {
+ console.log(e.constructor.name, e.message);
+ }
+}
+
+// Iterables that step through the iterator protocol, reached through an
+// untyped lookup: a generator, an object with a Symbol.iterator method, a
+// Set, a Map's keys() and an iterator that ends early.
+function* generate(values) {
+ for (const value of values) yield value;
+}
+const makers = {
+ generator: () => generate([4, "1", 7]),
+ custom: () => ({
+ *[Symbol.iterator]() {
+ yield 10;
+ yield -2;
+ },
+ }),
+ set: () => new Set([3, 8, 3]),
+ keys: () => new Map([[2, "a"], [6, "b"]]).keys(),
+ empty: () => generate([]),
+};
+for (const k of Object.keys(makers)) {
+ console.log(k, Math.min(...makers[k]()), Math.max(...makers[k]()));
+}
+const stepped = [];
+const counting = {
+ [Symbol.iterator]() {
+ let n = 0;
+ return {
+ next() {
+ stepped.push(n);
+ return n < 3 ? { value: n++, done: false } : { value: undefined, done: true };
+ },
+ };
+ },
+};
+const lookup = { counting };
+console.log(Math.max(...lookup.counting), stepped.join(","));
diff --git a/tests/corpus/7489-math-spread-generator-only.js b/tests/corpus/7489-math-spread-generator-only.js
new file mode 100644
index 000000000..168057aac
--- /dev/null
+++ b/tests/corpus/7489-math-spread-generator-only.js
@@ -0,0 +1,8 @@
+// Math.max and Math.min as the only consumers of an untyped generator: the
+// spread steps the generator, whose values fold with ToNumber as in Node.
+function* generate(limit) {
+ for (let i = 1; i <= limit; i++) yield i % 2 === 0 ? String(i * 3) : i;
+}
+const lookup = { generate };
+console.log(Math.max(...lookup.generate(4)), Math.min(...lookup.generate(4)));
+console.log(Math.max(...lookup.generate(0)), Math.min(...lookup.generate(1)));
diff --git a/tests/corpus/7540-undefined-element-index-keys.ts b/tests/corpus/7540-undefined-element-index-keys.ts
new file mode 100644
index 000000000..8da1b591b
--- /dev/null
+++ b/tests/corpus/7540-undefined-element-index-keys.ts
@@ -0,0 +1,53 @@
+// An element index the checker types as a number can be undefined at run
+// time (an element read from a holey array). JavaScript then names the
+// array's "undefined" property, not "NaN": plain, compound, update and
+// destructuring writes store it, a write used as a value yields the value,
+// and reads (number, string, record and array elements) read it back.
+const holey: number[] = [1];
+holey[3] = 3;
+
+const values: number[] = [0, 0, 0, 0];
+values[NaN] = 10;
+for (const i of holey) values[i] = 7;
+console.log(values[NaN], values.join(","));
+let total = 0;
+for (const i of holey) total += values[i]!;
+console.log(total);
+
+const counts: number[] = [0, 0, 0, 0];
+counts[NaN] = 10;
+let trace = "";
+for (const i of holey) trace += counts[i]++ + ",";
+for (const i of holey) counts[i] += 2;
+for (const i of holey) trace += --counts[i] + ";";
+console.log(counts[NaN], counts.join(","), trace);
+
+let last = 0;
+for (const i of holey) last = values[i] = 5;
+for (const i of holey) [values[i]] = [8];
+console.log(last, values[NaN], values.join(","));
+
+const names: string[] = ["a", "b", "c", "d"];
+for (const i of holey) names[i] += "!";
+const seen: (string | undefined)[] = [];
+for (const i of holey) seen.push(names[i]);
+console.log(names.join("|"), seen.join("|"));
+
+interface Point {
+ x: number;
+}
+const points: Point[] = [{ x: 0 }, { x: 1 }];
+const shared: Point = { x: 9 };
+let same = "";
+for (const i of holey) {
+ points[i] = shared;
+ same += String(points[i] === shared) + " ";
+}
+shared.x = 10;
+for (const i of holey) same += String(points[i]?.x) + " ";
+console.log(same);
+
+const fresh: number[] = [1, 2];
+let missing = "";
+for (const i of holey) missing += String(fresh[i]) + " ";
+console.log(missing);
diff --git a/tests/corpus/7560-new-target-constructors.ts b/tests/corpus/7560-new-target-constructors.ts
new file mode 100644
index 000000000..9e94200bf
--- /dev/null
+++ b/tests/corpus/7560-new-target-constructors.ts
@@ -0,0 +1,55 @@
+// new.target in class constructors: the class under construction, through
+// inherited and explicit subclass constructors, Error subclasses, arrows,
+// identity comparisons and class values.
+abstract class AppError extends Error {
+ abstract readonly status: number;
+ constructor(message: string) {
+ super(message);
+ this.name = new.target.name;
+ }
+}
+class NotFound extends AppError {
+ readonly status = 404;
+}
+class Invalid extends AppError {
+ readonly status = 400;
+ readonly field: string;
+ constructor(field: string, detail: number) {
+ super(`bad ${field} ${detail}`);
+ this.field = field;
+ }
+}
+class Strict extends Invalid {}
+const errors: AppError[] = [new NotFound("missing"), new Invalid("isbn", 3), new Strict("title", 1)];
+for (const e of errors) console.log(e.name, e.status, e.message, String(e), e instanceof Invalid);
+
+class Shape {
+ readonly kind: string;
+ readonly exact: boolean;
+ constructor() {
+ this.kind = new.target.name;
+ this.exact = new.target === Shape;
+ const probe = () => new.target !== Circle;
+ console.log("probe", probe(), new.target == Square, new.target === undefined);
+ }
+ describe(): string {
+ return `${this.kind}${this.exact ? " (exact)" : ""}`;
+ }
+}
+class Circle extends Shape {}
+class Square extends Shape {
+ constructor() {
+ super();
+ console.log("square ctor sees", new.target.name);
+ }
+}
+for (const s of [new Shape(), new Circle(), new Square()]) console.log(s.describe());
+
+class Node2 {
+ static make(): Node2 { return new this(); }
+ readonly origin: typeof Node2;
+ constructor() { this.origin = new.target; }
+ where(): string | undefined { return typeof (() => 1)() === "number" ? String(this.origin.name) : undefined; }
+}
+class Leaf extends Node2 {}
+console.log(new Node2().origin === Node2, new Leaf().origin === Leaf, new Leaf().where(), Node2.make().origin.name);
diff --git a/tests/corpus/7561-new-target-generic-registry.ts b/tests/corpus/7561-new-target-generic-registry.ts
new file mode 100644
index 000000000..17866d143
--- /dev/null
+++ b/tests/corpus/7561-new-target-generic-registry.ts
@@ -0,0 +1,32 @@
+// new.target in generic classes and as a construction registry key.
+class Box {
+ readonly label: string;
+ readonly value: T;
+ constructor(value: T) {
+ this.value = value;
+ this.label = `${new.target.name}<${typeof value}>`;
+ }
+}
+class NumBox extends Box {
+ constructor(n: number) {
+ super(n * 2);
+ }
+}
+const a = new Box("s");
+const b = new NumBox(4);
+console.log(a.label, b.label, b.value);
+
+class Registry {
+ static readonly created: string[] = [];
+ constructor() {
+ Registry.created.push(new.target.name);
+ if (new.target === Registry) console.log("plain registry");
+ }
+}
+class Users extends Registry {}
+class Books extends Registry {}
+new Users();
+new Books();
+new Registry();
+new Books();
+console.log(Registry.created.join(","));
diff --git a/tests/corpus/7602-error-stack-limit-value.js b/tests/corpus/7602-error-stack-limit-value.js
new file mode 100644
index 000000000..4b3f8cc7e
--- /dev/null
+++ b/tests/corpus/7602-error-stack-limit-value.js
@@ -0,0 +1,18 @@
+// Error.stackTraceLimit through the Error constructor held as a value
+// (zod 4's `const E = "captureStackTrace" in Error ? Error : null`): reads
+// and writes reach the same limit as the static member.
+const E = "captureStackTrace" in Error ? Error : null;
+console.log(typeof E, E === Error, E.stackTraceLimit, Error.stackTraceLimit);
+const saved = E.stackTraceLimit;
+E.stackTraceLimit = 0;
+console.log(Error.stackTraceLimit, E.stackTraceLimit, typeof saved === "number");
+console.log(new Error("no frames").stack);
+try {
+ E.stackTraceLimit = 3;
+ console.log(Error.stackTraceLimit);
+} finally {
+ E.stackTraceLimit = saved;
+}
+console.log(E.stackTraceLimit, Error.stackTraceLimit);
+Error.stackTraceLimit = 4;
+console.log(E.stackTraceLimit);
diff --git a/tests/corpus/7603-error-stack-limit-own.js b/tests/corpus/7603-error-stack-limit-own.js
new file mode 100644
index 000000000..dfd4b7cdf
--- /dev/null
+++ b/tests/corpus/7603-error-stack-limit-own.js
@@ -0,0 +1,13 @@
+// An own stackTraceLimit on the Error constructor held as a value (a
+// non-number assigned, or one defined with Object.defineProperty) answers
+// reads through the value, and writes to it keep its descriptor.
+const E = /** @type {any} */ (Error);
+E.stackTraceLimit = "deep";
+console.log(E.stackTraceLimit, typeof E.stackTraceLimit);
+E.stackTraceLimit = 2;
+console.log(E.stackTraceLimit, Error.stackTraceLimit);
+Object.defineProperty(E, "stackTraceLimit", { value: 6, writable: false, configurable: true });
+try {
+ E.stackTraceLimit = 1;
+} catch {}
+console.log(E.stackTraceLimit);
diff --git a/tests/corpus/7604-error-stack-limit-frozen.js b/tests/corpus/7604-error-stack-limit-frozen.js
new file mode 100644
index 000000000..9c73d1403
--- /dev/null
+++ b/tests/corpus/7604-error-stack-limit-frozen.js
@@ -0,0 +1,9 @@
+// A frozen Error keeps stackTraceLimit read-only through the constructor
+// held as a value.
+const E = /** @type {any} */ (Error);
+E.stackTraceLimit = 3;
+Object.freeze(E);
+try {
+ E.stackTraceLimit = 1;
+} catch {}
+console.log(E.stackTraceLimit, Error.stackTraceLimit);
diff --git a/tests/diagnostics/class-call-shadowed-static.ts b/tests/diagnostics/class-call-shadowed-static.ts
new file mode 100644
index 000000000..53a5dc9ad
--- /dev/null
+++ b/tests/diagnostics/class-call-shadowed-static.ts
@@ -0,0 +1,15 @@
+// Through a class value that may hold a subclass, `C.call(...)` reaches the
+// subclass's own static `call` when it declares one, so the call is not the
+// constructor call that throws Node's TypeError.
+class Base {
+ n = 1;
+}
+class OwnCall extends Base {
+ static call(value: unknown): string {
+ return `own call ${typeof value}`;
+ }
+}
+function callThrough(C: typeof Base): unknown {
+ return C.call(new Base());
+}
+console.log(callThrough(OwnCall));
diff --git a/tests/diagnostics/new-target-late-local-subclass.ts b/tests/diagnostics/new-target-late-local-subclass.ts
new file mode 100644
index 000000000..658a7148e
--- /dev/null
+++ b/tests/diagnostics/new-target-late-local-subclass.ts
@@ -0,0 +1,15 @@
+// A function-local subclass refuses new.target in its base's constructor
+// also when lowering collects it after that constructor: make() is lowered
+// after the top-level code that constructs Base.
+class Base {
+ readonly label: string;
+ constructor() {
+ this.label = new.target.name;
+ }
+}
+console.log(new Base().label);
+function make(): Base {
+ const Local = class extends Base {};
+ return new Local();
+}
+console.log(make().label);
diff --git a/tests/diagnostics/new-target-local-subclass.ts b/tests/diagnostics/new-target-local-subclass.ts
new file mode 100644
index 000000000..a5f153049
--- /dev/null
+++ b/tests/diagnostics/new-target-local-subclass.ts
@@ -0,0 +1,5 @@
+// A function-local subclass makes a new class per evaluation, so the
+// base constructor's new.target has no static dispatch.
+class A { constructor() { console.log(new.target.name); } }
+function make() { class Local extends A {} return new Local(); }
+make();
diff --git a/tests/diagnostics/new-target-plain-function.ts b/tests/diagnostics/new-target-plain-function.ts
new file mode 100644
index 000000000..da592e084
--- /dev/null
+++ b/tests/diagnostics/new-target-plain-function.ts
@@ -0,0 +1,3 @@
+// new.target in a plain function has no invocation record to read.
+function F(this: unknown) { console.log(new.target === undefined); }
+F.call(null);
diff --git a/tests/diagnostics/new-target-value-generic-subclass.ts b/tests/diagnostics/new-target-value-generic-subclass.ts
new file mode 100644
index 000000000..2107a39a5
--- /dev/null
+++ b/tests/diagnostics/new-target-value-generic-subclass.ts
@@ -0,0 +1,5 @@
+// new.target as a class value where a subclass is generic: its
+// instantiations have no common class value.
+class A { readonly t: typeof A; constructor() { this.t = new.target; } }
+class B extends A { readonly v: T | undefined = undefined; }
+console.log(new B().t.name);
diff --git a/tests/diagnostics/new-target-value-signatures.ts b/tests/diagnostics/new-target-value-signatures.ts
new file mode 100644
index 000000000..af0c906e5
--- /dev/null
+++ b/tests/diagnostics/new-target-value-signatures.ts
@@ -0,0 +1,5 @@
+// new.target as a class value needs every subclass to construct with the
+// base's signature; B's constructor takes a parameter A's does not.
+class A { readonly t: typeof A; constructor() { this.t = new.target; } }
+class B extends A { constructor(readonly n: number) { super(); } }
+console.log(new B(1).t.name);
diff --git a/tests/fixtures/npm/cases/jsdoc-unresolved-generic-return/main.ts b/tests/fixtures/npm/cases/jsdoc-unresolved-generic-return/main.ts
new file mode 100644
index 000000000..ce6ef5a94
--- /dev/null
+++ b/tests/fixtures/npm/cases/jsdoc-unresolved-generic-return/main.ts
@@ -0,0 +1,19 @@
+// A published bundle's generic factory whose JSDoc names its result by a
+// type that only the package's .d.ts declares (hono's `bearerAuth`:
+// `@template E` + `@returns {MiddlewareHandler}`), passed straight into
+// a generic registration method. The unresolved JSDoc result is not a
+// contract, so the call's result is checked-dynamic.
+import { guard, Router } from "jsdocmw";
+
+const router = new Router();
+router.on("/admin", guard({ token: "s3cret" }), async (ctx, next) => {
+ ctx.log.push(`handler ${ctx.path}`);
+ await next();
+});
+const open = guard({ token: "" });
+router.on("/open", open, async (ctx) => {
+ ctx.log.push(`open handler ${ctx.path}`);
+});
+console.log(await router.dispatch("/admin", "s3cret"));
+console.log(await router.dispatch("/admin", "nope"));
+console.log(await router.dispatch("/open", ""));
diff --git a/tests/fixtures/npm/node_modules/jsdocmw/index.d.ts b/tests/fixtures/npm/node_modules/jsdocmw/index.d.ts
new file mode 100644
index 000000000..1c93f3348
--- /dev/null
+++ b/tests/fixtures/npm/node_modules/jsdocmw/index.d.ts
@@ -0,0 +1,18 @@
+export interface Env {
+ Variables?: Record;
+}
+export interface Ctx {
+ path: string;
+ token: string;
+ log: string[];
+ env?: E;
+}
+export type Middleware = (ctx: Ctx, next: () => Promise) => Promise;
+export interface GuardOptions {
+ token: string;
+}
+export declare const guard: (options: GuardOptions) => Middleware;
+export declare class Router {
+ on(path: string, ...handlers: Middleware[]): this;
+ dispatch(path: string, token: string): Promise;
+}
diff --git a/tests/fixtures/npm/node_modules/jsdocmw/index.js b/tests/fixtures/npm/node_modules/jsdocmw/index.js
new file mode 100644
index 000000000..f1e38383b
--- /dev/null
+++ b/tests/fixtures/npm/node_modules/jsdocmw/index.js
@@ -0,0 +1,43 @@
+/**
+ * Guard middleware.
+ *
+ * @template E - The environment type.
+ * @param {GuardOptions} options - The options.
+ * @returns {Middleware} The middleware.
+ */
+export const guard = (options) => {
+ return async function guard(ctx, next) {
+ if (ctx.token !== options.token) {
+ ctx.log.push(`denied ${ctx.path}`);
+ return;
+ }
+ ctx.log.push(`allowed ${ctx.path}`);
+ await next();
+ };
+};
+
+export class Router {
+ constructor() {
+ this.routes = [];
+ }
+
+ on(path, ...handlers) {
+ this.routes.push({ path, handlers });
+ return this;
+ }
+
+ async dispatch(path, token) {
+ const log = [];
+ for (const route of this.routes) {
+ if (route.path !== path) continue;
+ const ctx = { path, token, log };
+ let index = 0;
+ const next = async () => {
+ const handler = route.handlers[index++];
+ if (handler) await handler(ctx, next);
+ };
+ await next();
+ }
+ return log;
+ }
+}
diff --git a/tests/fixtures/npm/node_modules/jsdocmw/package.json b/tests/fixtures/npm/node_modules/jsdocmw/package.json
new file mode 100644
index 000000000..a3283642f
--- /dev/null
+++ b/tests/fixtures/npm/node_modules/jsdocmw/package.json
@@ -0,0 +1,8 @@
+{
+ "name": "jsdocmw",
+ "version": "1.0.0",
+ "description": "a bundled middleware factory whose JSDoc @template result names a type only its .d.ts declares (hono's bearerAuth shape)",
+ "type": "module",
+ "main": "index.js",
+ "types": "index.d.ts"
+}
diff --git a/tests/harness/__snapshots__/class-call-shadowed-static.ts.txt b/tests/harness/__snapshots__/class-call-shadowed-static.ts.txt
new file mode 100644
index 000000000..e04c8423a
--- /dev/null
+++ b/tests/harness/__snapshots__/class-call-shadowed-static.ts.txt
@@ -0,0 +1,8 @@
+class-call-shadowed-static.ts:13:10 - error SC2020: 'typeof Base.call' is part of the standard library types but has no scriptc lowering yet
+
+ 12 | function callThrough(C: typeof Base): unknown {
+ 13 | return C.call(new Base());
+ | ^~~~~~
+ 14 | }
+
+ hint: the type checker sees the full standard library, but only the supported surface compiles (https://scriptc.dev/limitations)
\ No newline at end of file
diff --git a/tests/harness/__snapshots__/new-target-late-local-subclass.ts.txt b/tests/harness/__snapshots__/new-target-late-local-subclass.ts.txt
new file mode 100644
index 000000000..feb663813
--- /dev/null
+++ b/tests/harness/__snapshots__/new-target-late-local-subclass.ts.txt
@@ -0,0 +1,6 @@
+new-target-late-local-subclass.ts:7:18 - error SC1090: 'new.target' where 'Local' is a function-local class (each evaluation creates a new class) is not supported yet
+
+ 6 | constructor() {
+ 7 | this.label = new.target.name;
+ | ^~~~~~~~~~
+ 8 | }
\ No newline at end of file
diff --git a/tests/harness/__snapshots__/new-target-local-subclass.ts.txt b/tests/harness/__snapshots__/new-target-local-subclass.ts.txt
new file mode 100644
index 000000000..0f1d465b1
--- /dev/null
+++ b/tests/harness/__snapshots__/new-target-local-subclass.ts.txt
@@ -0,0 +1,6 @@
+new-target-local-subclass.ts:3:39 - error SC1090: 'new.target' where 'Local' is a function-local class (each evaluation creates a new class) is not supported yet
+
+ 2 | // base constructor's new.target has no static dispatch.
+ 3 | class A { constructor() { console.log(new.target.name); } }
+ | ^~~~~~~~~~
+ 4 | function make() { class Local extends A {} return new Local(); }
\ No newline at end of file
diff --git a/tests/harness/__snapshots__/new-target-plain-function.ts.txt b/tests/harness/__snapshots__/new-target-plain-function.ts.txt
new file mode 100644
index 000000000..5005f5915
--- /dev/null
+++ b/tests/harness/__snapshots__/new-target-plain-function.ts.txt
@@ -0,0 +1,6 @@
+new-target-plain-function.ts:2:41 - error SC1090: 'new.target' in a plain function (a constructible function's invocation record does not exist in compiled code; use a class constructor) is not supported yet
+
+ 1 | // new.target in a plain function has no invocation record to read.
+ 2 | function F(this: unknown) { console.log(new.target === undefined); }
+ | ^~~~~~~~~~
+ 3 | F.call(null);
\ No newline at end of file
diff --git a/tests/harness/__snapshots__/new-target-value-generic-subclass.ts.txt b/tests/harness/__snapshots__/new-target-value-generic-subclass.ts.txt
new file mode 100644
index 000000000..0e5cf2e52
--- /dev/null
+++ b/tests/harness/__snapshots__/new-target-value-generic-subclass.ts.txt
@@ -0,0 +1,6 @@
+new-target-value-generic-subclass.ts:3:58 - error SC1090: 'new.target' as a value where 'B' is a generic subclass (its instantiations have no common class value; read 'new.target.name' or compare 'new.target' with a class instead) is not supported yet
+
+ 2 | // instantiations have no common class value.
+ 3 | class A { readonly t: typeof A; constructor() { this.t = new.target; } }
+ | ^~~~~~~~~~
+ 4 | class B extends A { readonly v: T | undefined = undefined; }
\ No newline at end of file
diff --git a/tests/harness/__snapshots__/new-target-value-signatures.ts.txt b/tests/harness/__snapshots__/new-target-value-signatures.ts.txt
new file mode 100644
index 000000000..9c38e4d51
--- /dev/null
+++ b/tests/harness/__snapshots__/new-target-value-signatures.ts.txt
@@ -0,0 +1,6 @@
+new-target-value-signatures.ts:3:58 - error SC1090: 'new.target' as a value where 'B' constructs with a different signature than 'A' (read 'new.target.name' or compare 'new.target' with a class instead) is not supported yet
+
+ 2 | // base's signature; B's constructor takes a parameter A's does not.
+ 3 | class A { readonly t: typeof A; constructor() { this.t = new.target; } }
+ | ^~~~~~~~~~
+ 4 | class B extends A { constructor(readonly n: number) { super(); } }
\ No newline at end of file
diff --git a/tests/harness/atomics-emission.test.ts b/tests/harness/atomics-emission.test.ts
index 9ae06d904..2d708e382 100644
--- a/tests/harness/atomics-emission.test.ts
+++ b/tests/harness/atomics-emission.test.ts
@@ -61,3 +61,32 @@ console.log(work(new Int32Array(buffer), new Uint8Array(buffer), new Int16Array(
expect(calls).toHaveLength(9);
for (const call of calls) expect(call).toMatch(/\) cold$/);
});
+
+test("an unchanged local array is borrowed across Atomics operations", async () => {
+ const llvm = await emittedLlvm(
+ "borrow",
+ `function count(buffer: SharedArrayBuffer, n: number): number {
+ const cells = new Int32Array(buffer);
+ for (let i = 0; i < n; i++) {
+ Atomics.add(cells, 0, 1);
+ Atomics.compareExchange(cells, 1, Atomics.load(cells, 1), i);
+ }
+ return Atomics.load(cells, 0);
+}
+function swap(buffer: SharedArrayBuffer, other: SharedArrayBuffer): number {
+ let cells = new Int32Array(buffer);
+ return Atomics.add(cells, ((cells = new Int32Array(other)), 0), 1);
+}
+const buffer = new SharedArrayBuffer(16);
+console.log(count(buffer, 3), swap(buffer, new SharedArrayBuffer(16)));
+`,
+ );
+ // The local owns the array across each operation: no reference count
+ // operations around the atomic instructions.
+ const counted = body(llvm, "sc_bf_count");
+ expect(counted).toMatch(/atomicrmw add ptr/);
+ expect(counted).not.toMatch(/call ptr @scr_bytes_retain_v/);
+ // An operand that reassigns the binding releases the old array, so the
+ // operation keeps its own reference.
+ expect(body(llvm, "sc_bf_swap")).toMatch(/call ptr @scr_bytes_retain_v/);
+});
diff --git a/tests/harness/corpus-costs/7037-js-var-open-writes/main.ts.json b/tests/harness/corpus-costs/7037-js-var-open-writes/main.ts.json
new file mode 100644
index 000000000..2e747487d
--- /dev/null
+++ b/tests/harness/corpus-costs/7037-js-var-open-writes/main.ts.json
@@ -0,0 +1 @@
+{ "plain": 1970, "san": 2150 }
diff --git a/tests/harness/corpus-costs/7038-js-var-later-writes.mjs.json b/tests/harness/corpus-costs/7038-js-var-later-writes.mjs.json
new file mode 100644
index 000000000..29130736b
--- /dev/null
+++ b/tests/harness/corpus-costs/7038-js-var-later-writes.mjs.json
@@ -0,0 +1 @@
+{ "plain": 1570, "san": 2390 }
diff --git a/tests/harness/corpus-costs/7060-array-numeric-keys.cjs.json b/tests/harness/corpus-costs/7060-array-numeric-keys.cjs.json
new file mode 100644
index 000000000..184d2ea9f
--- /dev/null
+++ b/tests/harness/corpus-costs/7060-array-numeric-keys.cjs.json
@@ -0,0 +1 @@
+{ "plain": 4400, "san": 6000 }
diff --git a/tests/harness/corpus-costs/7061-array-object-keys.ts.json b/tests/harness/corpus-costs/7061-array-object-keys.ts.json
new file mode 100644
index 000000000..b39c9d973
--- /dev/null
+++ b/tests/harness/corpus-costs/7061-array-object-keys.ts.json
@@ -0,0 +1 @@
+{ "plain": 4000, "san": 5400 }
diff --git a/tests/harness/corpus-costs/7062-inspect-array-properties.ts.json b/tests/harness/corpus-costs/7062-inspect-array-properties.ts.json
new file mode 100644
index 000000000..dcd73aea7
--- /dev/null
+++ b/tests/harness/corpus-costs/7062-inspect-array-properties.ts.json
@@ -0,0 +1 @@
+{ "plain": 4300, "san": 6000 }
diff --git a/tests/harness/corpus-costs/7325-array-index-products-near-2-53.ts.json b/tests/harness/corpus-costs/7325-array-index-products-near-2-53.ts.json
new file mode 100644
index 000000000..f71f3eb98
--- /dev/null
+++ b/tests/harness/corpus-costs/7325-array-index-products-near-2-53.ts.json
@@ -0,0 +1 @@
+{ "plain": 1800, "san": 2400 }
diff --git a/tests/harness/corpus-costs/7326-int32-element-arrays.ts.json b/tests/harness/corpus-costs/7326-int32-element-arrays.ts.json
new file mode 100644
index 000000000..168ae57fb
--- /dev/null
+++ b/tests/harness/corpus-costs/7326-int32-element-arrays.ts.json
@@ -0,0 +1 @@
+{ "plain": 3700, "san": 4100 }
diff --git a/tests/harness/corpus-costs/7327-int32-array-widening.ts.json b/tests/harness/corpus-costs/7327-int32-array-widening.ts.json
new file mode 100644
index 000000000..ac0e8497d
--- /dev/null
+++ b/tests/harness/corpus-costs/7327-int32-array-widening.ts.json
@@ -0,0 +1 @@
+{ "plain": 4500, "san": 6100 }
diff --git a/tests/harness/corpus-costs/7364-new-target-late-instantiations.ts.json b/tests/harness/corpus-costs/7364-new-target-late-instantiations.ts.json
new file mode 100644
index 000000000..725cf769c
--- /dev/null
+++ b/tests/harness/corpus-costs/7364-new-target-late-instantiations.ts.json
@@ -0,0 +1 @@
+{ "plain": 4300, "san": 17500 }
diff --git a/tests/harness/corpus-costs/7402-unknown-array-methods.ts.json b/tests/harness/corpus-costs/7402-unknown-array-methods.ts.json
new file mode 100644
index 000000000..9eb118813
--- /dev/null
+++ b/tests/harness/corpus-costs/7402-unknown-array-methods.ts.json
@@ -0,0 +1 @@
+{ "plain": 2600, "san": 20000 }
diff --git a/tests/harness/corpus-costs/7441-untyped-receiver-own-string-method-names.ts.json b/tests/harness/corpus-costs/7441-untyped-receiver-own-string-method-names.ts.json
new file mode 100644
index 000000000..615c617ae
--- /dev/null
+++ b/tests/harness/corpus-costs/7441-untyped-receiver-own-string-method-names.ts.json
@@ -0,0 +1 @@
+{ "plain": 5800 }
diff --git a/tests/harness/corpus-costs/7480-builtin-constructor-in.js.json b/tests/harness/corpus-costs/7480-builtin-constructor-in.js.json
new file mode 100644
index 000000000..20168ce9a
--- /dev/null
+++ b/tests/harness/corpus-costs/7480-builtin-constructor-in.js.json
@@ -0,0 +1 @@
+{ "plain": 3500, "san": 3800 }
diff --git a/tests/harness/corpus-costs/7480-class-call-without-new.ts.json b/tests/harness/corpus-costs/7480-class-call-without-new.ts.json
new file mode 100644
index 000000000..f14d666a4
--- /dev/null
+++ b/tests/harness/corpus-costs/7480-class-call-without-new.ts.json
@@ -0,0 +1 @@
+{ "plain": 3114, "san": 2571 }
diff --git a/tests/harness/corpus-costs/7481-class-call-without-new.mjs.json b/tests/harness/corpus-costs/7481-class-call-without-new.mjs.json
new file mode 100644
index 000000000..cecf59f78
--- /dev/null
+++ b/tests/harness/corpus-costs/7481-class-call-without-new.mjs.json
@@ -0,0 +1 @@
+{ "plain": 2315, "san": 2145 }
diff --git a/tests/harness/corpus-costs/7481-class-runtime-heritage-object-error.js.json b/tests/harness/corpus-costs/7481-class-runtime-heritage-object-error.js.json
new file mode 100644
index 000000000..20168ce9a
--- /dev/null
+++ b/tests/harness/corpus-costs/7481-class-runtime-heritage-object-error.js.json
@@ -0,0 +1 @@
+{ "plain": 3500, "san": 3800 }
diff --git a/tests/harness/corpus-costs/7482-define-property-function-class-values.js.json b/tests/harness/corpus-costs/7482-define-property-function-class-values.js.json
new file mode 100644
index 000000000..20168ce9a
--- /dev/null
+++ b/tests/harness/corpus-costs/7482-define-property-function-class-values.js.json
@@ -0,0 +1 @@
+{ "plain": 3500, "san": 3800 }
diff --git a/tests/harness/corpus-costs/7483-constructor-factory-traits.js.json b/tests/harness/corpus-costs/7483-constructor-factory-traits.js.json
new file mode 100644
index 000000000..20168ce9a
--- /dev/null
+++ b/tests/harness/corpus-costs/7483-constructor-factory-traits.js.json
@@ -0,0 +1 @@
+{ "plain": 3500, "san": 3800 }
diff --git a/tests/harness/corpus-costs/7484-hoisted-function-captures-later-class.js.json b/tests/harness/corpus-costs/7484-hoisted-function-captures-later-class.js.json
new file mode 100644
index 000000000..20168ce9a
--- /dev/null
+++ b/tests/harness/corpus-costs/7484-hoisted-function-captures-later-class.js.json
@@ -0,0 +1 @@
+{ "plain": 3500, "san": 3800 }
diff --git a/tests/harness/corpus-costs/7485-global-object-own-properties.js.json b/tests/harness/corpus-costs/7485-global-object-own-properties.js.json
new file mode 100644
index 000000000..20168ce9a
--- /dev/null
+++ b/tests/harness/corpus-costs/7485-global-object-own-properties.js.json
@@ -0,0 +1 @@
+{ "plain": 3500, "san": 3800 }
diff --git a/tests/harness/corpus-costs/7486-prototype-accessor-string-method-names.js.json b/tests/harness/corpus-costs/7486-prototype-accessor-string-method-names.js.json
new file mode 100644
index 000000000..20168ce9a
--- /dev/null
+++ b/tests/harness/corpus-costs/7486-prototype-accessor-string-method-names.js.json
@@ -0,0 +1 @@
+{ "plain": 3500, "san": 3800 }
diff --git a/tests/harness/corpus-costs/7487-namespace-spread-untyped/main.js.json b/tests/harness/corpus-costs/7487-namespace-spread-untyped/main.js.json
new file mode 100644
index 000000000..20168ce9a
--- /dev/null
+++ b/tests/harness/corpus-costs/7487-namespace-spread-untyped/main.js.json
@@ -0,0 +1 @@
+{ "plain": 3500, "san": 3800 }
diff --git a/tests/harness/corpus-costs/7488-math-spread-untyped.js.json b/tests/harness/corpus-costs/7488-math-spread-untyped.js.json
new file mode 100644
index 000000000..20168ce9a
--- /dev/null
+++ b/tests/harness/corpus-costs/7488-math-spread-untyped.js.json
@@ -0,0 +1 @@
+{ "plain": 3500, "san": 3800 }
diff --git a/tests/harness/corpus-costs/7489-math-spread-generator-only.js.json b/tests/harness/corpus-costs/7489-math-spread-generator-only.js.json
new file mode 100644
index 000000000..2aa0f8038
--- /dev/null
+++ b/tests/harness/corpus-costs/7489-math-spread-generator-only.js.json
@@ -0,0 +1 @@
+{ "plain": 3384, "san": 4732 }
diff --git a/tests/harness/corpus-costs/7540-undefined-element-index-keys.ts.json b/tests/harness/corpus-costs/7540-undefined-element-index-keys.ts.json
new file mode 100644
index 000000000..1da1b737f
--- /dev/null
+++ b/tests/harness/corpus-costs/7540-undefined-element-index-keys.ts.json
@@ -0,0 +1 @@
+{ "plain": 3700, "san": 5300 }
diff --git a/tests/harness/corpus-costs/7560-new-target-constructors.ts.json b/tests/harness/corpus-costs/7560-new-target-constructors.ts.json
new file mode 100644
index 000000000..2c7339a24
--- /dev/null
+++ b/tests/harness/corpus-costs/7560-new-target-constructors.ts.json
@@ -0,0 +1 @@
+{ "plain": 6000, "san": 26000 }
diff --git a/tests/harness/corpus-costs/7561-new-target-generic-registry.ts.json b/tests/harness/corpus-costs/7561-new-target-generic-registry.ts.json
new file mode 100644
index 000000000..eceea988d
--- /dev/null
+++ b/tests/harness/corpus-costs/7561-new-target-generic-registry.ts.json
@@ -0,0 +1 @@
+{ "plain": 4500, "san": 23500 }
diff --git a/tests/harness/corpus-costs/7602-error-stack-limit-value.js.json b/tests/harness/corpus-costs/7602-error-stack-limit-value.js.json
new file mode 100644
index 000000000..bb380e241
--- /dev/null
+++ b/tests/harness/corpus-costs/7602-error-stack-limit-value.js.json
@@ -0,0 +1 @@
+{ "plain": 2500, "san": 3000 }
diff --git a/tests/harness/corpus-costs/7603-error-stack-limit-own.js.json b/tests/harness/corpus-costs/7603-error-stack-limit-own.js.json
new file mode 100644
index 000000000..bb380e241
--- /dev/null
+++ b/tests/harness/corpus-costs/7603-error-stack-limit-own.js.json
@@ -0,0 +1 @@
+{ "plain": 2500, "san": 3000 }
diff --git a/tests/harness/corpus-costs/7604-error-stack-limit-frozen.js.json b/tests/harness/corpus-costs/7604-error-stack-limit-frozen.js.json
new file mode 100644
index 000000000..bb380e241
--- /dev/null
+++ b/tests/harness/corpus-costs/7604-error-stack-limit-frozen.js.json
@@ -0,0 +1 @@
+{ "plain": 2500, "san": 3000 }
diff --git a/tests/harness/errors.test.ts b/tests/harness/errors.test.ts
index c8b0b51b8..319e7329f 100644
--- a/tests/harness/errors.test.ts
+++ b/tests/harness/errors.test.ts
@@ -305,6 +305,26 @@ describe("checked catch-binding casts", () => {
);
});
+ test("a typed-array write through an undefined index is a named runtime fence", async () => {
+ // Node stores an ordinary property named "undefined" on the typed
+ // array, which a compiled typed array cannot hold; defined indexes
+ // before it store as usual.
+ const r = await compileAndRun(
+ "typed-array-undefined-index",
+ `const holey: number[] = [1];
+holey[3] = 3;
+const cells = new Float64Array(4);
+for (const i of holey) cells[i] = 9;
+console.log(cells.join(","));
+`,
+ );
+ expect(r.exitCode).toBe(1);
+ expect(r.stdout).toBe("");
+ expect(r.stderr).toMatch(
+ /^Uncaught Error: typed array properties named 'undefined' \(an element index that is undefined\) are not supported yet \[SC1090 at .*typed-array-undefined-index\.ts:4\]\n$/,
+ );
+ });
+
test("an EFFECTFUL surplus argument is a named deferred fence (JS lane)", async () => {
// Effect-free surplus args drop at compile time, JS-exact (corpus
// 1760 — the test-string-decoder writeSequences shape). An effectful
@@ -1711,9 +1731,10 @@ describe("builtin constructor values (JS lane)", () => {
test("members without a lowering stay refused instead of reading undefined", async () => {
// Builtin constructors read as interned function values that model only
// name, length and constructor. Every other static member or prototype
- // read (direct, bracketed, `in`, through an alias or globalThis), and
+ // read (direct, bracketed, through an alias or globalThis), and
// reflection on or redefinition of their members, must stay a located
// SC2020/SC1090 refusal, never an undefined or otherwise wrong value.
+ // `key in Ctor` answers from the constructor's fixed keys.
// Test262 lost 769 refusals to undefined reads before this was pinned.
const reads = [
["Map.prototype", "Map.prototype"],
@@ -1731,7 +1752,6 @@ describe("builtin constructor values (JS lane)", () => {
],
["isExtensible(Date.prototype)", "Object.isExtensible(Date.prototype)"],
["Map['prototype']", 'Map["prototype"]'],
- ["'prototype' in Map", '"prototype" in Map'],
["globalThis.Map.prototype", "globalThis.Map.prototype"],
["Map alias prototype", "MapAlias.prototype"],
["Date alias prototype", "DateAlias.prototype"],
@@ -1754,6 +1774,7 @@ console.log(Date.length, RegExp.length, Promise.length, Object.length, Map.lengt
console.log(Date.name, Map.name, Map.constructor === Function, Object.constructor === Function);
const promisePrototype = Promise.prototype;
console.log(typeof promisePrototype, typeof promisePrototype.then, promisePrototype.constructor === Promise);
+console.log("prototype" in Map, "groupBy" in Map, "captureStackTrace" in Error, "nope" in Map);
`;
const r = await compileAndRun("builtin-constructor-members", source, "cjs");
expect(r.stderr).toBe("");
@@ -1768,11 +1789,13 @@ console.log(typeof promisePrototype, typeof promisePrototype.then, promiseProtot
),
),
);
- // Promise.prototype reads as an object holding the runtime's methods.
+ // Promise.prototype reads as an object holding the runtime's methods,
+ // and `in` on a builtin constructor answers from its fixed keys.
expect(lines.slice(reads.length)).toEqual([
"7 2 1 1 0 0",
"Date Map true true",
"object function true",
+ "true true true false",
]);
});
});
@@ -1985,3 +2008,45 @@ console.log("unreachable");
);
});
});
+
+describe("numeric properties of native arrays", () => {
+ // A native array keeps its numeric properties ("-1", "1.5") in a property
+ // table that holds values without attributes, so defining one with a
+ // descriptor refuses instead of dropping the attributes.
+ test("Object.defineProperty on a numeric key is a named fence", async () => {
+ const r = await compileAndRun(
+ "array-numeric-define-property",
+ `const words: string[] = ["a"];
+words[-1] = "x";
+console.log(words[-1]);
+Object.defineProperty(words as unknown as Record, "-3", { value: "kept" });
+console.log("unreachable");
+`,
+ );
+ expect(r.exitCode).toBe(1);
+ expect(r.stdout).toBe("x\n");
+ expect(r.stderr).toMatch(
+ /^Uncaught Error: Object\.defineProperty on a numeric property of a native array is not supported yet\n$/,
+ );
+ });
+
+ test("a number key that is not an array index takes the same fence", async () => {
+ const r = await compileAndRun(
+ "array-numeric-define-property-number",
+ `const words: string[] = ["a"];
+for (const key of [-3, 1.5]) {
+ try {
+ Object.defineProperty(words as unknown as Record, key, { value: "kept" });
+ } catch (e) {
+ console.log((e as Error).message);
+ }
+}
+console.log(words.length);
+`,
+ );
+ const fence =
+ "Object.defineProperty on a numeric property of a native array is not supported yet\n";
+ expect(r.exitCode).toBe(0);
+ expect(r.stdout).toBe(`${fence}${fence}1\n`);
+ });
+});
diff --git a/tests/harness/library-mode.test.ts b/tests/harness/library-mode.test.ts
index ffd42bd8e..0d4052952 100644
--- a/tests/harness/library-mode.test.ts
+++ b/tests/harness/library-mode.test.ts
@@ -351,6 +351,7 @@ bump: 1 2
note: 1 2
indexed: 72100
recall: a,b
+globals: undefined fenced
`;
test("K4: three init sessions are byte-identical", async () => {
diff --git a/tests/harness/npm.test.ts b/tests/harness/npm.test.ts
index 795e42846..9a13c0968 100644
--- a/tests/harness/npm.test.ts
+++ b/tests/harness/npm.test.ts
@@ -102,7 +102,7 @@ const KNOWN: Record = {
"package-imports": { build: "SC2013" },
"promise-bridge": { build: "SC1090" },
"self-name": { build: "SC2013" },
- "stream-shims": { run: "SC1090" },
+ "stream-shims": { run: "SC2020" },
"util-shims": { run: "SC2020" },
"web-streams": { run: "SC1090" },
"workspace-copied": { build: "SC2013" },
@@ -112,10 +112,15 @@ const KNOWN: Record = {
const cases = shardSelect(npmCases(fixturesRoot), (c) => c.name);
/** Per-case timeouts past the default 120 s: the undici cases compile the
- * built-in undici implementation's module graph, and zod3 compiles zod 3's
- * 3,700-line types module. A sanitized build of either takes minutes on a
- * CI runner. */
-const TIMEOUTS: Record = { undici5: 600_000, undici7: 600_000, zod3: 600_000 };
+ * built-in undici implementation's module graph, and zod3 and zod3-infer
+ * compile zod 3's 3,700-line types module. A sanitized build of either
+ * takes minutes on a CI runner. */
+const TIMEOUTS: Record = {
+ undici5: 600_000,
+ undici7: 600_000,
+ zod3: 600_000,
+ "zod3-infer": 600_000,
+};
interface RunResult {
stdout: Buffer;
diff --git a/tests/library-mode/reinit/lib.ts b/tests/library-mode/reinit/lib.ts
index dad54090a..73e466f70 100644
--- a/tests/library-mode/reinit/lib.ts
+++ b/tests/library-mode/reinit/lib.ts
@@ -29,4 +29,22 @@ export function indexedUnicode(): number {
return s.length + s.charCodeAt(n) + s.slice(n - 1, n + 2).length;
}
+// A symbol-keyed global access creates the global object before any read of
+// `globalThis` in a session: the builtin names must still be fenced on the
+// stored global object, in every session.
+const probeKey = Symbol.for("scriptc.reinit");
+function stored(global: typeof globalThis): any {
+ return global;
+}
+export function globalFences(): string {
+ // @ts-expect-error: globalThis has no symbol index signature
+ const before: unknown = globalThis[probeKey];
+ const view = stored(globalThis);
+ try {
+ return `${typeof before} ${typeof view.Reflect}`;
+ } catch {
+ return `${typeof before} fenced`;
+ }
+}
+
console.log(`session start counter=${counter}`);
diff --git a/tests/library-mode/reinit/probe.c b/tests/library-mode/reinit/probe.c
index 8cd17be1e..409766d0d 100644
--- a/tests/library-mode/reinit/probe.c
+++ b/tests/library-mode/reinit/probe.c
@@ -12,6 +12,7 @@ extern double kr_bump(void);
extern double kr_note(const uint8_t *p, size_t len);
extern double kr_indexed_unicode(void);
extern void kr_recall(const uint8_t **out, size_t *out_len);
+extern void kr_global_fences(const uint8_t **out, size_t *out_len);
static void sink(void *ctx, const uint8_t *msg, size_t len, uint64_t addr) {
(void)ctx; (void)addr;
@@ -30,6 +31,8 @@ static void session(void) {
const uint8_t *s; size_t n;
kr_recall(&s, &n);
printf("recall: %.*s\n", (int)n, s);
+ kr_global_fences(&s, &n);
+ printf("globals: %.*s\n", (int)n, s);
}
int main(void) {
diff --git a/tests/library-mode/reinit/profile.json b/tests/library-mode/reinit/profile.json
index 620150db1..cfcfebc9a 100644
--- a/tests/library-mode/reinit/profile.json
+++ b/tests/library-mode/reinit/profile.json
@@ -14,6 +14,7 @@
{ "export": "bump", "symbol": "kr_bump", "params": [], "returns": "f64" },
{ "export": "note", "symbol": "kr_note", "params": ["string"], "returns": "f64" },
{ "export": "recall", "symbol": "kr_recall", "params": [], "returns": "string" },
- { "export": "indexedUnicode", "symbol": "kr_indexed_unicode", "params": [], "returns": "f64" }
+ { "export": "indexedUnicode", "symbol": "kr_indexed_unicode", "params": [], "returns": "f64" },
+ { "export": "globalFences", "symbol": "kr_global_fences", "params": [], "returns": "string" }
]
}