diff --git a/docs/content/docs/cli.mdx b/docs/content/docs/cli.mdx
index 8b0353620..9fa5e9447 100644
--- a/docs/content/docs/cli.mdx
+++ b/docs/content/docs/cli.mdx
@@ -135,6 +135,9 @@ With `--sparse`, the profile leaves out the functions the runs never reached. Th
--detached-exit
On Linux, when the program calls process.exit(), hand the release of its memory to a helper process so the parent sees the exit sooner. The kernel otherwise frees the whole address space before the exit is reported, which takes tens of milliseconds per gigabyte. Opt in only where the trade-offs are acceptable: for up to about 100 ms after the exit, the helper keeps the executable mapped (overwriting the binary in place fails with "text file busy"), its memory stays charged to the cgroup, and the teardown CPU time no longer appears in the parent's resource usage. Valid only for executable builds; it has no effect on other platforms.
+ --target-cpu <baseline|x86-64-v2|x86-64-v3|x86-64-v4|native>
+ On x86-64 targets, compile the program's code for an x86-64 microarchitecture level. The default, baseline, runs on any x86-64 CPU. A higher level lets LLVM use more instructions, and the executable then runs only on CPUs that support that level. native uses the highest level the build machine supports. Valid for executable, assembly, and object output on x86-64 targets. See Target CPU.
+
--profile-generate <pattern>
On Linux, build an instrumented executable for profile-guided optimization. Each run writes a raw profile to the path pattern, where %p expands to the process ID; the LLVM_PROFILE_FILE environment variable overrides the pattern at run time. A relative pattern resolves from the current working directory. Instrumented programs run slower. Valid only for executable builds with --optimization=release or speed.
@@ -228,6 +231,43 @@ $ scriptc build app.ts --optimization=speed --profile-use app.profdata
Train on workloads like the ones the program will run: code the training never reached is optimized as without a profile. The profile can be committed and reused as the program changes. Functions whose control flow changed since the profile was collected lose their profile, and the rest keep theirs, so regenerate it when much of the program has changed. [Build parallelism](/docs/how-it-works#build-parallelism) describes what a profile adds to build time.
+## Target CPU
+
+By default, x86-64 executables run on any x86-64 CPU, so the compiler uses only the baseline instruction set (SSE2). `--target-cpu` compiles the program's code for a higher x86-64 microarchitecture level. Each level includes the ones below it:
+
+
+
+
+ | Level |
+ Adds |
+
+
+
+
+ x86-64-v2 |
+ SSE3, SSSE3, SSE4.1, SSE4.2, POPCNT |
+
+
+ x86-64-v3 |
+ AVX, AVX2, BMI1, BMI2, F16C, FMA, LZCNT, MOVBE |
+
+
+ x86-64-v4 |
+ AVX-512 (F, BW, CD, DQ, VL) |
+
+
+
+
+```console
+$ scriptc build app.ts --target-cpu=x86-64-v3 -o app
+```
+
+Loops over typed arrays and numbers gain the most, because LLVM vectorizes them with wider registers. Some loops run slower at a higher level, so measure the program on the machines it will run on.
+
+An executable built for a level runs only on CPUs that support that level. On an older CPU, it crashes with an illegal-instruction signal (`SIGILL`) when it reaches an instruction the CPU lacks. A named level is portable to every machine of that level. `native` selects the highest level the build machine supports, so use it only for executables that run on the machine that built them or on identical machines.
+
+The level applies to the program's own code. The runtime pack keeps its baseline code. Builds for different levels never share cached artifacts, and `baseline` builds are the same as builds without the option.
+
## LLVM backend
scriptc generates LLVM IR and compiles it to native code. Executable builds link the result with the matching precompiled runtime pack. Use --emit=llvm to inspect the generated code.
diff --git a/docs/content/limitations/builtin-apis/filesystem-url-paths.mdx b/docs/content/limitations/builtin-apis/filesystem-url-paths.mdx
index d5079ed5a..9b3580647 100644
--- a/docs/content/limitations/builtin-apis/filesystem-url-paths.mdx
+++ b/docs/content/limitations/builtin-apis/filesystem-url-paths.mdx
@@ -1,3 +1,3 @@
-- **Filesystem URL paths:** Synchronous `fs` functions accept `file:` URL paths and convert them as `fileURLToPath` does.
- `existsSync` and the `fs.promises` functions refuse URL paths; pass `fileURLToPath(url)` instead.
- A URL path after a string path, or with options that are not literals, is refused, because Node checks those arguments before it converts the URL.
+- **Filesystem URL paths:** `fs` and `fs.promises` functions accept `file:` URL paths and convert them as `fileURLToPath` does; `existsSync` answers `false` and the `fs.promises` functions reject for a URL that does not convert.
+ A URL path with options that are not literals is refused where Node checks those options before it converts the URL, except a `readFileSync` encoding.
+ `fs.promises` functions also refuse a string path before a URL path; pass `fileURLToPath(url)` instead.
diff --git a/docs/content/limitations/untyped-values.mdx b/docs/content/limitations/untyped-values.mdx
index 6a881aec3..05245a396 100644
--- a/docs/content/limitations/untyped-values.mdx
+++ b/docs/content/limitations/untyped-values.mdx
@@ -15,3 +15,6 @@ JavaScript argument and return conversions preserve native array references; oth
Supported operations on `unknown` include truthiness, `typeof` narrowing, property access, `+`, `switch`, and `throw`.
Cast before using other operations.
+
+In JavaScript, a call whose untyped argument could select an overload with another result type returns a checked value, whichever overload the type checker picks, and so do the unannotated bindings and functions that hold or return it.
+In TypeScript, an `any` argument keeps the overload the type checker picks, and a value that selects another overload at runtime throws a `TypeError` where the result is used.
diff --git a/packages/cli/src/bootstrap.ts b/packages/cli/src/bootstrap.ts
index 242a7c6f7..f14866a9c 100644
--- a/packages/cli/src/bootstrap.ts
+++ b/packages/cli/src/bootstrap.ts
@@ -70,6 +70,8 @@ async function tryFastPath(): Promise {
values["profile-generate"] !== undefined ||
values["profile-use"] !== undefined ||
values.sparse ||
+ // The full compiler resolves `native` against the build host.
+ values["target-cpu"] !== undefined ||
(values["external-types"] ?? []).length > 0
)
return null;
@@ -200,6 +202,7 @@ async function coverageFastPath(
values.emit !== undefined ||
values.strip ||
values["detached-exit"] ||
+ values["target-cpu"] !== undefined ||
values.lib ||
values.profile !== undefined ||
values.ffi !== undefined ||
diff --git a/packages/cli/test/target-cpu.test.ts b/packages/cli/test/target-cpu.test.ts
new file mode 100644
index 000000000..ca5243564
--- /dev/null
+++ b/packages/cli/test/target-cpu.test.ts
@@ -0,0 +1,100 @@
+import { execFile } from "node:child_process";
+import { createRequire } from "node:module";
+import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
+import { dirname, join } from "node:path";
+import { promisify } from "node:util";
+import { expect, test } from "vitest";
+import { hostTargetCpu } from "../../compiler/src/backend/target-cpu.js";
+
+const execFileAsync = promisify(execFile);
+const require = createRequire(import.meta.url);
+const repoRoot = join(import.meta.dirname, "../../..");
+const cliEntry = join(repoRoot, "packages/cli/src/main.ts");
+const bootstrapEntry = join(repoRoot, "packages/cli/dist/bootstrap.js");
+const tsxLoader = join(dirname(require.resolve("tsx/package.json")), "dist/loader.mjs");
+const host = hostTargetCpu();
+const runsV3 = host === "x86-64-v3" || host === "x86-64-v4";
+
+const program = `const a = new Int32Array(4096);
+for (let i = 0; i < a.length; i++) a[i] = (i * 2654435761) | 0;
+let s = 0;
+for (let r = 0; r < 64; r++) for (let i = 0; i < a.length; i++) s = (s + a[i]!) | 0;
+console.log(s);
+`;
+
+test.runIf(process.platform === "linux" && process.arch === "x64")(
+ "--target-cpu compiles the program for the level without mixing cache variants",
+ async () => {
+ const dir = await mkdtemp(join("/tmp", "scriptc-target-cpu-"));
+ const entry = join(dir, "main.ts");
+ const outPath = join(dir, "program");
+ const llvmPath = join(dir, "main.ll");
+ const env = { ...process.env, SCRIPTC_CACHE_DIR: join(dir, "cache") };
+ const build = async (args: string[], bootstrap = false): Promise => {
+ await execFileAsync(
+ process.execPath,
+ [
+ ...(bootstrap ? [bootstrapEntry] : ["--import", tsxLoader, cliEntry]),
+ "build",
+ entry,
+ "-o",
+ outPath,
+ ...args,
+ ],
+ { env, maxBuffer: 4 * 1024 * 1024 },
+ );
+ return readFile(llvmPath, "utf8");
+ };
+ try {
+ await writeFile(entry, program);
+ const expected = `${(await execFileAsync(process.execPath, [entry])).stdout}`;
+ const baseline = await build([]);
+ expect(baseline).not.toContain('"target-cpu"');
+ expect(await build(["--target-cpu=baseline"])).toBe(baseline);
+ const v3 = await build(["--target-cpu=x86-64-v3"]);
+ expect(v3).toMatch(/^attributes #0 = \{ sanitize_address "target-cpu"="x86-64-v3" \}$/m);
+ if (runsV3) expect((await execFileAsync(outPath)).stdout).toBe(expected);
+ // The bootstrap's cached route builds without a level.
+ expect(await build([], true)).toBe(baseline);
+ expect((await execFileAsync(outPath)).stdout).toBe(expected);
+ await expect(build(["--target-cpu=haswell"])).rejects.toThrow(/unknown target CPU "haswell"/);
+ } finally {
+ await rm(dir, { recursive: true, force: true });
+ }
+ },
+ 240_000,
+);
+
+test("--target-cpu levels are refused for arm64 and WASI targets", async () => {
+ const dir = await mkdtemp(join("/tmp", "scriptc-target-cpu-refusal-"));
+ const entry = join(dir, "main.ts");
+ try {
+ await writeFile(entry, program);
+ for (const target of ["aarch64-unknown-linux-gnu", "wasm32-wasi"]) {
+ for (const kind of ["llvm", "asm", "obj", "exe"]) {
+ const build = execFileAsync(
+ process.execPath,
+ [
+ "--import",
+ tsxLoader,
+ cliEntry,
+ "build",
+ entry,
+ `--emit=${kind}`,
+ "-o",
+ join(dir, `out-${kind}`),
+ "--target-cpu=x86-64-v3",
+ ],
+ {
+ env: { ...process.env, SCRIPTC_TARGET: target, SCRIPTC_CACHE_DIR: join(dir, "cache") },
+ },
+ );
+ await expect(build, `${target} ${kind}`).rejects.toThrow(
+ /--target-cpu=x86-64-v3 requires an x86-64 target/,
+ );
+ }
+ }
+ } finally {
+ await rm(dir, { recursive: true, force: true });
+ }
+}, 240_000);
diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts
index 0be5bdca2..b97548e3b 100644
--- a/packages/compiler/ambient/scriptc-node-fallback.d.ts
+++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts
@@ -1590,7 +1590,7 @@ declare module "node:fs" {
export function writeFileSync(path: string | URL, data: string, encoding: "utf8" | "utf-8"): void;
export function writeFileSync(path: string | URL, data: string | Uint8Array): void;
export function appendFileSync(path: string | URL, data: string | Uint8Array): void;
- export function existsSync(path: string): boolean;
+ export function existsSync(path: string | URL): boolean;
export function mkdirSync(path: string | URL): void;
export function mkdirSync(
path: string | URL,
@@ -2231,7 +2231,7 @@ declare module "fs/promises" {
stat(): Promise;
[Symbol.asyncDispose](): Promise;
}
- export function open(path: string, flags?: string, mode?: number): Promise;
+ export function open(path: string | URL, flags?: string, mode?: number): Promise;
export function link(
existingPath: string | Uint8Array | URL,
newPath: string | Uint8Array | URL,
@@ -2268,14 +2268,14 @@ declare module "fs/promises" {
atime: number | string | Date,
mtime: number | string | Date,
): Promise;
- export function readFile(path: string, encoding: "utf8" | "utf-8"): Promise;
+ export function readFile(path: string | URL, encoding: "utf8" | "utf-8"): Promise;
export function readFile(
- path: string,
+ path: string | URL,
options: BufferEncoding | { encoding: BufferEncoding },
): Promise;
- export function readFile(path: string): Promise;
+ export function readFile(path: string | URL): Promise;
export function writeFile(
- path: string,
+ path: string | URL,
data: string,
options?: {
mode?: number;
@@ -2284,14 +2284,18 @@ declare module "fs/promises" {
[option: string]: unknown;
},
): Promise;
- export function writeFile(path: string, data: string, encoding: "utf8" | "utf-8"): Promise;
+ export function writeFile(
+ path: string | URL,
+ data: string,
+ encoding: "utf8" | "utf-8",
+ ): Promise;
export function mkdir(
- path: string,
+ path: string | URL,
options?: { recursive?: boolean; mode?: number },
): Promise;
- export function readdir(path: string): Promise;
+ export function readdir(path: string | URL): Promise;
export function readdir(
- path: string,
+ path: string | URL,
options: {
encoding?: "utf8" | "utf-8";
withFileTypes: true;
@@ -2308,12 +2312,12 @@ declare module "fs/promises" {
pattern: string | readonly string[],
options: import("node:fs").GlobOptionsWithFileTypes,
): AsyncIterableIterator;
- export function rm(path: string): Promise;
+ export function rm(path: string | URL): Promise;
export function rm(
- path: string,
+ path: string | URL,
options: { recursive?: boolean; force?: boolean; maxRetries?: number; retryDelay?: number },
): Promise;
- export function stat(path: string): Promise;
+ export function stat(path: string | URL): Promise;
export function statfs(
path: string | Uint8Array | URL,
options?: import("node:fs").StatFsOptions & { bigint?: false },
@@ -2326,21 +2330,21 @@ declare module "fs/promises" {
path: string | Uint8Array | URL,
options?: import("node:fs").StatFsOptions,
): Promise;
- export function realpath(path: string): Promise;
- export function lstat(path: string): Promise;
- export function unlink(path: string): Promise;
- export function chmod(path: string, mode: number): Promise;
- export function chown(path: string, uid: number, gid: number): Promise;
- export function lchown(path: string, uid: number, gid: number): Promise;
- export function access(path: string, mode?: number): Promise;
- export function truncate(path: string, len?: number): Promise;
- export function appendFile(path: string, data: string | Uint8Array): Promise;
- export function copyFile(src: string, dest: string): Promise;
- export function mkdtemp(prefix: string): Promise;
- export function rmdir(path: string): Promise;
- export function rename(oldPath: string, newPath: string): Promise;
+ export function realpath(path: string | URL): Promise;
+ export function lstat(path: string | URL): Promise;
+ export function unlink(path: string | URL): Promise;
+ export function chmod(path: string | URL, mode: number): Promise;
+ export function chown(path: string | URL, uid: number, gid: number): Promise;
+ export function lchown(path: string | URL, uid: number, gid: number): Promise;
+ export function access(path: string | URL, mode?: number): Promise;
+ export function truncate(path: string | URL, len?: number): Promise;
+ export function appendFile(path: string | URL, data: string | Uint8Array): Promise;
+ export function copyFile(src: string | URL, dest: string | URL): Promise;
+ export function mkdtemp(prefix: string | URL): Promise;
+ export function rmdir(path: string | URL): Promise;
+ export function rename(oldPath: string | URL, newPath: string | URL): Promise;
export function opendir(
- path: string,
+ path: string | URL,
options?: import("node:fs").OpenDirOptions,
): Promise;
}
diff --git a/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts b/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts
index de71a6ef0..aef39ef75 100644
--- a/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts
+++ b/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts
@@ -385,7 +385,10 @@ test("while loop counters below a bound version their accesses", () => {
]);
const declared = (loop: IrStmt): IrFunction =>
fn([{ kind: "varDecl", localId: "i", init: ref("k"), loc }, loop]);
- const versionOf = (loop: IrStmt & { kind: "while" }): BytesLoopVersion | null => {
+ const versionOf = (
+ loop: IrStmt & { kind: "while" },
+ workers = false,
+ ): BytesLoopVersion | null => {
const f = declared(loop);
const ranges = analyzeIntegerRanges(f);
return matchBytesLoopVersion(loop, null, {
@@ -396,7 +399,7 @@ test("while loop counters below a bound version their accesses", () => {
initialized: findInitializedByteLoopBindings(f).get(loop) ?? new Set(),
proven: new Set(),
scalarStrings: new Set(),
- workers: false,
+ workers,
owned: new Set(),
});
};
@@ -413,6 +416,11 @@ test("while loop counters below a bound version their accesses", () => {
// An access after the counter advances in the same pass stays checked.
const advanced = whileLoop([step, set(ref("out", f64s), ref("i"), num(0))]);
expect(versionOf(advanced)).toBeNull();
+ // In a worker program, a while loop versions for its counter's accesses,
+ // and not for thread-owned receivers alone.
+ expect(versionOf(copy, true)!.ownedReceivers).toHaveLength(2);
+ const job = whileLoop([set(ref("out", f64s), ref("k"), get(ref("src", f64s), ref("k"))), step]);
+ expect(versionOf(job, true)).toBeNull();
const mod: IrModule = {
irVersion: 17,
sourceFile: loc.file,
diff --git a/packages/compiler/src/backend/llvm/bytes-loop-versions.ts b/packages/compiler/src/backend/llvm/bytes-loop-versions.ts
index 2013d03e4..1b51deee1 100644
--- a/packages/compiler/src/backend/llvm/bytes-loop-versions.ts
+++ b/packages/compiler/src/backend/llvm/bytes-loop-versions.ts
@@ -581,6 +581,11 @@ export function matchBytesLoopVersion(
pairedKept.length === 0
)
return null;
+ // A while loop versions only for accesses at its counters. Its other
+ // checks (thread-owned receivers, static indices) save a test or two per
+ // pass, while a second copy of its body doubles the calls it makes, which
+ // can keep a worker's job loop from inlining the function that runs a job.
+ if (loop.kind === "while" && boundedKept.length === 0) return null;
const bounds = new Set();
const ownedNodes = new Set();
const used = new Set();
diff --git a/packages/compiler/src/backend/llvm/emit-module.ts b/packages/compiler/src/backend/llvm/emit-module.ts
index 518bbf270..5aa18bf9f 100644
--- a/packages/compiler/src/backend/llvm/emit-module.ts
+++ b/packages/compiler/src/backend/llvm/emit-module.ts
@@ -64,6 +64,7 @@ import { literalEqualityDefinition } from "./string-literal-equality.js";
import { STRING_READ_HELPERS, stringScanDecl } from "./string-reads.js";
import { emitLibraryIdentityLines } from "../library-identity-markers.js";
import { InternalCompilerError } from "../../errors.js";
+import { targetCpuAttribute } from "../target-cpu.js";
/** The function bodies emitted in `chunks` contiguous ranges, each by its
* own emitter (LlEmitter.chunkEmitter), then merged in function order: the
@@ -191,6 +192,20 @@ function readRange(dir: string, range: number): { result: ChunkResult; texts: st
}
}
+/** The attribute groups of the emitted functions: #0 for every function,
+ * #1 for coroutine bodies and #2 for out-of-line record clones. Each carries
+ * the build's `--target-cpu` level. */
+function functionAttributes(emitter: LlEmitter, hasNoInlineRecordClone: boolean): string[] {
+ const cpu = targetCpuAttribute(emitter.targetCpu);
+ return [
+ `attributes #0 = { sanitize_address${cpu} }`,
+ ...(emitter.usesCoroutines
+ ? [`attributes #1 = { sanitize_address presplitcoroutine${cpu} }`]
+ : []),
+ ...(hasNoInlineRecordClone ? [`attributes #2 = { noinline sanitize_address${cpu} }`] : []),
+ ];
+}
+
export function emitModuleParts(emitter: LlEmitter): string[] {
// Function bodies first (the literal/unit/fn-value tables fill as they
// emit), then the file assembles around them — the runtime ABI’s order.
@@ -972,9 +987,7 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
// symbols specified by the library IR instead.
for (const line of emitter.emitLibDefs(globals, globalReleaseLines, stamps)) out.push(line);
out.push(...emitter.inlineRcTail(flushedDecls));
- out.push(`attributes #0 = { sanitize_address }`);
- if (emitter.usesCoroutines) out.push(`attributes #1 = { sanitize_address presplitcoroutine }`);
- if (hasNoInlineRecordClone) out.push(`attributes #2 = { noinline sanitize_address }`);
+ out.push(...functionAttributes(emitter, hasNoInlineRecordClone));
out.push(``);
out.push(...emitter.fieldAliasMetadata());
if (emitter.debug !== null) out.push(emitter.debug.render());
@@ -1122,9 +1135,7 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
// sanitize_address is inert under the plain pipeline; the sanitized
// lane's -fsanitize=address link activates instrumentation over the
// emitted functions too (the runtime TUs get theirs from clang).
- `attributes #0 = { sanitize_address }`,
- ...(emitter.usesCoroutines ? [`attributes #1 = { sanitize_address presplitcoroutine }`] : []),
- ...(hasNoInlineRecordClone ? [`attributes #2 = { noinline sanitize_address }`] : []),
+ ...functionAttributes(emitter, hasNoInlineRecordClone),
``,
);
if (emitter.mod.workers) {
diff --git a/packages/compiler/src/backend/llvm/emitter.test.ts b/packages/compiler/src/backend/llvm/emitter.test.ts
index 23ca9d9d6..876476cc1 100644
--- a/packages/compiler/src/backend/llvm/emitter.test.ts
+++ b/packages/compiler/src/backend/llvm/emitter.test.ts
@@ -201,6 +201,16 @@ test("detached exit defines the runtime opt-in symbol only when requested", () =
expect(detached.replace("@scr_detached_exit = constant i8 1\n", "")).toBe(plain);
});
+test("a target CPU level reaches every function attribute group and nothing else", () => {
+ const module = moduleFor([call()]);
+ const plain = emitLlvmModule(module);
+ expect(plain).not.toContain("target-cpu");
+ const v3 = emitLlvmModule(module, { targetCpu: "x86-64-v3" });
+ expect(v3).toMatch(/^attributes #0 = \{ sanitize_address "target-cpu"="x86-64-v3" \}$/m);
+ expect(v3.replaceAll(' "target-cpu"="x86-64-v3"', "")).toBe(plain);
+ expect(emitLlvmModule(module, { targetCpu: "baseline" })).toBe(plain);
+});
+
test("inline RC inlines every fast path at speed and keeps box releases as calls in release", () => {
for (const boxed of [false, true]) {
const module = moduleFor(Array.from({ length: 4 }, call), { boxed });
diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts
index 13011f21e..fe50e974f 100644
--- a/packages/compiler/src/backend/llvm/emitter.ts
+++ b/packages/compiler/src/backend/llvm/emitter.ts
@@ -1,3 +1,4 @@
+import type { TargetCpu } from "../target-cpu.js";
import { relieveHeapPressure } from "../../heap-pressure.js";
import { wasmBoundsChecks } from "./wasm-modules.js";
import { jsTruthy } from "./jsval.js";
@@ -482,6 +483,10 @@ export interface LlvmTargetOptions {
* store the instance's address over 8 in 32 bits (classes.ts
* compactRefLoadLines). Elsewhere they keep 8-byte pointers. */
lowRegion?: boolean;
+ /** The x86-64 level the program's code may require (`--target-cpu`):
+ * every emitted function carries it as its `target-cpu`. Unset, code
+ * runs on any x86-64 CPU. */
+ targetCpu?: TargetCpu;
}
/** LlvmTargetOptions.lowRegion for an executable: Linux targets without
@@ -1048,6 +1053,7 @@ export class LlEmitter {
readonly emitLibraryIdentity: boolean;
readonly runtimeAbiMarker: boolean;
readonly detachedExit: boolean;
+ readonly targetCpu: TargetCpu | undefined;
readonly wasmBoundsChecks: boolean;
/** Interned string literals: UTF-8 text → { symbol, byte length } —
* first-use order, the runtime ABI’s determinism discipline. */
@@ -1630,6 +1636,7 @@ export class LlEmitter {
this.emitLibraryIdentity = options.emitLibraryIdentity !== false;
this.runtimeAbiMarker = options.runtimeAbiMarker === true;
this.detachedExit = options.detachedExit === true;
+ this.targetCpu = options.targetCpu;
this.wasmBoundsChecks =
options.wasmBoundsChecks === true ||
wasmBoundsChecks({
@@ -3801,8 +3808,17 @@ export class LlEmitter {
/** A union value's tag (i32): the box's tag word, or the NULL test of a
* nullable union's pointer. */
+ /** A box's tag and payload words are written only where the box is
+ * built (runtime constructors and untagged stack stores), so their loads
+ * carry their own alias tags: element and field stores cannot change
+ * them, and loops keep them hoisted. */
unionTag(uName: string, unionId: string): string {
- return emitUnionTag(this.B, this.nullableUnions.get(unionId), uName);
+ return emitUnionTag(
+ this.B,
+ this.nullableUnions.get(unionId),
+ uName,
+ this.memoryAliasAttachment("union.tag"),
+ );
}
/** Guard for helpers that build or read a union's `%ScrUnion` box
@@ -3818,7 +3834,12 @@ export class LlEmitter {
/** The BORROWED payload pointer of a ref arm (scr_union_peek inlined —
* the runtime's is a static inline); a nullable union is its payload. */
unionPeek(uName: string, unionId: string): string {
- return emitUnionPeek(this.B, this.nullableUnions.get(unionId), uName);
+ return emitUnionPeek(
+ this.B,
+ this.nullableUnions.get(unionId),
+ uName,
+ this.memoryAliasAttachment("union.payload"),
+ );
}
/** Emits `switch` over a union's tag with one block per arm; each arm
diff --git a/packages/compiler/src/backend/llvm/expr-bytes.ts b/packages/compiler/src/backend/llvm/expr-bytes.ts
index 41ddf2b9d..23025d00c 100644
--- a/packages/compiler/src/backend/llvm/expr-bytes.ts
+++ b/packages/compiler/src/backend/llvm/expr-bytes.ts
@@ -4,7 +4,7 @@ import { emitBorrowedInput } from "./borrowed-inputs.js";
import { BYTES_ELEMENT_SIZE, F64, type IrBytesElem, type IrExpr } from "../../ir/ir.js";
import type { LlvmEmitterContext, LlValue } from "./expr-context.js";
import { F64_INF, exactIndexLines, f64Lit, sizeToDouble } from "./common.js";
-import { exactInteger, widenInteger } from "./integer-values.js";
+import { alwaysExactInteger, exactInteger } from "./integer-values.js";
import { byteNumberAccess } from "../../ir/byte-numbers.js";
import { emitByteNumber } from "./byte-numbers.js";
import { integerOrNaNIndex, integerOrNaNRange } from "./wrapping-sums.js";
@@ -86,17 +86,18 @@ export function emitBytesIndex(
): string {
const B = host.B;
const index = value.name;
- const integer = exactInteger(host, value, expr);
- if (inBounds && integer) {
- const wide = widenInteger(host, integer);
- if (host.sizeType === "i64") return wide;
+ // The exact integer, or a wide view that always equals the double (such
+ // as `i + 1` of a speculated counter).
+ const exact = alwaysExactInteger(host, value, exactInteger(host, value, expr));
+ if (inBounds && exact !== null) {
+ if (host.sizeType === "i64") return exact;
const narrow = B.tmp();
- B.line(`${narrow} = trunc i64 ${wide} to ${host.sizeType}`);
+ B.line(`${narrow} = trunc i64 ${exact} to ${host.sizeType}`);
return narrow;
}
const len = emitBytesHeaderLength(host, receiver);
- if (integer) {
- const wideIndex = widenInteger(host, integer);
+ if (exact !== null) {
+ const wideIndex = exact;
const wideLength = host.sizeType === "i64" ? len : B.tmp();
if (host.sizeType !== "i64") B.line(`${wideLength} = zext ${host.sizeType} ${len} to i64`);
const inRange = B.tmp();
@@ -936,6 +937,23 @@ export function emitBytesIntrinsic(
B.line(`${t} = fcmp one double ${idx.name}, ${f64Lit(-1)}`);
return { name: t, type: e.type };
}
+ case "indexOfElem":
+ case "lastIndexOfElem":
+ return call(
+ "scr_bytes_index_of_elem",
+ "double (ptr, double, double, i1 zeroext)",
+ `ptr ${r.name}, double ${args[0]!.name}, double ${args[1]!.name}, i1 ${method === "lastIndexOfElem"}`,
+ false,
+ false,
+ );
+ case "includesElem":
+ return call(
+ "scr_bytes_includes_elem",
+ "zeroext i1 (ptr, double, double)",
+ `ptr ${r.name}, double ${args[0]!.name}, double ${args[1]!.name}`,
+ false,
+ false,
+ );
case "fillElem":
// Per-element TypedArray fill (non-u8): slice-style index
// defaults, never throws; the receiver comes back +1.
diff --git a/packages/compiler/src/backend/llvm/expr-primitives.ts b/packages/compiler/src/backend/llvm/expr-primitives.ts
index 2c6ce35db..04af0fcef 100644
--- a/packages/compiler/src/backend/llvm/expr-primitives.ts
+++ b/packages/compiler/src/backend/llvm/expr-primitives.ts
@@ -13,6 +13,7 @@ import { emitStringInputs } from "./string-lifetimes.js";
import { emitStringParts, numberPart, stringParts } from "./string-construction.js";
import { emitBorrowedInputs } from "./borrowed-inputs.js";
import {
+ alwaysExactInteger,
exactInteger,
int32Number,
integerNumber,
@@ -46,6 +47,16 @@ import {
} from "./dense-array-access.js";
import { emitWrappingSum, markWrappingOperands } from "./wrapping-sums.js";
+/** The signed integer predicates of the number compares. */
+const INTEGER_COMPARES: Record = {
+ "<": "slt",
+ "<=": "sle",
+ ">": "sgt",
+ ">=": "sge",
+ "===": "eq",
+ "!==": "ne",
+};
+
export function emitLiteralExpr(
host: LlvmEmitterContext,
e: ExprOf<"numLit" | "boolLit" | "strLit" | "moduleNsRef" | "unitLit" | "varRef">,
@@ -318,15 +329,7 @@ export function emitOperatorExpr(
if (integer) return integer;
}
if (cmp[e.op] !== undefined) {
- const integerCmp: Record = {
- "<": "slt",
- "<=": "sle",
- ">": "sgt",
- ">=": "sge",
- "===": "eq",
- "!==": "ne",
- };
- B.line(`${t} = icmp ${integerCmp[e.op]} i64 ${left}, ${right}`);
+ B.line(`${t} = icmp ${INTEGER_COMPARES[e.op]} i64 ${left}, ${right}`);
return { name: t, type: e.type };
}
}
@@ -356,7 +359,17 @@ export function emitOperatorExpr(
if (wideInteger) value.wideInteger = wideInteger;
return value;
}
- } else B.line(`${t} = fcmp ${cmp[e.op]} double ${l.name}, ${r.name}`);
+ } else {
+ // An operand with only a wide view, such as `i + 1` of a speculated
+ // counter: compare the integers when both always equal the doubles.
+ const left = alwaysExactInteger(host, l, li),
+ right = left === null ? null : alwaysExactInteger(host, r, ri);
+ if (left !== null && right !== null) {
+ B.line(`${t} = icmp ${INTEGER_COMPARES[e.op]} i64 ${left}, ${right}`);
+ return { name: t, type: e.type };
+ }
+ 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") {
throw new LlvmUnsupportedError(
diff --git a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts
index 1acc75cec..46c2d884f 100644
--- a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts
+++ b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts
@@ -993,6 +993,28 @@ export function streamTypedRefMaterializeAdapter(
: `call void @scr_error_dyn_place_name(ptr ${out}, ptr ${host.cstr(nameAfter)}, ${host.sizeType} ${Buffer.byteLength(nameAfter, "utf8")})`,
);
}
+ // A JavaScript Error subclass's property bag records, once a dynamic
+ // write has committed the view, where `name` and `message` were created
+ // (zod's error factory assigns `name` before it defines `message`); the
+ // slots hold their values. A `name` the class's construction sets is
+ // already where errorNameAfter places it among the fields.
+ if (
+ meta.def.errorBagOrder &&
+ meta.root.def.name === "%Error" &&
+ meta.def.fields.some((field) => field.name === DYN_CLASS_PROPERTIES)
+ ) {
+ host.declare(`declare void @scr_error_dyn_follow_bag(ptr, ptr, ptr, i1)`);
+ const { index } = classFieldIndex(meta, DYN_CLASS_PROPERTIES);
+ const slot = B.tmp(),
+ bag = B.tmp();
+ B.line(
+ `${slot} = getelementptr inbounds %${classStructSym(t.className)}, ptr %p, i64 0, i32 ${index}`,
+ );
+ B.line(`${bag} = load ptr, ptr ${slot}`);
+ B.line(
+ `call void @scr_error_dyn_follow_bag(ptr ${out}, ptr ${bag}, ptr %p, i1 ${nameAfter === undefined ? "false" : "true"})`,
+ );
+ }
// A frozen, sealed or non-extensible instance's view says so.
if (withAttributes) {
host.declare(`declare void @scr_dyn_class_view_attributes(ptr, double)`);
diff --git a/packages/compiler/src/backend/llvm/integer-values.ts b/packages/compiler/src/backend/llvm/integer-values.ts
index ddbcdd8da..90954f4c6 100644
--- a/packages/compiler/src/backend/llvm/integer-values.ts
+++ b/packages/compiler/src/backend/llvm/integer-values.ts
@@ -65,6 +65,22 @@ function magnitude(range: IntegerRange): number {
return Math.max(Math.abs(range.min), Math.abs(range.max));
}
+/** The bounds of a sum and a product of two magnitude bounds, as doubles.
+ * Below 2^53 the double result is the integer; above it the result is past
+ * every exactness limit either way, and rounding moves it far less than the
+ * margin from WIDE_BOUND to the i64 range. A result of exactly 2^53 may
+ * stand for 2^53 + 1 (2^53 - 1 + 2 rounds to 2^53), so it moves to the next
+ * double up unless the sum is exactly 2^53. */
+function sumBound(a: number, b: number): number {
+ const sum = a + b;
+ return sum === EXACT_DOUBLES && EXACT_DOUBLES - a !== b ? sum + 2 : sum;
+}
+
+function productBound(a: number, b: number): number {
+ const product = a * b;
+ return product === EXACT_DOUBLES ? product + 2 : product;
+}
+
/** 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).
*
@@ -98,14 +114,14 @@ export function wideArithmetic(
if (op === "*") {
if (lv.below !== Infinity || rv.below !== Infinity) return null;
below = EXACT_DOUBLES;
- bound = lv.bound * rv.bound;
+ bound = productBound(lv.bound, rv.bound);
} else if (lv.below === Infinity && rv.below === Infinity) {
below = EXACT_DOUBLES;
- bound = lv.bound + rv.bound;
+ bound = sumBound(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;
+ bound = sumBound(exact.bound, wide.bound);
} else return null;
if (!(bound <= WIDE_BOUND) || below < INDEX_LIMIT) return null;
const operand = (value: LlValue, integer: LlInteger | null): string =>
@@ -134,6 +150,20 @@ export function indexInteger(
return wide && wide.below >= INDEX_LIMIT ? wide.name : null;
}
+/** An i64 equal to a value's double wherever the value can be: its exact
+ * integer, or a wide view whose magnitude bound is within the magnitude below
+ * which the view is exact. Every integer of magnitude at most 2^53 is a
+ * double, so a bound equal to that limit is exact too. Null otherwise. */
+export function alwaysExactInteger(
+ host: LlvmEmitterContext,
+ value: LlValue,
+ integer: LlInteger | null,
+): string | null {
+ if (integer) return widenInteger(host, integer);
+ const wide = value.wideInteger;
+ return wide && wide.bound <= wide.below ? 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 e41a386eb..d58431d29 100644
--- a/packages/compiler/src/backend/llvm/lib-shared.ts
+++ b/packages/compiler/src/backend/llvm/lib-shared.ts
@@ -658,6 +658,7 @@ export const LIB_FN_SYMS: Record = {
"process.stderrWriteBytes": "scr_process_stderr_write_bytes",
"insp.buffer": "scr_insp_buffer",
"insp.typedArray": "scr_insp_typed_array",
+ "insp.bytes": "scr_insp_bytes",
// util.inspect (scr_inspect.c): all args borrowed; string results +1;
// the begin/entry/key/moreItems/end accumulator drives the emitted
// container walks. None of these throw.
@@ -713,6 +714,9 @@ export const LIB_FN_SYMS: Record = {
"url.hash": "scr_url_hash",
"url.searchParams": "scr_url_search_params",
"url.fileURLToPathUrl": "scr_url_to_path",
+ "url.fileURLToPathValue": "scr_url_to_path_value",
+ "fs.checkPath": "scr_fs_check_path",
+ "fs.checkReadEncoding": "scr_fs_check_read_encoding",
"url.fileURLToPathChecked": "scr_url_checked_to_path",
"url.fileURLToPathOptions": "scr_url_checked_to_path_options",
"url.fileURLToPathBuffer": "scr_url_checked_to_path_buffer",
diff --git a/packages/compiler/src/backend/llvm/reference-effects.ts b/packages/compiler/src/backend/llvm/reference-effects.ts
index 6ebe1222b..43a9176d5 100644
--- a/packages/compiler/src/backend/llvm/reference-effects.ts
+++ b/packages/compiler/src/backend/llvm/reference-effects.ts
@@ -181,6 +181,9 @@ function expressionPreservesEdges(
case "bytesIntrinsic":
return (
e.method === "get" ||
+ e.method === "indexOfElem" ||
+ e.method === "lastIndexOfElem" ||
+ e.method === "includesElem" ||
e.method === "length" ||
e.method === "byteLength" ||
e.method === "byteOffset" ||
diff --git a/packages/compiler/src/backend/llvm/union-repr.ts b/packages/compiler/src/backend/llvm/union-repr.ts
index 959f9e51e..8c6fb4c85 100644
--- a/packages/compiler/src/backend/llvm/union-repr.ts
+++ b/packages/compiler/src/backend/llvm/union-repr.ts
@@ -8,8 +8,14 @@
import type { BlockBuilder } from "./blocks.js";
import { NULLABLE_NULL, shapeTagFn, type PointerUnion } from "./nullable-unions.js";
-/** The i32 tag of a union value. */
-export function emitUnionTag(B: BlockBuilder, nullable: PointerUnion | null, u: string): string {
+/** The i32 tag of a union value. `alias` is the tag word's alias
+ * attachment (", !tbaa !N"), when the caller has one. */
+export function emitUnionTag(
+ B: BlockBuilder,
+ nullable: PointerUnion | null,
+ u: string,
+ alias = "",
+): string {
const t = B.tmp();
if (nullable?.repr === "shapes") {
B.line(`${t} = call i32 ${shapeTagFn(nullable.unionId)}(ptr ${u})`);
@@ -31,7 +37,7 @@ export function emitUnionTag(B: BlockBuilder, nullable: PointerUnion | null, u:
}
const p = B.tmp();
B.line(`${p} = getelementptr inbounds %ScrUnion, ptr ${u}, i64 0, i32 1`);
- B.line(`${t} = load i32, ptr ${p}`);
+ B.line(`${t} = load i32, ptr ${p}${alias}`);
return t;
}
@@ -81,12 +87,17 @@ export function emitNullableIsTag(
}
/** The BORROWED payload pointer of a reference arm. */
-export function emitUnionPeek(B: BlockBuilder, nullable: PointerUnion | null, u: string): string {
+export function emitUnionPeek(
+ B: BlockBuilder,
+ nullable: PointerUnion | null,
+ u: string,
+ alias = "",
+): string {
if (nullable) return u;
const p = B.tmp();
const t = B.tmp();
B.line(`${p} = getelementptr inbounds %ScrUnion, ptr ${u}, i64 0, i32 5`);
- B.line(`${t} = load ptr, ptr ${p}`);
+ B.line(`${t} = load ptr, ptr ${p}${alias}`);
return t;
}
diff --git a/packages/compiler/src/backend/target-cpu.test.ts b/packages/compiler/src/backend/target-cpu.test.ts
new file mode 100644
index 000000000..428caee32
--- /dev/null
+++ b/packages/compiler/src/backend/target-cpu.test.ts
@@ -0,0 +1,51 @@
+import { expect, test } from "vitest";
+import {
+ isTargetCpuOption,
+ targetCpuAttribute,
+ targetCpuField,
+ targetCpuKeyParts,
+ targetCpuTargetRefusal,
+ x86Level,
+} from "./target-cpu.js";
+
+const V2 = ["cx16", "lahf_lm", "popcnt", "pni", "sse4_1", "sse4_2", "ssse3"];
+const V3 = ["avx", "avx2", "bmi1", "bmi2", "f16c", "fma", "abm", "movbe", "xsave"];
+const V4 = ["avx512f", "avx512bw", "avx512cd", "avx512dq", "avx512vl"];
+const level = (flags: string[]) => x86Level((flag) => flags.includes(flag));
+
+test("a host's level is the highest one whose features and lower levels' features it has", () => {
+ expect(level([])).toBe("baseline");
+ expect(level(V2)).toBe("x86-64-v2");
+ expect(level([...V2, ...V3])).toBe("x86-64-v3");
+ expect(level([...V2, ...V3, ...V4])).toBe("x86-64-v4");
+ // AVX-512 without BMI2 is still v2.
+ expect(level([...V2, ...V3.filter((f) => f !== "bmi2"), ...V4])).toBe("x86-64-v2");
+ expect(level([...V3, ...V4])).toBe("baseline");
+});
+
+test("the baseline adds no key part, attribute or option field", () => {
+ for (const cpu of [undefined, "baseline"] as const) {
+ expect(targetCpuKeyParts(cpu)).toEqual([]);
+ expect(targetCpuAttribute(cpu)).toBe("");
+ expect(targetCpuField(cpu)).toEqual({});
+ expect(targetCpuTargetRefusal(cpu, "linux", "arm64")).toBeNull();
+ }
+ expect(targetCpuKeyParts("x86-64-v3")).toEqual(["target-cpu-x86-64-v3"]);
+ expect(targetCpuAttribute("x86-64-v4")).toBe(' "target-cpu"="x86-64-v4"');
+ expect(targetCpuField("x86-64-v2")).toEqual({ targetCpu: "x86-64-v2" });
+});
+
+test("a level is refused for targets that are not x86-64", () => {
+ expect(targetCpuTargetRefusal("x86-64-v3", "linux", "x64")).toBeNull();
+ expect(targetCpuTargetRefusal("x86-64-v3", "darwin", "arm64")).toBe(
+ "--target-cpu=x86-64-v3 requires an x86-64 target",
+ );
+ expect(targetCpuTargetRefusal("x86-64-v3", "wasi", "x64")).not.toBeNull();
+});
+
+test("the option accepts the levels and native", () => {
+ for (const value of ["baseline", "x86-64-v2", "x86-64-v3", "x86-64-v4", "native"])
+ expect(isTargetCpuOption(value)).toBe(true);
+ for (const value of ["x86-64", "haswell", "", undefined])
+ expect(isTargetCpuOption(value)).toBe(false);
+});
diff --git a/packages/compiler/src/backend/target-cpu.ts b/packages/compiler/src/backend/target-cpu.ts
new file mode 100644
index 000000000..51db87bbb
--- /dev/null
+++ b/packages/compiler/src/backend/target-cpu.ts
@@ -0,0 +1,118 @@
+import { execFileSync } from "node:child_process";
+import { readFileSync } from "node:fs";
+
+/** The x86-64 microarchitecture levels an executable may require
+ * (`--target-cpu`). `baseline` is the x86-64 baseline every release build
+ * targets; the other levels are LLVM's `x86-64-v2` to `x86-64-v4`. */
+export type TargetCpu = "baseline" | "x86-64-v2" | "x86-64-v3" | "x86-64-v4";
+
+/** The values `--target-cpu` accepts: a level, or `native` for the highest
+ * level the build host supports. */
+export const TARGET_CPU_OPTIONS = ["baseline", "x86-64-v2", "x86-64-v3", "x86-64-v4", "native"];
+
+export function isTargetCpuOption(value: unknown): value is TargetCpu | "native" {
+ return typeof value === "string" && TARGET_CPU_OPTIONS.includes(value);
+}
+
+/** The CPU features each level adds to the one below it, as Linux names
+ * them in /proc/cpuinfo (`pni` is SSE3, `abm` includes LZCNT). */
+const LEVEL_FLAGS: readonly [TargetCpu, readonly string[]][] = [
+ ["x86-64-v2", ["cx16", "lahf_lm", "popcnt", "pni", "sse4_1", "sse4_2", "ssse3"]],
+ ["x86-64-v3", ["avx", "avx2", "bmi1", "bmi2", "f16c", "fma", "abm", "movbe", "xsave"]],
+ ["x86-64-v4", ["avx512f", "avx512bw", "avx512cd", "avx512dq", "avx512vl"]],
+];
+
+/** The macOS `machdep.cpu` spellings of the same features. */
+const DARWIN_FLAGS: Readonly> = {
+ cx16: "CX16",
+ lahf_lm: "LAHF",
+ popcnt: "POPCNT",
+ pni: "SSE3",
+ sse4_1: "SSE4.1",
+ sse4_2: "SSE4.2",
+ ssse3: "SSSE3",
+ avx: "AVX1.0",
+ avx2: "AVX2",
+ bmi1: "BMI1",
+ bmi2: "BMI2",
+ f16c: "F16C",
+ fma: "FMA",
+ abm: "LZCNT",
+ movbe: "MOVBE",
+ xsave: "XSAVE",
+ avx512f: "AVX512F",
+ avx512bw: "AVX512BW",
+ avx512cd: "AVX512CD",
+ avx512dq: "AVX512DQ",
+ avx512vl: "AVX512VL",
+};
+
+/** The highest level whose features (and every lower level's) are all in
+ * `has`. */
+export function x86Level(has: (flag: string) => boolean): TargetCpu {
+ let level: TargetCpu = "baseline";
+ for (const [next, flags] of LEVEL_FLAGS) {
+ if (!flags.every(has)) break;
+ level = next;
+ }
+ return level;
+}
+
+/** The highest level the build host supports, or null when the host is not
+ * an x86-64 Linux or macOS machine. */
+export function hostTargetCpu(
+ platform: string = process.platform,
+ arch: string = process.arch,
+): TargetCpu | null {
+ if (arch !== "x64") return null;
+ try {
+ if (platform === "linux") {
+ const info = readFileSync("/proc/cpuinfo", "utf8");
+ const line = /^flags\s*:(.*)$/m.exec(info);
+ if (!line) return null;
+ const flags = new Set(line[1]!.trim().split(/\s+/));
+ return x86Level((flag) => flags.has(flag));
+ }
+ if (platform === "darwin") {
+ const text = execFileSync(
+ "sysctl",
+ ["-n", "machdep.cpu.features", "machdep.cpu.leaf7_features", "machdep.cpu.extfeatures"],
+ { encoding: "utf8" },
+ );
+ const flags = new Set(text.trim().split(/\s+/));
+ return x86Level((flag) => flags.has(DARWIN_FLAGS[flag]!));
+ }
+ } catch {
+ return null;
+ }
+ return null;
+}
+
+/** Cache-key parts for a target level; empty for the baseline so existing
+ * keys are unchanged. */
+export function targetCpuKeyParts(cpu: TargetCpu | undefined): string[] {
+ return cpu === undefined || cpu === "baseline" ? [] : [`target-cpu-${cpu}`];
+}
+
+/** The function attribute that makes LLVM generate code for the level, or
+ * "" for the baseline. */
+export function targetCpuAttribute(cpu: TargetCpu | undefined): string {
+ return cpu === undefined || cpu === "baseline" ? "" : ` "target-cpu"="${cpu}"`;
+}
+
+/** The options field for a level: omitted for the baseline. */
+export function targetCpuField(cpu: TargetCpu | undefined): { targetCpu?: TargetCpu } {
+ return cpu === undefined || cpu === "baseline" ? {} : { targetCpu: cpu };
+}
+
+/** The error for a level on a target that is not x86-64, or null. */
+export function targetCpuTargetRefusal(
+ cpu: TargetCpu | undefined,
+ platform: string,
+ architecture: string,
+): string | null {
+ if (cpu === undefined || cpu === "baseline") return null;
+ if (architecture !== "x64" || platform === "wasi")
+ return `--target-cpu=${cpu} requires an x86-64 target`;
+ return null;
+}
diff --git a/packages/compiler/src/cli/command.ts b/packages/compiler/src/cli/command.ts
index 3285aa7fa..57b15349e 100644
--- a/packages/compiler/src/cli/command.ts
+++ b/packages/compiler/src/cli/command.ts
@@ -15,6 +15,13 @@ import { buildEnvelope } from "../diagnostics/envelope.js";
import { renderWarnings } from "../diagnostics/render.js";
import { isPerformanceCode, renderPerformanceWarnings } from "../diagnostics/dynamic-tier.js";
import { isNativeOptimization } from "../backend/optimization.js";
+import {
+ hostTargetCpu,
+ isTargetCpuOption,
+ TARGET_CPU_OPTIONS,
+ targetCpuField,
+ type TargetCpu,
+} from "../backend/target-cpu.js";
import { setProvenanceSources } from "../frontend/provenance-registry.js";
import type { CliHost, NativeCacheWarmProfile } from "./host.js";
import { resolveOutputOptions } from "./output-options.js";
@@ -96,6 +103,7 @@ async function main(args: string[], host: CliHost): Promise {
values.strip ||
values["windows-subsystem"] !== undefined ||
values["detached-exit"] ||
+ values["target-cpu"] !== undefined ||
values["profile-generate"] !== undefined ||
values["profile-use"] !== undefined ||
values.sparse ||
@@ -154,6 +162,7 @@ async function main(args: string[], host: CliHost): Promise {
values.sanitize ||
values["windows-subsystem"] !== undefined ||
values["detached-exit"] ||
+ values["target-cpu"] !== undefined ||
values["profile-generate"] !== undefined ||
values["profile-use"] !== undefined ||
(values["npm-static"] ?? []).length > 0 ||
@@ -210,6 +219,7 @@ async function main(args: string[], host: CliHost): Promise {
values.ffi !== undefined ||
values["windows-subsystem"] !== undefined ||
values["detached-exit"] ||
+ values["target-cpu"] !== undefined ||
values["profile-generate"] !== undefined ||
values["profile-use"] !== undefined ||
(values["npm-static"] ?? []).length > 0 ||
@@ -218,7 +228,7 @@ async function main(args: string[], host: CliHost): Promise {
values["explain-dynamic"] !== undefined
) {
fail(
- "scriptc build --lib takes no --backend/--emit/--print/--optimization/--strip/--windows-subsystem/--detached-exit/--profile-generate/--profile-use/--npm-static/--ffi/--external-types/--tier/--explain-dynamic: the profile pins the emission and optimization, npm imports are judged automatically, outbound FFI belongs to executable builds, external type mappings belong to coverage, and library artifacts are static-tier only",
+ "scriptc build --lib takes no --backend/--emit/--print/--optimization/--strip/--windows-subsystem/--detached-exit/--target-cpu/--profile-generate/--profile-use/--npm-static/--ffi/--external-types/--tier/--explain-dynamic: the profile pins the emission and optimization, npm imports are judged automatically, outbound FFI belongs to executable builds, external type mappings belong to coverage, and library artifacts are static-tier only",
);
}
const profilePath = resolve(profileArg);
@@ -261,6 +271,9 @@ async function main(args: string[], host: CliHost): Promise {
if (command === "coverage" && values.strip) {
fail(`--strip is a build/run option\n\n${USAGE}`);
}
+ if (command === "coverage" && values["target-cpu"] !== undefined) {
+ fail(`--target-cpu is a build/run option\n\n${USAGE}`);
+ }
if (command === "coverage" && values["detached-exit"]) {
fail(`--detached-exit is a build/run option\n\n${USAGE}`);
}
@@ -377,6 +390,19 @@ async function main(args: string[], host: CliHost): Promise {
if (windowsSubsystem !== undefined && command === "coverage") {
fail(`--windows-subsystem is only supported for executable builds\n\n${USAGE}`);
}
+ const targetCpuOption = values["target-cpu"];
+ if (targetCpuOption !== undefined && !isTargetCpuOption(targetCpuOption)) {
+ fail(
+ `unknown target CPU "${targetCpuOption}" (supported: ${TARGET_CPU_OPTIONS.join(", ")})\n\n${USAGE}`,
+ );
+ }
+ let targetCpu: TargetCpu | undefined = targetCpuOption === "native" ? undefined : targetCpuOption;
+ if (targetCpuOption === "native") {
+ const level = hostTargetCpu();
+ if (level === null)
+ fail(`--target-cpu=native needs an x86-64 Linux or macOS build host; name a level instead`);
+ targetCpu = level;
+ }
const output =
command === "coverage"
? null
@@ -498,6 +524,7 @@ async function main(args: string[], host: CliHost): Promise {
...(values.strip ? { strip: true } : {}),
...(windowsSubsystem !== undefined ? { windowsSubsystem } : {}),
...(values["detached-exit"] ? { detachedExit: true } : {}),
+ ...targetCpuField(targetCpu),
// The instrumented program resolves a relative pattern against its own
// working directory; resolve it here, where the user wrote it.
...(values["profile-generate"] === undefined
diff --git a/packages/compiler/src/cli/usage.ts b/packages/compiler/src/cli/usage.ts
index c4d07656a..01afbdd41 100644
--- a/packages/compiler/src/cli/usage.ts
+++ b/packages/compiler/src/cli/usage.ts
@@ -54,6 +54,12 @@ Options:
a helper process so the parent sees the exit sooner. The
helper keeps the executable mapped and its memory charged
for up to ~100 ms after exit (opt in; --emit=exe only)
+ --target-cpu
+ x86-64: compile the program's code for a CPU level
+ (default: baseline, which runs on any x86-64 CPU). The
+ executable then requires that level: v3 adds AVX2, BMI2
+ and FMA, v4 adds AVX-512. native uses the build host's
+ level. The runtime keeps its baseline code
--profile-generate
Linux: instrument the executable for profile-guided
optimization. Each run writes a raw profile to the
@@ -123,6 +129,7 @@ export const CLI_OPTIONS = {
strip: { type: "boolean", default: false },
"windows-subsystem": { type: "string" },
"detached-exit": { type: "boolean", default: false },
+ "target-cpu": { type: "string" },
"profile-generate": { type: "string" },
"profile-use": { type: "string" },
sparse: { type: "boolean", default: false },
diff --git a/packages/compiler/src/compile-types.ts b/packages/compiler/src/compile-types.ts
index 9ecd75974..c50931c6a 100644
--- a/packages/compiler/src/compile-types.ts
+++ b/packages/compiler/src/compile-types.ts
@@ -1,3 +1,4 @@
+import type { TargetCpu } from "./backend/target-cpu.js";
import type { WindowsSubsystem } from "./backend/targets.js";
import type { NativeLinkInfo } from "./backend/native-link-info.js";
import type { NativeOptimization } from "./backend/optimization.js";
@@ -36,6 +37,12 @@ export interface CompileBaseOptions {
* executable mapped and the memory charged for up to ~100 ms after exit.
* Executables only; no effect on other platforms. */
detachedExit?: boolean;
+ /** The x86-64 microarchitecture level the executable's code may require
+ * (`x86-64-v2` to `x86-64-v4`). The program's functions are compiled for
+ * that level, and the executable then runs only on CPUs that support it.
+ * Unset or `baseline`, the code runs on any x86-64 CPU. x86-64 targets
+ * only; the runtime pack keeps its baseline code. */
+ targetCpu?: TargetCpu;
/** Instrument the executable for profile-guided optimization. Each run
* writes a raw profile to this path pattern (`%p` expands to the process
* id; `LLVM_PROFILE_FILE` overrides the pattern at run time). Linux
diff --git a/packages/compiler/src/executable/executable-cache.test.ts b/packages/compiler/src/executable/executable-cache.test.ts
index e066c0412..e1235d8ac 100644
--- a/packages/compiler/src/executable/executable-cache.test.ts
+++ b/packages/compiler/src/executable/executable-cache.test.ts
@@ -236,6 +236,8 @@ test("early executable keys isolate compile modes, paths, implementation, checke
{ ...f.options, optimization: "dev" },
{ ...f.options, optimization: "speed" },
{ ...f.options, detachedExit: true },
+ { ...f.options, targetCpu: "x86-64-v3" },
+ { ...f.options, targetCpu: "x86-64-v4" },
{ ...f.options, profile: "profile-generate:/tmp/%p.profraw" },
{ ...f.options, profile: "profile-use:digest" },
{ ...f.options, checker: "tsc-ts-executable-identity" },
diff --git a/packages/compiler/src/executable/executable-cache.ts b/packages/compiler/src/executable/executable-cache.ts
index 96922ca13..50f783761 100644
--- a/packages/compiler/src/executable/executable-cache.ts
+++ b/packages/compiler/src/executable/executable-cache.ts
@@ -18,6 +18,7 @@ import { compilerReleaseVersion } from "../library/sidecar.js";
import { nativeArtifactDependenciesStillMatch } from "../backend/native/artifact-stamps.js";
import { type NativeArtifactDependency } from "../backend/native/contracts.js";
import { optimizationKeyParts } from "../backend/optimization.js";
+import { type TargetCpu, targetCpuKeyParts } from "../backend/target-cpu.js";
import { privateSiblingPath } from "../backend/build-cache.js";
import type { CompilerImplementationDependency } from "../library/compiler-self-identity.js";
import {
@@ -139,6 +140,8 @@ export interface EarlyExecutableCacheOptions {
windowsSubsystem?: "gui";
/** Omitted unless --detached-exit is set. */
detachedExit?: true;
+ /** Omitted for the x86-64 baseline (`--target-cpu`). */
+ targetCpu?: TargetCpu;
/** Omitted without --profile-generate/--profile-use; otherwise the
* pattern, or the digest of the profile's bytes. */
profile?: string;
@@ -281,6 +284,7 @@ function cacheKey(options: EarlyExecutableCacheOptions): string {
...(options.strip ? ["strip"] : []),
...(options.windowsSubsystem === "gui" ? ["windows-subsystem-gui"] : []),
...(options.detachedExit ? ["detached-exit"] : []),
+ ...targetCpuKeyParts(options.targetCpu),
...(options.profile !== undefined ? [options.profile] : []),
...(options.checker !== undefined ? [`checker:${options.checker}`] : []),
...(options.youngNursery ? ["young-nursery"] : []),
@@ -323,6 +327,7 @@ function routeKey(options: Omit
-> = {
+interface FsPathRow {
+ /** The path parameter indexes. */
+ readonly paths: readonly number[];
+ /** Whether Node validates the paths before every other argument
+ * (otherwise the options come first). */
+ readonly pathsFirst: boolean;
+ /** The path parameters' names in Node's errors, when there are two. */
+ readonly names?: readonly string[];
+}
+
+/** The fs functions whose path parameters accept a URL. */
+const FS_URL_PATHS: Readonly> = {
accessSync: { paths: [0], pathsFirst: true },
appendFileSync: { paths: [0], pathsFirst: false },
chmodSync: { paths: [0], pathsFirst: true },
chownSync: { paths: [0], pathsFirst: true },
- copyFileSync: { paths: [0, 1], pathsFirst: true },
+ copyFileSync: { paths: [0, 1], pathsFirst: true, names: ["src", "dest"] },
lchownSync: { paths: [0], pathsFirst: true },
lstatSync: { paths: [0], pathsFirst: true },
mkdirSync: { paths: [0], pathsFirst: false },
@@ -39,7 +49,7 @@ const FS_URL_PATHS: Readonly<
readdirSync: { paths: [0], pathsFirst: false },
realpathSync: { paths: [0], pathsFirst: false },
"realpathSync.native": { paths: [0], pathsFirst: false },
- renameSync: { paths: [0, 1], pathsFirst: true },
+ renameSync: { paths: [0, 1], pathsFirst: true, names: ["oldPath", "newPath"] },
rmSync: { paths: [0], pathsFirst: false },
rmdirSync: { paths: [0], pathsFirst: true },
statSync: { paths: [0], pathsFirst: true },
@@ -48,43 +58,66 @@ const FS_URL_PATHS: Readonly<
writeFileSync: { paths: [0], pathsFirst: false },
};
-/** The fs.promises functions with path parameters, by path indexes. */
-const FSP_PATHS: Readonly> = {
- access: [0],
- appendFile: [0],
- chmod: [0],
- chown: [0],
- copyFile: [0, 1],
- lchown: [0],
- lstat: [0],
- mkdir: [0],
- mkdtemp: [0],
- open: [0],
- opendir: [0],
- readFile: [0],
- readdir: [0],
- realpath: [0],
- rename: [0, 1],
- rm: [0],
- rmdir: [0],
- stat: [0],
- truncate: [0],
- unlink: [0],
- writeFile: [0],
+/** The fs.promises functions with path parameters. */
+const FSP_PATHS: Readonly> = {
+ access: { paths: [0], pathsFirst: true },
+ appendFile: { paths: [0], pathsFirst: false },
+ chmod: { paths: [0], pathsFirst: true },
+ chown: { paths: [0], pathsFirst: true },
+ copyFile: { paths: [0, 1], pathsFirst: true },
+ lchown: { paths: [0], pathsFirst: true },
+ lstat: { paths: [0], pathsFirst: true },
+ mkdir: { paths: [0], pathsFirst: false },
+ mkdtemp: { paths: [0], pathsFirst: false },
+ open: { paths: [0], pathsFirst: true },
+ opendir: { paths: [0], pathsFirst: true },
+ readFile: { paths: [0], pathsFirst: false },
+ readdir: { paths: [0], pathsFirst: false },
+ realpath: { paths: [0], pathsFirst: false },
+ rename: { paths: [0, 1], pathsFirst: true },
+ rm: { paths: [0], pathsFirst: true },
+ rmdir: { paths: [0], pathsFirst: true },
+ stat: { paths: [0], pathsFirst: true },
+ truncate: { paths: [0], pathsFirst: true },
+ unlink: { paths: [0], pathsFirst: true },
+ writeFile: { paths: [0], pathsFirst: false },
};
+/** What a path argument of an fs call lowers to (Lowerer.fsPathPlans). */
+export type FsPathPlan =
+ /** The first URL path: the later arguments with effects, the earlier
+ * string paths to check, and a runtime encoding to check, all before the
+ * conversion. */
+ | {
+ kind: "convert";
+ staged: ts.Expression[];
+ checks: { path: ts.Expression; name: string }[];
+ encoding: ts.Expression | null;
+ }
+ /** A string path that a later URL path checks: the local that holds it,
+ * once lowered. */
+ | { kind: "save"; local: string | null }
+ /** A URL path whose conversion error becomes false or a rejection: the
+ * local that holds the conversion's value. */
+ | { kind: "value"; local: string };
+
function isUrlArgument(lowerer: Lowerer, node: ts.Expression | undefined): boolean {
return node !== undefined && lowerer.mapTypeOf(lowerer.typeOf(node))?.kind === "url";
}
+function stripParens(node: ts.Expression): ts.Expression {
+ let e = node;
+ while (ts.isParenthesizedExpression(e)) e = e.expression;
+ return e;
+}
+
/** A literal that Node's option validation accepts before it reads the
* path: a string (the checker admits only the declared encodings, flags
* and type names), a boolean, an integer in [0, 2^31 - 1] (every numeric
* option of these functions accepts that range), undefined or null, or an
* object literal of those. */
function preValidatedLiteral(node: ts.Expression): boolean {
- let e = node;
- while (ts.isParenthesizedExpression(e)) e = e.expression;
+ const e = stripParens(node);
if (ts.isStringLiteralLike(e)) return true;
if (
e.kind === ts.SyntaxKind.TrueKeyword ||
@@ -108,39 +141,59 @@ function preValidatedLiteral(node: ts.Expression): boolean {
return false;
}
-/** SC2020 when the call passes a URL path in a form whose conversion
- * order would differ from Node's (see the file comment). */
-function checkUrlPaths(
+const HINT = "convert the URL first: fileURLToPath(url) from node:url";
+
+/** SC2020 for an argument Node validates before the paths that is neither
+ * absent nor a literal passing that validation, other than a string
+ * readFileSync encoding (returned: it is checked before the conversion).
+ * writeFileSync/appendFileSync's data is checked after the path. */
+function optionsBeforePaths(
lowerer: Lowerer,
- call: ts.CallExpression,
- module: string,
+ args: readonly ts.Expression[],
member: string,
-): void {
- const args = call.arguments.nodes;
- const name = module === "fs/promises" ? `fs.promises.${member}` : member;
- const hint = "convert the URL first: fileURLToPath(url) from node:url";
- if (module === "fs/promises") {
- if ((FSP_PATHS[member] ?? []).some((i) => isUrlArgument(lowerer, args[i])))
- lowerer.noLowering(`${name} with a URL path`, call, hint);
- return;
+ row: FsPathRow,
+ name: string,
+): ts.Expression | null {
+ if (row.pathsFirst) return null;
+ const data = /^(writeFile|appendFile)(Sync)?$/.test(member) ? 1 : -1;
+ let encoding: ts.Expression | null = null;
+ for (let i = 0; i < args.length; i++) {
+ const arg = args[i]!;
+ if (row.paths.includes(i) || i === data || preValidatedLiteral(arg)) continue;
+ if (
+ member === "readFileSync" &&
+ i === 1 &&
+ lowerer.mapTypeOf(lowerer.typeOf(arg))?.kind === "string"
+ ) {
+ encoding = stripTypeWrappers(arg);
+ continue;
+ }
+ lowerer.noLowering(`${name} with a URL path and options that are not literals`, arg, HINT);
}
- if (member === "existsSync" && isUrlArgument(lowerer, args[0]))
- lowerer.noLowering(`${name} with a URL path`, call, hint);
- const row = FS_URL_PATHS[member];
- if (!row) return;
- const first = row.paths.find((i) => isUrlArgument(lowerer, args[i]));
- if (first === undefined) return;
- if (args.some(ts.isSpreadElement))
- lowerer.noLowering(`${name} with spread arguments and a URL path`, call);
- // The runtime checks a string path after the conversion, so a string
- // path Node checks first cannot precede the URL.
- if (row.paths.some((i) => i < first && args[i] !== undefined))
- lowerer.noLowering(`${name} with a string path before a URL path`, call, hint);
- // Every argument from the URL through the last one with effects is
- // evaluated ahead of the conversion, in source order: a later argument's
- // effect (an assignment, say) must not change what an earlier one read.
+ return encoding;
+}
+
+/** The arguments after the URL path that are evaluated ahead of the
+ * conversion: every one through the last with effects or that `force`
+ * names, in source order. An object or array literal among them is
+ * SC2020: its members are lowered one by one, so it cannot be evaluated
+ * ahead of the conversion as one value. */
+function stagedAfter(
+ lowerer: Lowerer,
+ args: readonly ts.Expression[],
+ first: number,
+ name: string,
+ force: (e: ts.Expression, i: number) => boolean,
+): ts.Expression[] {
+ // Every argument from the URL through the last one with effects (or that
+ // the caller forces) is evaluated ahead of the conversion, in source
+ // order: a later argument's effect (an assignment, say) must not change
+ // what an earlier one read.
let last = first;
- for (let i = first + 1; i < args.length; i++) if (!sideEffectFreeOptionValue(args[i]!)) last = i;
+ for (let i = first + 1; i < args.length; i++) {
+ const e = stripTypeWrappers(args[i]!);
+ if (!sideEffectFreeOptionValue(e) || force(e, i)) last = i;
+ }
const staged: ts.Expression[] = [];
for (let i = first + 1; i <= last; i++) {
// Keyed without type-only wrappers (`satisfies`, `as`, `!`), so a
@@ -154,56 +207,190 @@ function checkUrlPaths(
? `${name} with a URL path and a literal before a later argument that has effects`
: `${name} with a URL path and a later literal that has effects`,
e,
- hint,
+ HINT,
);
}
staged.push(e);
}
- if (staged.length > 0) {
- lowerer.stagedAfterPath.set(args[first]!, staged);
- for (const e of staged) lowerer.stagedArguments.set(e, null);
- }
- if (row.pathsFirst) return;
- // writeFileSync/appendFileSync's data is checked after the path.
- const data = member === "writeFileSync" || member === "appendFileSync" ? 1 : -1;
- for (let i = 0; i < args.length; i++) {
- if (row.paths.includes(i) || i === data || preValidatedLiteral(args[i]!)) continue;
- lowerer.noLowering(`${name} with a URL path and options that are not literals`, args[i]!, hint);
- }
+ return staged;
}
-/** Fences URL paths that cannot convert in Node's order: the check every
- * fs and fs.promises call runs before lowering its arguments. */
+/** Plans the URL paths of a synchronous fs call (see the file comment):
+ * the check every fs call runs before lowering its arguments. */
export function checkFsUrlPaths(
lowerer: Lowerer,
call: ts.CallExpression,
bi: { module: string; member: string },
): void {
- if (bi.module === "fs" || bi.module === "fs/promises")
- checkUrlPaths(lowerer, call, bi.module, bi.member);
+ if (bi.module !== "fs") return;
+ const row = FS_URL_PATHS[bi.member];
+ if (!row) return;
+ const args = call.arguments.nodes;
+ const first = row.paths.find((i) => isUrlArgument(lowerer, args[i]));
+ if (first === undefined || lowerer.fsPathPlans.has(stripParens(args[first]!))) return;
+ const name = bi.member;
+ if (args.some(ts.isSpreadElement))
+ lowerer.noLowering(`${name} with spread arguments and a URL path`, call);
+ const checks: { path: ts.Expression; name: string }[] = [];
+ for (const i of row.paths) {
+ if (i >= first || args[i] === undefined) continue;
+ const path = stripParens(args[i]!);
+ checks.push({ path, name: row.names?.[i] ?? "path" });
+ lowerer.fsPathPlans.set(path, { kind: "save", local: null });
+ }
+ const encoding = optionsBeforePaths(lowerer, args, bi.member, row, name);
+ const staged = stagedAfter(lowerer, args, first, name, (e) => e === encoding);
+ for (const e of staged) lowerer.stagedArguments.set(e, null);
+ lowerer.fsPathPlans.set(stripParens(args[first]!), { kind: "convert", staged, checks, encoding });
+}
+
+/** existsSync and the fs.promises functions with URL paths: each URL
+ * converts to a value (url.fileURLToPathValue) after the arguments are
+ * evaluated, and the call runs on the converted paths only when every
+ * conversion produced a string; otherwise existsSync answers false and an
+ * fs.promises function a promise rejected with the first conversion's
+ * error. `lowerCall` lowers the call itself, whose URL paths then read the
+ * converted values. Null for every other call. */
+export function lowerFallibleFsUrlCall(
+ lowerer: Lowerer,
+ call: ts.CallExpression,
+ bi: { module: string; member: string },
+ lowerCall: () => IrExpr,
+): IrExpr | null {
+ const row =
+ bi.module === "fs/promises"
+ ? FSP_PATHS[bi.member]
+ : bi.module === "fs" && bi.member === "existsSync"
+ ? ({ paths: [0], pathsFirst: true } satisfies FsPathRow)
+ : undefined;
+ if (!row) return null;
+ const args = call.arguments.nodes;
+ const urls = row.paths.filter((i) => isUrlArgument(lowerer, args[i]));
+ if (urls.length === 0 || lowerer.fallibleFsCalls.has(call)) return null;
+ const name = bi.module === "fs/promises" ? `fs.promises.${bi.member}` : bi.member;
+ if (args.some(ts.isSpreadElement))
+ lowerer.noLowering(`${name} with spread arguments and a URL path`, call);
+ const first = urls[0]!;
+ // A string path before the URL would be checked in the call itself,
+ // after the conversion.
+ if (row.paths.some((i) => i < first && args[i] !== undefined))
+ lowerer.noLowering(`${name} with a string path before a URL path`, call, HINT);
+ if (optionsBeforePaths(lowerer, args, bi.member, row, name) !== null)
+ lowerer.noLowering(`${name} with a URL path and options that are not literals`, args[1]!, HINT);
+ const loc = locOf(call);
+ const stmts: IrStmt[] = [];
+ const values = new Map();
+ const save = (value: IrExpr, label: string, at: ts.Node): string => {
+ const slot = lowerer.declareHiddenLocal(label, value.type);
+ stmts.push({ kind: "varDecl", localId: slot.id, init: value, loc: locOf(at) });
+ return slot.id;
+ };
+ values.set(first, save(lowerer.lowerExprExpecting(args[first]!, URL_T), "%fsUrl", args[first]!));
+ const staged = stagedAfter(lowerer, args, first, name, (_e, i) => urls.includes(i));
+ for (const e of staged) {
+ // A call lowered again (an array-literal element) evaluates the
+ // argument anew; the slot an earlier lowering saved it in is gone.
+ lowerer.stagedArguments.delete(e);
+ const value = lowerer.lowerExpr(e);
+ const slot = save(value, "%fsArg", e);
+ lowerer.stagedArguments.set(e, { localId: slot, type: value.type });
+ const index = args.findIndex((arg) => stripTypeWrappers(arg) === e);
+ if (urls.includes(index)) values.set(index, slot);
+ }
+ const results: string[] = [];
+ for (const i of urls) {
+ const url = varRef(values.get(i)!, URL_T, loc);
+ const converted: IrExpr = {
+ kind: "libCall",
+ fn: "url.fileURLToPathValue",
+ args: [url],
+ type: DYN,
+ loc,
+ };
+ const local = save(converted, "%fsPathValue", args[i]!);
+ lowerer.fsPathPlans.set(stripParens(args[i]!), { kind: "value", local });
+ results.push(local);
+ }
+ lowerer.fallibleFsCalls.add(call);
+ let inner: IrExpr;
+ try {
+ inner = lowerCall();
+ } finally {
+ lowerer.fallibleFsCalls.delete(call);
+ }
+ let result = inner;
+ for (let k = results.length - 1; k >= 0; k--) {
+ const value = varRef(results[k]!, DYN, loc);
+ const failed: IrExpr =
+ bi.module === "fs/promises"
+ ? { kind: "intrinsic", name: "promise.reject", args: [value], type: inner.type, loc }
+ : boolLit(false, loc);
+ result = {
+ kind: "ternary",
+ cond: { kind: "dynTest", test: "string", value, type: BOOL, loc },
+ then: result,
+ else_: failed,
+ type: inner.type,
+ loc,
+ };
+ }
+ return { kind: "seqExpr", stmts, result, type: inner.type, loc };
}
/** An fs path argument as a string: a URL converts with fileURLToPath's
* conversion (url.fileURLToPathUrl), which throws Node's errors for other
- * schemes, hosts and encoded separators. */
+ * schemes, hosts and encoded separators, after the checks its plan names. */
export function lowerFsPath(lowerer: Lowerer, node: ts.Expression): IrExpr {
- if (!isUrlArgument(lowerer, node)) return lowerer.lowerExprExpecting(node, STRING);
const loc = locOf(node);
+ const plan = lowerer.fsPathPlans.get(stripParens(node));
+ if (plan?.kind === "value")
+ return { kind: "dynCheck", value: varRef(plan.local, DYN, loc), type: STRING, loc };
+ if (!isUrlArgument(lowerer, node)) {
+ const value = lowerer.lowerExprExpecting(node, STRING);
+ if (plan?.kind !== "save") return value;
+ const slot = lowerer.declareHiddenLocal("%fsPath", STRING);
+ plan.local = slot.id;
+ return {
+ kind: "seqExpr",
+ stmts: [{ kind: "varDecl", localId: slot.id, init: value, loc }],
+ result: varRef(slot.id, STRING, loc),
+ type: STRING,
+ loc,
+ };
+ }
const url = lowerer.lowerExprExpecting(node, URL_T);
- const staged = lowerer.stagedAfterPath.get(node);
- if (staged === undefined)
+ if (plan?.kind !== "convert")
return { kind: "libCall", fn: "url.fileURLToPathUrl", args: [url], type: STRING, loc };
// The URL and the later arguments with effects, in source order, then
- // the conversion.
+ // the checks Node runs first, then the conversion.
const saved = lowerer.declareHiddenLocal("%fsUrl", URL_T);
const stmts: IrStmt[] = [{ kind: "varDecl", localId: saved.id, init: url, loc }];
- for (const arg of staged) {
+ for (const arg of plan.staged) {
lowerer.stagedArguments.delete(arg);
const value = lowerer.lowerExpr(arg);
const slot = lowerer.declareHiddenLocal("%fsArg", value.type);
stmts.push({ kind: "varDecl", localId: slot.id, init: value, loc: locOf(arg) });
lowerer.stagedArguments.set(arg, { localId: slot.id, type: value.type });
}
+ const check = (fn: "fs.checkPath" | "fs.checkReadEncoding", args: IrExpr[]): void => {
+ stmts.push({ kind: "exprStmt", expr: { kind: "libCall", fn, args, type: VOID, loc }, loc });
+ };
+ for (const { path, name } of plan.checks) {
+ const saved = lowerer.fsPathPlans.get(path);
+ if (saved?.kind !== "save" || saved.local === null)
+ throw new InternalCompilerError("an fs path checked before it was lowered");
+ check("fs.checkPath", [
+ varRef(saved.local, STRING, loc),
+ { kind: "strLit", value: name, type: STRING, loc },
+ ]);
+ }
+ if (plan.encoding !== null) {
+ const slot = lowerer.stagedArguments.get(plan.encoding);
+ if (!slot) throw new InternalCompilerError("an fs encoding checked before it was saved");
+ check("fs.checkReadEncoding", [
+ lowerer.coerceToExpected(varRef(slot.localId, slot.type, loc), STRING),
+ ]);
+ }
return {
kind: "seqExpr",
stmts,
@@ -219,7 +406,12 @@ export function lowerFsPath(lowerer: Lowerer, node: ts.Expression): IrExpr {
};
}
-/** Whether `index` is a path parameter of the fs function `member`. */
-export function isFsPathParam(member: string, index: number): boolean {
- return FS_URL_PATHS[member]?.paths.includes(index) === true;
+/** Whether `index` is a path parameter of the fs or fs.promises function
+ * `member`. */
+export function isFsPathParam(module: string, member: string, index: number): boolean {
+ if (module === "fs/promises") return FSP_PATHS[member]?.paths.includes(index) === true;
+ if (module !== "fs") return false;
+ return (
+ (member === "existsSync" && index === 0) || FS_URL_PATHS[member]?.paths.includes(index) === true
+ );
}
diff --git a/packages/compiler/src/frontend/lowering/builtins/module-calls.ts b/packages/compiler/src/frontend/lowering/builtins/module-calls.ts
index e5446291f..2aaa2f80f 100644
--- a/packages/compiler/src/frontend/lowering/builtins/module-calls.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/module-calls.ts
@@ -25,7 +25,7 @@ import {
lowerFsWriteOptionsCall,
lowerFsReadPathCall,
} from "./filesystem-calls.js";
-import { checkFsUrlPaths, isFsPathParam, lowerFsPath } from "./fs-paths.js";
+import { checkFsUrlPaths, isFsPathParam, lowerFallibleFsUrlCall, lowerFsPath } from "./fs-paths.js";
import { lowerBufferEncodingCall } from "./text-codecs.js";
import { lowerUtilTypeCall } from "./util-types.js";
import { InternalCompilerError } from "../../../errors.js";
@@ -59,6 +59,10 @@ export function lowerBuiltinModuleCall(
loc: SrcLoc,
): IrExpr {
const name = ts.getTextOfNode(expr.expression);
+ const fallible = lowerFallibleFsUrlCall(lowerer, expr, bi, () =>
+ lowerBuiltinModuleCall(lowerer, expr, bi, fn, loc),
+ );
+ if (fallible) return fallible;
checkFsUrlPaths(lowerer, expr, bi);
if (bi.module === "util/types") {
return lowerUtilTypeCall(lowerer, expr, bi, loc, name);
@@ -258,7 +262,7 @@ export function lowerBuiltinModuleCall(
bi.member === "readFile" &&
expr.arguments.nodes.length === 1
) {
- const path = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING);
+ const path = lowerFsPath(lowerer, expr.arguments.nodes[0]!);
return {
kind: "libCall",
fn: "fsp.readFileBytes",
@@ -473,7 +477,7 @@ export function lowerBuiltinModuleCall(
args = lowerer.completeArgs(expr.arguments.nodes, shapes, loc, expr);
} else {
args = expr.arguments.nodes.map((a, i) =>
- bi.module === "fs" && isFsPathParam(bi.member, i)
+ isFsPathParam(bi.module, bi.member, i)
? lowerFsPath(lowerer, a)
: lowerer.lowerExprExpecting(a, fn.params[i]),
);
diff --git a/packages/compiler/src/frontend/lowering/cjs-inline-require.ts b/packages/compiler/src/frontend/lowering/cjs-inline-require.ts
index 2b28c6533..10549a030 100644
--- a/packages/compiler/src/frontend/lowering/cjs-inline-require.ts
+++ b/packages/compiler/src/frontend/lowering/cjs-inline-require.ts
@@ -28,8 +28,10 @@ import {
isModuleExportsAccess,
locOf,
npmStaticDepSf7,
+ requireBindingEarlyUse7,
requireSpecOf,
resolveImport,
+ splitRequireCallOf,
} from "../program.js";
import { createRequireCalleeFileOf } from "./builtins/module-bindings.js";
import { isCjsRequireGlobal } from "./lower-node-module.js";
@@ -43,6 +45,332 @@ export interface InlineRequire {
dep: ts.SourceFile;
}
+/** A top-level `var m; m = require("./x")` binding. Node evaluates the
+ * require at the assignment; the binding is alias plumbing exactly like
+ * `const m = require("./x")` for every later read. */
+export interface SplitRequireBinding {
+ decl: ts.VariableDeclaration;
+ assign: ts.BinaryExpression;
+ call: ts.CallExpression;
+ spec: string;
+ dep: ts.SourceFile;
+}
+
+function symbolOfIdentifierValue(lowerer: Lowerer, ident: ts.Identifier): ts.Symbol | undefined {
+ const parent = ident.parent;
+ // A destructuring shorthand target resolves to the property symbol at
+ // the name, but reads/writes the variable through its value symbol.
+ if (ts.isShorthandPropertyAssignment(parent) && parent.name === ident) {
+ return (
+ lowerer.checker.getShorthandAssignmentValueSymbol(parent) ??
+ lowerer.checker.getSymbolAtLocation(ident)
+ );
+ }
+ return lowerer.checker.getSymbolAtLocation(ident);
+}
+
+function destructuringContainerOf(node: ts.Node): ts.Node | null {
+ const parent = node.parent;
+ if (ts.isShorthandPropertyAssignment(parent) && parent.name === node) return parent;
+ if (ts.isPropertyAssignment(parent) && parent.initializer === node) return parent;
+ if (ts.isSpreadElement(parent) && parent.expression === node) return parent;
+ if (ts.isSpreadAssignment(parent) && parent.expression === node) return parent;
+ return null;
+}
+
+function writeTargetOf(node: ts.Node): ts.Node | null {
+ let target = node;
+ for (;;) {
+ const parent = target.parent;
+ if (
+ ts.isParenthesizedExpression(parent) ||
+ ts.isArrayLiteralExpression(parent) ||
+ ts.isObjectLiteralExpression(parent)
+ ) {
+ target = parent;
+ continue;
+ }
+ const container = destructuringContainerOf(target);
+ if (container === null) break;
+ target = container;
+ }
+ const parent = target.parent;
+ if (
+ ts.isBinaryExpression(parent) &&
+ parent.left === target &&
+ ts.isAssignmentOperator(parent.operatorToken.kind)
+ )
+ return parent;
+ if (
+ (ts.isPrefixUnaryExpression(parent) || ts.isPostfixUnaryExpression(parent)) &&
+ parent.operand === target &&
+ (parent.operator === ts.SyntaxKind.PlusPlusToken ||
+ parent.operator === ts.SyntaxKind.MinusMinusToken)
+ )
+ return parent;
+ if ((ts.isForInStatement(parent) || ts.isForOfStatement(parent)) && parent.initializer === target)
+ return parent;
+ return null;
+}
+
+/** The split form of a top-level require binding: `var m; m = require("./x")`.
+ * Exactly one write, at a top-level `m = require("")` statement.
+ * Null for the ordinary `const m = require("./x")` declaration and for
+ * every form that needs runtime module selection. */
+export function splitRequireBindingOf(
+ lowerer: Lowerer,
+ decl: ts.VariableDeclaration,
+): SplitRequireBinding | null {
+ // Cheap shape gate before the cache: ordinary declarations never enter the
+ // per-Lowerer map.
+ if (decl.initializer !== undefined || !ts.isIdentifier(decl.name)) return null;
+ const list = decl.parent as ts.VariableDeclarationList;
+ if ((list.flags & ts.NodeFlags.Const) !== 0) return null;
+ const stmt = list.parent;
+ if (!ts.isVariableStatement(stmt) || !ts.isSourceFile(stmt.parent)) return null;
+ const cached = lowerer.cjsSplitRequireBindings.get(decl);
+ if (cached !== undefined) return cached;
+ const result = computeSplitRequireBinding(lowerer, decl);
+ lowerer.cjsSplitRequireBindings.set(decl, result);
+ return result;
+}
+
+function computeSplitRequireBinding(
+ lowerer: Lowerer,
+ decl: ts.VariableDeclaration,
+): SplitRequireBinding | null {
+ const sf = ts.getSourceFileOfNode(decl);
+ if (!isCjsJsFile(sf, lowerer.program)) return null;
+ if (!sf.text.includes("require")) return null;
+ const symbol = lowerer.checker.getSymbolAtLocation(decl.name);
+ if (symbol === undefined) return null;
+ // Merged declarations make the binding's initialization order ambiguous;
+ // keep ordinary storage instead of guessing.
+ let declarations = 0;
+ for (const other of symbol.declarations) {
+ if (!ts.isVariableDeclaration(other) || ++declarations > 1) return null;
+ }
+ if (declarations !== 1) return null;
+
+ // Find the one top-level `m = ...` statement first: most uninitialized
+ // variables never reach that shape, so they avoid the full-file write scan.
+ let assign: ts.BinaryExpression | null = null;
+ for (const stmt of sf.statements.nodes) {
+ if (!ts.isExpressionStatement(stmt)) continue;
+ const expr = stmt.expression;
+ if (
+ !ts.isBinaryExpression(expr) ||
+ expr.operatorToken.kind !== ts.SyntaxKind.EqualsToken ||
+ !ts.isIdentifier(expr.left) ||
+ expr.left.text !== decl.name.text ||
+ lowerer.checker.getSymbolAtLocation(expr.left) !== symbol
+ )
+ continue;
+ if (assign !== null) return null;
+ assign = expr;
+ }
+ if (assign === null) return null;
+
+ const req = inlineProgramRequireOf(lowerer, assign.right);
+ if (req === null) return null;
+ // Preflight treats the same bindings as require declarations (module edge,
+ // cycle admission, early-read fence); a binding it does not know keeps
+ // ordinary storage.
+ if (splitRequireCallOf(lowerer.program, decl) !== req.call) return null;
+
+ // Any other write makes the value depend on runtime control flow.
+ let writes = 0;
+ ts.walkPreorder(sf, (node) => {
+ if (!ts.isIdentifier(node) || node.text !== decl.name.text) return;
+ if (symbolOfIdentifierValue(lowerer, node) !== symbol) return;
+ if (writeTargetOf(node) === null) return;
+ if (++writes > 1) return "stop";
+ });
+ if (writes !== 1) return null;
+
+ return { decl, assign, call: req.call, spec: req.spec, dep: req.dep };
+}
+
+/** The split binding an identifier names, or null. */
+export function splitRequireBindingOfIdent(
+ lowerer: Lowerer,
+ ident: ts.Identifier,
+): SplitRequireBinding | null {
+ const symbol = symbolOfIdentifierValue(lowerer, ident);
+ return symbol === undefined ? null : splitBindingOfSymbol(lowerer, symbol);
+}
+
+/** The exact top-level assignment statement that initializes `expr`, or null. */
+export function splitRequireAssignmentOf(
+ lowerer: Lowerer,
+ expr: ts.Expression,
+): SplitRequireBinding | null {
+ if (
+ !ts.isBinaryExpression(expr) ||
+ expr.operatorToken.kind !== ts.SyntaxKind.EqualsToken ||
+ !ts.isIdentifier(expr.left)
+ )
+ return null;
+ const bindingOfIdent = splitRequireBindingOfIdent(lowerer, expr.left);
+ return bindingOfIdent !== null && bindingOfIdent.assign === expr ? bindingOfIdent : null;
+}
+
+/** The whole-export symbol a module's `module.exports = ` names,
+ * or null when the module is a table. */
+export function wholeExportSymbolOfModule(lowerer: Lowerer, dep: ts.SourceFile): ts.Symbol | null {
+ const cached = lowerer.cjsWholeExportSymbols.get(dep);
+ if (cached !== undefined) return cached;
+ if (cjsNamespaceModule(lowerer, dep)) {
+ lowerer.cjsWholeExportSymbols.set(dep, null);
+ return null;
+ }
+ const moduleSymbol = lowerer.checker.getSymbolAtLocation(dep);
+ let symbol = moduleSymbol?.getExports()?.get("export=" as ts.__String);
+ if (symbol === undefined) {
+ lowerer.cjsWholeExportSymbols.set(dep, null);
+ return null;
+ }
+ if (symbol.flags & ts.SymbolFlags.Alias) symbol = lowerer.checker.getAliasedSymbol(symbol);
+ lowerer.cjsWholeExportSymbols.set(dep, symbol);
+ return symbol;
+}
+
+function splitBindingOfSymbol(lowerer: Lowerer, symbol: ts.Symbol): SplitRequireBinding | null {
+ const decl = symbol.declarations.find(ts.isVariableDeclaration);
+ return decl === undefined ? null : splitRequireBindingOf(lowerer, decl);
+}
+
+/** The target whole-export symbol behind a split-binding symbol. */
+function splitWholeExportSymbolOfSymbol(lowerer: Lowerer, symbol: ts.Symbol): ts.Symbol | null {
+ const binding = splitBindingOfSymbol(lowerer, symbol);
+ return binding === null ? null : wholeExportSymbolOfModule(lowerer, binding.dep);
+}
+
+/** True when a CommonJS module has a live member export (`exports.x` or
+ * `module.exports.x`) after all `module.exports =` replacements. Those
+ * members need a mutable whole-export root. */
+export function cjsHasLiveMemberExports(lowerer: Lowerer, sf: ts.SourceFile): boolean {
+ const cached = lowerer.cjsLiveMemberExports.get(sf);
+ if (cached !== undefined) return cached;
+ const stmts = sf.statements.nodes;
+ let lastTable = -1;
+ for (let i = 0; i < stmts.length; i++) {
+ if (cjsExportAssignmentOf(stmts[i]!)?.kind === "table") lastTable = i;
+ }
+ let live = false;
+ // Only members attached after the last replacement survive in Node.
+ if (lastTable >= 0) {
+ for (let i = lastTable + 1; i < stmts.length; i++) {
+ const stmt = stmts[i]!;
+ if (cjsExportAssignmentOf(stmt)?.kind === "member" && cjsExportDiscardReason(stmt) === null) {
+ live = true;
+ break;
+ }
+ }
+ }
+ lowerer.cjsLiveMemberExports.set(sf, live);
+ return live;
+}
+
+/** The wrapper's export= symbol when a split binding is re-exported whole
+ * (`module.exports = m`) and that assignment owns storage for later member
+ * writes. Importers must read the wrapper's storage, not the split
+ * binding's underlying value. */
+function splitWholeExportStorageSymbolOf(lowerer: Lowerer, symbol: ts.Symbol): ts.Symbol | null {
+ const binding = splitBindingOfSymbol(lowerer, symbol);
+ if (binding === null) return null;
+ if (wholeExportSymbolOfModule(lowerer, binding.dep) === null) return null;
+ const sf = ts.getSourceFileOfNode(binding.decl);
+ if (!cjsHasLiveMemberExports(lowerer, sf)) return null;
+ const whole = cjsWholeExportValueOf(sf);
+ if (whole === null) return null;
+ let rhs: ts.Expression = whole.rhs;
+ while (ts.isParenthesizedExpression(rhs)) rhs = rhs.expression;
+ if (!ts.isIdentifier(rhs) || lowerer.checker.getSymbolAtLocation(rhs) !== symbol) return null;
+ const moduleSymbol = lowerer.checker.getSymbolAtLocation(sf);
+ let exportSymbol = moduleSymbol?.getExports()?.get("export=" as ts.__String);
+ if (exportSymbol === undefined) return null;
+ if (exportSymbol.flags & ts.SymbolFlags.Alias)
+ exportSymbol = lowerer.checker.getAliasedSymbol(exportSymbol);
+ return exportSymbol;
+}
+
+/** The source file whose `module.exports =` assignment declares `symbol`. */
+function moduleExportSourceFileOf(symbol: ts.Symbol): ts.SourceFile | null {
+ for (const decl of symbol.declarations) {
+ if (
+ ts.isBinaryExpression(decl) &&
+ isModuleExportsAccess(decl.left) &&
+ ts.isExpressionStatement(decl.parent) &&
+ ts.isSourceFile(decl.parent.parent)
+ )
+ return decl.getSourceFile();
+ }
+ return null;
+}
+
+/** One hop from a wrapper's export= symbol to the value it re-exports
+ * (`module.exports = require(...)` or `= `), or null. A
+ * wrapper with live members owns storage for them, so it is not followed. */
+function wholeReexportTargetOf(lowerer: Lowerer, symbol: ts.Symbol): ts.Symbol | null {
+ const sf = moduleExportSourceFileOf(symbol);
+ if (sf === null) return null;
+ // A wrapper with live members owns storage for them; importers must keep
+ // reading that root rather than the inner value.
+ if (cjsHasLiveMemberExports(lowerer, sf)) return null;
+ const whole = cjsWholeExportValueOf(sf);
+ if (whole === null) return null;
+ const inner = inlineProgramRequireOf(lowerer, whole.rhs);
+ if (inner !== null) return wholeExportSymbolOfModule(lowerer, inner.dep);
+ let rhs: ts.Expression = whole.rhs;
+ while (ts.isParenthesizedExpression(rhs)) rhs = rhs.expression;
+ if (!ts.isIdentifier(rhs)) return null;
+ const rhsSymbol = lowerer.checker.getSymbolAtLocation(rhs);
+ return rhsSymbol === undefined ? null : splitWholeExportSymbolOfSymbol(lowerer, rhsSymbol);
+}
+
+/** Follow split bindings and whole re-exports to the ultimate value symbol. */
+function followSplitWholeTarget(lowerer: Lowerer, candidate: ts.Symbol | null): ts.Symbol | null {
+ if (candidate === null) return null;
+ let symbol = candidate;
+ const seen = new Set();
+ for (let depth = 0; depth < 32 && !seen.has(symbol); depth++) {
+ seen.add(symbol);
+ const next =
+ splitWholeExportSymbolOfSymbol(lowerer, symbol) ?? wholeReexportTargetOf(lowerer, symbol);
+ if (next === null) break;
+ symbol = next;
+ }
+ return symbol;
+}
+
+/** The target whole-export symbol behind `module.exports = `,
+ * letting importers of that wrapper resolve to the real value symbol. */
+export function splitWholeExportAliasTargetOf(
+ lowerer: Lowerer,
+ symbol: ts.Symbol,
+): ts.Symbol | null {
+ const storage = splitWholeExportStorageSymbolOf(lowerer, symbol);
+ if (storage !== null) return followSplitWholeTarget(lowerer, storage);
+ const direct = followSplitWholeTarget(lowerer, splitWholeExportSymbolOfSymbol(lowerer, symbol));
+ if (direct !== null) return direct;
+ return followSplitWholeTarget(lowerer, wholeReexportTargetOf(lowerer, symbol));
+}
+
+/** The first read of a split binding that code before its assignment can
+ * reach, using the same reachability model as ordinary require bindings. */
+export function splitRequireEarlyReadOf(
+ lowerer: Lowerer,
+ binding: SplitRequireBinding,
+): ts.Identifier | null {
+ const sf = ts.getSourceFileOfNode(binding.decl);
+ const stmt = binding.assign.parent;
+ // `binding` was recognized only from a top-level `m = require(...)`
+ // statement, so the statement index is always present.
+ const index = sf.statements.nodes.indexOf(stmt as ts.ExpressionStatement);
+ return requireBindingEarlyUse7(lowerer.program, sf, index, binding.decl, []);
+}
+
/** A `require("")` call of the CommonJS global in a CommonJS JS
* file whose target is a compiled program module (relative, aliased, or an
* opted-in --npm-static package). Builtins, JSON documents and
@@ -108,13 +436,6 @@ export function inlineNamespaceRequireOf(
/** `value` evaluated after the required module's run-once %init (and its
* module-registry link) at the require's own position. */
export function withRequireInit(lowerer: Lowerer, req: InlineRequire, value: IrExpr): IrExpr {
- if (lowerer.inlineRequireCycleCalls.has(req.call)) {
- lowerer.unsupported(
- "SC1090",
- req.call,
- `require() of '${req.spec}' while it is still evaluating (a require cycle: Node answers its partial exports, which the static export table does not model)`,
- );
- }
// Every inline program require is a preflight module edge, so its module
// has an initializer; never read its exports without evaluating it.
if (!lowerer.initNameOf.has(req.dep)) {
diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts
index abf03811e..f219fd5e5 100644
--- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts
+++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts
@@ -717,6 +717,105 @@ export function classForInHasKey(
return { kind: "call", callee: name, args: [value, key], type: BOOL, loc };
}
+/** Box class instances at their exact class. `%dyn.class.normalize` turns a
+ * capsule of a class with subclasses into a capsule of the instance's own
+ * class before any member table reads it, so every such box was followed
+ * by a second capsule. Each conversion of a value whose static class has a
+ * normalization branch now selects the exact class at the conversion, with
+ * the same tests in the same order (%dyn.class.boxExact:C), and the
+ * normalization of the resulting capsule falls through. Only JavaScript
+ * class hierarchies, whose instances live in untyped code, take the exact
+ * box. */
+export function boxClassInstancesExactly(lowerer: Lowerer, functions: IrFunction[]): void {
+ const normalize = lowerer.liftedFnNamed("%dyn.class.normalize");
+ if (!normalize) return;
+ // Static class → its constructible descendants in normalization order.
+ const branches = new Map();
+ for (const stmt of normalize.body) {
+ if (stmt.kind !== "if" || stmt.cond.kind !== "libCall" || stmt.cond.fn !== "dyn.typedRefIs")
+ continue;
+ const key = stmt.cond.args[1];
+ if (key?.kind !== "strLit" || !key.value.startsWith("object:")) continue;
+ const children: string[] = [];
+ for (const inner of stmt.then) {
+ if (inner.kind !== "if" || inner.cond.kind !== "instanceOf") continue;
+ children.push(inner.cond.className);
+ }
+ // TypeScript hierarchies keep their static-class capsules: their boxes
+ // are rarer, and an exact box would emit each subclass's checked view.
+ const className = key.value.slice("object:".length);
+ const decl = lowerer.classes.get(className)?.decl;
+ if (!decl || !isJsSourceFile(ts.getSourceFileOfNode(decl))) continue;
+ if (children.length) branches.set(className, children);
+ }
+ if (branches.size === 0) return;
+ const loc = normalize.loc;
+ const helpers = new Map();
+ const helperFor = (className: string): string => {
+ const known = helpers.get(className);
+ if (known !== undefined) return known;
+ const name = `%dyn.class.boxExact:${className}`;
+ helpers.set(className, name);
+ const type: IrType = { kind: "object", className };
+ const value = varRef("value", type, loc);
+ functions.push({
+ name,
+ params: [{ localId: "value", name: "value", type }],
+ returnType: DYN,
+ locals: [{ id: "value", name: "value", type, mutable: false }],
+ body: [
+ ...branches.get(className)!.map((child): IrStmt => ({
+ kind: "if",
+ cond: { kind: "instanceOf", value, className: child, type: BOOL, loc },
+ then: [
+ {
+ kind: "return",
+ value: {
+ kind: "dynFrom",
+ value: {
+ kind: "downcast",
+ value,
+ type: { kind: "object", className: child },
+ loc,
+ },
+ type: DYN,
+ loc,
+ },
+ loc,
+ },
+ ],
+ else_: null,
+ loc,
+ })),
+ { kind: "return", value: { kind: "dynFrom", value, type: DYN, loc }, loc },
+ ],
+ loc,
+ });
+ return name;
+ };
+ const rewrite = (fn: IrFunction): void => {
+ fn.body = transformStmtList(fn.body, {
+ stmt: (statement) => statement,
+ expr: (expression) =>
+ expression.kind === "dynFrom" &&
+ expression.value.type.kind === "object" &&
+ branches.has(expression.value.type.className)
+ ? {
+ kind: "call",
+ callee: helperFor(expression.value.type.className),
+ args: [expression.value],
+ type: DYN,
+ loc: expression.loc,
+ }
+ : expression,
+ });
+ };
+ for (const fn of [...functions]) {
+ if (fn === normalize || fn.name.startsWith("%dyn.class.boxExact:")) continue;
+ rewrite(fn);
+ }
+}
+
/** Record field creation after the dispatch worklist closes, including writes
* in generated member tables. Native values stay in their slots; the bag
* owns only their property presence, attributes, and insertion position. */
@@ -1634,6 +1733,15 @@ export class ClassDynamicDispatch {
if (this.deletedNames === undefined)
this.deletedNames = deletedPropertyNames(lowerer.moduleOrder);
if (!root.base?.def.runtime && !fixedFieldFamily(root, this.deletedNames)) track(root);
+ // An Error family's bag records where `name` and `message` were
+ // created, which the error slots do not.
+ if (root.base?.def.name === "%Error") {
+ const order = (current: ClassInfo): void => {
+ current.def.errorBagOrder = true;
+ current.subclasses.forEach(order);
+ };
+ order(info);
+ }
}
const loc = this.propertyBag.loc;
const { receiver, condition } = this.propertyReceiver(plan, loc);
diff --git a/packages/compiler/src/frontend/lowering/class-method-mutations.ts b/packages/compiler/src/frontend/lowering/class-method-mutations.ts
index a07a4d3e1..0ec97ee69 100644
--- a/packages/compiler/src/frontend/lowering/class-method-mutations.ts
+++ b/packages/compiler/src/frontend/lowering/class-method-mutations.ts
@@ -46,6 +46,13 @@ export function collectClassMethodMutations(
const typedClasses: (ts.ClassDeclaration | ts.ClassExpression)[] = [];
const symbolMethods = new Set();
let unknownKey: ts.Node | undefined;
+ /** Every write with an unknown key: each may install a replacement of any
+ * method name, and the key-order witness asks what each one installs. */
+ const unknownKeySites: ts.Node[] = [];
+ const markUnknownKey = (site: ts.Node): void => {
+ unknownKey = site;
+ unknownKeySites.push(site);
+ };
let numericKey: ts.Node | undefined;
let unknownOwnField = false;
// `sym:iterator` for `Symbol.iterator`: the name a class method declared
@@ -83,7 +90,7 @@ export function collectClassMethodMutations(
for (const key of computedMethods)
for (const name of literalValues(lowerer.typeOf(key)) ?? []) methods.add(String(name));
const markName = (name: string, site: ts.Node, arrow = false): void => {
- if (name === "prototype" || name === "__proto__") unknownKey = site;
+ if (name === "prototype" || name === "__proto__") markUnknownKey(site);
if (methods.has(name)) lowerer.noteMethodAccess(name, site);
if (!arrow) receiverFields.add(name);
};
@@ -449,11 +456,12 @@ export function collectClassMethodMutations(
(lowerer.isStdlibGlobal(node, "Object") || lowerer.isStdlibGlobal(node, "Reflect")) &&
(!ts.isPropertyAccessExpression(node.parent) || node.parent.expression !== node)
)
- unknownKey = node;
+ markUnknownKey(node);
for (const node of reflection) {
const objectMethod = lowerer.stdlibGlobalMember(node, "Object");
const reflectMethod = lowerer.stdlibGlobalMember(node, "Reflect");
- if (objectMethod === "setPrototypeOf" || reflectMethod === "setPrototypeOf") unknownKey = node;
+ if (objectMethod === "setPrototypeOf" || reflectMethod === "setPrototypeOf")
+ markUnknownKey(node);
const single =
objectMethod === "defineProperty" ||
reflectMethod === "defineProperty" ||
@@ -463,7 +471,7 @@ export function collectClassMethodMutations(
if (!single && !multiple) continue;
const call = node.parent;
if (!ts.isCallExpression(call) || call.expression !== node) {
- unknownKey = node;
+ markUnknownKey(node);
continue;
}
const receiver = call.arguments.nodes[0];
@@ -618,7 +626,7 @@ export function collectClassMethodMutations(
// Numeric writes can replace numeric names but not ordinary names.
numericKey = site;
} else if (!(flags & ts.TypeFlags.ESSymbolLike)) {
- unknownKey = site;
+ markUnknownKey(site);
}
}
if (numericKey) {
@@ -627,7 +635,10 @@ export function collectClassMethodMutations(
}
}
if (unknownKey) {
- for (const method of methods) lowerer.noteMethodAccess(method, unknownKey);
+ for (const method of methods) {
+ lowerer.prototypeMethodAccesses.set(method, unknownKey);
+ for (const site of unknownKeySites) lowerer.noteMethodSite(method, site);
+ }
for (const method of symbolMethods) lowerer.replacedSymbolMethods.add(method);
}
if (!unknownKey && !unknownOwnField)
diff --git a/packages/compiler/src/frontend/lowering/containers/array-construction.ts b/packages/compiler/src/frontend/lowering/containers/array-construction.ts
index 1f10640c0..1ca51f526 100644
--- a/packages/compiler/src/frontend/lowering/containers/array-construction.ts
+++ b/packages/compiler/src/frontend/lowering/containers/array-construction.ts
@@ -1,4 +1,5 @@
import { keyedHelperName } from "../stable-names.js";
+import { indexedUnionArms, lowerIndexedUnionToArray } from "../indexed-unions.js";
import { generatorDrain } from "../iterator-adapters.js";
import { identityPreservingWidening } from "../coercions/identity.js";
import { liftedFreshSpread } from "../coercions/fresh-spread.js";
@@ -1054,6 +1055,10 @@ export function lowerArrayFromCall(
loc,
};
}
+ // A union of typed arrays and number arrays (indexed-unions.ts).
+ const indexedArms = indexedUnionArms(lowerer, src.type);
+ if (indexedArms && args.length === 1)
+ return lowerIndexedUnionToArray(lowerer, src, indexedArms, loc, true);
if (src.type.kind === "set" && args.length === 1) {
return {
kind: "setIntrinsic",
diff --git a/packages/compiler/src/frontend/lowering/containers/bytes.ts b/packages/compiler/src/frontend/lowering/containers/bytes.ts
index e56d9fc54..a8890c431 100644
--- a/packages/compiler/src/frontend/lowering/containers/bytes.ts
+++ b/packages/compiler/src/frontend/lowering/containers/bytes.ts
@@ -1,7 +1,9 @@
import { keyedHelperName } from "../stable-names.js";
+import { allBytesArms, indexedUnionArms, lowerIndexedUnionMethod } from "../indexed-unions.js";
import { boolLit, dynUndefinedExpr, nodeThrowExpr, numLit, varRef } from "../../../ir/build.js";
import { BUF_NUM_METHODS } from "./buffer-numeric-methods.js";
import * as ts from "../../ts7/adapter.js";
+import { constituentTypes } from "../../ts7/checker.js";
import {
BIGINT_T,
BOOL,
@@ -42,7 +44,7 @@ import {
lowerStaticallyUndefinedArgument,
} from "../optional-arguments.js";
import { lowerArraySpreadItems, lowerCheckedArrayFrom } from "./array-construction.js";
-import { lowerToNumberArgument } from "../lower-exprs.js";
+import { lowerOptionalNumber, lowerToNumberArgument } from "../lower-exprs.js";
import { lowerBytesIteratorValue } from "../iterator-adapters.js";
import { lowerArrayCallback } from "./callback-arguments.js";
import { callbackSite, findPredicateOk, predicateToBoolean } from "./array-callbacks.js";
@@ -1032,6 +1034,119 @@ export function lowerBytesPrototypeSetCall(
* variable-width read/writeUIntLE quartet — BUF_NUM_METHODS). Everything
* else the lib declares (fill, indexOf, reverse, ...) falls through to
* the SC2020 member fence. Null when this isn't a bytes method call. */
+/** The [needle, fromIndex] arguments of TypedArray indexOf, lastIndexOf
+ * and includes. A present fromIndex converts even when it is undefined
+ * (NaN, which ToIntegerOrInfinity makes 0); an absent one is 0, or
+ * +Infinity for lastIndexOf, which starts at the last element. */
+function elementSearchArgs(lowerer: Lowerer, call: ts.CallExpression, name: string): IrExpr[] {
+ const nodes = call.arguments.nodes;
+ const loc = locOf(call);
+ const needle = lowerer.lowerExprExpecting(nodes[0]!, F64);
+ const from: IrExpr = nodes[1]
+ ? lowerOptionalNumber(lowerer, lowerer.lowerExpr(nodes[1]), locOf(nodes[1]), nodes[1])
+ : ({
+ kind: "numLit",
+ value: name === "lastIndexOf" ? Infinity : 0,
+ type: F64,
+ loc,
+ } satisfies IrExpr);
+ // An undefined fromIndex is NaN once converted, which starts at 0.
+ if (from.kind === "unitLit") return [needle, { kind: "numLit", value: NaN, type: F64, loc }];
+ if (from.type.kind === "undefinedT" || from.type.kind === "void")
+ return [
+ needle,
+ {
+ kind: "seqExpr",
+ stmts: [{ kind: "exprStmt", expr: from, loc }],
+ result: { kind: "numLit", value: NaN, type: F64, loc },
+ type: F64,
+ loc,
+ },
+ ];
+ if (from.type.kind !== "f64") lowerer.noLowering(`.${name} with a non-number fromIndex`, call);
+ return [needle, from];
+}
+
+const ELEMENT_SEARCHES: Readonly> = {
+ indexOf: "indexOfElem",
+ lastIndexOf: "lastIndexOfElem",
+ includes: "includesElem",
+};
+
+/** Whether a union has a Buffer arm, under any alias name: each arm's own
+ * symbol names it. */
+function hasBufferArm(type: ts.Type): boolean {
+ const arms = type.flags & ts.TypeFlags.Union ? constituentTypes(type) : [type];
+ return arms.some((arm) => {
+ const symbol = arm.getSymbol();
+ return symbol !== undefined && ts.symbolName(symbol) === "Buffer";
+ });
+}
+
+/** The element-kind-generic typed-array methods on a union of typed
+ * arrays (indexed-unions.ts): the arguments lower as for one typed array,
+ * and each arm runs the method on its own receiver. Null for the methods
+ * that need a narrowing to one kind first. */
+function lowerUnionBytesMethodCall(
+ lowerer: Lowerer,
+ call: ts.CallExpression,
+ access: ts.PropertyAccessExpression,
+ name: string,
+): IrExpr | null {
+ if (isJsSourceFile(ts.getSourceFileOfNode(call))) return null;
+ // A Buffer arm's methods differ from a typed array's (slice aliases, fill
+ // checks its range, searches take strings and wrap numbers), so such a
+ // union narrows to its arms first.
+ if (hasBufferArm(lowerer.typeOf(access.expression))) return null;
+ const nodes = call.arguments.nodes;
+ if (nodes.some(ts.isSpreadElement)) return null;
+ const loc = locOf(call);
+ const num = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc });
+ const optional = (i: number, fallback: IrExpr, type: IrType = F64): IrExpr =>
+ nodes[i] ? lowerOptionalArgument(lowerer, nodes[i]!, type, fallback) : fallback;
+ let method: IrBytesIntrinsicMethod;
+ let args: () => IrExpr[];
+ let result: "receiver" | IrType;
+ if ((name === "slice" || name === "subarray") && nodes.length <= 2) {
+ method = name;
+ args = () => [optional(0, num(0)), optional(1, num(Infinity))];
+ result = "receiver";
+ } else if (name === "fill" && nodes.length <= 3) {
+ method = "fillElem";
+ args = () => [optional(0, num(0)), optional(1, num(0)), optional(2, num(Infinity))];
+ result = "receiver";
+ } else if (name === "copyWithin" && nodes.length <= 3) {
+ method = "copyWithin";
+ args = () => [optional(0, num(0)), optional(1, num(0)), optional(2, num(Infinity))];
+ result = "receiver";
+ } else if (name === "toReversed" && nodes.length === 0) {
+ method = "toReversed";
+ args = () => [];
+ result = "receiver";
+ } else if (Object.hasOwn(ELEMENT_SEARCHES, name) && (nodes.length === 1 || nodes.length === 2)) {
+ method = ELEMENT_SEARCHES[name]!;
+ args = () => elementSearchArgs(lowerer, call, name);
+ result = name === "includes" ? BOOL : F64;
+ } else if (name === "join" && nodes.length <= 1) {
+ method = "join";
+ args = () => [optional(0, { kind: "strLit", value: ",", type: STRING, loc }, STRING)];
+ result = STRING;
+ } else if (name === "set" && (nodes.length === 1 || nodes.length === 2)) {
+ const source = lowerer.mapTypeOf(lowerer.typeOf(nodes[0]!));
+ method = source?.kind === "bytes" ? "setFrom" : "setFromDyn";
+ args = () => {
+ const src = lowerer.lowerExpr(nodes[0]!);
+ const first = src.type.kind === "bytes" ? src : lowerer.coerceInto(nodes[0]!, src, DYN);
+ return nodes.length === 2 ? [first, optional(1, num(0))] : [first];
+ };
+ result = VOID;
+ } else return null;
+ const receiver = lowerer.lowerExpr(access.expression);
+ const arms = indexedUnionArms(lowerer, receiver.type);
+ if (!arms || !allBytesArms(arms)) return null;
+ return lowerIndexedUnionMethod(lowerer, receiver, arms, method, args(), result, loc);
+}
+
export function lowerBytesMethodCall(
lowerer: Lowerer,
call: ts.CallExpression,
@@ -1040,6 +1155,9 @@ export function lowerBytesMethodCall(
if (lowerer.chainBlocked(access, call)) return null;
const name = access.name.text;
const receiverIr = lowerer.mapTypeOf(lowerer.typeOf(access.expression));
+ const unionArms = receiverIr ? indexedUnionArms(lowerer, receiverIr) : null;
+ if (unionArms && allBytesArms(unionArms) && lowerer.isStdlibMember(access))
+ return lowerUnionBytesMethodCall(lowerer, call, access, name);
if (receiverIr?.kind !== "bytes") return null;
if (!lowerer.isStdlibMember(access)) return null;
const loc = locOf(call);
@@ -1086,6 +1204,29 @@ export function lowerBytesMethodCall(
if (value) return value;
}
}
+ // TypedArray indexOf / lastIndexOf / includes (the Buffer-declared forms,
+ // which also take string and Buffer needles, lowered above).
+ if (
+ Object.hasOwn(ELEMENT_SEARCHES, name) &&
+ !lowerer.checker
+ .getSymbolAtLocation(access.name)
+ ?.declarations.some(
+ (d) => ts.isInterfaceDeclaration(d.parent) && d.parent.name.text === "Buffer",
+ )
+ ) {
+ if (nArgs < 1 || nArgs > 2 || call.arguments.nodes.some(ts.isSpreadElement)) {
+ lowerer.noLowering(`.${name} with ${nArgs} arguments on typed arrays`, call);
+ }
+ const receiver = lowerer.lowerExprExpecting(access.expression, receiverIr);
+ return {
+ kind: "bytesIntrinsic",
+ method: ELEMENT_SEARCHES[name]!,
+ receiver,
+ args: elementSearchArgs(lowerer, call, name),
+ type: name === "includes" ? BOOL : F64,
+ loc,
+ };
+ }
if (name === "toReversed") {
if (nArgs !== 0) {
lowerer.noLowering(`.toReversed with ${nArgs} arguments on typed arrays`, call);
diff --git a/packages/compiler/src/frontend/lowering/expressions/array-literals.ts b/packages/compiler/src/frontend/lowering/expressions/array-literals.ts
index 6c7ce814e..c1b54a417 100644
--- a/packages/compiler/src/frontend/lowering/expressions/array-literals.ts
+++ b/packages/compiler/src/frontend/lowering/expressions/array-literals.ts
@@ -1,4 +1,5 @@
/* Array literals. */
+import { indexedUnionArms, lowerIndexedUnionToArray } from "../indexed-unions.js";
import * as ts from "../../ts7/adapter.js";
import { type Lowerer, neverTaintedJsType } from "../lowerer.js";
import { notIterableSpelling } from "../iterable-spelling.js";
@@ -504,6 +505,10 @@ export function lowerArrayLiteral(
loc: locOf(el),
};
}
+ // A union of typed arrays and number arrays (indexed-unions.ts):
+ // each arm's elements as a number[].
+ const indexedArms = indexedUnionArms(lowerer, src.type);
+ if (indexedArms) src = lowerIndexedUnionToArray(lowerer, src, indexedArms, locOf(el));
// `[...obj]` over a class whose [Symbol.iterator] is a generator
// method spreads the generator that method returns.
src = classGeneratorIterable(lowerer, src) ?? src;
diff --git a/packages/compiler/src/frontend/lowering/expressions/binary.ts b/packages/compiler/src/frontend/lowering/expressions/binary.ts
index b26a3dd9f..00a23caf5 100644
--- a/packages/compiler/src/frontend/lowering/expressions/binary.ts
+++ b/packages/compiler/src/frontend/lowering/expressions/binary.ts
@@ -243,37 +243,38 @@ function lowerLooseNullCompare(
// No unit arms: never null-ish (defensive — the fold below).
return { kind: "boolLit", value: negated, type: BOOL, loc };
}
- const isTag = (tag: number): IrExpr => ({
+ const isTag = (tag: number, value: IrExpr): IrExpr => ({
kind: "unionIsTag",
unionId: ut.unionId,
tag,
negated,
- value: other,
+ value,
type: BOOL,
loc,
});
- if (tags.length === 1) return isTag(tags[0]!);
+ if (tags.length === 1) return isTag(tags[0]!, other);
// Both null AND undefined arms: tag-in-set as a short-circuit pair
- // (De Morgan for `!=`). The operand IrExpr rides both tests, so the
- // backend emits it per test — restricted to re-emittable pure reads;
- // anything effectful gets its own actionable fence (the generic
- // SC1040 hint would wrongly suggest `== null` as the fix).
+ // (De Morgan for `!=`). The operand rides both tests, so an operand
+ // that is not a plain read (`m.get(k)! == null`) is evaluated once
+ // into a hidden local first.
+ let value = other;
+ let prefix: IrStmt[] = [];
if (!isSafeToRepeat(other)) {
- lowerer.unsupported(
- "SC1090",
- expr,
- `'== null' / '!= null' on a '${lowerer.fmt(ut)}' value that isn't a plain read ` +
- `(both unit arms need the operand twice — bind it to a const first)`,
- );
+ const tmp = lowerer.declareHiddenLocal("%nullish", ut);
+ value = varRef(tmp.id, ut, loc);
+ prefix = [{ kind: "varDecl", localId: tmp.id, init: other, loc }];
}
- return {
+ const test: IrExpr = {
kind: "logical",
op: negated ? "&&" : "||",
- left: isTag(tags[0]!),
- right: isTag(tags[1]!),
+ left: isTag(tags[0]!, value),
+ right: isTag(tags[1]!, value),
type: BOOL,
loc,
};
+ return prefix.length === 0
+ ? test
+ : { kind: "seqExpr", stmts: prefix, result: test, type: BOOL, loc };
}
if (other.type.kind === "void") {
return null; // no static tag to test — keep the fence
diff --git a/packages/compiler/src/frontend/lowering/expressions/element-access.ts b/packages/compiler/src/frontend/lowering/expressions/element-access.ts
index 0b6b41f3e..0f22a9622 100644
--- a/packages/compiler/src/frontend/lowering/expressions/element-access.ts
+++ b/packages/compiler/src/frontend/lowering/expressions/element-access.ts
@@ -71,6 +71,13 @@ import { isClassCallback } from "../class-callbacks.js";
import { isKeyedFieldAccess } from "./keyed-field-targets.js";
import { lowerFieldCompoundValue } from "./updates.js";
import { readsHeldEvolvingArray } from "../lower-stmts.js";
+import {
+ type IndexedArm,
+ indexedUnionArms,
+ lowerIndexedUnionNumberRead,
+ lowerIndexedUnionRead,
+ lowerIndexedUnionWrite,
+} from "../indexed-unions.js";
import { lowerBufferPrototypeMember } from "../containers/bytes.js";
/** `a[i]` reads. Only f64 indices into array receivers are modeled; JS
@@ -302,6 +309,31 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre
}
}
+ // An element read of a union of typed arrays and number arrays: each
+ // arm's own read, dispatched on the tag (indexed-unions.ts).
+ const indexedArms = receiverIr ? indexedUnionArms(lowerer, receiverIr) : null;
+ if (indexedArms && receiverIr && !isJsSourceFile(ts.getSourceFileOfNode(expr))) {
+ let recv = lowerer.lowerExpr(expr.expression);
+ if (recv.type.kind === "dyn") recv = lowerer.coerceInto(expr.expression, recv, receiverIr);
+ const arms = indexedUnionArms(lowerer, recv.type);
+ if (arms) {
+ const index = lowerOptionalNumber(
+ lowerer,
+ lowerer.lowerExpr(expr.argumentExpression),
+ locOf(expr.argumentExpression),
+ expr.argumentExpression,
+ );
+ if (index.type.kind !== "f64")
+ lowerer.unsupported("SC1090", expr.argumentExpression, "indexing with non-number keys");
+ if (
+ (ts.isNonNullExpression(expr.parent) && expr.parent.expression === expr) ||
+ lowerer.nanCodedReads?.isNumericRead(expr) === true
+ )
+ return lowerIndexedUnionNumberRead(lowerer, recv, arms, index, locOf(expr));
+ return lowerIndexedUnionRead(lowerer, recv, arms, index, locOf(expr));
+ }
+ }
+
// Typed-array element read `b[i]` — like arrayGet with a scalar
// result; any invalid index traps (the array discipline; JS would
// read undefined — divergence 4's policy).
@@ -1521,6 +1553,9 @@ export function lowerElementCompoundParts(
});
return { expr: { kind: "seqExpr", stmts, result: value, type: DYN, loc }, stmts: null };
}
+ const indexedArms = indexedUnionArms(lowerer, checked.type);
+ if (indexedArms && !isJsSourceFile(ts.getSourceFileOfNode(expr)))
+ return lowerIndexedUnionCompound(lowerer, expr, op, checked, indexedArms);
const receiverType =
checked.type.kind === "array" || checked.type.kind === "bytes"
? checked.type
@@ -1705,6 +1740,54 @@ export function lowerElementCompoundParts(
};
}
+/** `u[i] op= v` on a union of typed arrays and number arrays: the
+ * receiver and index evaluate once, the old element reads as a number (NaN
+ * for an invalid index, as in the typed-array form), and the result is
+ * stored through the union's element write. */
+function lowerIndexedUnionCompound(
+ lowerer: Lowerer,
+ expr: ts.BinaryExpression,
+ op: CompoundOp,
+ receiver: IrExpr,
+ arms: readonly IndexedArm[],
+): { expr: IrExpr | null; stmts: IrStmt[] | null } {
+ const target = expr.left as ts.ElementAccessExpression;
+ const loc = locOf(expr);
+ const index = lowerOptionalIndex(
+ lowerer,
+ lowerer.lowerExpr(target.argumentExpression),
+ 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);
+ const indexLocal = lowerer.declareHiddenLocal("%compoundIndex", F64);
+ const receiverRef = (): IrExpr => varRef(receiverLocal.id, receiver.type, loc);
+ const indexRef = (): IrExpr => varRef(indexLocal.id, F64, loc);
+ const oldValue = lowerIndexedUnionNumberRead(lowerer, receiverRef(), arms, indexRef(), loc);
+ const rhs = lowerOptionalNumber(lowerer, lowerer.lowerExpr(expr.right), loc);
+ if (rhs.type.kind !== "f64") compoundUnsupported(lowerer, expr, op, F64, rhs.type);
+ const computed: IrExpr = { kind: "bin", op, left: oldValue, right: rhs, type: F64, loc };
+ const resultLocal = lowerer.declareHiddenLocal("%compoundResult", F64);
+ const resultRef = (): IrExpr => varRef(resultLocal.id, F64, loc);
+ return {
+ expr: {
+ kind: "seqExpr",
+ stmts: [
+ { kind: "varDecl", localId: receiverLocal.id, init: receiver, loc },
+ { kind: "varDecl", localId: indexLocal.id, init: index, loc },
+ { kind: "varDecl", localId: resultLocal.id, init: computed, loc },
+ lowerIndexedUnionWrite(lowerer, receiverRef(), arms, indexRef(), resultRef(), loc),
+ ],
+ result: resultRef(),
+ type: F64,
+ loc,
+ },
+ stmts: null,
+ };
+}
+
/** ToString of one stabilized `string[]` element read for `+=`: a missing
* element concatenates as "undefined". */
function arrayStringOperand(arr: IrExpr, index: IrExpr, loc: SrcLoc): IrExpr {
@@ -2246,6 +2329,26 @@ export function lowerElementWrite(lowerer: Lowerer, expr: ts.BinaryExpression):
};
}
}
+ // An element write into a union of typed arrays and number arrays
+ // (indexed-unions.ts).
+ const indexedArms = receiverIr ? indexedUnionArms(lowerer, receiverIr) : null;
+ if (indexedArms && receiverIr && !isJsSourceFile(ts.getSourceFileOfNode(expr))) {
+ let recv = lowerer.lowerExpr(target.expression);
+ if (recv.type.kind === "dyn") recv = lowerer.coerceInto(target.expression, recv, receiverIr);
+ const arms = indexedUnionArms(lowerer, recv.type);
+ if (arms) {
+ const index = lowerOptionalNumber(
+ lowerer,
+ lowerer.lowerExpr(target.argumentExpression),
+ locOf(target.argumentExpression),
+ target.argumentExpression,
+ );
+ if (index.type.kind !== "f64")
+ lowerer.unsupported("SC1090", target.argumentExpression, "indexing with non-number keys");
+ const value = lowerer.lowerExprExpecting(expr.right, F64);
+ return lowerIndexedUnionWrite(lowerer, recv, arms, index, value, locOf(expr));
+ }
+ }
if (receiverIr?.kind !== "array") {
lowerer.unsupported("SC1090", target, "assignment to non-array elements");
}
diff --git a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts
index 41f915f15..f7ba7a115 100644
--- a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts
+++ b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts
@@ -25,6 +25,11 @@ import {
} from "../../../ir/ir.js";
import { lowerNodeModuleIdentifier } from "../lower-node-module.js";
import { lowerCjsExportsObjectValue } from "../cjs-exports-object.js";
+import {
+ splitRequireBindingOfIdent,
+ wholeExportSymbolOfModule,
+ wholeExportValue,
+} from "../cjs-inline-require.js";
import { captureContextArguments } from "../function-context.js";
import { argumentsOwner, typedArgumentsCandidate } from "../typed-arguments.js";
import {
@@ -979,6 +984,27 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc:
// Client`): the member holds that binding's value.
const memberAlias = cjsMemberAliasValueOf(lowerer, expr);
if (memberAlias !== null) return lowerer.lowerExpr(memberAlias);
+ // `var m; m = require("./x")` as a bare value: a whole-export target is
+ // the exported value; a table target has no first-class module object.
+ {
+ const split = splitRequireBindingOfIdent(lowerer, expr);
+ if (split !== null) {
+ if (wholeExportSymbolOfModule(lowerer, split.dep) !== null) {
+ const value = wholeExportValue(lowerer, split.dep, expr);
+ if (value !== null) return value;
+ lowerer.unsupported(
+ "SC1090",
+ expr,
+ `the whole export of '${split.spec}' as a value (its 'module.exports =' value has no static representation)`,
+ );
+ }
+ lowerer.unsupported(
+ "SC1090",
+ expr,
+ `the exports object of '${split.spec}' as a value (its exports compile to a static table: read require(${JSON.stringify(split.spec)}).member directly)`,
+ );
+ }
+ }
// Preflight guarantees no unresolved identifiers; anything else here
// is a blocked declaration's binding (the SC2004 cascade) or a
// binding form we don't model yet.
diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts
index 8148e85f5..1a4883e5a 100644
--- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts
+++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts
@@ -1,4 +1,5 @@
/* Property reads (`a.b`): lowerExprInner's property-access branch, intrinsic and union properties, field reads and targets, and match-group reads. */
+import { indexedUnionArms, lowerIndexedUnionMember } from "../indexed-unions.js";
import { keyedHelperName } from "../stable-names.js";
import { staticallySelected } from "../static-typeof.js";
import * as ts from "../../ts7/adapter.js";
@@ -3394,6 +3395,13 @@ export function lowerUnionProperty(
loc: locOf(expr),
};
}
+ // A union of typed arrays and number arrays: each arm's own length
+ // (and, for typed arrays only, byteLength, byteOffset, BYTES_PER_ELEMENT).
+ const indexedArms = indexedUnionArms(lowerer, value.type);
+ if (indexedArms) {
+ const member = lowerIndexedUnionMember(lowerer, value, indexedArms, field, locOf(expr));
+ if (member) return member;
+ }
lowerer.unsupported(
"SC1090",
expr,
diff --git a/packages/compiler/src/frontend/lowering/expressions/updates.ts b/packages/compiler/src/frontend/lowering/expressions/updates.ts
index 7cc77a2db..bdcd94a44 100644
--- a/packages/compiler/src/frontend/lowering/expressions/updates.ts
+++ b/packages/compiler/src/frontend/lowering/expressions/updates.ts
@@ -1,4 +1,9 @@
/* Increment, decrement and prefix unary operators, and compound field updates. */
+import {
+ indexedUnionArms,
+ lowerIndexedUnionNumberRead,
+ lowerIndexedUnionWrite,
+} from "../indexed-unions.js";
import * as ts from "../../ts7/adapter.js";
import {
lowerOptionalIndex,
@@ -390,6 +395,39 @@ export function lowerIncDec(
loc,
}));
}
+ // A union of typed arrays and number arrays (indexed-unions.ts): the
+ // element read as a number and the union's element write.
+ const indexedArms = indexedUnionArms(lowerer, raw.type);
+ if (
+ ts.isElementAccessExpression(access) &&
+ indexedArms &&
+ !isJsSourceFile(ts.getSourceFileOfNode(expr))
+ ) {
+ const receiver = save(raw, "%incrementArray");
+ const index = save(
+ lowerOptionalIndex(
+ lowerer,
+ lowerer.lowerExpr(access.argumentExpression),
+ locOf(access.argumentExpression),
+ access.argumentExpression,
+ ),
+ "%incrementIndex",
+ );
+ if (index.type.kind !== "f64")
+ lowerer.unsupported("SC1090", access, "incrementing non-number array keys");
+ return finish(
+ lowerIndexedUnionNumberRead(lowerer, receiver, indexedArms, index, loc),
+ (value) =>
+ lowerIndexedUnionWrite(
+ lowerer,
+ receiver,
+ indexedArms,
+ index,
+ lowerer.coerceInto(access, value, F64),
+ 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
// read checked into number like any other read of the member.
diff --git a/packages/compiler/src/frontend/lowering/function-returns.ts b/packages/compiler/src/frontend/lowering/function-returns.ts
index dde109ea7..6d0989e2b 100644
--- a/packages/compiler/src/frontend/lowering/function-returns.ts
+++ b/packages/compiler/src/frontend/lowering/function-returns.ts
@@ -17,6 +17,7 @@ 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";
+import { returnsGuessedOverloadValue } from "./overload-choice.js";
/** Keep the conservative completion rule shared by return construction
* and inference. Nested control flow may still make completion unreachable. */
@@ -256,6 +257,22 @@ export function declaredReturnType(
return decl.modifiers?.nodes.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword)
? { kind: "promise", inner: DYN }
: DYN;
+ // A JS function returning a value whose type rests on an undecided
+ // overload (guessedOverloadValue): the inferred result is the first
+ // overload's, which the returned value need not have.
+ if (
+ isJsSourceFile(ts.getSourceFileOfNode(decl)) &&
+ !decl.type &&
+ !decl.asteriskToken &&
+ !hasExplicitJsDocReturn(decl) &&
+ mappedReturn !== null &&
+ mappedReturn.kind !== "dyn" &&
+ !(mappedReturn.kind === "promise" && mappedReturn.inner.kind === "dyn") &&
+ returnsGuessedOverloadValue(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/indexed-unions.ts b/packages/compiler/src/frontend/lowering/indexed-unions.ts
new file mode 100644
index 000000000..774f3aef3
--- /dev/null
+++ b/packages/compiler/src/frontend/lowering/indexed-unions.ts
@@ -0,0 +1,569 @@
+import { keyedHelperName } from "./stable-names.js";
+import { helperLoc, varRef } from "../../ir/build.js";
+import {
+ BOOL,
+ BYTES_ELEMENT_SIZE,
+ F64,
+ VOID,
+ type IrBytesIntrinsicMethod,
+ type IrExpr,
+ type IrFunction,
+ type IrStmt,
+ type IrType,
+ type SrcLoc,
+ typeKey,
+} from "../../ir/ir.js";
+import { everyExprChild, everyStmtChild } from "../../ir/traverse.js";
+import { arrayNumberRead, lowerSafeBytesRead, lowerSafeIndexRead } from "./array-values.js";
+import type { Lowerer } from "./lowerer.js";
+
+/** Unions of number-indexed containers: typed arrays of any element kind
+ * (`Uint8Array | Uint16Array | Uint32Array`, as lru-cache picks by size)
+ * and `number[]` arms.
+ *
+ * The union keeps its ordinary tagged representation. Each operation on it
+ * lowers to a lifted helper per union and operation that tests the tag,
+ * narrows to the arm and runs the arm's own operation: the inline load or
+ * store of a typed array's element kind, or the dense read or write of a
+ * number array. The helpers are small, so LLVM inlines them, and a loop
+ * over one union value tests a loop-invariant tag. */
+
+/** One arm: its tag and its type (bytes, or an array of numbers). */
+export interface IndexedArm {
+ tag: number;
+ type: IrType & ({ kind: "bytes" } | { kind: "array" });
+}
+
+/** The arms of a union whose every arm is a typed array or a `number[]`,
+ * or null. Unions with any other arm (undefined included) are not
+ * indexed unions: their reads need a narrowing first. */
+export function indexedUnionArms(lowerer: Lowerer, type: IrType): IndexedArm[] | null {
+ if (type.kind !== "union") return null;
+ const arms = lowerer.unions.get(type.unionId)?.arms;
+ if (!arms || arms.length < 2) return null;
+ const result: IndexedArm[] = [];
+ for (const [tag, arm] of arms.entries()) {
+ if (arm.kind === "bytes") result.push({ tag, type: arm });
+ else if (arm.kind === "array" && arm.elem.kind === "f64") result.push({ tag, type: arm });
+ else return null;
+ }
+ return result;
+}
+
+/** Whether every arm is a typed array (so typed-array-only members such
+ * as `byteLength` apply). */
+export function allBytesArms(arms: readonly IndexedArm[]): boolean {
+ return arms.every((arm) => arm.type.kind === "bytes");
+}
+
+type ArmBody = (receiver: IrExpr, arm: IndexedArm, args: readonly IrExpr[]) => IrStmt[];
+
+/** The helper `name(u, ...args)`: for each arm but the last, a tag test
+ * that runs the arm's statements; the last arm's statements run without a
+ * test (the union holds one of its arms). */
+function dispatchHelper(
+ lowerer: Lowerer,
+ key: string,
+ prefix: string,
+ union: IrType,
+ arms: readonly IndexedArm[],
+ params: readonly { name: string; type: IrType }[],
+ returnType: IrType,
+ body: ArmBody,
+ loc: SrcLoc,
+): string {
+ const existing = lowerer.arrHofHelpers.get(key);
+ if (existing) return existing;
+ const name = keyedHelperName(lowerer, prefix, key);
+ lowerer.arrHofHelpers.set(key, name);
+ const at = helperLoc(loc);
+ if (union.kind !== "union") throw new Error("indexed-union helper over a non-union");
+ const u = varRef("u.0", union, at);
+ const args = params.map((p) => varRef(`${p.name}.0`, p.type, at));
+ const stmts: IrStmt[] = [];
+ arms.forEach((arm, i) => {
+ const receiver: IrExpr = {
+ kind: "unionNarrow",
+ unionId: union.unionId,
+ tag: arm.tag,
+ value: u,
+ type: arm.type,
+ loc: at,
+ };
+ const armStmts = body(receiver, arm, args);
+ if (i === arms.length - 1) {
+ stmts.push(...armStmts);
+ return;
+ }
+ stmts.push({
+ kind: "if",
+ cond: {
+ kind: "unionIsTag",
+ unionId: union.unionId,
+ tag: arm.tag,
+ negated: false,
+ value: u,
+ type: BOOL,
+ loc: at,
+ },
+ then: armStmts,
+ else_: null,
+ loc: at,
+ });
+ });
+ const fn: IrFunction = {
+ name,
+ params: [
+ { localId: "u.0", name: "u", type: union },
+ ...params.map((p) => ({ localId: `${p.name}.0`, name: p.name, type: p.type })),
+ ],
+ returnType,
+ locals: [
+ { id: "u.0", name: "u", type: union, mutable: false },
+ ...params.map((p) => ({ id: `${p.name}.0`, name: p.name, type: p.type, mutable: false })),
+ ],
+ body: stmts,
+ loc: at,
+ };
+ lowerer.liftedFns.push(fn);
+ return name;
+}
+
+const returning = (value: IrExpr, loc: SrcLoc): IrStmt[] => [{ kind: "return", value, loc }];
+
+/** `u[i]` read as an element or undefined (an invalid index), as the
+ * typed-array and number-array reads answer. */
+export function lowerIndexedUnionRead(
+ lowerer: Lowerer,
+ receiver: IrExpr,
+ arms: readonly IndexedArm[],
+ index: IrExpr,
+ loc: SrcLoc,
+): IrExpr {
+ const resultT = lowerer.withUndefinedArm(F64);
+ const key = `idxUnion:get:${unionKey(receiver.type)}`;
+ const name = dispatchHelper(
+ lowerer,
+ key,
+ "%union.idxOr",
+ receiver.type,
+ arms,
+ [{ name: "i", type: F64 }],
+ resultT,
+ (arm, info, [i]) =>
+ returning(
+ info.type.kind === "bytes"
+ ? lowerSafeBytesRead(lowerer, arm, i!, loc)
+ : lowerSafeIndexRead(lowerer, arm, i!, loc)!,
+ loc,
+ ),
+ loc,
+ );
+ const numeric = indexedUnionNumberHelper(lowerer, receiver.type, arms, loc);
+ lowerer.indexedUnionNumericTwins.set(name, numeric);
+ return { kind: "call", callee: name, args: [receiver, index], type: resultT, loc };
+}
+
+/** `u[i]` read as a number: an invalid index reads as NaN, which is
+ * ToNumber(undefined). */
+export function lowerIndexedUnionNumberRead(
+ lowerer: Lowerer,
+ receiver: IrExpr,
+ arms: readonly IndexedArm[],
+ index: IrExpr,
+ loc: SrcLoc,
+): IrExpr {
+ if (inlineDispatch(receiver, [index]))
+ return inlineArms(receiver, arms, F64, (arm, info) => numberRead(arm, info, index, loc), loc);
+ const name = indexedUnionNumberHelper(lowerer, receiver.type, arms, loc);
+ return { kind: "call", callee: name, args: [receiver, index], type: F64, loc };
+}
+
+function numberRead(arm: IrExpr, info: IndexedArm, index: IrExpr, loc: SrcLoc): IrExpr {
+ return info.type.kind === "bytes"
+ ? {
+ kind: "bytesIntrinsic",
+ method: "get",
+ receiver: arm,
+ args: [index],
+ type: F64,
+ loc,
+ invalidNaN: true,
+ }
+ : arrayNumberRead(arm, index, loc);
+}
+
+function indexedUnionNumberHelper(
+ lowerer: Lowerer,
+ union: IrType,
+ arms: readonly IndexedArm[],
+ loc: SrcLoc,
+): string {
+ return dispatchHelper(
+ lowerer,
+ `idxUnion:num:${unionKey(union)}`,
+ "%union.getNumber",
+ union,
+ arms,
+ [{ name: "i", type: F64 }],
+ F64,
+ (arm, info, [i]) => returning(numberRead(arm, info, i!, loc), loc),
+ loc,
+ );
+}
+
+/** The numeric twin of an indexed-union read helper call, or null. */
+export function indexedUnionNumericTwin(lowerer: Lowerer, call: IrExpr): IrExpr | null {
+ if (call.kind !== "call") return null;
+ const twin = lowerer.indexedUnionNumericTwins.get(call.callee);
+ return twin === undefined ? null : { ...call, callee: twin, type: F64 };
+}
+
+/** `u[i] = v` for a number `v`: each arm's element store (a typed array
+ * converts the number to its element kind and ignores an invalid index; a
+ * number array stores at the index). */
+export function lowerIndexedUnionWrite(
+ lowerer: Lowerer,
+ receiver: IrExpr,
+ arms: readonly IndexedArm[],
+ index: IrExpr,
+ value: IrExpr,
+ loc: SrcLoc,
+): IrStmt {
+ // In place, the value evaluates before the arms read the receiver and
+ // index: only when it cannot rebind either.
+ if (inlineDispatch(receiver, [index]) && !rebinds(value, [receiver, index])) {
+ const union = receiver.type;
+ const valueLocal = lowerer.declareHiddenLocal("%elementValue", F64);
+ const v: IrExpr = { kind: "varRef", localId: valueLocal.id, type: F64, loc };
+ let chain: IrStmt[] | null = null;
+ for (let i = arms.length - 1; i >= 0; i--) {
+ const info = arms[i]!;
+ const arm: IrExpr = {
+ kind: "unionNarrow",
+ unionId: union.kind === "union" ? union.unionId : "",
+ tag: info.tag,
+ value: receiver,
+ type: info.type,
+ loc,
+ };
+ const store: IrStmt =
+ info.type.kind === "bytes"
+ ? { kind: "bytesSet", arr: arm, index, value: v, loc }
+ : { kind: "arraySet", arr: arm, index, value: v, loc };
+ chain =
+ chain === null
+ ? [store]
+ : [
+ {
+ kind: "if",
+ cond: {
+ kind: "unionIsTag",
+ unionId: union.kind === "union" ? union.unionId : "",
+ tag: info.tag,
+ negated: false,
+ value: receiver,
+ type: BOOL,
+ loc,
+ },
+ then: [store],
+ else_: chain,
+ loc,
+ },
+ ];
+ }
+ return {
+ kind: "block",
+ body: [{ kind: "varDecl", localId: valueLocal.id, init: value, loc }, ...chain!],
+ loc,
+ };
+ }
+ const name = dispatchHelper(
+ lowerer,
+ `idxUnion:set:${unionKey(receiver.type)}`,
+ "%union.idxSet",
+ receiver.type,
+ arms,
+ [
+ { name: "i", type: F64 },
+ { name: "v", type: F64 },
+ ],
+ VOID,
+ (arm, info, [i, v]) => [
+ info.type.kind === "bytes"
+ ? { kind: "bytesSet", arr: arm, index: i!, value: v!, loc }
+ : { kind: "arraySet", arr: arm, index: i!, value: v!, loc },
+ { kind: "return", value: null, loc },
+ ],
+ loc,
+ );
+ return {
+ kind: "exprStmt",
+ expr: { kind: "call", callee: name, args: [receiver, index, value], type: VOID, loc },
+ loc,
+ };
+}
+
+/** The members every arm answers: `length` for all, and `byteLength`,
+ * `byteOffset` and `BYTES_PER_ELEMENT` when every arm is a typed array. */
+export function lowerIndexedUnionMember(
+ lowerer: Lowerer,
+ receiver: IrExpr,
+ arms: readonly IndexedArm[],
+ member: string,
+ loc: SrcLoc,
+): IrExpr | null {
+ const bytesOnly = member !== "length";
+ if (
+ member !== "length" &&
+ member !== "byteLength" &&
+ member !== "byteOffset" &&
+ member !== "BYTES_PER_ELEMENT"
+ )
+ return null;
+ if (bytesOnly && !allBytesArms(arms)) return null;
+ const memberOf = (arm: IrExpr, info: IndexedArm): IrExpr =>
+ info.type.kind === "array"
+ ? { kind: "arrIntrinsic", method: "length", receiver: arm, args: [], type: F64, loc }
+ : member === "BYTES_PER_ELEMENT"
+ ? { kind: "numLit", value: BYTES_ELEMENT_SIZE[info.type.elem], type: F64, loc }
+ : { kind: "bytesIntrinsic", method: member, receiver: arm, args: [], type: F64, loc };
+ if (inlineDispatch(receiver, [])) return inlineArms(receiver, arms, F64, memberOf, loc);
+ const name = dispatchHelper(
+ lowerer,
+ `idxUnion:${member}:${unionKey(receiver.type)}`,
+ `%union.${member}`,
+ receiver.type,
+ arms,
+ [],
+ F64,
+ (arm, info) => returning(memberOf(arm, info), loc),
+ loc,
+ );
+ return { kind: "call", callee: name, args: [receiver], type: F64, loc };
+}
+
+function unionKey(type: IrType): string {
+ return type.kind === "union" ? String(type.unionId) : "?";
+}
+
+/** A typed-array method on a union whose every arm is a typed array: the
+ * arguments evaluate once in the caller, and each arm runs the same
+ * `bytesIntrinsic` on its own receiver. `result` "receiver" is a method
+ * that answers a typed array of the receiver's kind (subarray, slice,
+ * fill, ...): each arm wraps its answer back into the union. */
+export function lowerIndexedUnionMethod(
+ lowerer: Lowerer,
+ receiver: IrExpr,
+ arms: readonly IndexedArm[],
+ method: IrBytesIntrinsicMethod,
+ args: readonly IrExpr[],
+ result: "receiver" | IrType,
+ loc: SrcLoc,
+): IrExpr {
+ const union = receiver.type;
+ if (union.kind !== "union") throw new Error("indexed-union method over a non-union");
+ const resultT = result === "receiver" ? union : result;
+ const params = args.map((arg, i) => ({ name: `a${i}`, type: arg.type }));
+ const signature = args.map((arg) => typeKey(arg.type)).join(",");
+ const name = dispatchHelper(
+ lowerer,
+ `idxUnion:${method}(${signature}):${unionKey(union)}`,
+ `%union.${method}`,
+ union,
+ arms,
+ params,
+ resultT,
+ (arm, info, refs) => {
+ const call: IrExpr = {
+ kind: "bytesIntrinsic",
+ method,
+ receiver: arm,
+ args: [...refs],
+ type: result === "receiver" ? info.type : result,
+ loc,
+ };
+ if (result === "receiver")
+ return returning(
+ {
+ kind: "unionWrap",
+ unionId: union.unionId,
+ tag: info.tag,
+ value: call,
+ type: union,
+ loc,
+ },
+ loc,
+ );
+ if (result.kind === "void")
+ return [
+ { kind: "exprStmt", expr: call, loc },
+ { kind: "return", value: null, loc },
+ ];
+ return returning(call, loc);
+ },
+ loc,
+ );
+ return { kind: "call", callee: name, args: [receiver, ...args], type: resultT, loc };
+}
+
+/** The elements of `u` as a `number[]`: a typed-array arm drains its
+ * elements into a fresh array. A number-array arm is the array itself for
+ * consumers that copy it (spreads), or with `fresh` a dense copy whose
+ * holes read as undefined (Array.from). */
+export function lowerIndexedUnionToArray(
+ lowerer: Lowerer,
+ receiver: IrExpr,
+ arms: readonly IndexedArm[],
+ loc: SrcLoc,
+ fresh = false,
+): IrExpr {
+ const arrayT: IrType = { kind: "array", elem: F64 };
+ const name = dispatchHelper(
+ lowerer,
+ `idxUnion:${fresh ? "from" : "toArray"}:${unionKey(receiver.type)}`,
+ fresh ? "%union.arrayFrom" : "%union.toArray",
+ receiver.type,
+ arms,
+ [],
+ arrayT,
+ (arm, info) =>
+ returning(
+ info.type.kind === "bytes"
+ ? {
+ kind: "bytesIntrinsic",
+ method: "toArray",
+ receiver: arm,
+ args: [],
+ type: arrayT,
+ loc,
+ }
+ : fresh
+ ? {
+ kind: "arrIntrinsic",
+ method: "reverse",
+ receiver: {
+ kind: "arrIntrinsic",
+ method: "toReversed",
+ receiver: arm,
+ args: [],
+ type: arrayT,
+ loc,
+ },
+ args: [],
+ type: arrayT,
+ loc,
+ }
+ : arm,
+ loc,
+ ),
+ loc,
+ );
+ return { kind: "call", callee: name, args: [receiver], type: arrayT, loc };
+}
+
+/** Whether a dispatch can run in place instead of through a helper call:
+ * the receiver is a plain read, so testing and narrowing it once per arm
+ * reads the same value, and the other operands are plain reads or
+ * literals. The arms' own reads then stay visible to the integer range
+ * analysis (a loop bounded by `u.length` keeps an integer counter). */
+function inlineDispatch(receiver: IrExpr, operands: readonly IrExpr[]): boolean {
+ return (
+ receiver.kind === "varRef" && operands.every((e) => e.kind === "varRef" || e.kind === "numLit")
+ );
+}
+
+/** Expressions that run no program code: reads, literals and operators
+ * over primitive values. */
+const CODE_FREE_EXPRS = new Set([
+ "numLit",
+ "boolLit",
+ "strLit",
+ "unitLit",
+ "varRef",
+ "unary",
+ "logical",
+ "ternary",
+ "toBool",
+ "strEq",
+ "strCmp",
+ "arrayGet",
+ "arrayHas",
+ "arrayState",
+ "bytesIntrinsic",
+ "fieldGet",
+ "recordGet",
+ "unionNarrow",
+ "unionIsTag",
+ "unionDisc",
+ "upcast",
+ "nullish",
+]);
+const PRIMITIVE_OPERANDS = new Set(["f64", "bool", "string", "bigint"]);
+
+/** Whether evaluating `e` can rebind one of the plain reads `reads`: by
+ * assigning it, or by running program code (a call, a getter, a
+ * conversion) that could assign a captured binding. */
+function rebinds(e: IrExpr, reads: readonly IrExpr[]): boolean {
+ const ids = new Set(reads.flatMap((r) => (r.kind === "varRef" ? [r.localId] : [])));
+ let found = false;
+ const expr = (x: IrExpr): boolean => {
+ if ((x.kind === "assignExpr" || x.kind === "incDec") && ids.has(x.localId)) found = true;
+ else if (x.kind === "bin") {
+ if (!PRIMITIVE_OPERANDS.has(x.left.type.kind) || !PRIMITIVE_OPERANDS.has(x.right.type.kind))
+ found = true;
+ } else if (x.kind === "call") {
+ // The union dispatch helpers only read and store elements.
+ if (!x.callee.startsWith("%union.")) found = true;
+ } else if (!CODE_FREE_EXPRS.has(x.kind)) found = true;
+ return !found && everyExprChild(x, expr, stmt);
+ };
+ const stmt = (x: IrStmt): boolean => {
+ if ((x.kind === "assign" || x.kind === "varDecl") && ids.has(x.localId)) found = true;
+ return !found && everyStmtChild(x, expr, stmt);
+ };
+ expr(e);
+ return found;
+}
+
+/** The in-place form of a dispatch: a ternary chain over the arms' tags,
+ * the last arm untested. */
+function inlineArms(
+ receiver: IrExpr,
+ arms: readonly IndexedArm[],
+ type: IrType,
+ arm: (narrowed: IrExpr, info: IndexedArm) => IrExpr,
+ loc: SrcLoc,
+): IrExpr {
+ const union = receiver.type;
+ if (union.kind !== "union") throw new Error("indexed-union dispatch over a non-union");
+ const narrowed = (info: IndexedArm): IrExpr => ({
+ kind: "unionNarrow",
+ unionId: union.unionId,
+ tag: info.tag,
+ value: receiver,
+ type: info.type,
+ loc,
+ });
+ let result = arm(narrowed(arms[arms.length - 1]!), arms[arms.length - 1]!);
+ for (let i = arms.length - 2; i >= 0; i--) {
+ const info = arms[i]!;
+ result = {
+ kind: "ternary",
+ cond: {
+ kind: "unionIsTag",
+ unionId: union.unionId,
+ tag: info.tag,
+ negated: false,
+ value: receiver,
+ type: BOOL,
+ loc,
+ },
+ then: arm(narrowed(info), info),
+ else_: result,
+ type,
+ loc,
+ };
+ }
+ return result;
+}
diff --git a/packages/compiler/src/frontend/lowering/key-order-witness.test.ts b/packages/compiler/src/frontend/lowering/key-order-witness.test.ts
index d68be96a8..21fe56815 100644
--- a/packages/compiler/src/frontend/lowering/key-order-witness.test.ts
+++ b/packages/compiler/src/frontend/lowering/key-order-witness.test.ts
@@ -29,7 +29,7 @@ interface Fixture {
notes: KeyOrderNotes;
record(fields: [string, IrType][], order: string[]): IrType & { kind: "record" };
literal(type: IrType, keys: string[], segments?: KeyOrderSegment[]): IrExpr;
- run(body: IrStmt[]): IrFunction;
+ run(body: IrStmt[], name?: string): IrFunction;
order(type: IrType & { kind: "record" }): string[] | undefined;
}
@@ -73,8 +73,8 @@ function fixture(): Fixture {
segments ?? keys.map((name) => ({ kind: "key", name, definite: true })),
);
};
- const run = (body: IrStmt[]): IrFunction => {
- const fn: IrFunction = { name: "main", params: [], returnType: F64, locals: [], body, loc };
+ const run = (body: IrStmt[], name = "main"): IrFunction => {
+ const fn: IrFunction = { name, params: [], returnType: F64, locals: [], body, loc };
applyKeyOrderWitnesses(lowerer, [fn]);
return fn;
};
@@ -706,7 +706,10 @@ test("a spread reads its source's order right after the literal evaluates the so
/** `{ a, b }` with a declared table `["a", "b"]` and a literal `{ b, a }`
* plus `{ ...value, a }` of the same shape, beside `extra`. */
-function selfCopy(extra: (f: Fixture, ab: IrType & { kind: "record" }) => IrStmt[]): string[] {
+function selfCopy(
+ extra: (f: Fixture, ab: IrType & { kind: "record" }) => IrStmt[],
+ fnName = "main",
+): string[] {
const f = fixture();
const ab = f.record(
[
@@ -716,11 +719,14 @@ function selfCopy(extra: (f: Fixture, ab: IrType & { kind: "record" }) => IrStmt
["a", "b"],
);
const self: KeyOrderSegment = { kind: "spread", shapeId: ab.shapeId, maybeAbsent: false };
- f.run([
- decl("v", f.literal(ab, ["b", "a"])),
- observe(f.literal(ab, ["a", "b"], [self, { kind: "key", name: "a", definite: true }])),
- ...extra(f, ab),
- ]);
+ f.run(
+ [
+ decl("v", f.literal(ab, ["b", "a"])),
+ observe(f.literal(ab, ["a", "b"], [self, { kind: "key", name: "a", definite: true }])),
+ ...extra(f, ab),
+ ],
+ fnName,
+ );
return f.order(ab)!;
}
@@ -762,6 +768,31 @@ test("a replaceable method's checked result builds records when a replacement ma
).toEqual(["a", "b"]);
});
+/** A native callback taking the shape, boxed for JavaScript. */
+const boxedCallback = (ab: IrType): IrExpr => ({
+ kind: "dynFrom",
+ value: {
+ kind: "closure",
+ fnName: "callback",
+ captures: [],
+ type: { kind: "func", params: [ab], ret: { kind: "void" } },
+ loc,
+ },
+ type: { kind: "dyn" },
+ loc,
+});
+
+test("a native callback boxed for JavaScript receives records JavaScript builds", () => {
+ expect(selfCopy((_, ab) => [observe(boxedCallback(ab))])).toEqual(["a", "b"]);
+});
+
+test("a class's prototype slots box its methods without building their parameters", () => {
+ expect(selfCopy((_, ab) => [observe(boxedCallback(ab))], "%prototype.data.Service")).toEqual([
+ "b",
+ "a",
+ ]);
+});
+
test("a replaceable method's result keeps its witness through an IR rewrite", () => {
expect(
selfCopy((f, ab) => {
diff --git a/packages/compiler/src/frontend/lowering/key-order-witness.ts b/packages/compiler/src/frontend/lowering/key-order-witness.ts
index 6e65d6f6d..a5e635e04 100644
--- a/packages/compiler/src/frontend/lowering/key-order-witness.ts
+++ b/packages/compiler/src/frontend/lowering/key-order-witness.ts
@@ -292,6 +292,17 @@ function replacesWithoutRecords(lowerer: Lowerer, site: ts.Node): boolean {
);
}
+/** A compiler helper that boxes a class's own state for JavaScript's reads:
+ * its prototype data and the property and method dispatch over boxed
+ * instances. */
+function boxesOwnState(fnName: string): boolean {
+ return (
+ fnName.startsWith("%prototype.data.") ||
+ fnName.startsWith("%dyn.class.") ||
+ fnName.startsWith("%class.")
+ );
+}
+
/** A type whose values are primitives (or void): it holds no record. */
function primitiveOnly(type: ts.Type): boolean {
if (type.isUnionType()) return ts.constituentTypes(type).every(primitiveOnly);
@@ -357,8 +368,18 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio
* its checked field setters, each a witness of its own. A checked
* function's parameters receive native values: a record parameter is
* passed out, never built, though a callback among them receives values
- * built from JavaScript again. */
- const shapesIn = (type: IrType, out: Set, seen: Set, produced = false): void => {
+ * built from JavaScript again. `built` false starts the walk at a value
+ * passed out (a native value boxed for JavaScript); `writes` false leaves
+ * out what JavaScript could write into the containers passed out, so the
+ * walk counts only what the functions among them receive. */
+ const shapesIn = (
+ type: IrType,
+ out: Set,
+ seen: Set,
+ produced = false,
+ built = true,
+ writes = true,
+ ): void => {
const visit = (t: IrType, built: boolean): boolean => {
const mark = built ? "" : "-";
// A container JavaScript builds converts into fresh native storage, so
@@ -367,7 +388,7 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio
// such as callbacks whose parameters it then builds (children passed
// out). Those children count in both directions.
const child = (type: IrType): boolean => {
- visit(type, true);
+ if (built || writes) visit(type, true);
if (!built) visit(type, false);
return true;
};
@@ -401,7 +422,7 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio
}
return everyTypeChild(t, child);
};
- visit(type, true);
+ visit(type, built);
};
const opaqueShapes = new Set();
const opaqueSeen = new Set();
@@ -412,6 +433,7 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio
* declaration's order, the order their table holds. */
const declaredShapes = new Set();
const declaredSeen = new Set();
+ const boxedSeen = new Set();
/** The checked result of a replaceable method call while every site that
* can replace the method installs functions that return no record. */
const unchangedMethodResult = (node: IrExpr): boolean => {
@@ -518,6 +540,16 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio
case "jsonStringify":
case "dynFrom":
observedRoots.push(e.value.type);
+ // A native value the program hands to JavaScript: the callbacks it
+ // holds (itself, a field, an element) receive records JavaScript
+ // builds. The helpers that box the program's own state for
+ // JavaScript's reads (a class's prototype slots and property
+ // dispatch) hand out the class's methods, whose arguments the
+ // program's own calls pass as native values, as a replaceable
+ // method's result above. Writes through the boxed value are left
+ // out: reading a boxed value creates no record.
+ if (!boxesOwnState(fnName))
+ shapesIn(e.value.type, runtimeShapes, boxedSeen, true, false, false);
return;
case "recordKeyGet":
observedRoots.push(e.obj.type);
diff --git a/packages/compiler/src/frontend/lowering/lower-assert.ts b/packages/compiler/src/frontend/lowering/lower-assert.ts
index bc51a9382..b996abed7 100644
--- a/packages/compiler/src/frontend/lowering/lower-assert.ts
+++ b/packages/compiler/src/frontend/lowering/lower-assert.ts
@@ -1355,7 +1355,7 @@ function lowerAssertMatch(
lowerer.noLowering(`${surface} with ${expr.arguments.nodes.length} arguments`, expr);
}
const s = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING);
- const re = lowerer.lowerExpr(expr.arguments.nodes[1]!);
+ const re = lowerExpectedValue(lowerer, expr.arguments.nodes[1]!);
if (re.type.kind !== "regex") {
lowerer.noLowering(
`${surface} with a non-regex pattern`,
@@ -1420,6 +1420,18 @@ function inErrorHierarchy(lowerer: Lowerer, className: string): boolean {
return false;
}
+/** An expected string or regex: a value JavaScript stores checked-dynamic
+ * (an object literal's field, an untyped binding) converts to the string
+ * or regex its checker type names, so the comparison takes that arm. */
+function lowerExpectedValue(lowerer: Lowerer, node: ts.Expression): IrExpr {
+ const value = lowerer.lowerExpr(node);
+ if (value.type.kind !== "dyn") return value;
+ const expected = lowerer.mapTypeOf(lowerer.typeOf(node));
+ return expected?.kind === "string" || expected?.kind === "regex"
+ ? lowerer.coerceToExpected(value, expected)
+ : value;
+}
+
/** Classify the expected argument: string → message, regex value → the
* regex test over String(error), an error-class identifier → instanceof,
* an object LITERAL over name/message/code (string or regex values) →
@@ -1436,7 +1448,7 @@ function classifyThrowsExpected(
return { form: "message", msg: m.msg, hasMsg: m.hasMsg };
}
if (expectedT?.kind === "regex") {
- return { form: "regex", value: lowerer.lowerExpr(node) };
+ return { form: "regex", value: lowerExpectedValue(lowerer, node) };
}
const sym = ts.isIdentifier(node) ? lowerer.resolveValueSymbol(node) : null;
const info =
@@ -1509,7 +1521,7 @@ function classifyThrowsExpected(
);
}
const valueNode = ts.isPropertyAssignment(named) ? named.initializer : named.name;
- const value = lowerer.lowerExpr(valueNode);
+ const value = lowerExpectedValue(lowerer, valueNode);
if (value.type.kind === "string") {
byId.set(id, { id, kind: "str", value, enameBake: null });
} else if (value.type.kind === "regex") {
diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts
index 8ac97fd32..073ebed91 100644
--- a/packages/compiler/src/frontend/lowering/lower-calls.ts
+++ b/packages/compiler/src/frontend/lowering/lower-calls.ts
@@ -260,6 +260,7 @@ import {
completeArgs,
} from "./call-arguments.js";
import { isThisParameter, type ParamShape } from "./call-signatures.js";
+import { dynamicOverloadChoice } from "./overload-choice.js";
import { lowerStringIteratorValue } from "./iterator-adapters.js";
import { checkedArmExtraction } from "./coercions/unions.js";
import { maybePromiseReturnStmt, promiseArmedUnionOf } from "./maybe-promise.js";
@@ -5506,6 +5507,9 @@ function reconcileOverloadReturn(
expr: ts.CallExpression | ts.TaggedTemplateExpression,
call: IrExpr,
): IrExpr {
+ // The checker's overload is a guess for a checked-dynamic argument
+ // another overload accepts: the implementation's value stands.
+ if (ts.isCallExpression(expr) && dynamicOverloadChoice(lowerer, expr)) return call;
const rsig = lowerer.checker.getResolvedSignature(expr);
const rdecl = rsig ? rsig.declaration : undefined;
// Nominal covariant overrides share the base's return ABI, but retain
diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts
index c1c5ef8c8..f2b601619 100644
--- a/packages/compiler/src/frontend/lowering/lower-exprs.ts
+++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts
@@ -72,7 +72,7 @@ import {
import { nsMemberIdentOf } from "./lower-namespaces.js";
import { bindingNeverReassigned } from "./binding-analysis.js";
import { implicitMonoFile } from "./generic-functions.js";
-import { lowerTaggedTemplate } from "./lower-calls.js";
+import { lowerPrimitiveUnionNumber, lowerTaggedTemplate } from "./lower-calls.js";
import { unitOnlyUnion } from "../type-mapper.js";
import { lowerYield } from "./lower-generators.js";
import { unionWideningTags } from "../../ir/analysis.js";
@@ -426,6 +426,12 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
return inner;
const use = runtimeOptionalUseOf(expr);
if (runtimeOptionalAssertionErases(lowerer, expr, inner, use)) return inner;
+ // A destination that holds null or undefined keeps the value, as `!`
+ // does in JavaScript (`return cache.get(key)!` from a function that
+ // returns `T | null`); the destination's own coercion re-tags it.
+ if (assertionDestinationHoldsNullish(lowerer, expr, inner.type)) return inner;
+ const operand = assertionOperand(lowerer, expr, inner, target ?? null, loc);
+ if (operand) return operand;
if (target && target.kind !== "union" && !typeEquals(target, inner.type)) {
const helper = lowerer.narrowedArmHelper(inner.type.unionId, target, loc);
if (helper) {
@@ -933,6 +939,210 @@ export function assertedElementKeepsUndefined(
);
}
+/** Whether `node!` is a returned value, an arrow function's body or a call
+ * argument whose destination type (the return or parameter type) is `any`
+ * or `unknown` (as for `String(x!)` and `Number(x!)`) or holds null or
+ * undefined that the union `inner` holds and the assertion would remove.
+ * Bindings and assignments are left out: the checker narrows them to the
+ * asserted type for later reads. */
+function assertionDestinationHoldsNullish(
+ lowerer: Lowerer,
+ node: ts.NonNullExpression,
+ inner: IrType & { kind: "union" },
+): boolean {
+ const holdsNull = lowerer.armTag(inner.unionId, NULL_T) >= 0;
+ const holdsUndefined = lowerer.armTag(inner.unionId, UNDEFINED_T) >= 0;
+ if (!holdsNull && !holdsUndefined) return false;
+ let outer: ts.Expression = node;
+ while (ts.isParenthesizedExpression(outer.parent)) outer = outer.parent;
+ const parent = outer.parent;
+ const destination =
+ (ts.isReturnStatement(parent) && parent.expression === outer) ||
+ (ts.isArrowFunction(parent) && parent.body === outer) ||
+ ((ts.isCallExpression(parent) || ts.isNewExpression(parent)) &&
+ parent.arguments?.nodes.includes(outer) === true);
+ if (!destination) return false;
+ const contextual = lowerer.checker.getContextualType(outer);
+ if (!contextual) return false;
+ const nullish =
+ ts.TypeFlags.Any |
+ ts.TypeFlags.Unknown |
+ (holdsNull ? ts.TypeFlags.Null : 0) |
+ (holdsUndefined && !optionalStdlibParameter(lowerer, parent, outer)
+ ? ts.TypeFlags.Undefined
+ : 0);
+ if ((contextual.flags & ts.TypeFlags.Union) === 0) return (contextual.flags & nullish) !== 0;
+ return constituentTypes(contextual).some((type) => (type.flags & nullish) !== 0);
+}
+
+/** Whether `argument` of `call` binds an optional parameter of a standard
+ * library declaration (`decoder.decode(input?)`). The undefined such a
+ * parameter's type holds comes from its optionality, and the builtin
+ * lowerings read the argument as the declared type, so `x!` keeps the
+ * checked narrowing there. */
+function optionalStdlibParameter(
+ lowerer: Lowerer,
+ call: ts.Node,
+ argument: ts.Expression,
+): boolean {
+ if (!ts.isCallExpression(call) && !ts.isNewExpression(call)) return false;
+ const declaration = lowerer.checker.getResolvedSignature(call)?.declaration;
+ if (declaration === undefined || !lowerer.isStdlibFile(ts.getSourceFileOfNode(declaration)))
+ return false;
+ const index = call.arguments?.nodes.indexOf(argument) ?? -1;
+ const parameter = declaration.parameters?.nodes[index];
+ return (
+ parameter !== undefined &&
+ parameter.dotDotDotToken === undefined &&
+ (parameter.questionToken !== undefined || parameter.initializer !== undefined)
+ );
+}
+
+/** `x!` as the operand of an operator that converts its operands: an
+ * arithmetic, bitwise, relational or equality operator, `+`, a compound
+ * assignment, a unary `-`, `+`, `~` or `!`, a template span, or an argument
+ * of a Math function. `!` performs no runtime check,
+ * so a value that still holds undefined or null reaches the operator, which
+ * converts it as JavaScript does: NaN or 0 in arithmetic, false in a
+ * comparison, "undefined" or "null" in a string. Returns the operand to
+ * use, or null where the assertion narrows as usual. A numeric use gets the
+ * value converted with ToNumber; other uses get the union itself. */
+function assertionOperand(
+ lowerer: Lowerer,
+ expr: ts.NonNullExpression,
+ inner: IrExpr,
+ target: IrType | null,
+ loc: SrcLoc,
+): IrExpr | null {
+ if (inner.type.kind !== "union" || target === null || target.kind === "union") return null;
+ const arms = lowerer.unions.get(inner.type.unionId)?.arms;
+ if (
+ !arms?.some(isUnitType) ||
+ !arms.every(
+ (arm) =>
+ arm.kind === "f64" || arm.kind === "string" || arm.kind === "bool" || isUnitType(arm),
+ )
+ )
+ return null;
+ // Array and typed-array element reads keep their own handling of a
+ // missing element.
+ let operand: ts.Expression = expr.expression;
+ while (ts.isParenthesizedExpression(operand)) operand = operand.expression;
+ if (ts.isElementAccessExpression(operand)) {
+ const kind = lowerer.mapTypeOf(lowerer.typeOf(operand.expression))?.kind;
+ if (kind === "array" || kind === "bytes") return null;
+ }
+ const use = convertingUseOf(lowerer, expr);
+ if (use === null) return null;
+ if (use === "value") return inner;
+ return lowerPrimitiveUnionNumber(lowerer, inner, expr, loc);
+}
+
+/** How the converting use of `x!` (see assertionOperand) reads it:
+ * "number" where the use applies ToNumber, "value" where the consumer
+ * lowers the union itself (strings, equality, truthiness), null for other
+ * uses. */
+function convertingUseOf(lowerer: Lowerer, expr: ts.NonNullExpression): "number" | "value" | null {
+ let outer: ts.Expression = expr;
+ while (ts.isParenthesizedExpression(outer.parent)) outer = outer.parent;
+ const parent = outer.parent;
+ if (ts.isTemplateSpan(parent)) return parent.expression === outer ? "value" : null;
+ if (ts.isPrefixUnaryExpression(parent)) {
+ if (parent.operator === ts.SyntaxKind.ExclamationToken) return "value";
+ return parent.operator === ts.SyntaxKind.MinusToken ||
+ parent.operator === ts.SyntaxKind.PlusToken ||
+ parent.operator === ts.SyntaxKind.TildeToken
+ ? "number"
+ : null;
+ }
+ if (ts.isCallExpression(parent) && parent.arguments.nodes.includes(outer)) {
+ const callee = parent.expression;
+ return ts.isPropertyAccessExpression(callee) &&
+ stdlibGlobalNameOf(lowerer, callee.expression) === "Math"
+ ? "number"
+ : null;
+ }
+ if (!ts.isBinaryExpression(parent)) return null;
+ const op = parent.operatorToken.kind;
+ if (EQUALITY_OPERATORS.has(op)) return "value";
+ const other = parent.left === outer ? parent.right : parent.left;
+ const isString = (node: ts.Expression): boolean =>
+ (lowerer.typeOf(node).flags & ts.TypeFlags.StringLike) !== 0;
+ // With a string on either side, `+` concatenates and a comparison
+ // compares strings. `undefined + 1` and `undefined + undefined` are NaN,
+ // which a string-typed result cannot hold, so `+` takes the union only
+ // with a string partner that is not itself asserted. A partner that may
+ // hold a string or a number at run time (`any`, `unknown`) also gets the
+ // union, and `+` decides between concatenation and addition when it runs.
+ if (op === ts.SyntaxKind.PlusToken || op === ts.SyntaxKind.PlusEqualsToken) {
+ if (op === ts.SyntaxKind.PlusEqualsToken && parent.right !== outer) return null;
+ let partner = other;
+ while (ts.isParenthesizedExpression(partner)) partner = partner.expression;
+ if (isString(other)) return ts.isNonNullExpression(partner) ? null : "value";
+ if (!numericPartner(lowerer.typeOf(other))) return "value";
+ return isString(expr) ? null : "number";
+ }
+ if (RELATIONAL_OPERATORS.has(op)) return isString(other) || isString(expr) ? "value" : "number";
+ if (NUMERIC_OPERATORS.has(op)) return "number";
+ return NUMERIC_COMPOUNDS.has(op) && parent.right === outer ? "number" : null;
+}
+
+/** A `+` partner whose every value is a number, a boolean, null or
+ * undefined: `+` adds with it. */
+function numericPartner(type: ts.Type): boolean {
+ const numeric =
+ ts.TypeFlags.NumberLike |
+ ts.TypeFlags.BooleanLike |
+ ts.TypeFlags.Null |
+ ts.TypeFlags.Undefined |
+ ts.TypeFlags.Void;
+ return (type.flags & ts.TypeFlags.Union) !== 0
+ ? constituentTypes(type).every((part) => (part.flags & numeric) !== 0)
+ : (type.flags & numeric) !== 0;
+}
+
+const EQUALITY_OPERATORS = new Set([
+ ts.SyntaxKind.EqualsEqualsEqualsToken,
+ ts.SyntaxKind.ExclamationEqualsEqualsToken,
+ ts.SyntaxKind.EqualsEqualsToken,
+ ts.SyntaxKind.ExclamationEqualsToken,
+]);
+
+const RELATIONAL_OPERATORS = new Set([
+ ts.SyntaxKind.LessThanToken,
+ ts.SyntaxKind.GreaterThanToken,
+ ts.SyntaxKind.LessThanEqualsToken,
+ ts.SyntaxKind.GreaterThanEqualsToken,
+]);
+
+const NUMERIC_OPERATORS = new Set([
+ ts.SyntaxKind.MinusToken,
+ ts.SyntaxKind.AsteriskToken,
+ ts.SyntaxKind.SlashToken,
+ ts.SyntaxKind.PercentToken,
+ ts.SyntaxKind.AsteriskAsteriskToken,
+ ts.SyntaxKind.AmpersandToken,
+ ts.SyntaxKind.BarToken,
+ ts.SyntaxKind.CaretToken,
+ ts.SyntaxKind.LessThanLessThanToken,
+ ts.SyntaxKind.GreaterThanGreaterThanToken,
+ ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken,
+]);
+
+const NUMERIC_COMPOUNDS = new Set([
+ ts.SyntaxKind.MinusEqualsToken,
+ ts.SyntaxKind.AsteriskEqualsToken,
+ ts.SyntaxKind.SlashEqualsToken,
+ ts.SyntaxKind.PercentEqualsToken,
+ ts.SyntaxKind.AsteriskAsteriskEqualsToken,
+ ts.SyntaxKind.AmpersandEqualsToken,
+ ts.SyntaxKind.BarEqualsToken,
+ ts.SyntaxKind.CaretEqualsToken,
+ ts.SyntaxKind.LessThanLessThanEqualsToken,
+ ts.SyntaxKind.GreaterThanGreaterThanEqualsToken,
+ ts.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken,
+]);
+
export function runtimeOptionalAssertionErases(
lowerer: Lowerer,
expr: ts.Expression,
diff --git a/packages/compiler/src/frontend/lowering/lower-inspect.ts b/packages/compiler/src/frontend/lowering/lower-inspect.ts
index 2b1b002a7..84f807978 100644
--- a/packages/compiler/src/frontend/lowering/lower-inspect.ts
+++ b/packages/compiler/src/frontend/lowering/lower-inspect.ts
@@ -736,11 +736,7 @@ function inspectSupport(
case "array":
return inspectSupport(lowerer, t.elem, visiting, out);
case "bytes":
- // A Uint8Array and a Buffer share the u8 representation and print
- // differently; the other element kinds are never Buffers.
- return t.elem === "u8"
- ? "Uint8Array and Buffer values inside composites have no inspect lowering yet (Buffer's form vs Uint8Array's is not recorded in the static type here)"
- : null;
+ return null;
case "record": {
if (visiting.has(t.shapeId)) {
out.recursive = true;
@@ -1061,7 +1057,15 @@ function inspectExprBody(
loc,
};
}
- return { kind: "libCall", fn: "insp.buffer", args: [value], type: STRING, loc };
+ // A Uint8Array, a Buffer and a DataView share the u8 representation
+ // and print differently: the view's own flags pick.
+ return {
+ kind: "libCall",
+ fn: "insp.bytes",
+ args: [value, recurse, depth],
+ type: STRING,
+ loc,
+ };
case "dyn":
return { kind: "libCall", fn: "insp.dyn", args: [value, recurse, depth], type: STRING, loc };
case "generator":
@@ -2292,15 +2296,17 @@ export function lowerConsoleInspectArg(
);
}
if (!isBufferTypeName(tname)) {
+ // A u8 value the checker types as a Uint8Array may still hold a
+ // Buffer, which Node prints as one: the view's flags pick.
return {
kind: "libCall",
- fn: "insp.typedArray",
+ fn: t.elem === "u8" ? "insp.bytes" : "insp.typedArray",
args: [value, numLit(0, loc), numLit(2, loc)],
type: STRING,
loc,
};
}
- return inspectExpr(lowerer, t, value, numLit(0, loc), numLit(2, loc), loc);
+ return { kind: "libCall", fn: "insp.buffer", args: [value], type: STRING, loc };
}
if (t.kind === "void") {
// Node prints "undefined" for a void call's result; a void expression
@@ -2653,7 +2659,10 @@ export function lowerInspectValue(
"typedArray",
(input) => ({
kind: "libCall",
- fn: "insp.typedArray",
+ fn:
+ value.type.kind === "bytes" && value.type.elem === "u8"
+ ? "insp.bytes"
+ : "insp.typedArray",
args: [input, numLit(0, loc), numLit(options.depth, loc)],
type: STRING,
loc,
@@ -2662,7 +2671,7 @@ export function lowerInspectValue(
);
}
// Buffers render fully at any depth and on one line (custom inspect).
- return inspectExpr(lowerer, value.type, value, numLit(0, loc), numLit(2, loc), loc);
+ return { kind: "libCall", fn: "insp.buffer", args: [value], type: STRING, loc };
}
const walk = { recursive: false };
const reason = inspectSupport(lowerer, value.type, new Set(), walk);
diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts
index 5238333ad..75f18fe37 100644
--- a/packages/compiler/src/frontend/lowering/lower-modules.ts
+++ b/packages/compiler/src/frontend/lowering/lower-modules.ts
@@ -43,10 +43,14 @@ import {
} from "./cycle-initialization.js";
import { markRequireEarlyBindings } from "./require-initialization.js";
import {
+ cjsHasLiveMemberExports,
cjsTableMethodEntry,
inlineProgramRequireOf,
nestedCjsExportAssignmentsOf,
requireNestedPatternsOf,
+ splitRequireBindingOf,
+ splitRequireBindingOfIdent,
+ wholeExportSymbolOfModule,
} from "./cjs-inline-require.js";
import { isDeadTscHelperDecl, tscInteropBindingOfDecl } from "./cjs-tsc-interop.js";
import { collectJsonRequires, isJsonRequireDecl } from "./json-require.js";
@@ -1597,14 +1601,26 @@ export function collectGlobals(
// require() of a program module is not: the exporter re-exports
// that module's exports, which requirers resolve through the
// checker's alias (the statement lowers to the dep's %init).
+ // A split binding re-exported whole with later member writes needs
+ // a mutable root: `module.exports = m; module.exports.extra = 1`
+ // extends the SAME object Node exposes. Without later members the
+ // binder is pure alias plumbing (no storage).
+ const splitWholeBinding = ts.isIdentifier(rhs)
+ ? splitRequireBindingOfIdent(lowerer, rhs)
+ : null;
+ const hasMemberExports = cjsHasLiveMemberExports(lowerer, sf);
+ const splitWhole =
+ splitWholeBinding !== null &&
+ wholeExportSymbolOfModule(lowerer, splitWholeBinding.dep) !== null &&
+ hasMemberExports;
const factoryValued =
- !ts.isIdentifier(rhs) &&
+ (!ts.isIdentifier(rhs) || splitWhole) &&
!ts.isClassExpression(rhs) &&
!ts.isObjectLiteralExpression(rhs) &&
!(
requireSpecOf(rhs) !== null &&
inlineProgramRequireOf(lowerer, rhs) !== null &&
- !sf.statements.nodes.some((s) => cjsExportAssignmentOf(s)?.kind === "member")
+ !hasMemberExports
) &&
cjsExportTargetLiteral(rhs, sf) === null;
if (cjsScalarLiteral(rhs) || fnValued || factoryValued) {
@@ -1617,6 +1633,9 @@ export function collectGlobals(
// A returned object remains the live CommonJS root; later
// exports.member writes must update that same open object.
if (factoryValued && t?.kind === "record" && lowerer.dynConvertible(t)) t = DYN;
+ // A split whole-export root is extended by later member writes;
+ // keep it checked-dynamic so those writes reach the same object.
+ if (splitWhole && t && t.kind !== "dyn") t = DYN;
if (t && t.kind !== "void") {
const g: IrGlobal = {
id: `%g.${tag}exports`,
@@ -2361,7 +2380,6 @@ export function collectGlobals(
// References from separately-declared functions cascade to
// their own per-site runtime fences.
const factoryType =
- !isLet &&
isJsSourceFile(sf) &&
!decl.type &&
!hasJsTypeAnnotation(decl) &&
@@ -2708,6 +2726,7 @@ function noteVarGlobalEntryInit(lowerer: Lowerer, sf: ts.SourceFile, g: IrGlobal
* plumbing (lowerVarDeclarator): no storage, the reads resolve through to
* the module. JSON documents and native FFI requires keep their storage. */
function requireAliasDecl(lowerer: Lowerer, decl: ts.VariableDeclaration): boolean {
+ if (splitRequireBindingOf(lowerer, decl) !== null) return true;
return (
decl.initializer !== undefined &&
requireSpecOf(decl.initializer) !== null &&
diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts
index 82c0dbb3e..c50676863 100644
--- a/packages/compiler/src/frontend/lowering/lower-stmts.ts
+++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts
@@ -1,4 +1,11 @@
import { lowerTupleAliasDecl, stripTypeWrappers } from "./typed-arguments.js";
+import {
+ allBytesArms,
+ indexedUnionArms,
+ lowerIndexedUnionMember,
+ lowerIndexedUnionNumberRead,
+ lowerIndexedUnionRead,
+} from "./indexed-unions.js";
import { declaredMemberOf, isPromisePrototypeRead } from "./expressions/property-access.js";
import { hostsAll } from "./expressions/hosted-narrowing.js";
import { requestGeneralStreamSurface } from "./general-streams.js";
@@ -39,6 +46,11 @@ import {
cjsTableMethodEntry,
inlineProgramRequireOf,
requireNestedPatternsOf,
+ splitRequireAssignmentOf,
+ splitRequireBindingOf,
+ splitRequireBindingOfIdent,
+ splitRequireEarlyReadOf,
+ wholeExportSymbolOfModule,
} from "./cjs-inline-require.js";
import {
cjsExportMemberWriteGlobalOf,
@@ -1934,6 +1946,23 @@ function lowerVarDeclarator(
isConst: boolean,
isLet: boolean,
): IrStmt[] {
+ // `var m; m = require("./x")`: the declaration is alias plumbing; the
+ // assignment owns the module's run-once %init (cjs-inline-require.ts).
+ // A read that code before the assignment can reach keeps Node's hoisted
+ // `undefined` divergence visible at build time, like a var require
+ // declaration below runnable code.
+ const split = splitRequireBindingOf(lowerer, decl);
+ if (split !== null) {
+ const early = splitRequireEarlyReadOf(lowerer, split);
+ if (early !== null) {
+ lowerer.unsupported(
+ "SC1013",
+ early,
+ `var require() binding '${(decl.name as ts.Identifier).text}' below code that can reach it (Node reads undefined before the require runs, which is not modeled — assign the require above the code that can run first)`,
+ );
+ }
+ return [];
+ }
// The canonical ESM bridge to a compiled program module. Like a
// top-level CommonJS require declaration, the binding is alias
// plumbing while the dependency evaluates at this statement's
@@ -7537,6 +7566,26 @@ function lowerCjsSingleValueExport(
return lowerer.requireInitStmt(req.spec, req.call) ?? { kind: "block", body: [], loc };
}
if (ts.isIdentifier(rhs)) {
+ const split = splitRequireBindingOfIdent(lowerer, rhs);
+ if (split !== null) {
+ if (wholeExportSymbolOfModule(lowerer, split.dep) === null) {
+ lowerer.unsupported(
+ "SC1090",
+ rhs,
+ `the exports object of '${split.spec}' as a value (its exports compile to a static table: read require(${JSON.stringify(split.spec)}).member directly)`,
+ );
+ }
+ const root = lowerer.globalsByDeclNode.get(expr);
+ if (root !== undefined) {
+ return {
+ kind: "assign",
+ localId: root.id,
+ value: lowerer.lowerExprExpecting(rhs, root.type),
+ loc: locOf(rhs),
+ };
+ }
+ return { kind: "block", body: [], loc };
+ }
const sym = lowerer.resolveValueSymbol(rhs);
if (!sym) return null;
if (
@@ -8447,6 +8496,16 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm
if (!ts.isIdentifier(expr.left)) {
lowerer.unsupported("SC1090", expr.left, "assignment to non-variables");
}
+ const splitAssign = splitRequireAssignmentOf(lowerer, expr);
+ if (splitAssign !== null) {
+ return (
+ lowerer.requireInitStmt(splitAssign.spec, splitAssign.call) ?? {
+ kind: "block",
+ body: [],
+ loc: locOf(expr),
+ }
+ );
+ }
const target = lowerer.resolveWritable(expr.left);
if (!target)
lowerer.rejectUnresolved(
@@ -10161,13 +10220,8 @@ export function lowerForOf(lowerer: Lowerer, stmt: ts.ForOfStatement): IrStmt {
loc: locOf(iterSrc),
};
}
- if (iterable.type.kind === "bytes") {
- return lowerForOfBytes(
- lowerer,
- stmt,
- iterable as IrExpr & { type: IrType & { kind: "bytes" } },
- labels,
- );
+ if (iterable.type.kind === "bytes" || indexedUnionArms(lowerer, iterable.type)) {
+ return lowerForOfBytes(lowerer, stmt, iterable, labels);
}
if (iterable.type.kind !== "array") {
// The lib types strings as iterable too, so those for-ofs typecheck;
@@ -10725,7 +10779,7 @@ export function lowerForOf(lowerer: Lowerer, stmt: ts.ForOfStatement): IrStmt {
function lowerForOfBytes(
lowerer: Lowerer,
stmt: ts.ForOfStatement,
- iterable: IrExpr & { type: IrType & { kind: "bytes" } },
+ iterable: IrExpr,
labels?: string[],
): IrStmt {
const loc = locOf(stmt);
@@ -10819,7 +10873,16 @@ function lowerForOfStoredNumericIterator(
const loc = locOf(stmt);
const sourceT = state.sourceType;
- if (!((sourceT.kind === "array" && sourceT.elem.kind === "f64") || sourceT.kind === "bytes")) {
+ // A union of typed arrays and number arrays (indexed-unions.ts) reads
+ // through its arms' own length and element reads.
+ const unionArms = indexedUnionArms(lowerer, sourceT);
+ if (
+ !(
+ (sourceT.kind === "array" && sourceT.elem.kind === "f64") ||
+ sourceT.kind === "bytes" ||
+ unionArms
+ )
+ ) {
throw new InternalCompilerError("internal: retained numeric iterator has a non-numeric source");
}
@@ -10827,35 +10890,44 @@ function lowerForOfStoredNumericIterator(
const doneRef = (): IrExpr => varRef(state.doneLocalId, BOOL, loc);
const sourceRef = (): IrExpr => varRef(state.sourceLocalId, sourceT, loc);
const sourceLength = (): IrExpr =>
- sourceT.kind === "array"
- ? {
- kind: "arrIntrinsic",
- method: "length",
- receiver: sourceRef(),
- args: [],
- type: F64,
- loc,
- }
- : {
- kind: "bytesIntrinsic",
- method: "length",
- receiver: sourceRef(),
- args: [],
- type: F64,
- loc,
- };
- const valueT = sourceT.kind === "array" ? arrayValueType(lowerer, F64) : F64;
+ unionArms
+ ? lowerIndexedUnionMember(lowerer, sourceRef(), unionArms, "length", loc)!
+ : sourceT.kind === "array"
+ ? {
+ kind: "arrIntrinsic",
+ method: "length",
+ receiver: sourceRef(),
+ args: [],
+ type: F64,
+ loc,
+ }
+ : {
+ kind: "bytesIntrinsic",
+ method: "length",
+ receiver: sourceRef(),
+ args: [],
+ type: F64,
+ loc,
+ };
+ const valueT =
+ sourceT.kind === "array" || (unionArms && !allBytesArms(unionArms))
+ ? arrayValueType(lowerer, F64)
+ : F64;
const sourceValue = (): IrExpr =>
- sourceT.kind === "array"
- ? arrayValueRead(lowerer, sourceRef(), indexRef(), F64, loc)
- : {
- kind: "bytesIntrinsic",
- method: "get",
- receiver: sourceRef(),
- args: [indexRef()],
- type: F64,
- loc,
- };
+ unionArms
+ ? allBytesArms(unionArms)
+ ? lowerIndexedUnionNumberRead(lowerer, sourceRef(), unionArms, indexRef(), loc)
+ : lowerIndexedUnionRead(lowerer, sourceRef(), unionArms, indexRef(), loc)
+ : sourceT.kind === "array"
+ ? arrayValueRead(lowerer, sourceRef(), indexRef(), F64, loc)
+ : {
+ kind: "bytesIntrinsic",
+ method: "get",
+ receiver: sourceRef(),
+ args: [indexRef()],
+ type: F64,
+ loc,
+ };
const one: IrExpr = { kind: "numLit", value: 1, type: F64, loc };
lowerer.scopes.push(new Map());
diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts
index 35080e8f8..2fcfc063b 100644
--- a/packages/compiler/src/frontend/lowering/lowerer.ts
+++ b/packages/compiler/src/frontend/lowering/lowerer.ts
@@ -147,9 +147,14 @@ import {
} from "./function-context.js";
import type { FrontendServices } from "../services.js";
import { InternalCompilerError } from "../../errors.js";
+import type { FsPathPlan } from "./builtins/fs-paths.js";
import { defaultAfterUndefined, lowerStaticallyUndefinedArgument } from "./optional-arguments.js";
import { type ProtocolDispatch } from "./class-protocol-dispatch.js";
-import { ClassDynamicDispatch, trackClassFieldCreation } from "./class-dynamic-dispatch.js";
+import {
+ ClassDynamicDispatch,
+ boxClassInstancesExactly,
+ trackClassFieldCreation,
+} from "./class-dynamic-dispatch.js";
import { guardClassAttributeWrites } from "./class-attributes.js";
import { guardFrozenWrites, markSloppyRecordWrites } from "./frozen-records.js";
import { applyHandleProperties } from "./handle-properties.js";
@@ -343,10 +348,13 @@ import {
} from "./lower-modules.js";
import { prepareCjsModuleGraph, usesCjsTopLevelThis } from "./lower-node-module.js";
import {
+ type SplitRequireBinding,
cjsNamespaceModule,
cjsWholeExportValueOf,
inlineNamespaceRequireOf,
inlineProgramRequireOf,
+ splitRequireBindingOf,
+ splitWholeExportAliasTargetOf,
} from "./cjs-inline-require.js";
import { tscInteropModuleOf } from "./cjs-tsc-interop.js";
import { usesCjsExportsIdentity } from "./cjs-exports-object.js";
@@ -426,6 +434,7 @@ import {
collectSignatureInner,
lambdaSignature,
} from "./call-signatures.js";
+import { dynamicOverloadChoice, guessedOverloadIdentifier } from "./overload-choice.js";
import {
implicitAnyParamSymbolsOf,
implicitLocalFnNodeOf,
@@ -2291,6 +2300,14 @@ export class Lowerer {
/** Per CommonJS file using top-level `this`: the global holding the
* object that stands for its original exports (lowerCjsTopLevelThis). */
readonly cjsExportsThis = new Map();
+ /** `var m; m = require("./x")` alias bindings (cjs-inline-require.ts).
+ * Null entries cache declarations that keep ordinary storage. */
+ readonly cjsSplitRequireBindings = new Map();
+ /** Per-module whole-export symbol (cjs-inline-require.ts). Null means the
+ * module's exports are a table. */
+ readonly cjsWholeExportSymbols = new Map();
+ /** Per-module live member-export classification (cjs-inline-require.ts). */
+ readonly cjsLiveMemberExports = new Map();
/** Inline program-module require calls whose %init the enclosing member
* read or member call is already sequencing (cjs-inline-require.ts). */
readonly inlineRequireInitDone = new Set();
@@ -2829,6 +2846,8 @@ export class Lowerer {
/** Parameters read as `any` in the error-path copy of an error-first
* callback. */
readonly anySymbols = new Set();
+ /** overload-choice's answers per call, binding and function. */
+ readonly dynamicOverloadAnswers = new Map();
/** Error parameters of an error-first callback with a known value in
* the copy being lowered: true while it holds an Error, false while it
* holds null (staticCondition folds conditions on them). */
@@ -2904,6 +2923,9 @@ export class Lowerer {
/** util.inspect's per-type traversal helpers (%util.insp.N), interned
* by typeKey — the assertHelpers pattern with its own namespace. */
readonly inspectHelpers = new Map();
+ /** Indexed-union safe-read helpers mapped to their numeric twins, for the
+ * fusion of an immediate ToNumber (tryLowerNumericIndexRead). */
+ readonly indexedUnionNumericTwins = new Map();
private readonly unionLiteralOwners = new Map<
string,
{ owners: Map | null; shapes: number; unions: number }
@@ -3145,8 +3167,13 @@ export class Lowerer {
* (builtins/fs-paths.ts): each lowers to the hidden local that holds its
* value, which is null until the path's lowering has saved it. */
readonly stagedArguments = new Map();
- /** The staged arguments that follow each such URL path, in order. */
- readonly stagedAfterPath = new Map();
+ /** What each fs path argument of a URL-path call lowers to (the plans
+ * of builtins/fs-paths.ts). */
+ readonly fsPathPlans = new Map();
+ /** The existsSync/fs.promises calls whose converted URL paths are being
+ * lowered into the call itself (lowerFallibleFsUrlCall). A call may be
+ * lowered more than once, and each lowering converts again. */
+ readonly fallibleFsCalls = new Set();
readonly moduleNsParamOverrides = new Map<
ts.ParameterDeclaration,
IrType & { kind: "moduleNs" }
@@ -4130,6 +4157,11 @@ export class Lowerer {
// registry (globals, fn signatures, classes) applies unchanged.
const cjsValue = this.cjsExportValueSymbol(symbol);
if (cjsValue) symbol = cjsValue;
+ // `var m; m = require("./x"); module.exports = m` over a whole-export
+ // target: TS types the wrapper's export= as `any`, so reconnect it to
+ // the target's value symbol explicitly.
+ const splitAlias = splitWholeExportAliasTargetOf(this, symbol);
+ if (splitAlias) symbol = splitAlias;
this.flushDeferred(symbol);
return symbol;
}
@@ -4523,8 +4555,8 @@ export class Lowerer {
const decl = decls.find(ts.isImportClause) ?? decls[0];
if (!decl) return null;
if (ts.isImportClause(decl)) return this.cjsDefaultImportDepOf(decl);
- if (!ts.isVariableDeclaration(decl) || !ts.isIdentifier(decl.name) || !decl.initializer)
- return null;
+ if (!ts.isVariableDeclaration(decl) || !ts.isIdentifier(decl.name)) return null;
+ if (decl.initializer === undefined) return splitRequireBindingOf(this, decl)?.dep ?? null;
{
const interop = tscInteropModuleOf(this, expr);
if (interop !== null) return cjsNamespaceModule(this, interop.dep) ? interop.dep : null;
@@ -4629,6 +4661,9 @@ export class Lowerer {
// a binding (cjs-inline-require.ts sequences the module's %init).
if (!ts.isIdentifier(expr)) return this.cjsModuleRefDepOf(expr) !== null;
const sym = this.checker.getSymbolAtLocation(expr);
+ const splitDecl = sym?.declarations.find(ts.isVariableDeclaration);
+ const split = splitDecl === undefined ? null : splitRequireBindingOf(this, splitDecl);
+ if (split !== null) return cjsNamespaceModule(this, split.dep);
if (this.localModuleBindingDepOf(expr) === null) return false;
if (this.createRequireWholeExportSymbolOf(expr) !== null) return false;
// SINGLE-VALUE exporters (`module.exports = Countdown` / `= double` /
@@ -4936,6 +4971,13 @@ export class Lowerer {
* their external dependencies by name). Null for everything else
* (builtins load nothing; the rest kept its preflight fence). */
requireInitStmt(spec: string, node: ts.Node): IrStmt | null {
+ if (this.inlineRequireCycleCalls.has(node)) {
+ this.unsupported(
+ "SC1090",
+ node,
+ `require() of '${spec}' while it is still evaluating (a require cycle: Node answers its partial exports, which the static export table does not model)`,
+ );
+ }
if (canonicalBuiltinModule(spec) === "punycode") return this.punycodeRequireWarning(node);
// Project requires resolve within the program; another bare require can
// be a program-module edge when it names an opted-in --npm-static package
@@ -7732,6 +7774,7 @@ export class Lowerer {
finalizeClassMethodValues(this, functions);
finalizePrivateBrands(this, functions);
trackClassFieldCreation(this, functions);
+ boxClassInstancesExactly(this, functions);
guardClassAttributeWrites(this, functions);
guardFrozenWrites(this, functions);
applyHandleProperties(this, functions);
@@ -8997,6 +9040,20 @@ export class Lowerer {
typeOf(node: ts.Node): ts.Type {
if (this.anySymbols.size > 0 && this.readsAnySymbol(node)) return this.checker.getAnyType();
+ // A checked-dynamic argument that another overload could take at run
+ // time leaves the resolved overload's return a guess, and JS bindings
+ // initialized with that result hold whatever it was.
+ if (
+ (ts.isCallExpression(node) || ts.isNewExpression(node)) &&
+ dynamicOverloadChoice(this, node)
+ )
+ return this.checker.getAnyType();
+ if (
+ ts.isIdentifier(node) &&
+ isJsSourceFile(ts.getSourceFileOfNode(node)) &&
+ guessedOverloadIdentifier(this, node)
+ )
+ return this.checker.getAnyType();
if (ts.isPropertyAccessExpression(node) && isJsSourceFile(ts.getSourceFileOfNode(node))) {
const receiver =
node.expression.kind === ts.SyntaxKind.ThisKeyword && this.fnStack.length > 0
diff --git a/packages/compiler/src/frontend/lowering/object-factory-new.ts b/packages/compiler/src/frontend/lowering/object-factory-new.ts
index bcb675692..c672f82e2 100644
--- a/packages/compiler/src/frontend/lowering/object-factory-new.ts
+++ b/packages/compiler/src/frontend/lowering/object-factory-new.ts
@@ -6,10 +6,14 @@ import { bindingNeverReassigned } from "./binding-analysis.js";
import { implicitDefaultInstance } from "./generic-functions.js";
import { type FnSig } from "./call-signatures.js";
-/** An ordinary constructor's explicit object return replaces its receiver.
- * Without observing this or new.target, it can use the function's call ABI. */
-function isObjectFactoryNew(lowerer: Lowerer, expr: ts.NewExpression): boolean {
- if (!ts.isIdentifier(expr.expression) || lowerer.peekLocal(expr.expression)) return false;
+/** The JavaScript function declaration (or never-reassigned function
+ * expression binding) `new` calls, when it is an ordinary constructor: not
+ * a generator or async function. */
+function jsConstructorFunctionOf(
+ lowerer: Lowerer,
+ expr: ts.NewExpression,
+): ts.FunctionDeclaration | ts.FunctionExpression | null {
+ if (!ts.isIdentifier(expr.expression) || lowerer.peekLocal(expr.expression)) return null;
// This probe also sees ordinary classes during global collection. Leave
// their deferred diagnostics for the class declaration's lowering.
const wasCollecting = lowerer.collecting;
@@ -20,7 +24,7 @@ function isObjectFactoryNew(lowerer: Lowerer, expr: ts.NewExpression): boolean {
} finally {
lowerer.collecting = wasCollecting;
}
- if (!symbol) return false;
+ if (!symbol) return null;
const owner = symbol.declarations.find(
(node) =>
(ts.isFunctionDeclaration(node) && node.body !== undefined) ||
@@ -45,7 +49,15 @@ function isObjectFactoryNew(lowerer: Lowerer, expr: ts.NewExpression): boolean {
declaration.asteriskToken ||
declaration.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)
)
- return false;
+ return null;
+ return declaration;
+}
+
+/** An ordinary constructor's explicit object return replaces its receiver.
+ * Without observing this or new.target, it can use the function's call ABI. */
+function isObjectFactoryNew(lowerer: Lowerer, expr: ts.NewExpression): boolean {
+ const declaration = jsConstructorFunctionOf(lowerer, expr);
+ if (!declaration?.body) return false;
const objectReturn = (expression: ts.Expression): boolean =>
ts.isObjectLiteralExpression(expression) ||
ts.isArrayLiteralExpression(expression) ||
@@ -81,8 +93,13 @@ function isObjectFactoryNew(lowerer: Lowerer, expr: ts.NewExpression): boolean {
/** Collection must register the shared result before reaching factory
* bodies: those bodies can reference later globals in the same module. */
+/** The storage of a JavaScript module binding initialized with `new` of an
+ * ordinary function: a factory's result and a constructor function's
+ * receiver (`const head = new Segment()`) are both checked-dynamic
+ * values, which functions declared elsewhere in the module read through
+ * the global. */
export function objectFactoryGlobalType(lowerer: Lowerer, expr: ts.NewExpression): IrType | null {
- return isObjectFactoryNew(lowerer, expr) ? DYN : null;
+ return jsConstructorFunctionOf(lowerer, expr) !== null ? DYN : null;
}
export function objectFactorySignature(lowerer: Lowerer, expr: ts.NewExpression): FnSig | null {
diff --git a/packages/compiler/src/frontend/lowering/overload-choice.ts b/packages/compiler/src/frontend/lowering/overload-choice.ts
new file mode 100644
index 000000000..f973ccf2e
--- /dev/null
+++ b/packages/compiler/src/frontend/lowering/overload-choice.ts
@@ -0,0 +1,197 @@
+import * as ts from "../ts7/adapter.js";
+import type { Lowerer } from "./lowerer.js";
+import { typeEquals, type IrType } from "../../ir/ir.js";
+import { isJsSourceFile } from "../program.js";
+import { isThisParameter } from "./call-signatures.js";
+import { hasExplicitJsDocReturn } from "./function-returns.js";
+import { hasJsTypeAnnotation } from "./lower-stmts.js";
+
+/** Whether a checked-dynamic argument leaves a JS call's overload
+ * undecided.
+ *
+ * The checker resolves an overloaded call whose argument is `any` (an
+ * untyped parameter, a value read from one) to the FIRST overload that
+ * argument satisfies, but the value can match any overload at run time:
+ * `generateKeyPairSync("ec", options)` answers KeyObjects or encoded
+ * strings by the options' contents, `digest(encoding)` a Buffer when the
+ * encoding is undefined. When another overload the call could take at
+ * run time returns a different type, the resolved overload's return is a
+ * guess, and the result is checked-dynamic instead.
+ *
+ * An overload is reachable when the arguments fit its arity (trailing
+ * checked-dynamic or `undefined` arguments may be absent) and every
+ * statically typed argument is assignable to its parameter. Generic
+ * overloads whose return cannot be mapped without instantiation do not
+ * count, so the resolved instantiation stands for them. */
+export function dynamicOverloadChoice(
+ lowerer: Lowerer,
+ expr: ts.CallExpression | ts.NewExpression,
+): boolean {
+ return cachedAnswer(lowerer, expr, () => computeDynamicOverloadChoice(lowerer, expr));
+}
+
+/** Whether a JavaScript value's checker type rests on an undecided
+ * overload (dynamicOverloadChoice): the call itself, an awaited one, a
+ * call to an unannotated JS function returning such a value, or a read of
+ * an unannotated JS binding initialized with one (destructured or not).
+ * Such a value is checked-dynamic wherever the checker's inference would
+ * otherwise carry the guess into a binding or a return. */
+export function guessedOverloadValue(lowerer: Lowerer, expr: ts.Expression): boolean {
+ while (ts.isParenthesizedExpression(expr) || ts.isAwaitExpression(expr)) expr = expr.expression;
+ if (ts.isCallExpression(expr) || ts.isNewExpression(expr)) {
+ if (!isJsSourceFile(ts.getSourceFileOfNode(expr))) return false;
+ if (dynamicOverloadChoice(lowerer, expr)) return true;
+ const decl = lowerer.checker.getResolvedSignature(expr)?.declaration;
+ return (
+ decl !== undefined &&
+ (ts.isFunctionDeclaration(decl) ||
+ ts.isFunctionExpression(decl) ||
+ ts.isArrowFunction(decl)) &&
+ isJsSourceFile(ts.getSourceFileOfNode(decl)) &&
+ returnsGuessedOverloadValue(lowerer, decl)
+ );
+ }
+ return ts.isIdentifier(expr) && guessedOverloadIdentifier(lowerer, expr);
+}
+
+/** Whether a JS identifier, as a binding's declared name or as a read,
+ * names a guessedOverloadBinding. */
+export function guessedOverloadIdentifier(lowerer: Lowerer, node: ts.Identifier): boolean {
+ return cachedAnswer(lowerer, node, () => {
+ const parent = node.parent;
+ if (
+ parent !== undefined &&
+ (ts.isVariableDeclaration(parent) || ts.isBindingElement(parent)) &&
+ parent.name === node
+ )
+ return guessedOverloadBinding(lowerer, parent);
+ const decl = lowerer.checker.getSymbolAtLocation(node)?.valueDeclaration;
+ return decl !== undefined && guessedOverloadBinding(lowerer, decl);
+ });
+}
+
+/** Whether `decl` is an unannotated JS variable or destructured binding
+ * whose declaration's initializer is a guessedOverloadValue. */
+function guessedOverloadBinding(lowerer: Lowerer, decl: ts.Node): boolean {
+ let root: ts.Node | undefined = decl;
+ while (root && ts.isBindingElement(root) && root.parent && ts.isBindingPattern(root.parent)) {
+ root = root.parent.parent;
+ }
+ if (!root) return false;
+ if (!ts.isVariableDeclaration(root) || !root.initializer || root.type) return false;
+ if (!isJsSourceFile(ts.getSourceFileOfNode(root)) || hasJsTypeAnnotation(root)) return false;
+ const init = root.initializer;
+ return cachedAnswer(lowerer, root, () => guessedOverloadValue(lowerer, init));
+}
+
+/** Whether a return of the unannotated JS function `decl` itself is a
+ * guessedOverloadValue. */
+export function returnsGuessedOverloadValue(
+ lowerer: Lowerer,
+ decl: ts.SignatureDeclaration,
+): boolean {
+ if (decl.type || decl.asteriskToken || hasExplicitJsDocReturn(decl)) return false;
+ return cachedAnswer(lowerer, decl, () => {
+ if (ts.isArrowFunction(decl) && decl.body && !ts.isBlock(decl.body)) {
+ return guessedOverloadValue(lowerer, decl.body);
+ }
+ let found = false;
+ const scan = (node: ts.Node): void => {
+ if (found || (node !== decl && ts.isFunctionLike(node))) return;
+ if (ts.isReturnStatement(node) && node.expression) {
+ found = guessedOverloadValue(lowerer, node.expression);
+ }
+ ts.forEachChild(node, scan);
+ };
+ scan(decl);
+ return found;
+ });
+}
+
+/** One answer per node; a node asked again while its answer is being
+ * computed (recursive functions, self-referencing bindings) answers
+ * false. */
+function cachedAnswer(lowerer: Lowerer, node: ts.Node, compute: () => boolean): boolean {
+ const cached = lowerer.dynamicOverloadAnswers.get(node);
+ if (cached !== undefined) return cached;
+ lowerer.dynamicOverloadAnswers.set(node, false);
+ const answer = compute();
+ lowerer.dynamicOverloadAnswers.set(node, answer);
+ return answer;
+}
+
+function computeDynamicOverloadChoice(
+ lowerer: Lowerer,
+ expr: ts.CallExpression | ts.NewExpression,
+): boolean {
+ // TypeScript sources keep the resolved overload: their reads validate
+ // the value against it where it is used.
+ if (!isJsSourceFile(ts.getSourceFileOfNode(expr))) return false;
+ const args = expr.arguments?.nodes ?? [];
+ if (args.length === 0 || args.some(ts.isSpreadElement)) return false;
+ const dynamic = args.map((arg) => checkedDynamicArgument(lowerer, arg));
+ if (!dynamic.includes(true)) return false;
+ const checker = lowerer.checker;
+ const calleeType = checker.getTypeAtLocation(expr.expression);
+ const signatures = ts.isNewExpression(expr)
+ ? checker.getConstructSignatures(calleeType)
+ : checker.getCallSignatures(calleeType);
+ if (signatures.length < 2) return false;
+ const resolved = checker.getResolvedSignature(expr);
+ if (!resolved) return false;
+ const resolvedReturn = lowerer.mapTypeOf(checker.getReturnTypeOfSignature(resolved));
+ if (resolvedReturn === null) return false;
+ // Arguments that may be absent at run time: the trailing run of
+ // checked-dynamic and `undefined` arguments.
+ let present = args.length;
+ while (present > 0 && (dynamic[present - 1] || isUndefinedArgument(args[present - 1]!))) {
+ present--;
+ }
+ for (const signature of signatures) {
+ const decl = signature.declaration;
+ if (!decl || decl === resolved.declaration || !ts.isFunctionLike(decl)) continue;
+ const generic = signature.getTypeParameters().length > 0;
+ const params = decl.parameters.nodes.filter((param) => !isThisParameter(param));
+ const required = params.filter(
+ (param) => !param.questionToken && !param.initializer && !param.dotDotDotToken,
+ ).length;
+ const maximum = params.some((param) => param.dotDotDotToken) ? Infinity : params.length;
+ if (required > args.length || present > maximum) continue;
+ const symbols = signature.getParameters();
+ let fits = true;
+ for (let i = 0; i < args.length && i < params.length && fits; i++) {
+ if (dynamic[i] || generic || params[i]!.dotDotDotToken) continue;
+ const symbol = symbols[i];
+ if (!symbol) continue;
+ fits = checker.isTypeAssignableTo(
+ checker.getTypeAtLocation(args[i]!),
+ checker.getTypeOfSymbol(symbol),
+ );
+ }
+ if (!fits) continue;
+ const candidate = lowerer.mapTypeOf(checker.getReturnTypeOfSignature(signature));
+ if (candidate !== null && !holdsResult(resolvedReturn, candidate)) return true;
+ }
+ return false;
+}
+
+/** Whether a value of the resolved overload's mapped return also holds
+ * every value of `candidate`: the same type, or a checked-dynamic one (a
+ * promise of one fulfills with anything). */
+function holdsResult(resolved: IrType, candidate: IrType): boolean {
+ if (resolved.kind === "dyn" || typeEquals(resolved, candidate)) return true;
+ return (
+ resolved.kind === "promise" &&
+ candidate.kind === "promise" &&
+ holdsResult(resolved.inner, candidate.inner)
+ );
+}
+
+function checkedDynamicArgument(lowerer: Lowerer, arg: ts.Expression): boolean {
+ if (lowerer.checker.getTypeAtLocation(arg).flags & ts.TypeFlags.Any) return true;
+ return (lowerer.typeOf(arg).flags & ts.TypeFlags.Any) !== 0;
+}
+
+function isUndefinedArgument(arg: ts.Expression): boolean {
+ return (ts.isIdentifier(arg) && arg.text === "undefined") || ts.isVoidExpression(arg);
+}
diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts
index ebf09f9e6..d133eeb43 100644
--- a/packages/compiler/src/frontend/lowering/surfaces.ts
+++ b/packages/compiler/src/frontend/lowering/surfaces.ts
@@ -2085,7 +2085,16 @@ export const BUILTIN_MODULE_FN_ALIASES: Record<
"fs.readFdEncoded",
"fs.readFdSync",
"fs.readFdSyncBytes",
+ // The runtime encoding's check before a URL path converts
+ // (fs-paths.ts).
+ "fs.checkReadEncoding",
],
+ // The string source's check before a URL destination converts
+ // (fs-paths.ts).
+ copyFileSync: ["fs.checkPath"],
+ renameSync: ["fs.checkPath"],
+ copyFile: ["fs.checkPath"],
+ rename: ["fs.checkPath"],
readlinkSync: ["fs.readlinkSyncStr", "fs.readlinkSyncBuffer"],
// The bytes-data form and the { mode } options form.
writeFileSync: ["fs.writeFileSyncBytes", "fs.writeFileModeSync"],
diff --git a/packages/compiler/src/frontend/pipeline.ts b/packages/compiler/src/frontend/pipeline.ts
index 6da81dfbc..c2f528b25 100644
--- a/packages/compiler/src/frontend/pipeline.ts
+++ b/packages/compiler/src/frontend/pipeline.ts
@@ -228,7 +228,8 @@ function detectAutoPackages(
} else if (mode === "lib" && !viaTransitive) {
// CJS packages spell their dep edges as top-level requires; the
// import-statement scan alone would miss every one of them.
- for (const req of requiresOf(stmt)) edges.push({ spec: req.spec, loc: locOf(req.node) });
+ for (const req of requiresOf(stmt, load.program))
+ edges.push({ spec: req.spec, loc: locOf(req.node) });
}
}
// Transitive selection counts every literal require() and import()
diff --git a/packages/compiler/src/frontend/program-analysis.ts b/packages/compiler/src/frontend/program-analysis.ts
index e49b9307e..d4ec9fa98 100644
--- a/packages/compiler/src/frontend/program-analysis.ts
+++ b/packages/compiler/src/frontend/program-analysis.ts
@@ -1,4 +1,5 @@
import type {
+ CallExpression,
ExportDeclaration,
Identifier,
SourceFile,
@@ -30,6 +31,13 @@ export class ProgramAnalysis {
readonly requireEarlyReferences = new Map();
/** Per file: the first top-level statement that can run user code. */
readonly requireFirstRunnable = new Map();
+ /** Per file: split require bindings (`var m; m = require("./x")`), from
+ * declaration to the require call of their one assignment
+ * (splitRequireBindings7). */
+ readonly splitRequireBindings = new Map<
+ SourceFile,
+ ReadonlyMap
+ >();
clear(): void {
this.createRequireReasons.clear();
@@ -41,5 +49,6 @@ export class ProgramAnalysis {
this.moduleEarlyBindings = null;
this.requireEarlyReferences.clear();
this.requireFirstRunnable.clear();
+ this.splitRequireBindings.clear();
}
}
diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts
index c5b72d98f..8075379b8 100644
--- a/packages/compiler/src/frontend/program.ts
+++ b/packages/compiler/src/frontend/program.ts
@@ -1423,17 +1423,31 @@ function isRequireStatement7(stmt: ts.Statement): boolean {
);
}
+/** A require() occurrence of a top-level statement. `split` marks the
+ * assignment of a split require binding (splitRequireBindings7): `node`
+ * is its require call and `decl` the binding's declaration. */
+interface RequireOccurrence7 {
+ spec: string;
+ node: ts.Node;
+ decl: ts.VariableDeclaration | null;
+ split?: true;
+}
+
/** The require() occurrences of one top-level statement: the declaration
- * forms and the bare side-effect call. Empty for everything else. */
-function requiresOf7(
- stmt: ts.Statement,
-): { spec: string; node: ts.Node; decl: ts.VariableDeclaration | null }[] {
+ * forms, the bare side-effect call, and the assignment of a split require
+ * binding. Empty for everything else. */
+function requiresOf7(stmt: ts.Statement, program: ts.Program): RequireOccurrence7[] {
if (ts.isExpressionStatement(stmt)) {
+ const split = splitRequireOfStatement7(program, stmt);
+ if (split !== null)
+ return [
+ { spec: requireSpecOf7(split.call)!, node: split.call, decl: split.decl, split: true },
+ ];
const spec = requireSpecOf7(stmt.expression);
return spec !== null ? [{ spec, node: stmt.expression, decl: null }] : [];
}
if (!ts.isVariableStatement(stmt)) return [];
- const out: { spec: string; node: ts.Node; decl: ts.VariableDeclaration | null }[] = [];
+ const out: RequireOccurrence7[] = [];
for (const decl of stmt.declarationList.declarations.nodes) {
const spec = decl.initializer !== undefined ? requireSpecOf7(decl.initializer) : null;
if (spec !== null) out.push({ spec, node: decl, decl });
@@ -1441,6 +1455,172 @@ function requiresOf7(
return out;
}
+/** CommonJS require bindings split into a declaration and one later
+ * assignment (`var m; m = require("./x")`): a top-level `var` or `let`
+ * declaration without initializer in a CommonJS file, assigned by exactly one
+ * top-level `m = require("")` statement of the require global over
+ * a compiled program module, and written nowhere else. The binding holds the
+ * module's exports from the assignment on, as `const m = require("./x")`
+ * does, so preflight treats the assignment as that require: its module edge,
+ * its cycle admission and its early-read fence. The lowering's recognizer
+ * (splitRequireBindingOf in cjs-inline-require.ts) accepts only these
+ * bindings. Keyed by declaration, with the assignment's require call. */
+function splitRequireBindings7(
+ program: ts.Program,
+ sf: ts.SourceFile,
+): ReadonlyMap {
+ const memo = program.analysis.splitRequireBindings;
+ const cached = memo.get(sf);
+ if (cached !== undefined) return cached;
+ const out = new Map();
+ memo.set(sf, out);
+ if (sf.isDeclarationFile || !isCjsJsFile7(sf, program) || !sf.text.includes("require"))
+ return out;
+ const checker = program.getTypeChecker();
+ const stmts = sf.statements.nodes;
+ const candidates = new Map<
+ ts.Symbol,
+ { decl: ts.VariableDeclaration; call: ts.CallExpression | null; assigns: number }
+ >();
+ for (const stmt of stmts) {
+ if (!ts.isVariableStatement(stmt)) continue;
+ if ((stmt.declarationList.flags & (ts.NodeFlags.Const | ts.NodeFlags.Using)) !== 0) continue;
+ for (const decl of stmt.declarationList.declarations.nodes) {
+ if (!ts.isIdentifier(decl.name) || decl.initializer !== undefined) continue;
+ const symbol = checker.getSymbolAtLocation(decl.name);
+ if (symbol === undefined || symbol.declarations.length !== 1) continue;
+ candidates.set(symbol, { decl, call: null, assigns: 0 });
+ }
+ }
+ if (candidates.size === 0) return out;
+ // The one top-level `m = ...` statement, whose right side is the require.
+ for (const stmt of stmts) {
+ if (!ts.isExpressionStatement(stmt) || !ts.isBinaryExpression(stmt.expression)) continue;
+ const assign = stmt.expression;
+ if (assign.operatorToken.kind !== ts.SyntaxKind.EqualsToken || !ts.isIdentifier(assign.left))
+ continue;
+ const symbol = checker.getSymbolAtLocation(assign.left);
+ const candidate = symbol !== undefined ? candidates.get(symbol) : undefined;
+ if (candidate === undefined) continue;
+ candidate.assigns++;
+ let right = assign.right;
+ while (ts.isParenthesizedExpression(right)) right = right.expression;
+ if (ts.isCallExpression(right) && splitRequireTarget7(program, sf, right))
+ candidate.call = right;
+ }
+ const names = new Set();
+ for (const candidate of candidates.values()) {
+ if (candidate.assigns === 1 && candidate.call !== null)
+ names.add((candidate.decl.name as ts.Identifier).text);
+ }
+ if (names.size === 0) return out;
+ // Exactly one write in the whole file: the assignment itself.
+ const writes = new Map();
+ ts.walkPreorder(sf, (node) => {
+ if (!ts.isIdentifier(node) || !names.has(node.text)) return undefined;
+ const parent = node.parent;
+ const symbol =
+ parent !== undefined && ts.isShorthandPropertyAssignment(parent) && parent.name === node
+ ? (checker.getShorthandAssignmentValueSymbol(parent) ?? checker.getSymbolAtLocation(node))
+ : checker.getSymbolAtLocation(node);
+ if (symbol === undefined || !candidates.has(symbol) || !isWriteReference7(node))
+ return undefined;
+ writes.set(symbol, (writes.get(symbol) ?? 0) + 1);
+ return undefined;
+ });
+ for (const [symbol, candidate] of candidates) {
+ if (candidate.assigns === 1 && candidate.call !== null && writes.get(symbol) === 1)
+ out.set(candidate.decl, candidate.call);
+ }
+ return out;
+}
+
+/** True when `call` is a require of the CommonJS global over a compiled
+ * program module: not a builtin, a JSON document or a declaration file. */
+function splitRequireTarget7(
+ program: ts.Program,
+ sf: ts.SourceFile,
+ call: ts.CallExpression,
+): boolean {
+ const spec = requireSpecOf7(call);
+ if (spec === null || canonicalBuiltinModule(spec) !== null) return false;
+ if (requireCallBindingKind7(program, call) !== "ambient") return false;
+ const dep =
+ resolveImport7(program, sf, spec, "require") ?? npmStaticDepSf7(program, sf, spec, "require");
+ return dep !== null && !dep.isDeclarationFile && !dep.fileName.endsWith(".json");
+}
+
+/** The split require binding a top-level statement assigns, or null. */
+function splitRequireOfStatement7(
+ program: ts.Program,
+ stmt: ts.Statement,
+): { decl: ts.VariableDeclaration; call: ts.CallExpression } | null {
+ if (
+ !ts.isExpressionStatement(stmt) ||
+ !ts.isBinaryExpression(stmt.expression) ||
+ !ts.isSourceFile(stmt.parent)
+ )
+ return null;
+ let right = stmt.expression.right;
+ while (ts.isParenthesizedExpression(right)) right = right.expression;
+ for (const [decl, call] of splitRequireBindings7(program, stmt.parent))
+ if (call === right) return { decl, call };
+ return null;
+}
+
+/** The top-level statement of a split require binding's assignment, from
+ * its require call (`m = require("./x")` or `m = (require("./x"))`). */
+function splitRequireStatementOf7(call: ts.CallExpression): ts.Node | undefined {
+ let node: ts.Node = call;
+ while (node.parent !== undefined && ts.isParenthesizedExpression(node.parent)) node = node.parent;
+ return node.parent?.parent;
+}
+
+/** The require call of a split require binding's declaration, or null. */
+function splitRequireCallOf7(
+ program: ts.Program,
+ decl: ts.VariableDeclaration,
+): ts.CallExpression | null {
+ return splitRequireBindings7(program, ts.getSourceFileOfNode(decl)).get(decl) ?? null;
+}
+
+/** True when `id` is written: an assignment target (compound and
+ * destructuring included), an update operand, or a for-in/of target. */
+function isWriteReference7(id: ts.Identifier): boolean {
+ let n: ts.Node = id;
+ for (;;) {
+ const p: ts.Node | undefined = n.parent;
+ if (p === undefined) return false;
+ if (ts.isBinaryExpression(p)) {
+ const k = p.operatorToken.kind;
+ return (
+ p.left === n && k >= ts.SyntaxKind.FirstAssignment && k <= ts.SyntaxKind.LastAssignment
+ );
+ }
+ if (ts.isPostfixUnaryExpression(p) || ts.isPrefixUnaryExpression(p)) {
+ return (
+ (p.operator === ts.SyntaxKind.PlusPlusToken ||
+ p.operator === ts.SyntaxKind.MinusMinusToken) &&
+ p.operand === n
+ );
+ }
+ if (ts.isForOfStatement(p) || ts.isForInStatement(p)) return p.initializer === n;
+ if (
+ ts.isParenthesizedExpression(p) ||
+ ts.isArrayLiteralExpression(p) ||
+ ts.isSpreadElement(p) ||
+ ts.isSpreadAssignment(p) ||
+ ts.isShorthandPropertyAssignment(p) ||
+ ts.isObjectLiteralExpression(p) ||
+ (ts.isPropertyAssignment(p) && p.initializer === n)
+ ) {
+ n = p;
+ continue;
+ }
+ return false;
+ }
+}
+
/** `true`, or `!0`, its minified spelling. */
function isTrueLiteral(expr: ts.Expression): boolean {
return (
@@ -1580,7 +1760,7 @@ function purePrefixDecl7(decl: ts.VariableDeclaration): boolean {
function purePrefixStmt7(program: ts.Program, s: ts.Statement): boolean {
if (ts.isEmptyStatement(s) || ts.isFunctionDeclaration(s)) return true;
if (ts.isExpressionStatement(s) && ts.isStringLiteral(s.expression)) return true; // directive
- if (isRequireStatement7(s)) return true;
+ if (isRequireStatement7(s) || splitRequireOfStatement7(program, s) !== null) return true;
if (isCjsJsFile7(ts.getSourceFileOfNode(s), program)) {
if (ts.isExpressionStatement(s) && isEsModuleStamp(s.expression)) {
const call = s.expression as ts.CallExpression;
@@ -1707,12 +1887,20 @@ function requireEarlyReference7(
const cached = memo.get(decl);
if (cached !== undefined) return cached;
let found: ts.Identifier | null = null;
- const stmt = decl.parent?.parent;
const sf = ts.getSourceFileOfNode(decl);
- if (stmt !== undefined && ts.isVariableStatement(stmt) && ts.isSourceFile(stmt.parent)) {
+ // A split require binding is bound at its assignment statement.
+ const splitCall = splitRequireCallOf7(program, decl);
+ const stmt = splitCall !== null ? splitRequireStatementOf7(splitCall) : decl.parent?.parent;
+ if (
+ stmt !== undefined &&
+ (ts.isVariableStatement(stmt) || splitCall !== null) &&
+ ts.isSourceFile(stmt.parent)
+ ) {
const stmts = sf.statements.nodes;
- const k = stmts.indexOf(stmt);
- const declarations = stmt.declarationList.declarations.nodes;
+ const k = stmts.indexOf(stmt as ts.Statement);
+ const declarations = ts.isVariableStatement(stmt)
+ ? stmt.declarationList.declarations.nodes
+ : [];
// Earlier declarators run too: `const x = read(), dep = require('./dep')`.
const precedingDecls = declarations.slice(0, declarations.indexOf(decl));
let firstRunnable = program.analysis.requireFirstRunnable.get(sf);
@@ -1765,6 +1953,12 @@ export function requireBindingEarlyUse7(
if (sym) bindings.add(sym);
}
if (bindings.size === 0) return null;
+ // A split require binding's assignment writes the binding; it reads nothing.
+ let splitWrite: ts.Node | null = splitRequireCallOf7(program, decl);
+ while (splitWrite !== null && ts.isParenthesizedExpression(splitWrite.parent))
+ splitWrite = splitWrite.parent;
+ splitWrite =
+ splitWrite !== null && ts.isBinaryExpression(splitWrite.parent) ? splitWrite.parent.left : null;
const stmts = sf.statements.nodes;
const cjs = isCjsJsFile7(sf, program);
const symbolOf = (id: ts.Identifier): ts.Symbol | undefined => {
@@ -1988,7 +2182,7 @@ export function requireBindingEarlyUse7(
const sym = symbolOf(n);
if (sym === undefined) return undefined;
if (bindings.has(sym)) {
- if (exactEarlyRead?.(n) === true) return undefined;
+ if (n === splitWrite || exactEarlyRead?.(n) === true) return undefined;
hit = n;
return "stop";
}
@@ -3499,7 +3693,7 @@ function cjsCycleRefusal7(
}
// Bindings of other members must stay unreachable from top-level code.
for (const st of stmts) {
- for (const req of requiresOf7(st)) {
+ for (const req of requiresOf7(st, program)) {
if (req.decl === null) continue;
const dep = resolveImport7(program, m, req.spec, "require");
if (dep === null || !members.has(dep)) continue;
@@ -5775,7 +5969,7 @@ function preflight7(load: LoadResult): {
// module aliases cannot represent; createRequire bindings have no
// checked form either.
const earlyRequireFence = (
- req: { decl: ts.VariableDeclaration | null },
+ req: RequireOccurrence7,
bindingKind: RequireCallBindingKind7,
loc: { file: string; start: number; end: number },
): boolean => {
@@ -5783,6 +5977,7 @@ function preflight7(load: LoadResult): {
if (
early === null ||
(bindingKind !== "createRequire" &&
+ req.split === undefined &&
((req.decl!.parent?.flags ?? 0) & (ts.NodeFlags.Const | ts.NodeFlags.Let)) !== 0)
) {
return false;
@@ -5793,14 +5988,16 @@ function preflight7(load: LoadResult): {
loc,
bindingKind === "createRequire"
? `createRequire() binding '${early.text}' below code that can reach it (Node throws ReferenceError on any earlier access, which is not modeled — move the require above the code that can run first)`
- : `var require() binding '${early.text}' below code that can reach it (Node reads undefined before the require runs, which is not modeled — declare it with const or let, or move the require above the code that can run first)`,
+ : req.split !== undefined
+ ? `require() binding '${early.text}' assigned below code that can reach it (Node reads undefined before the assignment runs, which is not modeled — move the assignment above the code that can run first)`
+ : `var require() binding '${early.text}' below code that can reach it (Node reads undefined before the require runs, which is not modeled — declare it with const or let, or move the require above the code that can run first)`,
),
);
return true;
};
for (let k = 0; k < stmts.length; k++) {
const stmt = stmts[k]!;
- for (const req of requiresOf7(stmt)) {
+ for (const req of requiresOf7(stmt, program)) {
const loc = {
file: sf.fileName,
start: ts.tokenStartOfNode(req.node),
@@ -5827,8 +6024,8 @@ function preflight7(load: LoadResult): {
const jsonDep = resolveImport7(program, sf, req.spec, "require");
if (jsonDep !== null && jsonDep.fileName.endsWith(".json")) {
if (bindingKind === "createRequire") continue;
- if (ts.isVariableStatement(stmt) && requiresOf7(stmt).length > 1) {
- const others = requiresOf7(stmt).some((r) => {
+ if (ts.isVariableStatement(stmt) && requiresOf7(stmt, program).length > 1) {
+ const others = requiresOf7(stmt, program).some((r) => {
const d = resolveImport7(program, sf, r.spec, "require");
return d === null || !d.fileName.endsWith(".json");
});
@@ -6000,8 +6197,12 @@ function preflight7(load: LoadResult): {
// counts, and targets outside the compiled graph add nothing here:
// their use sites keep their existing lowering fences.
if (jsRequireFile && !isNodeEsmFile7(sf, program)) {
+ // A split require binding's assignment is a statement-level require
+ // edge (requiresOf7 above), parenthesized or not.
+ const splitCalls = new Set(splitRequireBindings7(program, sf).values());
for (const call of inlineRequiresOf7(sf)) {
if (requireCallBindingKind7(program, call) !== "ambient") continue;
+ if (splitCalls.has(call)) continue;
const spec = requireSpecOf7(call)!;
if (load.externalTypes.has(spec) || canonicalBuiltinModule(spec) !== null) continue;
const lazy = inDeferredPosition7(call);
@@ -6750,6 +6951,7 @@ export {
requireSpecOf7 as requireSpecOf,
isRequireStatement7 as isRequireStatement,
requiresOf7 as requiresOf,
+ splitRequireCallOf7 as splitRequireCallOf,
isNodeEsmFile7 as isNodeEsmFile,
resolveImport7 as resolveImport,
resolveNpmImport7 as resolveNpmImport,
diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts
index 58dc39c7b..c0b02e8db 100644
--- a/packages/compiler/src/index.ts
+++ b/packages/compiler/src/index.ts
@@ -1,4 +1,5 @@
import { optimizationField, type NativeOptimization } from "./backend/optimization.js";
+import { targetCpuField, targetCpuTargetRefusal } from "./backend/target-cpu.js";
import { compilationTiming, type CompilationTiming, type TimingClock } from "./timing.js";
import { prepareExecutableModule } from "./executable/prepare.js";
import {
@@ -807,6 +808,7 @@ async function prepareExecutableInput(
lowRegion: lowRegionTarget(buildPlatform, opts.sanitize === true),
...(opts.sanitize === true ? { wasmBoundsChecks: true } : {}),
...inlineRcOption(opts.optimization, opts.sanitize === true),
+ ...targetCpuField(opts.targetCpu),
});
} catch (err) {
if (!(err instanceof LlvmUnsupportedError)) throw err;
@@ -877,6 +879,7 @@ async function prepareExecutableInput(
...(opts.sanitize === true ? { wasmBoundsChecks: true } : {}),
...inlineRcOption(opts.optimization, opts.sanitize === true),
...(opts.detachedExit === true ? { detachedExit: true } : {}),
+ ...targetCpuField(opts.targetCpu),
});
} catch (err) {
if (!(err instanceof LlvmUnsupportedError)) throw err;
@@ -1445,6 +1448,18 @@ async function compileTracked(
}
}
}
+ const targetCpuRefusal = targetCpuTargetRefusal(
+ opts.targetCpu,
+ buildPlatform,
+ nativeCodegenTarget()?.architecture ?? process.arch,
+ );
+ if (targetCpuRefusal !== null) {
+ return {
+ ok: false,
+ diagnostics: [nativeCodegenDiag("SC3002", targetCpuRefusal, entryPath)],
+ sourceTexts: new Map(),
+ };
+ }
try {
windowsSubsystemLinkerArgs(buildPlatform, opts.windowsSubsystem);
} catch (err) {
@@ -1499,6 +1514,7 @@ async function compileTracked(
...(opts.strip ? { strip: true as const } : {}),
...(opts.windowsSubsystem === "gui" ? { windowsSubsystem: "gui" as const } : {}),
...(opts.detachedExit === true ? { detachedExit: true as const } : {}),
+ ...targetCpuField(opts.targetCpu),
...(profile === undefined ? {} : { profile: nativeProfileKey(profile) }),
...checkerField(),
...(youngNurseryRequested() ? { youngNursery: true as const } : {}),
diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts
index f1b64ab82..43d7374ec 100644
--- a/packages/compiler/src/ir/builtin-effects.ts
+++ b/packages/compiler/src/ir/builtin-effects.ts
@@ -746,6 +746,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([
"url.set",
"url.setChecked",
"url.fileURLToPathUrl",
+ "fs.checkPath",
+ "fs.checkReadEncoding",
"url.fileURLToPathChecked",
"url.fileURLToPathOptions",
"url.fileURLToPathBuffer",
diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts
index dfb7e413d..9b960bd36 100644
--- a/packages/compiler/src/ir/builtin-signatures.ts
+++ b/packages/compiler/src/ir/builtin-signatures.ts
@@ -1006,6 +1006,14 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
"url.set": { argTypes: [URL_T, STRING, STRING], result: VOID },
"url.setChecked": { argTypes: [URL_T, STRING, DYN], result: VOID },
"url.fileURLToPathUrl": { argTypes: [URL_T], result: STRING },
+ /** fileURLToPathUrl as a value: the path string, or the error it would
+ * throw (fs.existsSync and fs.promises URL paths). Never throws. */
+ "url.fileURLToPathValue": { argTypes: [URL_T], result: DYN },
+ /** getValidatedPath's null-byte check on a string path, and
+ * readFileSync's encoding check: what Node validates before it converts
+ * a later URL path. (path, argument name) and (encoding). */
+ "fs.checkPath": { argTypes: [STRING, STRING], result: VOID },
+ "fs.checkReadEncoding": { argTypes: [STRING], result: VOID },
"url.fileURLToPathChecked": { argTypes: [DYN], result: STRING },
"url.fileURLToPathOptions": { argTypes: [DYN, DYN], result: STRING },
/** node:url's legacy API (scr_url_legacy.c): parse(url, parseQueryString,
@@ -4452,6 +4460,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
"insp.buffer": { argTypes: [BYTES_U8], result: STRING },
/** A numeric typed array that is not a Buffer (any element kind). */
"insp.typedArray": { argTypes: [null, F64, F64], result: STRING },
+ /** A u8 byte view in a union arm, element or field: a DataView, Buffer
+ * or Uint8Array by its own flags. */
+ "insp.bytes": { argTypes: [null, F64, F64], result: STRING },
"insp.error": { argTypes: [null, F64, F64], result: STRING },
"insp.errorHasCause": { argTypes: [null], result: BOOL },
"insp.errorBagHasProperties": { argTypes: [DYN], result: BOOL },
diff --git a/packages/compiler/src/ir/integer-loops.ts b/packages/compiler/src/ir/integer-loops.ts
index d6942718a..2f7d2bea6 100644
--- a/packages/compiler/src/ir/integer-loops.ts
+++ b/packages/compiler/src/ir/integer-loops.ts
@@ -119,13 +119,26 @@ function stableLoopValue(
stableLoopValue(value.receiver, locals, body, invariant, counter)
);
case "bytesIntrinsic":
- case "arrIntrinsic":
+ case "arrIntrinsic": {
+ // The receiver may be one arm of a union binding (indexed-unions.ts).
+ const receiver =
+ value.receiver.kind === "unionNarrow" ? value.receiver.value : value.receiver;
return (
(value.method === "length" ||
(value.kind === "bytesIntrinsic" && value.method === "byteLength")) &&
- value.receiver.kind === "varRef" &&
- (value.kind === "bytesIntrinsic" || !invariant || stableLength(value.receiver.localId)) &&
- stableLoopValue(value.receiver, locals, body, invariant, counter)
+ receiver.kind === "varRef" &&
+ (value.kind === "bytesIntrinsic" || !invariant || stableLength(receiver.localId)) &&
+ stableLoopValue(receiver, locals, body, invariant, counter)
+ );
+ }
+ // A union binding dispatched on its tag (indexed-unions.ts): stable
+ // when the binding is and every arm is.
+ case "ternary":
+ return (
+ value.cond.kind === "unionIsTag" &&
+ stableLoopValue(value.cond.value, locals, body, invariant, counter) &&
+ stableLoopValue(value.then, locals, body, invariant, counter, stableLength) &&
+ stableLoopValue(value.else_, locals, body, invariant, counter, stableLength)
);
default:
return false;
diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts
index cb070f662..8fb636608 100644
--- a/packages/compiler/src/ir/ir.ts
+++ b/packages/compiler/src/ir/ir.ts
@@ -1503,6 +1503,10 @@ export interface IrClassDef {
* class of the chain sets `name` during construction, and then an own
* `name` follows every other own property. */
errorNameAfter?: string | null;
+ /** JavaScript Error subclasses with a property bag: a view committed by a
+ * dynamic write keeps `name` and `message` where the bag recorded them
+ * (scr_error_dyn_follow_bag). */
+ errorBagOrder?: boolean;
/** Stable module symbol identities for public instance layout fields. */
symbolFields?: { field: string; globalId: string }[];
/** RUNTIME-PROVIDED class (the builtin Error hierarchy): the struct, RC
@@ -2511,6 +2515,14 @@ export type IrBytesIntrinsicMethod =
| "indexOfNum"
| "lastIndexOfNum"
| "includesNum"
+ /** TypedArray.prototype.indexOf / lastIndexOf / includes on any element
+ * kind: [f64 needle, f64 fromIndex]. indexOf and lastIndexOf compare
+ * with strict equality (NaN is never found, -0 matches 0), includes with
+ * SameValueZero (NaN is found). An omitted fromIndex is 0, or +Infinity
+ * for lastIndexOf (which clamps to the last index). */
+ | "indexOfElem"
+ | "lastIndexOfElem"
+ | "includesElem"
| "fill"
| "fillNum"
| "fillStr"
diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts
index d7eee18bf..a694e8d2c 100644
--- a/packages/compiler/src/ir/validate.ts
+++ b/packages/compiler/src/ir/validate.ts
@@ -2196,6 +2196,9 @@ function validateFunction(
indexOfNum: { argTypes: [F64, F64], minArgs: 1, result: F64 },
lastIndexOfNum: { argTypes: [F64, F64], minArgs: 1, result: F64 },
includesNum: { argTypes: [F64, F64], minArgs: 1, result: BOOL },
+ indexOfElem: { argTypes: [F64, F64], minArgs: 2, result: F64 },
+ lastIndexOfElem: { argTypes: [F64, F64], minArgs: 2, result: F64 },
+ includesElem: { argTypes: [F64, F64], minArgs: 2, result: BOOL },
fill: { argTypes: [BYTES_U8, F64, F64], minArgs: 1, result: BYTES_U8 },
fillNum: { argTypes: [F64, F64, F64], minArgs: 1, result: BYTES_U8 },
// Per-element TypedArray fill (any elem — the non-u8 fill path).
diff --git a/packages/compiler/src/native/prepare.test.ts b/packages/compiler/src/native/prepare.test.ts
index c03bfdd26..7ae2d1d5f 100644
--- a/packages/compiler/src/native/prepare.test.ts
+++ b/packages/compiler/src/native/prepare.test.ts
@@ -2,7 +2,11 @@ import { mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSyn
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, expect, test } from "vitest";
-import { nativeCodegenTarget } from "../backend/targets.js";
+import {
+ LINUX_ARM64_GNU_TARGET,
+ nativeCodegenTarget,
+ WASM32_WASI_TARGET,
+} from "../backend/targets.js";
import { runFrontend, type FrontendFactory } from "../frontend/pipeline.js";
import { loadProgram } from "../frontend/program-node.js";
import { ts7Executable } from "../frontend/ts7/rpc-api.js";
@@ -183,3 +187,24 @@ test("serialized IR travels as bounded parts through the frontend cache", () =>
expect(payload.llvm).toBe(first.llvm.join(""));
expect(payload.ir).toEqual(first.ir);
});
+
+test("an x86-64 level is refused for targets that are not x86-64 before the frontend runs", () => {
+ const f = fixture();
+ for (const target of [LINUX_ARM64_GNU_TARGET, WASM32_WASI_TARGET]) {
+ const result = prepareNativeExecutable(
+ f.entry,
+ { ...f.options, targetCpu: "x86-64-v3" },
+ null,
+ { ...fixtureToolchain(), target },
+ () => {
+ throw new Error("the frontend ran");
+ },
+ null,
+ );
+ expect(result.ok).toBe(false);
+ if (result.ok) continue;
+ expect(result.diagnostics.map((d) => [d.code, d.message])).toEqual([
+ ["SC3002", "--target-cpu=x86-64-v3 requires an x86-64 target"],
+ ]);
+ }
+});
diff --git a/packages/compiler/src/native/prepare.ts b/packages/compiler/src/native/prepare.ts
index ffd5bfd28..67a4b5a9e 100644
--- a/packages/compiler/src/native/prepare.ts
+++ b/packages/compiler/src/native/prepare.ts
@@ -1,4 +1,5 @@
-import type { ScrDiagnostic } from "../diagnostics/diagnostic.js";
+import { targetCpuField, targetCpuTargetRefusal } from "../backend/target-cpu.js";
+import { nativeCodegenDiag, type ScrDiagnostic } from "../diagnostics/diagnostic.js";
import type { CompilationTiming } from "../timing.js";
import { readFileSync } from "node:fs";
import { basename, dirname, join, resolve } from "node:path";
@@ -316,6 +317,17 @@ export function prepareNativeExecutable(
timing: CompilationTiming = () => {},
): NativeExecutableInput | CompileFailure {
const outputKind = options.outputKind ?? "exe";
+ const targetCpuRefusal = targetCpuTargetRefusal(
+ options.targetCpu,
+ toolchain.target.platform,
+ toolchain.target.architecture,
+ );
+ if (targetCpuRefusal !== null)
+ return {
+ ok: false,
+ diagnostics: [nativeCodegenDiag("SC3002", targetCpuRefusal, entry)],
+ sourceTexts: new Map(),
+ };
const stem = basename(entry).replace(/\.(ts|mts|cts|js|mjs|cjs)$/, "");
const outputPaths = [
resolve(options.outPath),
@@ -396,6 +408,7 @@ export function prepareNativeExecutable(
forkPrimitives: emitForkPrimitives(),
...inlineRcOption(options.optimization, options.sanitize === true),
...(options.detachedExit === true && outputKind === "exe" ? { detachedExit: true } : {}),
+ ...targetCpuField(options.targetCpu),
...(options.optimization === "dev" && !options.strip
? { debugSources: prepared.sourceTexts }
: {}),
diff --git a/packages/compiler/test/integer-view-compares.test.ts b/packages/compiler/test/integer-view-compares.test.ts
new file mode 100644
index 000000000..e988d48bd
--- /dev/null
+++ b/packages/compiler/test/integer-view-compares.test.ts
@@ -0,0 +1,52 @@
+import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { expect, test } from "vitest";
+import { compile, deserializeModule } from "../src/index.js";
+import { emitLlvmModule } from "../src/backend/llvm/emitter.js";
+
+const source = `function siftDown(a: Int32Array, start: number, end: number): void {
+ let root = start;
+ while (2 * root + 1 < end) {
+ let child = 2 * root + 1;
+ if (child + 1 < end && a[child]! < a[child + 1]!) child++;
+ if (a[root]! >= a[child]!) return;
+ const t = a[root]!;
+ a[root] = a[child]!;
+ a[child] = t;
+ root = child;
+ }
+}
+const a = new Int32Array([3, 9, 1, 7, 5]);
+siftDown(a, 0, Number(process.argv[2] ?? "5"));
+console.log(Array.from(a).join());
+`;
+
+test("integer sums of speculated locals compare and index as integers", async () => {
+ const dir = await mkdtemp(join(tmpdir(), "scriptc-integer-views-"));
+ try {
+ const entry = join(dir, "sift.ts");
+ await writeFile(entry, source);
+ const outPath = join(dir, "sift.ir.json");
+ const result = await compile(entry, { outPath, outDir: dir, outputKind: "ir" });
+ if (!result.ok) throw new Error(result.diagnostics.map((d) => d.message).join("\n"));
+ const ll = emitLlvmModule(deserializeModule(await readFile(outPath, "utf8")));
+ const body = ll.match(
+ /define internal [^\n]+ @sc_bf_siftDown(?:\.i32)?\([^\n]*\) #0 \{([\s\S]*?)\n\}/,
+ )![1]!;
+ // `child + 1`: the integer the speculated copy already adds.
+ const sums = [...body.matchAll(/(%t\d+) = add nsw i64 %t\d+, 1\n/g)].map((m) => m[1]!);
+ expect(sums.length).toBeGreaterThan(0);
+ const uses = (op: string) =>
+ sums.filter((sum) => new RegExp(`= ${op} i64 \\${sum}, %t\\d+\\n`).test(body));
+ // `child + 1 < end` compares the integers,
+ expect(uses("icmp slt").length).toBeGreaterThan(0);
+ // and `a[child + 1]` bounds the integer index without a double round trip.
+ expect(uses("icmp ult").length).toBeGreaterThan(0);
+ // `2 * root + 1 < end` may pass 2^53, so it keeps the double compare.
+ expect(body).toMatch(/fmul double 0x4000000000000000, %t\d+\n\s+%t\d+ = mul nsw i64 2/);
+ expect(body).toContain("fcmp olt double");
+ } finally {
+ await rm(dir, { recursive: true, force: true });
+ }
+});
diff --git a/packages/compiler/test/js-class-layouts.test.ts b/packages/compiler/test/js-class-layouts.test.ts
index 110d007ee..987703110 100644
--- a/packages/compiler/test/js-class-layouts.test.ts
+++ b/packages/compiler/test/js-class-layouts.test.ts
@@ -221,3 +221,21 @@ test("a JavaScript file TypeScript checks uses its JSDoc types for class layouts
expect(layout(unchecked, "Account").fields).toBe("owner:dyn balance:dyn");
expect(params(unchecked, "%Account.constructor")).toBe("(object, dyn, dyn): void");
});
+
+test("instances of a class with subclasses box at their exact class", async () => {
+ const module = await lower(`${SHAPES}
+/** @param {any} value */
+function keep(value) {
+ return value;
+}
+/** @param {Shape} shape */
+function cross(shape) {
+ return keep(shape);
+}
+console.log(cross(new Square()).size, cross(new Shape()).label);
+`);
+ const helper = module.functions.find((fn) => fn.name === "%dyn.class.boxExact:Shape");
+ expect(helper).toBeDefined();
+ expect(calls(module, "cross")).toContain("%dyn.class.boxExact:Shape");
+ expect(JSON.stringify(helper!.body)).toContain('"className":"Square"');
+});
diff --git a/packages/compiler/test/target-cpu-emit.test.ts b/packages/compiler/test/target-cpu-emit.test.ts
new file mode 100644
index 000000000..5dee8243c
--- /dev/null
+++ b/packages/compiler/test/target-cpu-emit.test.ts
@@ -0,0 +1,46 @@
+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";
+
+const program = `const a = new Int32Array(4096);
+for (let i = 0; i < a.length; i++) a[i] = (i * 2654435761) | 0;
+let s = 0;
+for (let r = 0; r < 64; r++) for (let i = 0; i < a.length; i++) s = (s + a[i]!) | 0;
+console.log(s);
+`;
+
+test.runIf(process.platform === "linux" && process.arch === "x64")(
+ "assembly, object and LLVM builds honor the target CPU",
+ async () => {
+ const dir = await mkdtemp(join(tmpdir(), "scriptc-target-cpu-emit-"));
+ const entry = join(dir, "main.ts");
+ const emit = async (
+ outputKind: "llvm" | "asm" | "obj",
+ targetCpu?: "x86-64-v3",
+ ): Promise => {
+ const outPath = join(dir, `main-${outputKind}-${targetCpu ?? "baseline"}`);
+ const result = await compile(entry, {
+ outPath,
+ outDir: dir,
+ outputKind,
+ ...(targetCpu === undefined ? {} : { targetCpu }),
+ });
+ if (!result.ok) throw new Error(result.diagnostics.map((d) => d.message).join("\n"));
+ return readFile(outPath);
+ };
+ try {
+ await writeFile(entry, program);
+ expect((await emit("llvm")).toString()).not.toContain('"target-cpu"');
+ expect((await emit("llvm", "x86-64-v3")).toString()).toContain('"target-cpu"="x86-64-v3"');
+ // The v3 build vectorizes the sums with AVX2's 256-bit registers.
+ expect((await emit("asm")).toString()).not.toContain("%ymm");
+ expect((await emit("asm", "x86-64-v3")).toString()).toContain("%ymm");
+ expect((await emit("obj", "x86-64-v3")).equals(await emit("obj"))).toBe(false);
+ } finally {
+ await rm(dir, { recursive: true, force: true });
+ }
+ },
+ 240_000,
+);
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6935-fs-url-paths-ordered.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6935-fs-url-paths-ordered.ts.json
new file mode 100644
index 000000000..ec3d3f004
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6935-fs-url-paths-ordered.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6935-fs-url-paths-ordered.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7189-non-null-assertion-nullish-destination.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7189-non-null-assertion-nullish-destination.ts.json
new file mode 100644
index 000000000..dc0d277ee
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7189-non-null-assertion-nullish-destination.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/7189-non-null-assertion-nullish-destination.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7392-non-null-operands.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7392-non-null-operands.ts.json
new file mode 100644
index 000000000..be7ffaa53
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7392-non-null-operands.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/7392-non-null-operands.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7453-untyped-overload-results.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7453-untyped-overload-results.cjs.json
new file mode 100644
index 000000000..9ba935c1e
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7453-untyped-overload-results.cjs.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/7453-untyped-overload-results.cjs"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7624-key-order-boxed-callback-records/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7624-key-order-boxed-callback-records/main.ts.json
new file mode 100644
index 000000000..1a617ef56
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7624-key-order-boxed-callback-records/main.ts.json
@@ -0,0 +1,7 @@
+{
+ "order": [
+ "/tests/corpus/7624-key-order-boxed-callback-records/lib.js",
+ "/tests/corpus/7624-key-order-boxed-callback-records/main.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7680-typed-array-search.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7680-typed-array-search.ts.json
new file mode 100644
index 000000000..d93d9afdc
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7680-typed-array-search.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/7680-typed-array-search.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7681-typed-array-search-dynamic.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7681-typed-array-search-dynamic.mjs.json
new file mode 100644
index 000000000..ab91940c0
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7681-typed-array-search-dynamic.mjs.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/7681-typed-array-search-dynamic.mjs"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7682-constructor-function-globals.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7682-constructor-function-globals.js.json
new file mode 100644
index 000000000..26d05338c
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7682-constructor-function-globals.js.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/7682-constructor-function-globals.js"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7745-typed-array-unions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7745-typed-array-unions.ts.json
new file mode 100644
index 000000000..9c770f8dd
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7745-typed-array-unions.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/7745-typed-array-unions.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7747-typed-array-class-values.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7747-typed-array-class-values.ts.json
new file mode 100644
index 000000000..a18e9e7df
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7747-typed-array-class-values.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/7747-typed-array-class-values.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7748-typed-array-number-array-unions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7748-typed-array-number-array-unions.ts.json
new file mode 100644
index 000000000..783fb1435
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7748-typed-array-number-array-unions.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/7748-typed-array-number-array-unions.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7749-typed-array-element-searches.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7749-typed-array-element-searches.ts.json
new file mode 100644
index 000000000..8a1372cea
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7749-typed-array-element-searches.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/7749-typed-array-element-searches.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7750-typed-array-union-inspect.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7750-typed-array-union-inspect.ts.json
new file mode 100644
index 000000000..26cc31982
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7750-typed-array-union-inspect.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/7750-typed-array-union-inspect.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7796-boxed-subclass-identity.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7796-boxed-subclass-identity.js.json
new file mode 100644
index 000000000..37f785cc2
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7796-boxed-subclass-identity.js.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/7796-boxed-subclass-identity.js"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7982-integer-view-compares.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7982-integer-view-compares.ts.json
new file mode 100644
index 000000000..94919aab0
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7982-integer-view-compares.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/7982-integer-view-compares.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8112-error-factory-own-key-order.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8112-error-factory-own-key-order.js.json
new file mode 100644
index 000000000..4b64d1d75
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8112-error-factory-own-key-order.js.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/8112-error-factory-own-key-order.js"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8113-error-class-field-before-name.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8113-error-class-field-before-name.js.json
new file mode 100644
index 000000000..ddf5981f0
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8113-error-class-field-before-name.js.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/8113-error-class-field-before-name.js"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8160-assert-expected-from-fields.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8160-assert-expected-from-fields.cjs.json
new file mode 100644
index 000000000..fcaacbbee
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/8160-assert-expected-from-fields.cjs.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/8160-assert-expected-from-fields.cjs"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/split-cjs-require-binding/main.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/split-cjs-require-binding/main.cjs.json
new file mode 100644
index 000000000..60b9b0736
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/split-cjs-require-binding/main.cjs.json
@@ -0,0 +1,12 @@
+{
+ "order": [
+ "/tests/corpus/split-cjs-require-binding/lib.cjs",
+ "/tests/corpus/split-cjs-require-binding/whole.cjs",
+ "/tests/corpus/split-cjs-require-binding/reexport.cjs",
+ "/tests/corpus/split-cjs-require-binding/reexport-extra.cjs",
+ "/tests/corpus/split-cjs-require-binding/chain.cjs",
+ "/tests/corpus/split-cjs-require-binding/chain-wrap.cjs",
+ "/tests/corpus/split-cjs-require-binding/main.cjs"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/split-cjs-require-cycle/main.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/split-cjs-require-cycle/main.cjs.json
new file mode 100644
index 000000000..0ea3366d3
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/split-cjs-require-cycle/main.cjs.json
@@ -0,0 +1,8 @@
+{
+ "order": [
+ "/tests/corpus/split-cjs-require-cycle/odd.cjs",
+ "/tests/corpus/split-cjs-require-cycle/even.cjs",
+ "/tests/corpus/split-cjs-require-cycle/main.cjs"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/typed-array-masked-index-stores.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/typed-array-masked-index-stores.ts.json
new file mode 100644
index 000000000..dde5a96fa
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/typed-array-masked-index-stores.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/typed-array-masked-index-stores.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/split-cjs-require-cycle/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/split-cjs-require-cycle/main.js.json
new file mode 100644
index 000000000..90d4a2353
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/split-cjs-require-cycle/main.js.json
@@ -0,0 +1,19 @@
+{
+ "order": [
+ "/tests/diagnostics/split-cjs-require-cycle/b.js",
+ "/tests/diagnostics/split-cjs-require-cycle/main.js"
+ ],
+ "diags": [
+ {
+ "code": "SC1016",
+ "message": "circular imports (/tests/diagnostics/split-cjs-require-cycle/main.js → /tests/diagnostics/split-cjs-require-cycle/b.js → /tests/diagnostics/split-cjs-require-cycle/main.js; the cycle closes through a require() edge: /tests/diagnostics/split-cjs-require-cycle/main.js uses its exports at /tests/diagnostics/split-cjs-require-cycle/main.js:10 outside the export statements that precede its first require, so a module of the cycle can observe them before they settle) are not supported yet",
+ "loc": {
+ "file": "/tests/diagnostics/split-cjs-require-cycle/main.js",
+ "start": 0,
+ "end": 0
+ },
+ "milestone": "later",
+ "hint": "ES-module cycles of let/const/function bindings compile as-is; rewrite the named construct (a named import instead of a namespace, a const instead of a default-export expression or var, a class declared before the code that can reach it) or break the named edge"
+ }
+ ]
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/split-cjs-require-early/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/split-cjs-require-early/main.js.json
new file mode 100644
index 000000000..acb338494
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/split-cjs-require-early/main.js.json
@@ -0,0 +1,27 @@
+{
+ "order": [
+ "/tests/diagnostics/split-cjs-require-early/main.js"
+ ],
+ "diags": [
+ {
+ "code": "SC1013",
+ "message": "require() binding 'lib' assigned below code that can reach it (Node reads undefined before the assignment runs, which is not modeled — move the assignment above the code that can run first) is not supported yet",
+ "loc": {
+ "file": "/tests/diagnostics/split-cjs-require-early/main.js",
+ "start": 396,
+ "end": 415
+ },
+ "milestone": "later"
+ },
+ {
+ "code": "SC1013",
+ "message": "require() binding 'wrapped' assigned below code that can reach it (Node reads undefined before the assignment runs, which is not modeled — move the assignment above the code that can run first) is not supported yet",
+ "loc": {
+ "file": "/tests/diagnostics/split-cjs-require-early/main.js",
+ "start": 530,
+ "end": 549
+ },
+ "milestone": "later"
+ }
+ ]
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/fs-url-fallible.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/fs-url-fallible.ts.json
new file mode 100644
index 000000000..e4967ea93
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/fs-url-fallible.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/fixtures/node-types/fs-url-fallible.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/fs-url-path-fences.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/fs-url-path-fences.ts.json
deleted file mode 100644
index 7b63a5564..000000000
--- a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/fs-url-path-fences.ts.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "order": [
- "/tests/fixtures/node-types/fs-url-path-fences.ts"
- ],
- "diags": []
-}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hono-zod-default-hook/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hono-zod-default-hook/main.ts.json
new file mode 100644
index 000000000..2310d6e0f
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/hono-zod-default-hook/main.ts.json
@@ -0,0 +1,61 @@
+{
+ "order": [
+ "/tests/fixtures/npm/cases/hono-zod-default-hook/main.ts"
+ ],
+ "diags": [
+ {
+ "code": "SC0001",
+ "message": "Cannot find name 'WebSocket'.",
+ "loc": {
+ "file": "/tests/fixtures/npm/node_modules/hono/dist/types/client/types.d.ts",
+ "start": 1398,
+ "end": 1407
+ }
+ },
+ {
+ "code": "SC0001",
+ "message": "Cannot find name 'WebSocket'.",
+ "loc": {
+ "file": "/tests/fixtures/npm/node_modules/hono/dist/types/client/types.d.ts",
+ "start": 1413,
+ "end": 1422
+ }
+ },
+ {
+ "code": "SC0001",
+ "message": "Cannot find name 'WebSocket'.",
+ "loc": {
+ "file": "/tests/fixtures/npm/node_modules/hono/dist/types/client/types.d.ts",
+ "start": 3716,
+ "end": 3725
+ }
+ },
+ {
+ "code": "SC0001",
+ "message": "Cannot find name 'FormData'.",
+ "loc": {
+ "file": "/tests/fixtures/npm/node_modules/hono/dist/types/client/types.d.ts",
+ "start": 4798,
+ "end": 4806
+ }
+ },
+ {
+ "code": "SC0001",
+ "message": "Cannot find name 'FormData'.",
+ "loc": {
+ "file": "/tests/fixtures/npm/node_modules/hono/dist/types/request.d.ts",
+ "start": 549,
+ "end": 557
+ }
+ },
+ {
+ "code": "SC0001",
+ "message": "Cannot find name 'FormData'.",
+ "loc": {
+ "file": "/tests/fixtures/npm/node_modules/hono/dist/types/request.d.ts",
+ "start": 5572,
+ "end": 5580
+ }
+ }
+ ]
+}
diff --git a/packages/compiler/test/typed-array-union-emission.test.ts b/packages/compiler/test/typed-array-union-emission.test.ts
new file mode 100644
index 000000000..e9bb22103
--- /dev/null
+++ b/packages/compiler/test/typed-array-union-emission.test.ts
@@ -0,0 +1,116 @@
+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-typed-array-unions-"));
+ 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 });
+ }
+}
+
+function body(llvm: string, name: string): string {
+ return new RegExp(`^define [^@]*@sc_b?f_${name}(\\.i32)?\\([^]*?^}`, "m").exec(llvm)?.[0] ?? "";
+}
+
+const source = `
+type UintArray = Uint8Array | Uint16Array | Uint32Array;
+const pick = (max: number, n: number): UintArray =>
+ max <= 256 ? new Uint8Array(n) : max <= 65536 ? new Uint16Array(n) : new Uint32Array(n);
+function total(xs: UintArray): number {
+ let s = 0;
+ for (let i = 0; i < xs.length; i++) s += xs[i]!;
+ return s;
+}
+function first(max: number): number {
+ return pick(max, 4)[0]!;
+}
+const xs = pick(Number(process.argv.length) * 100000, 8);
+for (let i = 0; i < xs.length; i++) xs[i] = i;
+console.log(total(xs), first(10));
+`;
+
+test("element reads of a union of typed arrays load each arm's own element kind in place", async () => {
+ const llvm = await emitLlvm(source);
+ const total = body(llvm, "total");
+ // One load per arm, each of its element width, behind the union's tag.
+ for (const width of ["i8", "i16", "i32"]) expect(total).toMatch(new RegExp(`load ${width}, ptr`));
+ expect(total).not.toContain("@sc_bf__x25_union_getNumber");
+ // The loop over the union's length keeps an integer counter.
+ expect(total).toMatch(/alloca i64/);
+});
+
+test("a typed-array union read through a computed receiver goes through one helper", async () => {
+ const llvm = await emitLlvm(source);
+ // The receiver is a call: evaluated once, then dispatched by the helper.
+ expect(body(llvm, "first")).toContain("@sc_bf__x25_union_getNumber");
+});
+
+const stores = `
+type Wide = Uint8Array | Uint16Array;
+const pick = (wide: boolean): Wide => (wide ? new Uint16Array(4) : new Uint8Array(4));
+function fill(xs: Wide): void {
+ for (let i = 0; i < xs.length; i++) xs[i] = i + 1;
+}
+function rebind(): void {
+ let u = pick(false);
+ let i = 0;
+ const swap = (): number => {
+ u = pick(true);
+ i = 2;
+ return 3;
+ };
+ u[i] = swap();
+ console.log(u, i);
+}
+fill(pick(process.argv.length > 9));
+rebind();
+`;
+
+test("a typed-array union store runs in place unless its value can rebind the receiver or index", async () => {
+ const llvm = await emitLlvm(stores);
+ expect(body(llvm, "fill")).not.toContain("@sc_bf__x25_union_idxSet");
+ // The call may assign the captured receiver and index, which the store
+ // read before it: the helper evaluates them first.
+ expect(body(llvm, "rebind")).toContain("@sc_bf__x25_union_idxSet");
+});
+
+test("typed-array methods on a union with a Buffer arm need a narrowing first", async () => {
+ const dir = await mkdtemp(join(tmpdir(), "scriptc-typed-array-unions-"));
+ try {
+ const entry = join(dir, "main.ts");
+ // The Buffer arm is found by its type, also through an alias name.
+ for (const [declaration, method] of [
+ ["const u: Buffer | Uint16Array", "slice(1)"],
+ ["type Bytes = Uint16Array | Buffer;\nconst u: Bytes", "slice(1)"],
+ ["type Bytes = Uint16Array | Buffer;\nconst u: Bytes", "fill(1)"],
+ ["type Bytes = Uint16Array | Buffer;\nconst u: Bytes", "indexOf(1)"],
+ ]) {
+ await writeFile(
+ entry,
+ `import { Buffer } from "node:buffer";
+${declaration} = process.argv.length > 9 ? new Uint16Array(2) : Buffer.alloc(2);
+console.log(u.${method});
+`,
+ );
+ const result = await compile(entry, {
+ outDir: dir,
+ outPath: join(dir, "main.ll"),
+ outputKind: "llvm",
+ });
+ expect(result.ok, `${declaration} ${method}`).toBe(false);
+ expect(JSON.stringify(result.diagnostics)).toContain(method.slice(0, method.indexOf("(")));
+ }
+ } finally {
+ await rm(dir, { recursive: true, force: true });
+ }
+});
diff --git a/packages/runtime/src/scr_bytes.c b/packages/runtime/src/scr_bytes.c
index 113d6ff34..3926bf83e 100644
--- a/packages/runtime/src/scr_bytes.c
+++ b/packages/runtime/src/scr_bytes.c
@@ -3329,6 +3329,55 @@ double scr_bytes_index_of_num(const ScrBytes *b, double v, double off, bool fwd)
return scr_bytes_index_of(b, &needle, off, 1, fwd);
}
+/* The first index a TypedArray search from `from` starts at
+ * (ToIntegerOrInfinity, then relative to the end when negative), or false
+ * when it starts past the last element. */
+static bool scr_bytes_search_start(size_t len, double from, size_t *start) {
+ double n = from != from ? 0 : trunc(from);
+ if (n >= (double)len) return false;
+ if (n < 0) n = n + (double)len < 0 ? 0 : n + (double)len;
+ *start = (size_t)n;
+ return true;
+}
+
+double scr_bytes_index_of_elem(const ScrBytes *b, double x, double from, bool last) {
+ /* A Buffer viewed as a Uint8Array keeps Buffer's own search: the needle
+ * is taken as a byte, and a NaN byteOffset searches the whole buffer. */
+ if (scr_bytes_is_buffer(b)) return scr_bytes_index_of_num(b, x, from, !last);
+ SCR_BYTES_SNAPSHOT(b);
+ size_t len = b->len;
+ size_t size = scr_bytes_elem_size(b->elem);
+ if (len == 0 || x != x) return -1;
+ if (!last) {
+ size_t k;
+ if (!scr_bytes_search_start(len, from, &k)) return -1;
+ for (; k < len; k++)
+ if (scr_bytes_decode(b->elem, b->data + k * size) == x) return (double)k;
+ return -1;
+ }
+ double n = from != from ? 0 : trunc(from);
+ double k = n >= 0 ? (n < (double)len - 1 ? n : (double)len - 1) : (double)len + n;
+ if (k < 0) return -1;
+ for (size_t i = (size_t)k + 1; i-- > 0;)
+ if (scr_bytes_decode(b->elem, b->data + i * size) == x) return (double)i;
+ return -1;
+}
+
+bool scr_bytes_includes_elem(const ScrBytes *b, double x, double from) {
+ if (scr_bytes_is_buffer(b)) return scr_bytes_index_of_num(b, x, from, true) >= 0;
+ SCR_BYTES_SNAPSHOT(b);
+ size_t len = b->len;
+ size_t size = scr_bytes_elem_size(b->elem);
+ size_t k;
+ if (len == 0 || !scr_bytes_search_start(len, from, &k)) return false;
+ bool nan = x != x;
+ for (; k < len; k++) {
+ double v = scr_bytes_decode(b->elem, b->data + k * size);
+ if (v == x || (nan && v != v)) return true;
+ }
+ return false;
+}
+
/* fill's shared core over a normalized byte pattern. nargs counts the
* present offset/end args. An EMPTY pattern zero-fills when zero_ok
* (Node's '' string) and is the constant TypeError otherwise (an empty
diff --git a/packages/runtime/src/scr_bytes.h b/packages/runtime/src/scr_bytes.h
index f3e36f9ad..7c0a935cc 100644
--- a/packages/runtime/src/scr_bytes.h
+++ b/packages/runtime/src/scr_bytes.h
@@ -538,6 +538,16 @@ ScrEmitter *scr_emitter_set_max_chk(ScrEmitter *em, const ScrDyn *n);
void scr_emitter_set_default_max_chk(const ScrDyn *n, const ScrStr *name);
double scr_bytes_index_of(const ScrBytes *b, const ScrBytes *needle, double off, double align, bool fwd);
double scr_bytes_index_of_num(const ScrBytes *b, double v, double off, bool fwd);
+/* TypedArray indexOf (last false) and lastIndexOf (last true) over any
+ * element kind, with strict equality: NaN is never found and -0 matches 0.
+ * `from` is the fromIndex argument (ToIntegerOrInfinity applies); the
+ * caller passes 0, or +Infinity for lastIndexOf, when it is omitted. A
+ * Buffer (a Uint8Array receiver can be one) searches as Buffer's
+ * indexOf and lastIndexOf do. */
+double scr_bytes_index_of_elem(const ScrBytes *b, double x, double from, bool last);
+/* TypedArray includes: indexOf's search from `from` with SameValueZero,
+ * so NaN is found. A Buffer searches as Buffer's includes does. */
+bool scr_bytes_includes_elem(const ScrBytes *b, double x, double from);
ScrBytes *scr_bytes_fill(ScrBytes *b, const ScrBytes *pattern, double nargs, double offset, double end);
ScrBytes *scr_bytes_fill_num(ScrBytes *b, double v, double nargs, double offset, double end);
ScrBytes *scr_bytes_fill_str(ScrBytes *b, const ScrStr *s, const ScrStr *enc,
diff --git a/packages/runtime/src/scr_bytes_io.c b/packages/runtime/src/scr_bytes_io.c
index 72053a6ab..253cad1da 100644
--- a/packages/runtime/src/scr_bytes_io.c
+++ b/packages/runtime/src/scr_bytes_io.c
@@ -59,6 +59,10 @@ static bool scr_fs_read_utf8(ScrStr *encoding) {
return encoding->len == 0 || scr_bytes_encoding_is_utf8(encoding);
}
+void scr_fs_check_read_encoding(ScrStr *encoding) {
+ (void)scr_fs_read_encoding_valid(encoding);
+}
+
ScrStr *scr_fs_read_file_encoded(ScrStr *path, ScrStr *encoding) {
if (!scr_fs_read_encoding_valid(encoding)) return NULL;
if (scr_fs_read_utf8(encoding)) return scr_fs_read_file(path);
diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h
index 5b1598a8b..bd76c12e2 100644
--- a/packages/runtime/src/scr_dyn.h
+++ b/packages/runtime/src/scr_dyn.h
@@ -1330,6 +1330,11 @@ ScrDyn *scr_dyn_from_error(const ScrError *e);
void scr_error_register_dyn(const ScrVt *vt, ScrDyn *(*box)(void *));
ScrDyn *scr_error_dyn_fields(const ScrError *e);
void scr_error_dyn_place_name(ScrDyn *view, const char *after, size_t after_len);
+/* An Error subclass view of an instance whose class keeps a property bag:
+ * `name` and `message` take their committed positions, and data values
+ * from the slots (scr_json.c). `name_placed`: the class's construction sets
+ * `name`, and the view already has it in place. */
+void scr_error_dyn_follow_bag(ScrDyn *view, const ScrDyn *bag, const ScrError *e, bool name_placed);
/* An accessor property of a fresh object: enumerable and configurable,
* with the given getter and setter (either may be NULL). Moves both. */
void scr_dyn_obj_set_accessor(ScrDyn *obj, const char *key, size_t key_len, ScrDyn *getter, ScrDyn *setter);
diff --git a/packages/runtime/src/scr_events.c b/packages/runtime/src/scr_events.c
index 3391f54b1..6e0bf5146 100644
--- a/packages/runtime/src/scr_events.c
+++ b/packages/runtime/src/scr_events.c
@@ -388,8 +388,80 @@ static SCR_TL bool scr_stdin_destroyed = false;
static SCR_TL bool scr_stdin_paused = false;
static SCR_TL bool scr_stdin_resumed = false;
static SCR_TL bool scr_stdin_read_eof = false;
-static SCR_TL ScrBytes *scr_stdin_buffer;
+/* Input held for read() and later consumers, as Node's BufferList: the
+ * chunks in arrival order and their total length, so an arrival appends
+ * without copying what is already held. */
+static SCR_TL ScrBytes **scr_stdin_held = NULL;
+static SCR_TL size_t scr_stdin_nheld = 0, scr_stdin_held_cap = 0;
+static SCR_TL size_t scr_stdin_buffered = 0;
static SCR_TL size_t scr_stdin_read_demand;
+/* 'readable' listeners (paused mode, as Node's once one is added): input
+ * stays buffered for read(), and the event fires on the next tick when
+ * input arrives while a read wanted more (`need`), and once at EOF. */
+static SCR_TL ScrStdinEndL *scr_stdin_readable = NULL;
+static SCR_TL size_t scr_stdin_nreadable = 0, scr_stdin_readable_cap = 0;
+static SCR_TL bool scr_stdin_readable_need = false;
+static SCR_TL bool scr_stdin_readable_queued = false;
+
+/* Appends an arrived chunk (moves) to the held input. */
+static void scr_stdin_hold(ScrBytes *chunk) {
+ if (chunk->len == 0) {
+ scr_bytes_release(chunk);
+ return;
+ }
+ if (scr_stdin_nheld == scr_stdin_held_cap) {
+ scr_stdin_held_cap = scr_stdin_held_cap ? scr_stdin_held_cap * 2 : 4;
+ scr_stdin_held = realloc(scr_stdin_held, scr_stdin_held_cap * sizeof *scr_stdin_held);
+ if (!scr_stdin_held) scr_events_oom();
+ }
+ scr_stdin_held[scr_stdin_nheld++] = chunk;
+ scr_stdin_buffered += chunk->len;
+}
+
+/* The first n held bytes as one chunk (+1), 0 < n <= scr_stdin_buffered.
+ * A whole first chunk moves out without a copy; otherwise the bytes are
+ * joined once, and a partly taken chunk keeps its rest. */
+static ScrBytes *scr_stdin_take(size_t n) {
+ ScrBytes *first = scr_stdin_held[0];
+ size_t used = 0;
+ ScrBytes *out;
+ if (n == first->len) {
+ out = first;
+ used = 1;
+ } else if (n < first->len) {
+ out = scr_bytes_slice(first, 0, (double)n);
+ scr_stdin_held[0] = scr_bytes_slice(first, (double)n, (double)first->len);
+ scr_bytes_release(first);
+ } else {
+ out = scr_bytes_new(SCR_BYTES_U8, (double)n);
+ size_t at = 0;
+ while (at < n) {
+ ScrBytes *chunk = scr_stdin_held[used];
+ size_t part = chunk->len <= n - at ? chunk->len : n - at;
+ memcpy(out->data + at, chunk->data, part);
+ at += part;
+ if (part == chunk->len) {
+ scr_bytes_release(chunk);
+ used++;
+ } else {
+ scr_stdin_held[used] = scr_bytes_slice(chunk, (double)part, (double)chunk->len);
+ scr_bytes_release(chunk);
+ }
+ }
+ }
+ memmove(scr_stdin_held, scr_stdin_held + used, (scr_stdin_nheld - used) * sizeof *scr_stdin_held);
+ scr_stdin_nheld -= used;
+ scr_stdin_buffered -= n;
+ return out;
+}
+
+static void scr_stdin_drop_held(void) {
+ for (size_t i = 0; i < scr_stdin_nheld; i++) scr_bytes_release(scr_stdin_held[i]);
+ free(scr_stdin_held);
+ scr_stdin_held = NULL;
+ scr_stdin_nheld = scr_stdin_held_cap = 0;
+ scr_stdin_buffered = 0;
+}
#ifdef SCR_STDIO_EXTENDED
/* process.stdin.setEncoding(enc): the canonical encoding name (NULL until
@@ -475,6 +547,78 @@ void scr_stdin_on_end(ScrClosure *cb /*moves*/, bool once) {
scr_stdin_nend++;
}
+static void scr_stdin_finish(bool fire_end);
+
+static void scr_stdin_emit_readable(void) {
+ scr_stdin_readable_queued = false;
+ size_t length = scr_stdin_buffered;
+ if (scr_stdin_eof || scr_stdin_destroyed || (!length && !scr_stdin_read_eof)) return;
+ size_t n = scr_stdin_nreadable;
+ ScrStdinEndL *snap = malloc((n ? n : 1) * sizeof *snap);
+ if (!snap) scr_events_oom();
+ for (size_t i = 0; i < n; i++) {
+ snap[i] = scr_stdin_readable[i];
+ scr_closure_retain(snap[i].cb);
+ }
+ for (size_t i = 0; i < n; i++) {
+ if (snap[i].once) {
+ for (size_t j = 0; j < scr_stdin_nreadable; j++) {
+ if (scr_stdin_readable[j].cb == snap[i].cb) {
+ scr_closure_release(scr_stdin_readable[j].cb);
+ memmove(scr_stdin_readable + j, scr_stdin_readable + j + 1,
+ (scr_stdin_nreadable - j - 1) * sizeof *scr_stdin_readable);
+ scr_stdin_nreadable--;
+ break;
+ }
+ }
+ }
+ if (!scr_exc_pending() && !scr_stdin_eof && !scr_stdin_destroyed)
+ ((void (*)(ScrClosure *))snap[i].cb->fn)(snap[i].cb);
+ scr_closure_release(snap[i].cb);
+ }
+ free(snap);
+ length = scr_stdin_buffered;
+ /* Another event is due once the buffer reaches the 64 KiB mark, or the
+ * larger size a listener's read(size) asked for. */
+ scr_stdin_readable_need =
+ !scr_stdin_read_eof && (length <= 65536 || length < scr_stdin_read_demand);
+}
+
+static void scr_stdin_queue_readable(void) {
+ if (scr_stdin_readable_queued || !scr_stdin_nreadable) return;
+ scr_stdin_readable_queued = true;
+ scr_next_tick_raw(scr_stdin_emit_readable);
+}
+
+/* Adding a 'readable' listener starts reading ahead to the 64 KiB mark; a
+ * buffer that already holds input fires the event at once (next tick). */
+static void scr_stdin_on_readable(ScrClosure *cb /*moves*/, bool once) {
+ if (scr_stdin_eof || scr_stdin_destroyed) {
+ scr_closure_release(cb);
+ return;
+ }
+ if (scr_stdin_nreadable == scr_stdin_readable_cap) {
+ scr_stdin_readable_cap = scr_stdin_readable_cap ? scr_stdin_readable_cap * 2 : 2;
+ scr_stdin_readable = realloc(scr_stdin_readable, scr_stdin_readable_cap * sizeof *scr_stdin_readable);
+ if (!scr_stdin_readable) scr_events_oom();
+ }
+ scr_stdin_readable[scr_stdin_nreadable].cb = cb;
+ scr_stdin_readable[scr_stdin_nreadable].once = once;
+ scr_stdin_nreadable++;
+ scr_stdin_readable_need = true;
+ if (scr_stdin_read_demand < 65536) scr_stdin_read_demand = 65536;
+ if (scr_stdin_buffered) scr_stdin_queue_readable();
+}
+
+/* The input ended: with 'readable' listeners the event fires once more and
+ * 'end' waits for the read that finds nothing left; otherwise 'end' fires
+ * once the buffer is drained. */
+static void scr_stdin_reached_eof(void) {
+ scr_stdin_read_eof = true;
+ if (scr_stdin_nreadable) scr_stdin_queue_readable();
+ else if (!scr_stdin_buffered) scr_stdin_finish(true);
+}
+
void scr_stdin_on_error(ScrClosure *cb /*moves*/, ScrChildErrFn fn, bool once) {
if (scr_stdin_eof || scr_stdin_destroyed) {
scr_closure_release(cb);
@@ -516,10 +660,15 @@ bool scr_stdin_ended(void) { return scr_stdin_eof || scr_stdin_destroyed; }
* alive (a paused Node stdin with only those never flows — the process
* exits with them registered). */
static bool scr_stdin_watching(void) {
- return !scr_stdin_eof && !scr_stdin_destroyed &&
- ((!scr_stdin_paused && (scr_stdin_ndata > 0 || scr_stdin_resumed)) ||
- scr_stdin_waiter != NULL ||
- (!scr_stdin_read_eof && scr_stdin_read_demand > (scr_stdin_buffer ? scr_stdin_buffer->len : 0)));
+ if (scr_stdin_eof || scr_stdin_destroyed) return false;
+ /* 'readable' listeners keep the stream paused, as in Node, where
+ * resume() and 'data' listeners do not make it flow: it reads ahead only
+ * while a read wants more than is held (the 64 KiB mark, or a larger
+ * read(size)), and stops until something drains it. */
+ if (scr_stdin_nreadable) return !scr_stdin_read_eof && scr_stdin_read_demand > scr_stdin_buffered;
+ return (!scr_stdin_paused && (scr_stdin_ndata > 0 || scr_stdin_resumed)) ||
+ scr_stdin_waiter != NULL ||
+ (!scr_stdin_read_eof && scr_stdin_read_demand > scr_stdin_buffered);
}
bool scr_stdin_pending(void) { return scr_stdin_watching(); }
@@ -528,6 +677,10 @@ static void scr_stdin_drop_listeners(void) {
for (size_t i = 0; i < scr_stdin_ndata; i++) scr_closure_release(scr_stdin_data[i].cb);
for (size_t i = 0; i < scr_stdin_nend; i++) scr_closure_release(scr_stdin_end[i].cb);
for (size_t i = 0; i < scr_stdin_nerr; i++) scr_closure_release(scr_stdin_err[i].cb);
+ for (size_t i = 0; i < scr_stdin_nreadable; i++) scr_closure_release(scr_stdin_readable[i].cb);
+ free(scr_stdin_readable);
+ scr_stdin_readable = NULL;
+ scr_stdin_nreadable = scr_stdin_readable_cap = 0;
free(scr_stdin_data);
free(scr_stdin_end);
free(scr_stdin_err);
@@ -593,8 +746,7 @@ static void scr_stdin_finish(bool fire_end) {
void scr_stdin_destroy(void) {
if (scr_stdin_destroyed) return;
scr_stdin_destroyed = true;
- scr_bytes_release(scr_stdin_buffer);
- scr_stdin_buffer = NULL;
+ scr_stdin_drop_held();
scr_stdin_read_demand = 0;
scr_stdin_settle_done();
scr_stdin_drop_listeners();
@@ -627,16 +779,9 @@ static void scr_stdin_deliver(ScrBytes *chunk);
/* A chunk that arrived (moves): delivered, or buffered while paused or
* without a consumer. */
static void scr_stdin_arrived(ScrBytes *chunk) {
- if (scr_stdin_paused || (!scr_stdin_ndata && !scr_stdin_resumed && !scr_stdin_waiter)) {
- if (scr_stdin_buffer) {
- size_t before = scr_stdin_buffer->len;
- ScrBytes *joined = scr_bytes_new(SCR_BYTES_U8, (double)(before + chunk->len));
- memcpy(joined->data, scr_stdin_buffer->data, before);
- memcpy(joined->data + before, chunk->data, chunk->len);
- scr_bytes_release(scr_stdin_buffer);
- scr_bytes_release(chunk);
- scr_stdin_buffer = joined;
- } else scr_stdin_buffer = chunk;
+ if (scr_stdin_paused || scr_stdin_nreadable || (!scr_stdin_ndata && !scr_stdin_resumed && !scr_stdin_waiter)) {
+ if (scr_stdin_nreadable && scr_stdin_readable_need) scr_stdin_queue_readable();
+ scr_stdin_hold(chunk);
return;
}
scr_stdin_deliver(chunk);
@@ -645,15 +790,18 @@ static void scr_stdin_arrived(ScrBytes *chunk) {
static void scr_stdin_service(void) {
if (!scr_stdin_watching()) return;
- if (!scr_stdin_paused && scr_stdin_buffer) {
- ScrBytes *chunk = scr_stdin_buffer;
- scr_stdin_buffer = NULL;
+ /* Buffered input goes to a consumer: 'data' listeners, a resumed
+ * stream, or a parked for-await read. After the last 'readable' listener
+ * it stays buffered until one arrives, as in Node. */
+ if (!scr_stdin_paused && !scr_stdin_nreadable && scr_stdin_buffered &&
+ (scr_stdin_ndata > 0 || scr_stdin_resumed || scr_stdin_waiter != NULL)) {
+ ScrBytes *chunk = scr_stdin_take(scr_stdin_buffered);
scr_stdin_deliver(chunk);
scr_bytes_release(chunk);
return;
}
if (scr_stdin_read_eof) {
- if (!scr_stdin_buffer) scr_stdin_finish(true);
+ if (!scr_stdin_buffered && !scr_stdin_nreadable) scr_stdin_finish(true);
return;
}
if (scr_stdin_fed) {
@@ -662,10 +810,7 @@ static void scr_stdin_service(void) {
bool ended = false;
ScrBytes *chunk = scr_stdin_fed_take(&ended);
if (chunk) scr_stdin_arrived(chunk);
- else if (ended) {
- scr_stdin_read_eof = true;
- if (!scr_stdin_buffer) scr_stdin_finish(true);
- }
+ else if (ended) scr_stdin_reached_eof();
return;
}
#ifdef _WIN32
@@ -727,8 +872,7 @@ static void scr_stdin_service(void) {
return;
}
if (n == 0) {
- scr_stdin_read_eof = true;
- if (!scr_stdin_buffer) scr_stdin_finish(true);
+ scr_stdin_reached_eof();
return;
}
ScrBytes *chunk = scr_bytes_new(SCR_BYTES_U8, (double)n);
@@ -802,6 +946,8 @@ static void scr_stdin_deliver(ScrBytes *chunk) {
#endif
ScrPromise *p = scr_stdin_waiter;
scr_stdin_waiter = NULL;
+ /* Node's stdin yields Buffers. */
+ chunk->is_buffer = true;
scr_promise_fulfill_ref(p, scr_bytes_retain(chunk), scr_bytes_retain_v,
scr_bytes_release_v, NULL);
scr_promise_release(p);
@@ -878,6 +1024,7 @@ static void scr_stdio_data_text(ScrClosure *cb, ScrStr *text) {
}
#endif
static void scr_stdio_end(ScrClosure *cb) { scr_stdio_fire(cb, NULL, 0); }
+static void scr_stdio_readable(ScrClosure *cb) { scr_stdio_fire(cb, NULL, 0); }
static void scr_stdio_error(ScrClosure *cb, ScrStr *message) {
ScrError *error = scr_error_new(SCR_ERR_ERROR, message);
ScrDyn *arg = scr_dyn_from_error(error);
@@ -971,7 +1118,7 @@ static void scr_stdio_queue_write_error(ScrStdio *stream, ScrDyn *callback, int
}
static void scr_stdio_finish_read(void) {
- if (scr_stdin_read_eof && !scr_stdin_buffer && !scr_stdin_eof && !scr_stdin_destroyed)
+ if (scr_stdin_read_eof && !scr_stdin_buffered && !scr_stdin_eof && !scr_stdin_destroyed)
scr_stdin_finish(true);
}
@@ -980,21 +1127,21 @@ static ScrDyn *scr_stdio_read(ScrDyn *size) {
if (!all && (size->kind != SCR_DYN_NUM || !isfinite(size->v.num) ||
size->v.num < 0 || size->v.num > 1073741824 || floor(size->v.num) != size->v.num))
return scr_stdio_refusal("read size");
- size_t available = scr_stdin_buffer ? scr_stdin_buffer->len : 0;
+ size_t available = scr_stdin_buffered;
size_t wanted = all ? available : (size_t)size->v.num;
if (scr_stdin_read_eof && wanted > available) wanted = available;
if (wanted == 0 || wanted > available) {
if (!scr_stdin_eof && !scr_stdin_destroyed) {
+ scr_stdin_readable_need = true;
scr_stdin_read_demand = wanted > 65536 ? wanted : 65536;
if (scr_stdin_read_eof) scr_next_tick_raw(scr_stdio_finish_read);
}
return scr_dyn_new_null();
}
- ScrBytes *chunk = scr_bytes_slice(scr_stdin_buffer, 0, (double)wanted);
- ScrBytes *rest = wanted < available ? scr_bytes_slice(scr_stdin_buffer, (double)wanted, (double)available) : NULL;
- scr_bytes_release(scr_stdin_buffer);
- scr_stdin_buffer = rest;
+ ScrBytes *chunk = scr_stdin_take(wanted);
+ bool rest = scr_stdin_buffered > 0;
scr_stdin_read_demand = 65536;
+ if (!rest && !scr_stdin_read_eof) scr_stdin_readable_need = true;
scr_stdin_deliver(chunk);
ScrDyn *result = scr_dyn_new_chunk(chunk);
scr_bytes_release(chunk);
@@ -1150,12 +1297,14 @@ static ScrDyn *scr_stdio_method(ScrClosure *cb, ScrDyn *const *args, size_t argc
bool data = arg->kind == SCR_DYN_STR && arg->v.str->len == 4 && memcmp(event, "data", 4) == 0;
bool end = arg->kind == SCR_DYN_STR && arg->v.str->len == 3 && memcmp(event, "end", 3) == 0;
bool error = arg->kind == SCR_DYN_STR && arg->v.str->len == 5 && memcmp(event, "error", 5) == 0;
+ bool readable = stream->fd == 0 && arg->kind == SCR_DYN_STR && arg->v.str->len == 8 &&
+ memcmp(event, "readable", 8) == 0;
#ifdef SCR_STDIO_EXTENDED
bool resize = stream->fd != 0 && arg->kind == SCR_DYN_STR && arg->v.str->len == 6 &&
memcmp(event, "resize", 6) == 0;
- if ((!data && !end && !error && !resize) || (stream->fd != 0 && !error && !resize)) {
+ if ((!data && !end && !error && !readable && !resize) || (stream->fd != 0 && !error && !resize)) {
#else
- if ((!data && !end && !error) || (stream->fd != 0 && !error)) {
+ if ((!data && !end && !error && !readable) || (stream->fd != 0 && !error)) {
#endif
result = scr_stdio_refusal("event");
goto done;
@@ -1175,6 +1324,7 @@ static ScrDyn *scr_stdio_method(ScrClosure *cb, ScrDyn *const *args, size_t argc
if (data) scr_stdin_on_data(scr_stdio_callback(listener, (void *)scr_stdio_data), scr_stdio_data, once);
#endif
if (end) scr_stdin_on_end(scr_stdio_callback(listener, (void *)scr_stdio_end), once);
+ if (readable) scr_stdin_on_readable(scr_stdio_callback(listener, (void *)scr_stdio_readable), once);
if (error) scr_stdin_on_error(scr_stdio_callback(listener, (void *)scr_stdio_error), scr_stdio_error, once);
} else {
#define REMOVE_STDIO_LISTENER(list, count, fire) \
@@ -1187,6 +1337,7 @@ static ScrDyn *scr_stdio_method(ScrClosure *cb, ScrDyn *const *args, size_t argc
}
if (data) { REMOVE_STDIO_LISTENER(scr_stdin_data, scr_stdin_ndata, scr_stdio_data); }
if (end) { REMOVE_STDIO_LISTENER(scr_stdin_end, scr_stdin_nend, scr_stdio_end); }
+ if (readable) { REMOVE_STDIO_LISTENER(scr_stdin_readable, scr_stdin_nreadable, scr_stdio_readable); }
if (error) { REMOVE_STDIO_LISTENER(scr_stdin_err, scr_stdin_nerr, scr_stdio_error); }
#undef REMOVE_STDIO_LISTENER
}
@@ -1246,7 +1397,7 @@ static ScrDyn *scr_stdio_get(void *ptr, const char *key, size_t length) {
return dimension < 0 ? scr_dyn_retain(scr_dyn_undefined()) : scr_dyn_new_num(dimension);
}
if (stream->fd == 0 && length == 14 && memcmp(key, "readableLength", 14) == 0)
- return scr_dyn_new_num(scr_stdin_buffer ? (double)scr_stdin_buffer->len : 0);
+ return scr_dyn_new_num((double)scr_stdin_buffered);
if (stream->fd == 0 && length == 5 && memcmp(key, "isRaw", 5) == 0)
return scr_process_is_tty(0) ? scr_dyn_new_bool(scr_stdio_raw) : scr_dyn_retain(scr_dyn_undefined());
if (stream->fd == 0 && length == 9 && memcmp(key, "destroyed", 9) == 0) return scr_dyn_new_bool(scr_stdin_destroyed);
@@ -1531,8 +1682,7 @@ static void scr_events_cleanup_atexit(void) {
if (scr_context_is_main()) scr_sig_cleanup();
scr_stdin_settle_done();
scr_stdin_drop_listeners();
- scr_bytes_release(scr_stdin_buffer);
- scr_stdin_buffer = NULL;
+ scr_stdin_drop_held();
if (scr_stdio_initialized) scr_stdio_cleanup();
}
diff --git a/packages/runtime/src/scr_inspect.c b/packages/runtime/src/scr_inspect.c
index 9cf30464e..5f888e4c4 100644
--- a/packages/runtime/src/scr_inspect.c
+++ b/packages/runtime/src/scr_inspect.c
@@ -1655,6 +1655,12 @@ static const char *insp_hidden_refusal(const ScrDyn *d) {
}
}
+ScrStr *scr_insp_bytes(ScrBytes *b, double recurse, double depth) {
+ if (b->is_data_view) return scr_insp_data_view(b, recurse, depth);
+ if (b->is_buffer) return scr_insp_buffer(b);
+ return scr_insp_typed_array(b, recurse, depth);
+}
+
ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) {
if (d->kind == SCR_DYN_PROXY) return scr_insp_dyn(d->v.proxy.target, recurse, depth);
const char *refused = g_show_hidden ? insp_hidden_refusal(d) : NULL;
diff --git a/packages/runtime/src/scr_inspect.h b/packages/runtime/src/scr_inspect.h
index f36335a3b..8af60f0c0 100644
--- a/packages/runtime/src/scr_inspect.h
+++ b/packages/runtime/src/scr_inspect.h
@@ -45,6 +45,10 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth);
/* A numeric typed array that is not a Buffer: `Uint8Array(n) [ ... ]`,
* `[Uint8Array]` past the depth limit. Borrows b; +1. */
ScrStr *scr_insp_typed_array(ScrBytes *b, double recurse, double depth);
+/* A u8 byte view whose static type does not say which one it is (a union
+ * arm, an element or field): a DataView, a Buffer () or a
+ * Uint8Array, by the view's own flags. */
+ScrStr *scr_insp_bytes(ScrBytes *b, double recurse, double depth);
/* format's %s / rest-args twin: dyn strings pass verbatim. */
ScrStr *scr_insp_dyn_s(ScrDyn *d, double depth);
ScrStr *scr_insp_dyn_hidden(ScrDyn *d, double depth); /* %o: showHidden */
@@ -120,6 +124,9 @@ ScrBytes *scr_fs_read_file_bytes(ScrStr *path); /* +1 */
* with the exception pending. */
ScrDyn *scr_fs_read_file_sync_dyn(ScrStr *path, const ScrDyn *enc);
ScrStr *scr_fs_read_file_encoded(ScrStr *path, ScrStr *encoding);
+/* readFileSync's encoding check (ERR_INVALID_ARG_VALUE), on its own: the
+ * check Node runs before it converts a URL path. */
+void scr_fs_check_read_encoding(ScrStr *encoding);
ScrStr *scr_fs_read_fd_encoded(double fd, ScrStr *encoding);
ScrDyn *scr_fs_callback_value(ScrStr *member);
ScrDyn *scr_fs_callback_call(ScrStr *member, const ScrDyn *args);
diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c
index 40fd73508..4af8c63b5 100644
--- a/packages/runtime/src/scr_json.c
+++ b/packages/runtime/src/scr_json.c
@@ -8960,6 +8960,102 @@ void scr_error_dyn_place_name(ScrDyn *view, const char *after, size_t after_len)
view->v.obj.index = NULL;
}
+static size_t scr_dyn_entry_index(const ScrDyn *object, const char *key, size_t key_len) {
+ for (size_t i = 0; i < object->v.obj.len; i++) {
+ const ScrDynEntry *entry = &object->v.obj.entries[i];
+ if (entry->key_len == key_len && memcmp(entry->key, key, key_len) == 0) return i;
+ }
+ return SIZE_MAX;
+}
+
+/* Whether bag entry b is one of the entries Error's constructor creates
+ * before the instance's class fields: `stack`, a hidden entry (a key
+ * starting with '%'), and a `message` that directly follows them. */
+static bool scr_error_bag_constructed(const ScrDyn *bag, size_t b) {
+ for (size_t i = 0; i <= b; i++) {
+ const ScrDynEntry *entry = &bag->v.obj.entries[i];
+ bool constructed = (entry->key_len == 5 && memcmp(entry->key, "stack", 5) == 0) ||
+ (entry->key_len > 0 && entry->key[0] == '%') ||
+ (entry->key_len == 7 && memcmp(entry->key, "message", 7) == 0);
+ if (!constructed) return false;
+ }
+ return true;
+}
+
+/* The view index at which bag entry b, now at `from`, belongs: after the
+ * nearest earlier bag property the view has. An entry created after
+ * construction (any `name` placed here, since no constructor of the class
+ * sets it, and a `message` that is not one of Error's constructor entries)
+ * also follows the class fields, which live in slots and not in the bag: it
+ * passes over the constructor's entries and over a `name` the construction
+ * set, and without another earlier property goes before the nearest later
+ * one the view has, or stays where the view put it. A `message` Error's
+ * constructor created follows the nearest earlier entry, or goes first. */
+static size_t scr_error_bag_place(const ScrDyn *view, const ScrDyn *bag, size_t b, bool name,
+ bool name_placed, size_t from) {
+ bool late = name || !scr_error_bag_constructed(bag, b);
+ for (size_t p = b; p-- > 0;) {
+ const ScrDynEntry *anchor = &bag->v.obj.entries[p];
+ if (late && scr_error_bag_constructed(bag, p)) break;
+ if (late && name_placed && anchor->key_len == 4 && memcmp(anchor->key, "name", 4) == 0) continue;
+ size_t after = scr_dyn_entry_index(view, anchor->key, anchor->key_len);
+ if (after != SIZE_MAX) return after < from ? after + 1 : after;
+ }
+ if (!late) return 0;
+ for (size_t p = b + 1; p < bag->v.obj.len; p++) {
+ size_t before = scr_dyn_entry_index(view, bag->v.obj.entries[p].key, bag->v.obj.entries[p].key_len);
+ if (before != SIZE_MAX) return before > from ? before - 1 : before;
+ }
+ return from;
+}
+
+/* The bag holds the view as it was last committed, in the order its
+ * properties were created: zod's error factory assigns `name` and then
+ * defines `message`, and Node lists them in that order. The slots rebuild
+ * both at fixed positions (scr_error_dyn_fields, scr_error_dyn_place_name),
+ * so each one present in the bag moves to follow the property it follows
+ * there. A typed write to `name` or `message` reaches only the slot, so a
+ * data property takes the slot's value. */
+void scr_error_dyn_follow_bag(ScrDyn *view, const ScrDyn *bag, const ScrError *e, bool name_placed) {
+ if (!bag || bag->kind != SCR_DYN_OBJ || view->kind != SCR_DYN_OBJ) return;
+ static const struct { const char *key; size_t len; } slots[] = {{"name", 4}, {"message", 7}};
+ for (size_t s = 0; s < 2; s++) {
+ bool name = s == 0;
+ if (!(name ? e->name_present : e->message_present)) continue;
+ size_t at = scr_dyn_entry_index(view, slots[s].key, slots[s].len);
+ if (at == SIZE_MAX || view->v.obj.entries[at].accessor) continue;
+ ScrDyn *value = scr_dyn_new_str(name ? e->name : e->message);
+ scr_dyn_region_store(view, value);
+ ScrDyn *old = view->v.obj.entries[at].value;
+ view->v.obj.entries[at].value = value;
+ scr_dyn_release(old);
+ }
+ bool moved = false;
+ for (size_t b = 0; b < bag->v.obj.len; b++) {
+ const ScrDynEntry *bag_entry = &bag->v.obj.entries[b];
+ bool slot = (bag_entry->key_len == 4 && memcmp(bag_entry->key, "name", 4) == 0) ||
+ (bag_entry->key_len == 7 && memcmp(bag_entry->key, "message", 7) == 0);
+ bool name = bag_entry->key_len == 4;
+ /* A `name` the class's construction sets is in place already. */
+ if (!slot || (name && name_placed)) continue;
+ size_t from = scr_dyn_entry_index(view, bag_entry->key, bag_entry->key_len);
+ if (from == SIZE_MAX) continue;
+ size_t to = scr_error_bag_place(view, bag, b, name, name_placed, from);
+ if (to == from) continue;
+ ScrDynEntry *entries = view->v.obj.entries;
+ ScrDynEntry entry = entries[from];
+ if (to < from) memmove(&entries[to + 1], &entries[to], (from - to) * sizeof *entries);
+ else memmove(&entries[from], &entries[from + 1], (to - from) * sizeof *entries);
+ entries[to] = entry;
+ moved = true;
+ }
+ if (moved) {
+ SCR_JS_SHAPE_RESET(view);
+ free(view->v.obj.index);
+ view->v.obj.index = NULL;
+ }
+}
+
ScrDyn *scr_dyn_from_error(const ScrError *e) {
size_t hit = scr_errdyn_find(e, true);
if (hit != SIZE_MAX) return scr_dyn_retain(scr_errdyn_cache[hit].dyn);
diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c
index 18d9c39e5..fb3524c81 100644
--- a/packages/runtime/src/scr_lib.c
+++ b/packages/runtime/src/scr_lib.c
@@ -1078,6 +1078,10 @@ bool scr_fs_path_ok(const ScrStr *path, const char *name) {
return false;
}
+void scr_fs_check_path(ScrStr *path, ScrStr *name) {
+ (void)scr_fs_path_ok(path, name->data);
+}
+
void scr_process_load_env_file(const ScrDyn *value) {
ScrStr *path;
if (value->kind == SCR_DYN_UNDEF || value->kind == SCR_DYN_NULL) path = scr_str_new("./.env", 6);
diff --git a/packages/runtime/src/scr_lib.h b/packages/runtime/src/scr_lib.h
index 95f67044a..fa447ff41 100644
--- a/packages/runtime/src/scr_lib.h
+++ b/packages/runtime/src/scr_lib.h
@@ -409,6 +409,9 @@ void scr_fs_invalid_named_path(const ScrDyn *value, const ScrStr *path, const ch
/* A string path argument: false after throwing Node's ERR_INVALID_ARG_VALUE
* when it holds a null byte (`name` is the argument's name). */
bool scr_fs_path_ok(const ScrStr *path, const char *name);
+/* scr_fs_path_ok as a statement: getValidatedPath's null-byte check on a
+ * path the call validates before converting a later URL path. */
+void scr_fs_check_path(ScrStr *path, ScrStr *name);
void scr_fs_futimes(double fd, double atime, double mtime);
void scr_fs_link(ScrStr *source, ScrStr *dest);
void scr_fs_symlink(ScrStr *target, ScrStr *destination, int kind);
diff --git a/packages/runtime/src/scr_url.c b/packages/runtime/src/scr_url.c
index 67de82471..025bd740f 100644
--- a/packages/runtime/src/scr_url.c
+++ b/packages/runtime/src/scr_url.c
@@ -1394,6 +1394,22 @@ ScrStr *scr_url_to_path(ScrUrl *u) {
#endif
}
+/* fs.existsSync and the fs.promises functions convert a URL path inside a
+ * try and turn its error into false or a rejection: the conversion's result
+ * as a value, the path string or the error. */
+ScrDyn *scr_url_to_path_value(ScrUrl *u) {
+ ScrStr *path = scr_url_to_path(u);
+ if (path) {
+ ScrDyn *value = scr_dyn_new_str(path);
+ scr_str_release(path);
+ return value;
+ }
+ ScrCaught *caught = scr_exc_take();
+ ScrDyn *error = scr_caught_to_dyn(caught);
+ scr_caught_release(caught);
+ return error;
+}
+
/* The string-receiver form parses first (Node accepts URL strings). */
ScrStr *scr_url_str_to_path(ScrStr *input) {
ScrUrl *u = scr_url_new(input);
diff --git a/packages/runtime/src/scr_url.h b/packages/runtime/src/scr_url.h
index 05e5702bb..435644419 100644
--- a/packages/runtime/src/scr_url.h
+++ b/packages/runtime/src/scr_url.h
@@ -64,6 +64,9 @@ ScrStr *scr_punycode_to_unicode(const ScrStr *input);
ScrArr *scr_punycode_ucs2_decode(const ScrStr *input);
ScrStr *scr_url_domain_to_unicode(const ScrDyn *domain, bool missing);
ScrStr *scr_url_to_path(ScrUrl *u); /* +1, or throws */
+/* scr_url_to_path as a value: the path string, or the error it would throw
+ * (+1 dyn, never throws). */
+ScrDyn *scr_url_to_path_value(ScrUrl *u);
ScrStr *scr_url_str_to_path(ScrStr *s); /* +1, or throws */
ScrUrl *scr_url_from_path(ScrStr *path); /* +1; throws on win32 UNC malformations only */
ScrUrl *scr_url_from_path_platform(ScrStr *path, bool windows); /* +1; explicit options.windows */
diff --git a/scripts/bench-runtime.mjs b/scripts/bench-runtime.mjs
index 61134fa4c..b45e3ebdf 100644
--- a/scripts/bench-runtime.mjs
+++ b/scripts/bench-runtime.mjs
@@ -40,6 +40,8 @@ const { values } = parseArgs({
layouts: { type: "string", default: "1" },
"baseline-optimization": { type: "string", default: "release" },
"candidate-optimization": { type: "string", default: "release" },
+ "baseline-target-cpu": { type: "string" },
+ "candidate-target-cpu": { type: "string" },
json: { type: "string" },
"no-lock": { type: "boolean", default: false },
node: { type: "boolean", default: true },
@@ -51,6 +53,7 @@ if (values.help) {
console.log(`Usage: node scripts/bench-runtime.mjs [--candidate=] [--baseline=]
[--workloads=a,b] [--runs=15] [--warmup=2] [--timeout=120] [--layouts=1] [--json=]
[--baseline-optimization=release] [--candidate-optimization=release]
+ [--baseline-target-cpu=] [--candidate-target-cpu=]
[--no-lock] [--no-node] [--keep]
Each checkout must have a built CLI (packages/cli/dist/bootstrap.js) and native
@@ -69,7 +72,10 @@ Workload names are the file names in benchmarks/runtime/workloads/*.json; for ex
--baseline-optimization/--candidate-optimization pick each contender's
--optimization posture (default release). The same checkout may be both
contenders, e.g. --baseline=. --candidate-optimization=speed compares speed with
-release. cold_build_ms records each contender's first, uncached build.`);
+release. --baseline-target-cpu/--candidate-target-cpu pass --target-cpu to each
+contender's builds, e.g. --baseline=. --candidate-target-cpu=x86-64-v3 compares
+x86-64-v3 code with the baseline. cold_build_ms records each contender's first,
+uncached build.`);
process.exit(0);
}
const runs = Number(values.runs);
@@ -428,7 +434,7 @@ function revision(root) {
});
return r.stdout.trim() + (dirty.stdout.trim() ? "+dirty" : "");
}
-function build(label, root, workload, layout, optimization) {
+function build(label, root, workload, layout, optimization, targetCpu) {
const cli = cliFor(root);
const out = join(work, label, layout === 0 ? workload.name : `${workload.name}.layout${layout}`);
mkdirSync(dirname(out), { recursive: true });
@@ -444,7 +450,15 @@ function build(label, root, workload, layout, optimization) {
const entry = join(suiteRoot, workload.entry);
const result = run(
process.execPath,
- [cli, "build", entry, `--optimization=${optimization}`, "-o", out],
+ [
+ cli,
+ "build",
+ entry,
+ `--optimization=${optimization}`,
+ ...(targetCpu === undefined ? [] : [`--target-cpu=${targetCpu}`]),
+ "-o",
+ out,
+ ],
{
env,
timeout: 900_000,
@@ -467,6 +481,7 @@ const contenders = [
label: "baseline",
root: resolve(values.baseline),
optimization: values["baseline-optimization"],
+ targetCpu: values["baseline-target-cpu"],
},
]
: []),
@@ -474,6 +489,7 @@ const contenders = [
label: "candidate",
root: resolve(values.candidate),
optimization: values["candidate-optimization"],
+ targetCpu: values["candidate-target-cpu"],
},
];
const report = {
@@ -580,7 +596,7 @@ try {
entry.results[c.label] = result;
result.binaries = [];
for (let layout = 0; layout < layouts && result.status === undefined; layout++) {
- const built = build(c.label, c.root, w, layout, c.optimization);
+ const built = build(c.label, c.root, w, layout, c.optimization, c.targetCpu);
result.build_ms += Math.round(built.buildMs);
if (layout === 0) result.cold_build_ms = Math.round(built.buildMs);
if (!built.ok) {
diff --git a/tests/corpus/6935-fs-url-paths-ordered.ts b/tests/corpus/6935-fs-url-paths-ordered.ts
new file mode 100644
index 000000000..8d39c40ff
--- /dev/null
+++ b/tests/corpus/6935-fs-url-paths-ordered.ts
@@ -0,0 +1,132 @@
+// URL paths that Node converts inside a try or after checking other
+// arguments: existsSync answers false for a URL it cannot convert, the
+// fs.promises functions return a promise rejected with the conversion's
+// error, copyFileSync and renameSync check a string source before they
+// convert a URL destination, and readFileSync checks a runtime encoding
+// before it converts the URL.
+import * as fs from "node:fs";
+import * as fsp from "node:fs/promises";
+import * as os from "node:os";
+import * as path from "node:path";
+import { pathToFileURL } from "node:url";
+
+const dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "fs-url-ordered-")));
+const at = (name: string): URL => pathToFileURL(path.join(dir, name));
+const scheme = new URL("http://example.com/a.txt");
+const host = new URL("file://example.com/a.txt");
+
+function report(label: string, error: unknown): void {
+ if (error instanceof Error) {
+ const code = (error as NodeJS.ErrnoException).code;
+ console.log(label, error.name, code, error.message.split(dir).join(""));
+ }
+}
+
+function attempt(label: string, run: () => void): void {
+ try {
+ run();
+ console.log(label, "returned");
+ } catch (error) {
+ report(label, error);
+ }
+}
+
+async function settle(label: string, run: () => Promise): Promise {
+ try {
+ await run();
+ console.log(label, "fulfilled");
+ } catch (error) {
+ report(label, error);
+ }
+}
+
+async function main(): Promise {
+ fs.writeFileSync(at("a b.txt"), "hello");
+ console.log(fs.existsSync(at("a b.txt")), fs.existsSync(at("missing")));
+ console.log(fs.existsSync(scheme), fs.existsSync(host), fs.existsSync(new URL("file:///tmp/a%2Fb")));
+ console.log(fs.existsSync(new URL("file:///tmp/a%00b")));
+
+ console.log(await fsp.readFile(at("a b.txt"), "utf8"));
+ console.log((await fsp.readFile(at("a b.txt"))).length);
+ await fsp.writeFile(at("w.txt"), "written");
+ await fsp.appendFile(at("w.txt"), "!");
+ console.log((await fsp.stat(at("w.txt"))).size, (await fsp.lstat(at("w.txt"))).isFile());
+ await fsp.access(at("w.txt"));
+ await fsp.chmod(at("w.txt"), 0o644);
+ await fsp.truncate(at("w.txt"), 3);
+ await fsp.copyFile(at("w.txt"), at("copy.txt"));
+ await fsp.rename(at("copy.txt"), at("moved.txt"));
+ console.log(await fsp.readFile(at("moved.txt"), "utf8"));
+ await fsp.mkdir(at("sub/deep"), { recursive: true });
+ console.log(await fsp.readdir(at("sub")));
+ console.log(path.dirname(await fsp.mkdtemp(at("tmp-"))) === dir);
+ console.log((await fsp.realpath(at("sub/../sub"))).split(dir).join(""));
+ // An array literal lowers its elements more than once.
+ console.log([await fsp.realpath(at("sub"))].length, [fs.existsSync(scheme)][0]);
+ // A later argument with effects runs once per call there, and the call
+ // receives its value.
+ let dataCalls = 0;
+ const data = (): string => {
+ dataCalls++;
+ return `data ${dataCalls}`;
+ };
+ console.log([await fsp.writeFile(at("w.txt"), data())].length, dataCalls);
+ console.log(await fsp.readFile(at("w.txt"), "utf8"));
+ const code = (error: unknown): string => String((error as { code?: string }).code);
+ const outcomes = [
+ await fsp.writeFile(scheme, data()).then(() => "fulfilled", code),
+ await fsp.appendFile(at("w.txt"), data()).then(() => "fulfilled", code),
+ ];
+ console.log(outcomes, dataCalls);
+ console.log(await fsp.readFile(at("w.txt"), "utf8"));
+ await fsp.rmdir(at("sub/deep"));
+ await fsp.unlink(at("moved.txt"));
+ await fsp.rm(at("w.txt"));
+ await fsp.rm(at("sub"), { recursive: true, force: true });
+ console.log(fs.existsSync(at("sub")));
+
+ // The promise is returned; the error arrives as its rejection.
+ const pending = fsp.stat(scheme);
+ console.log("returned a promise");
+ await pending.catch((error: unknown) => report("stat scheme", error));
+ await settle("readFile host", () => fsp.readFile(host, "utf8"));
+ await settle("copyFile both", () => fsp.copyFile(scheme, host));
+ await settle("rename dest", () => fsp.rename(at("a b.txt"), host));
+ await settle("writeFile separator", () => fsp.writeFile(new URL("file:///tmp/a%2fb"), "x"));
+ await settle("unlink missing", () => fsp.unlink(at("missing")));
+ await settle("rm scheme options", () => fsp.rm(scheme, { recursive: true, force: true }));
+
+ // Arguments inside type-only wrappers are evaluated once.
+ let modeCalls = 0;
+ const countedMode = (): number => {
+ modeCalls++;
+ return 0o644;
+ };
+ await settle("chmod satisfies", () => fsp.chmod(scheme, countedMode() satisfies number));
+ await settle("chmod as", () => fsp.chmod(at("a b.txt"), countedMode() as number));
+ await settle("chmod non-null", () => fsp.chmod(host, countedMode()!));
+ console.log("mode calls", modeCalls);
+ await settle("copyFile as", () => fsp.copyFile(scheme, at("c.txt") as URL));
+ await settle("copyFile dest as", () => fsp.copyFile(at("a b.txt"), host as URL));
+
+ attempt("copyFileSync source first", () => fs.copyFileSync("a\0b", scheme));
+ attempt("copyFileSync dest", () => fs.copyFileSync(path.join(dir, "a b.txt"), host));
+ attempt("renameSync source first", () => fs.renameSync("a\0b", host));
+ fs.copyFileSync(path.join(dir, "a b.txt"), at("copied.txt"));
+ console.log(fs.readFileSync(at("copied.txt"), "utf8"));
+
+ const encodings = ["utf8", "latin1", "base64", "bogus"];
+ for (const encoding of encodings) {
+ attempt(`readFileSync ${encoding}`, () => {
+ console.log(fs.readFileSync(at("a b.txt"), encoding as BufferEncoding));
+ });
+ attempt(`readFileSync ${encoding} scheme`, () => {
+ console.log(fs.readFileSync(scheme, encoding as BufferEncoding));
+ });
+ }
+
+ fs.rmSync(dir, { recursive: true, force: true });
+ console.log("done", fs.existsSync(dir));
+}
+
+void main();
diff --git a/tests/corpus/7189-non-null-assertion-nullish-destination.ts b/tests/corpus/7189-non-null-assertion-nullish-destination.ts
new file mode 100644
index 000000000..632655cff
--- /dev/null
+++ b/tests/corpus/7189-non-null-assertion-nullish-destination.ts
@@ -0,0 +1,57 @@
+// `!` has no runtime effect: a value asserted non-null that still holds
+// null or undefined reaches a return value, an arrow function's result or
+// an argument whose declared type holds it (or is any or unknown), unchanged.
+interface Shape {
+ size: number;
+}
+
+const shapes = new Map();
+function shapeOf(key: string): Shape | null {
+ if (shapes.has(key)) return shapes.get(key)!;
+ const result = key.length > 3 ? { size: key.length } : null;
+ shapes.set(key, result);
+ return result;
+}
+console.log(shapeOf("ab"), shapeOf("abcd"));
+console.log(shapeOf("ab"), shapeOf("abcd"), shapes.size);
+
+const counts = new Map([["a", 1]]);
+function countOf(key: string): number | undefined {
+ return counts.get(key)!;
+}
+console.log(countOf("a"), countOf("b"));
+
+function describe(shape: Shape | null): string {
+ return shape === null ? "none" : `size ${shape.size}`;
+}
+console.log(describe(shapes.get("ab")!), describe(shapes.get("abcd")!));
+
+class Slot {
+ readonly label: string;
+ constructor(shape: Shape | null) {
+ this.label = shape === null ? "empty" : `slot ${shape.size}`;
+ }
+}
+console.log(new Slot(shapes.get("ab")!).label, new Slot(shapes.get("abcd")!).label);
+
+const lookup = (key: string): Shape | null => shapes.get(key)!;
+console.log(lookup("ab"), lookup("abcd")?.size);
+
+const names = new Map([["a", null], ["b", "bee"]]);
+const amounts = new Map([["a", undefined], ["b", 2]]);
+function kindOf(value: unknown): string {
+ return value === null ? "null" : typeof value;
+}
+console.log(String(names.get("a")!), String(names.get("b")!));
+console.log(Number(amounts.get("a")!), Number(amounts.get("b")!));
+console.log(kindOf(names.get("a")!), kindOf(amounts.get("a")!), kindOf(names.get("b")!));
+
+// An optional parameter of a standard library method keeps the checked
+// narrowing: `decode(input?)` reads the asserted Uint8Array.
+const chunks = new Map([["a", new TextEncoder().encode("chunk")]]);
+interface Read {
+ done: boolean;
+ value?: Uint8Array;
+}
+const read: Read = { done: false, value: chunks.get("a") };
+console.log(new TextDecoder().decode(chunks.get("a")!), new TextDecoder().decode(read.value!).slice(0, 3));
diff --git a/tests/corpus/7392-non-null-operands.ts b/tests/corpus/7392-non-null-operands.ts
new file mode 100644
index 000000000..60928ddb6
--- /dev/null
+++ b/tests/corpus/7392-non-null-operands.ts
@@ -0,0 +1,137 @@
+// Non-null assertions on values that still hold undefined or null, used as
+// operands of operators that convert them: arithmetic, comparisons, string
+// conversion, Math functions, and loose equality with null. `!`
+// performs no runtime check, so the operator sees undefined or null and
+// converts it as JavaScript does.
+
+interface Settings {
+ limit?: number;
+ label?: string;
+ enabled?: boolean;
+ ratio: number | null;
+ note?: string | null;
+}
+
+const empty: Settings = { ratio: null };
+const full: Settings = { limit: 4, label: "abc", enabled: true, ratio: 0.5, note: "n" };
+const counts: Record = { a: 1, b: 2 };
+const sizes = new Map([["small", 1]]);
+const values = [3, 1, 4];
+
+function show(v: unknown): string {
+ return Object.is(v, -0) ? "-0" : String(v);
+}
+
+function line(label: string, parts: unknown[]): void {
+ console.log(label.padEnd(12), parts.map(show).join(" "));
+}
+
+for (const s of [empty, full]) {
+ const tag = s === empty ? "empty" : "full";
+ line(`${tag} arith`, [
+ s.limit! + 1,
+ s.limit! - 1,
+ s.limit! * 2,
+ s.limit! / 2,
+ s.limit! % 3,
+ s.limit! ** 2,
+ -s.limit!,
+ +s.limit!,
+ s.ratio! + 1,
+ s.ratio! * 4,
+ -s.ratio!,
+ s.limit! - s.ratio!,
+ ]);
+ line(`${tag} bits`, [s.limit! | 0, s.limit! & 6, s.limit! << 1, s.limit! >>> 0, ~s.limit!, s.ratio! ^ 1]);
+ line(`${tag} compare`, [
+ s.limit! < 5,
+ s.limit! >= 0,
+ s.ratio! < 1,
+ s.ratio! > -1,
+ s.limit! === 4,
+ s.limit! !== 4,
+ s.ratio! == 0,
+ s.label! < "b",
+ s.label! >= "a",
+ s.label! === "abc",
+ s.enabled! === true,
+ s.limit! > s.ratio!,
+ ]);
+ line(`${tag} strings`, [
+ `${s.limit!}`,
+ `${s.ratio!}`,
+ `${s.enabled!}`,
+ `[${s.label!}]`,
+ `${s.note!}`,
+ "n=" + s.limit!,
+ s.limit! + "!",
+ s.label! + "?",
+ "<" + s.note!,
+ ]);
+ line(`${tag} convert`, [
+ Math.round(s.limit!),
+ Math.max(s.limit!, 1),
+ Math.min(s.ratio!, 1),
+ !s.enabled!,
+ !s.ratio!,
+ ]);
+}
+
+line("record", [counts["z"]! + 1, counts["a"]! + 1, counts["z"]! > 0, `${counts["z"]!}`]);
+line("map", [sizes.get("large")! * 2, sizes.get("small")! * 2, `${sizes.get("large")!}`]);
+line("find", [values.find((v) => v > 9)! + 1, values.find((v) => v > 3)! + 1]);
+
+let total = 0;
+let text = "";
+let mask = 7;
+for (const key of ["a", "b", "c", "a"]) {
+ total += counts[key]!;
+ text += counts[key]!;
+ text += ",";
+ mask &= counts[key]!;
+}
+line("compound", [total, text, mask]);
+
+const tally: Record = {};
+for (const word of ["x", "y", "x"]) tally[word] = tally[word]! + 1;
+line("tally", [tally["x"], tally["y"]]);
+
+type Cell = number | string | null | undefined;
+for (const cell of [3, "4", null, undefined] as Cell[]) {
+ const m = new Map([["k", cell]]);
+ line("cell", [`${m.get("k")!}`, m.get("k")! === 3, String(m.get("k")!)]);
+}
+
+// Loose equality with null tests both unit arms of a value read once.
+const optional = new Map([["one", 1], ["none", null]]);
+let reads = 0;
+function lookup(key: string): string | null | undefined {
+ reads++;
+ return key === "a" ? "x" : key === "b" ? null : undefined;
+}
+for (const key of ["one", "none", "missing"]) {
+ line(`loose ${key}`, [optional.get(key)! == null, optional.get(key)! != undefined, optional.get(key)! == 1]);
+}
+line("loose call", [lookup("a") == null, lookup("b") == null, lookup("c") != null, reads]);
+
+// `+` with an untyped partner concatenates or adds depending on what the
+// partner holds when it runs.
+const untypedText: any = "x";
+const untypedNumber: any = 5;
+const untypedObject: any = { toString: () => "obj" };
+for (const s of [empty, full]) {
+ let joined: any = "t";
+ let summed: any = 1;
+ joined += s.limit!;
+ summed += s.limit!;
+ line(`untyped ${s === empty ? "empty" : "full"}`, [
+ s.limit! + untypedText,
+ untypedText + s.limit!,
+ s.limit! + untypedNumber,
+ untypedNumber + s.limit!,
+ s.label! + untypedText,
+ s.limit! + untypedObject,
+ joined,
+ summed,
+ ]);
+}
diff --git a/tests/corpus/7453-untyped-overload-results.cjs b/tests/corpus/7453-untyped-overload-results.cjs
new file mode 100644
index 000000000..21923827d
--- /dev/null
+++ b/tests/corpus/7453-untyped-overload-results.cjs
@@ -0,0 +1,85 @@
+// An untyped argument can match any overload of a call at run time. The
+// result follows the overload the value selects, through bindings,
+// destructuring and function returns.
+const crypto = require("crypto");
+const fs = require("fs");
+const os = require("os");
+const path = require("path");
+
+const ecPair = (options) => crypto.generateKeyPairSync("ec", options);
+const objects = ecPair({ namedCurve: "P-256" });
+console.log(typeof objects.privateKey, objects.privateKey.type, objects.publicKey.asymmetricKeyType);
+const pem = ecPair({
+ namedCurve: "P-256",
+ publicKeyEncoding: { type: "spki", format: "pem" },
+ privateKeyEncoding: { type: "pkcs8", format: "pem" },
+});
+console.log(typeof pem.privateKey, pem.publicKey.split("\n")[0]);
+
+function privateKeyKind(options) {
+ const pair = crypto.generateKeyPairSync("ec", options);
+ return typeof pair.privateKey;
+}
+console.log(privateKeyKind({ namedCurve: "P-384" }));
+
+function spread(options) {
+ const { privateKey, publicKey } = crypto.generateKeyPairSync("ec", {
+ namedCurve: "P-256",
+ ...options,
+ });
+ return [typeof privateKey, Buffer.isBuffer(publicKey)];
+}
+console.log(spread({}), spread({ publicKeyEncoding: { type: "spki", format: "der" } }));
+
+const digest = (encoding) => crypto.createHash("sha256").update("x").digest(encoding);
+console.log(typeof digest("hex"), Buffer.isBuffer(digest(undefined)), digest(undefined).length);
+const hmac = (encoding) => crypto.createHmac("sha256", "k").update("v").digest(encoding);
+console.log(hmac("base64"), Buffer.isBuffer(hmac()));
+function digestLength(encoding) {
+ let value = crypto.createHash("sha1").update("y").digest(encoding);
+ return Buffer.isBuffer(value) ? `buffer ${value.length}` : `string ${value.length}`;
+}
+console.log(digestLength(undefined), digestLength("hex"));
+async function digestLater(encoding) {
+ return crypto.createHash("md5").update("z").digest(encoding);
+}
+
+const key = crypto.generateKeyPairSync("ed25519").publicKey;
+const exported = (options) => key.export(options);
+console.log(
+ Buffer.isBuffer(exported({ format: "der", type: "spki" })),
+ typeof exported({ format: "pem", type: "spki" }),
+ exported({ format: "jwk" }).kty,
+);
+
+const file = path.join(fs.mkdtempSync(path.join(os.tmpdir(), "overloads-")), "text.txt");
+fs.writeFileSync(file, "hello");
+const read = (options) => fs.readFileSync(file, options);
+const text = read("utf8");
+const bytes = read(undefined);
+const latin1 = read({ encoding: "latin1" });
+console.log(text, Buffer.isBuffer(bytes), bytes.length, latin1);
+console.log(read("utf8"), read({ encoding: "hex" }));
+
+/**
+ * @overload
+ * @param {string} value
+ * @returns {string}
+ */
+/**
+ * @overload
+ * @param {number} value
+ * @returns {number}
+ */
+/** @param {string | number} value */
+function same(value) {
+ return value;
+}
+const viaSame = (value) => same(value);
+console.log(typeof viaSame("s"), typeof viaSame(3), viaSame(3) + 1);
+
+(async () => {
+ const later = await digestLater(undefined);
+ console.log(Buffer.isBuffer(later), typeof (await digestLater("hex")));
+ fs.rmSync(path.dirname(file), { recursive: true });
+})();
diff --git a/tests/corpus/7623-key-order-javascript-built-records/lib.js b/tests/corpus/7623-key-order-javascript-built-records/lib.js
index 4ff312846..a981505fc 100644
--- a/tests/corpus/7623-key-order-javascript-built-records/lib.js
+++ b/tests/corpus/7623-key-order-javascript-built-records/lib.js
@@ -1,11 +1,18 @@
// Records built in JavaScript in another key order than the TypeScript
// literals of the same type: a replacement method installed through a
-// computed key, and a function stored on the global object, read through a
-// typed view, that calls a callback inside its options record.
+// computed key (beside another computed write that installs a function
+// returning a primitive), and a function stored on the global object, read
+// through a typed view, that calls a callback inside its options record.
export function install(target, name) {
target[name] = () => ({ a: 1, b: 2 });
}
+// A later computed write of a function that returns a primitive does not
+// hide the record-returning one above.
+export function installLabel(target, name) {
+ target[name] = () => "label";
+}
+
globalThis.__eachRow = function (options) {
options.visit({ x: 5, y: 6 });
};
diff --git a/tests/corpus/7623-key-order-javascript-built-records/main.ts b/tests/corpus/7623-key-order-javascript-built-records/main.ts
index ae4876131..1615351d7 100644
--- a/tests/corpus/7623-key-order-javascript-built-records/main.ts
+++ b/tests/corpus/7623-key-order-javascript-built-records/main.ts
@@ -1,7 +1,7 @@
// Copies and enumerations of records JavaScript builds follow the order
// JavaScript wrote, also when the TypeScript literals of the type write
// another order and spread their own type. Each case uses its own type.
-import { install } from "./lib.js";
+import { install, installLabel } from "./lib.js";
interface Pair {
a: number;
@@ -39,6 +39,9 @@ showPair("method", store.get());
showPair("self copy", { ...store.get(), b: 3 });
install(store, "get");
showPair("replaced", store.get());
+const labels: { label(): string } = { label: () => "plain" };
+installLabel(labels, "label");
+console.log(labels.label());
// A JavaScript function, checked into a typed slot, calls a native callback
// it receives inside its options record with a record it builds.
diff --git a/tests/corpus/7624-key-order-boxed-callback-records/lib.js b/tests/corpus/7624-key-order-boxed-callback-records/lib.js
new file mode 100644
index 000000000..47ba5b99c
--- /dev/null
+++ b/tests/corpus/7624-key-order-boxed-callback-records/lib.js
@@ -0,0 +1,7 @@
+// A JavaScript function that calls a callback inside its options object
+// with records it builds, in another key order than the TypeScript literals
+// of the callback's parameter type.
+export function each(options) {
+ options.visit({ a: 1, b: 2 });
+ options.visit({ a: 3, b: 4 });
+}
diff --git a/tests/corpus/7624-key-order-boxed-callback-records/main.ts b/tests/corpus/7624-key-order-boxed-callback-records/main.ts
new file mode 100644
index 000000000..4cdfeff37
--- /dev/null
+++ b/tests/corpus/7624-key-order-boxed-callback-records/main.ts
@@ -0,0 +1,27 @@
+// A native callback handed to JavaScript inside an options record receives
+// records JavaScript builds: copies and enumerations of them follow the
+// order JavaScript wrote, also when the TypeScript literals of the type
+// write another order and spread their own type.
+import { each } from "./lib.js";
+
+interface Row {
+ a: number;
+ b: number;
+}
+
+function show(label: string, row: Row): void {
+ const copy = { ...row };
+ console.log(label, Object.keys(row).join(), JSON.stringify(copy), Object.keys(copy).join());
+}
+
+const literal: Row = { b: 20, a: 10 };
+show("literal", literal);
+show("self copy", { ...literal, b: 21 });
+const seen: Row[] = [];
+each({
+ visit: (row: Row) => {
+ seen.push(row);
+ show("callback", row);
+ },
+});
+console.log(JSON.stringify(seen), JSON.stringify(seen.map((row) => ({ ...row, a: row.a * 2 }))));
diff --git a/tests/corpus/7680-typed-array-search.ts b/tests/corpus/7680-typed-array-search.ts
new file mode 100644
index 000000000..7672c3712
--- /dev/null
+++ b/tests/corpus/7680-typed-array-search.ts
@@ -0,0 +1,70 @@
+// TypedArray.prototype.indexOf, lastIndexOf and includes on every numeric
+// element kind: strict equality (NaN never found) for indexOf/lastIndexOf,
+// SameValueZero (NaN found, -0 equals 0) for includes, and fromIndex as
+// ToIntegerOrInfinity, counted from the end when negative.
+const bytes = new Uint8Array([1, 2, 10, 3, 10, 255]);
+console.log("u8", bytes.indexOf(10), bytes.indexOf(10, 3), bytes.indexOf(10, 5), bytes.lastIndexOf(10));
+console.log("u8 from", bytes.indexOf(10, -2), bytes.indexOf(10, -100), bytes.indexOf(1, Infinity), bytes.indexOf(10, 2.9));
+console.log("u8 last", bytes.lastIndexOf(10, 3), bytes.lastIndexOf(10, -3), bytes.lastIndexOf(1, -100), bytes.lastIndexOf(255, 99));
+console.log("u8 values", bytes.indexOf(256 + 10), bytes.indexOf(-246), bytes.indexOf(10.5), bytes.includes(255), bytes.includes(0));
+console.log("u8 includes from", bytes.includes(1, 1), bytes.includes(255, -1), bytes.includes(2, -5));
+
+const clamped = new Uint8ClampedArray([0, 300, -5, 127.5]);
+console.log("u8c", clamped.indexOf(255), clamped.indexOf(0, 1), clamped.lastIndexOf(0), clamped.indexOf(128));
+
+const signed = new Int8Array([-1, 127, -128, 0]);
+console.log("i8", signed.indexOf(-1), signed.indexOf(255), signed.indexOf(-128), signed.lastIndexOf(0));
+
+const shorts = new Int16Array([5, -300, 5, 7]);
+const ushorts = new Uint16Array([65535, 1, 65535]);
+console.log("16", shorts.indexOf(-300), shorts.lastIndexOf(5), ushorts.indexOf(-1), ushorts.lastIndexOf(65535, 1));
+
+const ints = new Int32Array([-2147483648, 0, 2147483647, 0]);
+const uints = new Uint32Array([4294967295, 3]);
+console.log("32", ints.indexOf(2147483647), ints.lastIndexOf(0), uints.indexOf(4294967295), uints.includes(-1));
+
+const floats = new Float64Array([1.5, NaN, -0, 0, Infinity]);
+console.log("f64", floats.indexOf(NaN), floats.lastIndexOf(NaN), floats.includes(NaN), floats.indexOf(0), floats.indexOf(-0));
+console.log("f64 more", floats.lastIndexOf(0), floats.indexOf(Infinity), floats.includes(-Infinity), floats.includes(NaN, 2));
+
+const singles = new Float32Array([1.1, 0.5, NaN]);
+console.log("f32", singles.indexOf(1.1), singles.indexOf(Math.fround(1.1)), singles.indexOf(0.5), singles.includes(NaN), singles.indexOf(NaN));
+
+const empty = new Float64Array(0);
+console.log("empty", empty.indexOf(0), empty.lastIndexOf(0), empty.includes(0), empty.includes(0, -1));
+
+// Views search their own window of the buffer.
+const buffer = new ArrayBuffer(16);
+const whole = new Uint8Array(buffer);
+whole.set([9, 9, 1, 2, 9, 3, 9, 9]);
+const view = new Uint8Array(buffer, 2, 4);
+const sub = whole.subarray(3, 6);
+console.log("views", view.indexOf(9), view.lastIndexOf(9), view.indexOf(1), sub.indexOf(9), sub.includes(1), sub.lastIndexOf(3));
+const words = new Uint16Array(buffer, 2, 3);
+console.log("word view", words.indexOf(0x0201), words.length);
+
+// Shared memory searches the same way.
+const shared = new Int32Array(new SharedArrayBuffer(16));
+shared[1] = 7;
+shared[3] = 7;
+console.log("shared", shared.indexOf(7), shared.lastIndexOf(7), shared.includes(0), shared.indexOf(7, 2));
+
+// A loop that finds every occurrence, as a line splitter does.
+const text = new TextEncoder().encode("one\ntwo\n\nthree\n");
+const lines: number[] = [];
+for (let at = text.indexOf(10); at !== -1; at = text.indexOf(10, at + 1)) lines.push(at);
+console.log("lines", lines.join(","));
+
+// A Buffer passed as a Uint8Array keeps Buffer's search: a number needle
+// is taken as a byte, and a NaN byteOffset searches the whole buffer.
+function searchView(view: Uint8Array): string {
+ return [view.indexOf(97 + 256), view.lastIndexOf(98, NaN), view.includes(353), view.indexOf(98, -1)].join(",");
+}
+console.log(searchView(Buffer.from("abcab")), searchView(new Uint8Array([97, 98, 99, 97, 98])));
+
+// An explicit undefined fromIndex converts to NaN, which searches from 0:
+// lastIndexOf then looks at index 0 only.
+const values = new Int16Array([3, 1, 3, 2, 3]);
+const absent: number | undefined = process.argv.length > 99 ? 1 : undefined;
+console.log("undefined from", values.indexOf(3, undefined), values.lastIndexOf(3, undefined), values.lastIndexOf(1, undefined), values.includes(2, undefined));
+console.log("maybe undefined from", values.indexOf(3, absent), values.lastIndexOf(3, absent), values.includes(1, absent));
diff --git a/tests/corpus/7681-typed-array-search-dynamic.mjs b/tests/corpus/7681-typed-array-search-dynamic.mjs
new file mode 100644
index 000000000..643ba0cfa
--- /dev/null
+++ b/tests/corpus/7681-typed-array-search-dynamic.mjs
@@ -0,0 +1,28 @@
+// TypedArray search on untyped values (parameters of untyped functions and
+// object members): needles that are not numbers are never found (strict
+// equality), fromIndex may be undefined, a string or a fraction, and
+// Buffers keep their own indexOf, which wraps number needles and takes
+// strings.
+function views(bytes, floats, shorts) {
+ return { bytes, floats, shorts };
+}
+const { bytes, floats, shorts } = views(
+ new Uint8Array([1, 2, 10, 3, 10]),
+ new Float64Array([0.5, NaN, -0]),
+ new Int16Array([-1, 300, -1]),
+);
+console.log(bytes.indexOf(10), bytes.indexOf(10, 3), bytes.lastIndexOf(10), bytes.includes(3), bytes.includes(4));
+console.log(bytes.lastIndexOf(1, undefined), bytes.lastIndexOf(10), bytes.indexOf(10, undefined), bytes.includes(1, undefined));
+console.log(bytes.indexOf(10, '3'), bytes.indexOf(10, 2.9), bytes.indexOf(10, -1), bytes.lastIndexOf(10, -2), bytes.indexOf(10, null));
+console.log(bytes.indexOf('10'), bytes.includes('1'), bytes.indexOf(10n), bytes.indexOf(), bytes.includes(), bytes.indexOf(266));
+console.log(floats.indexOf(NaN), floats.includes(NaN), floats.indexOf(0), floats.lastIndexOf(-0), floats.includes(0.5, 1));
+console.log(shorts.indexOf(-1), shorts.lastIndexOf(-1), shorts.indexOf(300, -2), shorts.includes(65535));
+const holder = { view: bytes.subarray(2) };
+console.log(holder.view.indexOf(10), holder.view.lastIndexOf(10), holder.view.includes(1));
+const buffer = Buffer.from([1, 2, 10, 3, 10]);
+const boxed = [buffer];
+console.log(boxed[0].indexOf(10), boxed[0].indexOf(266), boxed[0].indexOf('\n'), boxed[0].includes(Buffer.from([3, 10])));
+console.log(boxed[0].subarray(3).indexOf(10), Buffer.isBuffer(boxed[0].subarray(3)), boxed[0].lastIndexOf(10, -2));
+let count = 0;
+for (let at = bytes.indexOf(10); at !== -1; at = bytes.indexOf(10, at + 1)) count++;
+console.log('count', count);
diff --git a/tests/corpus/7682-constructor-function-globals.js b/tests/corpus/7682-constructor-function-globals.js
new file mode 100644
index 000000000..58292e4ac
--- /dev/null
+++ b/tests/corpus/7682-constructor-function-globals.js
@@ -0,0 +1,55 @@
+// Module bindings that hold `new F()` of an ordinary constructor function
+// (const, let and var), read and reassigned by functions declared elsewhere
+// in the module, such as the head and tail of a linked list.
+'use strict';
+function Segment(prev) {
+ this.prev = prev;
+ this.next = null;
+ this.data = new Array(4);
+ this.pos = 0;
+}
+const head = new Segment();
+let current = head;
+var spare = new Segment(head);
+
+function append(value) {
+ if (current.pos === current.data.length) {
+ current = current.next || (current.next = new Segment(current));
+ }
+ current.data[current.pos++] = value;
+}
+
+function collect() {
+ const out = [];
+ for (let node = head; node; node = node.next) out.push(node.data.slice(0, node.pos).join(''));
+ return out.join('|');
+}
+
+function reset() {
+ current = head;
+ for (let node = head; node; node = node.next) node.pos = 0;
+}
+
+for (const ch of 'abcdefghij') append(ch);
+console.log(collect(), current === head, current.prev === head.next ? 'two' : 'more');
+reset();
+append('z');
+console.log(collect(), current === head, head.next !== null);
+console.log(spare.prev === head, spare instanceof Segment, typeof spare.data);
+spare = new Segment(spare);
+console.log(spare.prev.prev === head);
+
+function Counter(start) {
+ this.value = start;
+}
+Counter.prototype.bump = function () {
+ return ++this.value;
+};
+let counter = new Counter(10);
+function bumpTwice() {
+ counter.bump();
+ return counter.bump();
+}
+console.log(bumpTwice(), counter.value);
+counter = new Counter(0);
+console.log(bumpTwice());
diff --git a/tests/corpus/7745-typed-array-unions.ts b/tests/corpus/7745-typed-array-unions.ts
new file mode 100644
index 000000000..a7fc6f2cb
--- /dev/null
+++ b/tests/corpus/7745-typed-array-unions.ts
@@ -0,0 +1,93 @@
+// Unions of typed-array classes: every element operation runs on each arm.
+
+type UintArray = Uint8Array | Uint16Array | Uint32Array;
+type Samples = Int16Array | Float32Array | Float64Array;
+
+function uintArray(max: number, size: number): UintArray {
+ return max <= 2 ** 8
+ ? new Uint8Array(size)
+ : max <= 2 ** 16
+ ? new Uint16Array(size)
+ : new Uint32Array(size);
+}
+
+function samples(kind: number, size: number): Samples {
+ return kind === 0
+ ? new Int16Array(size)
+ : kind === 1
+ ? new Float32Array(size)
+ : new Float64Array(size);
+}
+
+function sum(xs: UintArray): number {
+ let total = 0;
+ for (let i = 0; i < xs.length; i++) total += xs[i]!;
+ return total;
+}
+
+function describe(xs: UintArray): string {
+ if (xs instanceof Uint8Array) return `u8 ${xs.length}`;
+ if (xs instanceof Uint16Array) return `u16 ${xs[0]}`;
+ return `u32 ${xs.BYTES_PER_ELEMENT}`;
+}
+
+for (const max of [200, 60000, 5000000]) {
+ const xs = uintArray(max, 6);
+ // Stores convert to each arm's element kind (wrapping past its range).
+ for (let i = 0; i < xs.length; i++) xs[i] = i * 70000 + 255;
+ console.log(xs.length, xs.byteLength, xs.byteOffset, xs.BYTES_PER_ELEMENT);
+ console.log(xs[0], xs[5], xs[6], xs[-1], sum(xs));
+ xs[1] += 3;
+ xs[2]++;
+ --xs[3];
+ xs[4] = -1;
+ xs[5] = 2.75;
+ console.log(xs[1], xs[2], xs[3], xs[4], xs[5]);
+ // Writes past the end are ignored, and reads there are undefined.
+ xs[10] = 9;
+ console.log(xs[10] === undefined, xs[10] ?? "missing");
+ let iterated = 0;
+ for (const v of xs) iterated += v;
+ console.log(iterated, [...xs].length, Array.from(xs)[1]);
+ const part = xs.subarray(1, 4);
+ part[0] = 42;
+ console.log(part.length, xs[1], xs.slice(2, 4).length, xs.slice(2, 4)[0] === xs[2]);
+ xs.fill(7, 4);
+ xs.copyWithin(0, 4, 5);
+ console.log(xs.join("-"), xs.toReversed()[0]);
+ xs.set([1, 2], 2);
+ console.log(xs[2], xs[3], describe(xs));
+}
+
+for (const kind of [0, 1, 2]) {
+ const s = samples(kind, 4);
+ s[0] = 1.5;
+ s[1] = -40000;
+ s[2] = 0.1;
+ s[3] = s[0]! * 3;
+ console.log(s.BYTES_PER_ELEMENT, s[0], s[1], s[2], s[3]);
+ console.log(s);
+ console.log(s instanceof Float32Array, s instanceof Int16Array);
+}
+
+// A stored value that reassigns the receiver or index stores into the
+// receiver and at the index evaluated before it.
+type Wide = Uint8Array | Uint16Array;
+function rebound(wide: boolean): void {
+ const before: Wide = wide ? new Uint16Array(4) : new Uint8Array(4);
+ let u: Wide = before;
+ let i = 0;
+ const swap = (): number => {
+ u = new Uint16Array(4);
+ i = 2;
+ return 300;
+ };
+ u[i] = swap();
+ console.log(before, u, i);
+ const target = u;
+ target[1] = 5;
+ u[i] += swap();
+ console.log(target, u, i);
+}
+rebound(false);
+rebound(true);
diff --git a/tests/corpus/7747-typed-array-class-values.ts b/tests/corpus/7747-typed-array-class-values.ts
new file mode 100644
index 000000000..523896c96
--- /dev/null
+++ b/tests/corpus/7747-typed-array-class-values.ts
@@ -0,0 +1,42 @@
+// Typed-array unions chosen through class values, and held in fields,
+// parameters, returns and records.
+
+type UintArray = Uint8Array | Uint16Array | Uint32Array;
+
+const pick = (max: number): { new (n: number): UintArray } =>
+ max <= 2 ** 8 ? Uint8Array : max <= 2 ** 16 ? Uint16Array : Uint32Array;
+
+class Stack {
+ heap: UintArray;
+ length = 0;
+ constructor(max: number) {
+ const Heap = pick(max);
+ this.heap = new Heap(max);
+ }
+ push(n: number): void {
+ this.heap[this.length++] = n;
+ }
+ pop(): number | undefined {
+ return this.heap[--this.length];
+ }
+}
+
+for (const max of [10, 1000, 100000]) {
+ const stack = new Stack(max);
+ for (let i = 1; i <= 4; i++) stack.push(i * 250);
+ console.log(stack.heap.BYTES_PER_ELEMENT, stack.pop(), stack.pop(), stack.length);
+}
+
+function grow(xs: UintArray, by: number): UintArray {
+ const Next = pick(xs.length * by);
+ const out = new Next(xs.length * by);
+ out.set(xs);
+ return out;
+}
+
+const record: { data: UintArray; label: string } = { data: new Uint8Array([1, 2, 3]), label: "r" };
+record.data = grow(record.data, 100);
+record.data[299] = 65535;
+console.log(record.label, record.data.length, record.data[0], record.data[2], record.data[299]);
+record.data = grow(record.data, 1000);
+console.log(record.data.BYTES_PER_ELEMENT, record.data.length, record.data[1]);
diff --git a/tests/corpus/7748-typed-array-number-array-unions.ts b/tests/corpus/7748-typed-array-number-array-unions.ts
new file mode 100644
index 000000000..706ed37b5
--- /dev/null
+++ b/tests/corpus/7748-typed-array-number-array-unions.ts
@@ -0,0 +1,78 @@
+// Unions of typed arrays and number arrays, as a cache picks its index
+// arrays by capacity: a linked list over next/prev index arrays.
+
+type NumberArray = Uint8Array | Uint16Array | Uint32Array | number[];
+
+function indexArray(max: number): NumberArray {
+ if (max <= 2 ** 8) return new Uint8Array(max);
+ if (max <= 2 ** 16) return new Uint16Array(max);
+ if (max <= 2 ** 32) return new Uint32Array(max);
+ return new Array(4).fill(0);
+}
+
+class Order {
+ next: NumberArray;
+ prev: NumberArray;
+ head = 0;
+ tail = 0;
+ size = 0;
+
+ constructor(max: number, force = false) {
+ this.next = force ? new Array(max).fill(0) : indexArray(max);
+ this.prev = force ? new Array(max).fill(0) : indexArray(max);
+ }
+
+ push(index: number): void {
+ if (this.size === 0) {
+ this.head = this.tail = index;
+ } else {
+ this.next[this.tail] = index;
+ this.prev[index] = this.tail;
+ this.tail = index;
+ }
+ this.size++;
+ }
+
+ moveToTail(index: number): void {
+ if (index === this.tail) return;
+ if (index === this.head) this.head = this.next[index]!;
+ else this.next[this.prev[index]!] = this.next[index]!;
+ this.prev[this.next[index]!] = this.prev[index]!;
+ this.next[this.tail] = index;
+ this.prev[index] = this.tail;
+ this.tail = index;
+ }
+
+ walk(): number[] {
+ const out: number[] = [];
+ for (let i = this.head, n = 0; n < this.size; n++, i = this.next[i]!) out.push(i);
+ return out;
+ }
+}
+
+for (const [max, force] of [
+ [8, false],
+ [300, false],
+ [70000, false],
+ [8, true],
+] as const) {
+ const order = new Order(max, force);
+ for (let i = 0; i < 6; i++) order.push(i);
+ order.moveToTail(0);
+ order.moveToTail(3);
+ order.moveToTail(5);
+ console.log(max, force, order.walk().join(","), order.next.length, order.prev[3]);
+}
+
+// The number-array arm reads holes and missing indexes as undefined,
+// and writes past its length grow it.
+const arms: NumberArray[] = [new Uint8Array(3), [1, 2, 3]];
+for (const a of arms) {
+ a[1] = 300;
+ a[2] += 0.5;
+ a[5] = 8;
+ console.log(a.length, a[1], a[2], a[4], a[5]);
+ let total = 0;
+ for (const v of a) total += v ?? -1;
+ console.log(total, [...a].length, Array.from(a).length);
+}
diff --git a/tests/corpus/7749-typed-array-element-searches.ts b/tests/corpus/7749-typed-array-element-searches.ts
new file mode 100644
index 000000000..7cae267ec
--- /dev/null
+++ b/tests/corpus/7749-typed-array-element-searches.ts
@@ -0,0 +1,22 @@
+// TypedArray indexOf, lastIndexOf and includes on every element kind, on
+// Buffers (their own search), and on unions of typed arrays: strict
+// equality for indexOf/lastIndexOf, SameValueZero for includes.
+const f = new Float64Array([1, NaN, -0, 0, 2.5, NaN, 1]);
+console.log(f.indexOf(NaN), f.includes(NaN), f.lastIndexOf(NaN));
+console.log(f.indexOf(0), f.indexOf(-0), f.lastIndexOf(0), f.lastIndexOf(-0), f.includes(-0));
+console.log(f.indexOf(1, 1), f.indexOf(1, -1), f.indexOf(1, -100), f.indexOf(1, 100), f.indexOf(1, 1.9));
+console.log(f.lastIndexOf(1, 5), f.lastIndexOf(1, -2), f.lastIndexOf(1, -100), f.lastIndexOf(1, 100), f.lastIndexOf(1, undefined));
+console.log(f.includes(2.5, 4), f.includes(2.5, 5), f.includes(2.5, -3), f.includes(NaN, 6));
+const u = new Uint8Array([0, 255, 3, 255]);
+console.log(u.indexOf(255), u.indexOf(-1), u.indexOf(511), u.indexOf(3.5), u.includes(NaN), u.lastIndexOf(255));
+const i = new Int16Array([-5, 7, -5]);
+console.log(i.indexOf(-5), i.lastIndexOf(-5), i.includes(7), i.indexOf(65531));
+const h = new Float32Array([0.1, 0.5, 16777217]);
+console.log(h.indexOf(0.1), h.indexOf(0.5), h.includes(16777216), h.indexOf(16777217));
+const c = new Uint8ClampedArray([300, -4]);
+console.log(c.indexOf(255), c.indexOf(0), new Uint32Array(0).indexOf(0), new Uint32Array(0).includes(0));
+const b = Buffer.from([1, 2, 3, 2]);
+console.log(b.indexOf(2), b.indexOf(258), b.lastIndexOf(2), b.includes("\u0003"));
+type UintArray = Uint8Array | Uint16Array | Uint32Array;
+const mk = (n: number): UintArray => (n > 1 ? new Uint16Array([1, 700, 1]) : new Uint8Array([1, 188, 1]));
+for (const x of [mk(1), mk(2)]) console.log(x.indexOf(700), x.lastIndexOf(1), x.includes(188), x.indexOf(1, 1));
diff --git a/tests/corpus/7750-typed-array-union-inspect.ts b/tests/corpus/7750-typed-array-union-inspect.ts
new file mode 100644
index 000000000..aebb0dbdf
--- /dev/null
+++ b/tests/corpus/7750-typed-array-union-inspect.ts
@@ -0,0 +1,14 @@
+// Printing u8 byte views whose static type does not say what they are:
+// union arms, elements and fields print as Node does by the value's kind.
+type UintArray = Uint8Array | Uint16Array | Uint32Array;
+const mk = (n: number): UintArray => (n > 1 ? new Uint16Array([1, 700]) : new Uint8Array([1, 188]));
+const a = mk(1), b = mk(2);
+console.log(a, b);
+console.log([a, b]);
+const bufArm: Uint8Array | Uint16Array = Buffer.from([1, 2, 3]);
+console.log(bufArm);
+const u8s: Uint8Array[] = [new Uint8Array([9]), Buffer.from([10, 11])];
+console.log(u8s);
+const rec: { bytes: Uint8Array; view: DataView } = { bytes: new Uint8Array(2), view: new DataView(new ArrayBuffer(2)) };
+console.log(rec);
+console.log(new Uint8Array([5, 6]), Buffer.from([7]));
diff --git a/tests/corpus/7796-boxed-subclass-identity.js b/tests/corpus/7796-boxed-subclass-identity.js
new file mode 100644
index 000000000..12ed8b3c5
--- /dev/null
+++ b/tests/corpus/7796-boxed-subclass-identity.js
@@ -0,0 +1,106 @@
+"use strict";
+// Class instances of a hierarchy cross into untyped code through their
+// base type many times. Each crossing names the same object: identity,
+// Map, Set and WeakMap keys, properties added through untyped code, and
+// field writes from either side stay one object, and every subclass keeps
+// its own members, as in Node.
+
+class Shape {
+ constructor(name) {
+ this.name = name;
+ this.hits = 0;
+ }
+ area() {
+ return 0;
+ }
+ describe() {
+ return `${this.name}:${this.area()}`;
+ }
+}
+
+class Square extends Shape {
+ constructor(side) {
+ super("square");
+ this.side = side;
+ }
+ area() {
+ return this.side * this.side;
+ }
+}
+
+class Cube extends Square {
+ constructor(side) {
+ super(side);
+ this.name = "cube";
+ }
+ area() {
+ return 6 * super.area();
+ }
+ get volume() {
+ return this.side ** 3;
+ }
+}
+
+class Circle extends Shape {
+ constructor(r) {
+ super("circle");
+ this.r = r;
+ }
+ area() {
+ return Math.round(Math.PI * this.r * this.r);
+ }
+}
+
+/** @param {any} value */
+function untyped(value) {
+ return value;
+}
+/** @param {any} value @param {string} key @param {unknown} extra */
+function tag(value, key, extra) {
+ value[key] = extra;
+ value.hits++;
+ return value;
+}
+
+/** @param {Shape} shape */
+function crossAsBase(shape) {
+ return untyped(shape);
+}
+
+const shapes = [new Shape("plain"), new Square(2), new Cube(3), new Circle(1)];
+const seen = new Map();
+const weak = new WeakMap();
+const set = new Set();
+for (let round = 0; round < 50; round++) {
+ for (const shape of shapes) {
+ const boxed = crossAsBase(shape);
+ if (round === 0) {
+ seen.set(boxed, shape.name);
+ weak.set(boxed, round);
+ }
+ set.add(boxed);
+ if (boxed !== shape || untyped(boxed) !== crossAsBase(shape)) console.log("identity lost", shape.name);
+ }
+}
+console.log(seen.size, set.size, shapes.map((s) => seen.get(crossAsBase(s))).join(","), shapes.every((s) => weak.has(untyped(s))));
+
+for (const shape of shapes) {
+ const boxed = crossAsBase(shape);
+ tag(boxed, "color", shape.name.length);
+ shape.hits += 10;
+ console.log(boxed.describe(), boxed.area(), boxed.hits, shape.hits, boxed.color, Object.keys(boxed).join(","));
+ console.log(boxed instanceof Shape, boxed instanceof Square, boxed instanceof Cube, boxed instanceof Circle, boxed.constructor.name);
+}
+
+/** @type {Shape} */
+const asBase = shapes[2];
+const cube = crossAsBase(asBase);
+console.log(cube.volume, JSON.stringify(cube), "side" in cube, "volume" in cube, Object.prototype.hasOwnProperty.call(cube, "volume"));
+/** @type {any} */ (shapes[1]).side = 5;
+console.log(crossAsBase(shapes[1]).area(), shapes[1].area(), untyped(shapes[1]).side);
+
+const many = [];
+for (let i = 0; i < 1000; i++) many.push(crossAsBase(i % 2 ? new Square(i) : new Circle(i)));
+let squares = 0;
+for (const item of many) if (item instanceof Square) squares += item.side;
+console.log(many.length, squares, many[999].describe(), many[998].describe());
diff --git a/tests/corpus/7982-integer-view-compares.ts b/tests/corpus/7982-integer-view-compares.ts
new file mode 100644
index 000000000..5978eaf86
--- /dev/null
+++ b/tests/corpus/7982-integer-view-compares.ts
@@ -0,0 +1,85 @@
+// Loops whose integer locals take part in compares and typed-array indexes as
+// `i + 1`, `i - 1` and `2 * i + 1`: heap sift-downs, neighbor reads at both
+// ends of an array, a binary search, and counters near 2^53, where the sum
+// of a counter and a constant is still an exact double.
+
+function siftDown(a: Int32Array, start: number, end: number): void {
+ let root = start;
+ while (2 * root + 1 < end) {
+ let child = 2 * root + 1;
+ if (child + 1 < end && a[child]! < a[child + 1]!) child++;
+ if (a[root]! >= a[child]!) return;
+ const t = a[root]!;
+ a[root] = a[child]!;
+ a[child] = t;
+ root = child;
+ }
+}
+
+function heapSort(a: Int32Array): string {
+ for (let start = (a.length >> 1) - 1; start >= 0; start--) siftDown(a, start, a.length);
+ for (let end = a.length - 1; end > 0; end--) {
+ const t = a[0]!;
+ a[0] = a[end]!;
+ a[end] = t;
+ siftDown(a, 0, end);
+ }
+ return Array.from(a).join();
+}
+
+function neighbors(a: Float64Array): string {
+ const out: number[] = [];
+ let i = 0;
+ while (i < a.length) {
+ out.push(a[i - 1]! + a[i + 1]!);
+ i = i + 1;
+ }
+ return out.join();
+}
+
+function lowerBound(a: Int32Array, value: number): number {
+ let lo = 0;
+ let hi = a.length;
+ while (lo < hi) {
+ const mid = lo + ((hi - lo) >> 1);
+ if (a[mid]! < value) lo = mid + 1;
+ else hi = mid;
+ }
+ return lo;
+}
+
+function nearLimit(start: number): string {
+ const seen: string[] = [];
+ let i = start;
+ let n = 0;
+ while (n < 4) {
+ seen.push(`${i + 1 > 9007199254740991}:${i + 1 === i + 2}:${i - 1 < i}`);
+ i = i + 1;
+ n = n + 1;
+ }
+ return seen.join(" ");
+}
+
+function atLimit(start: number, end: number): string {
+ // A counter of integer locals only, up to 2^53 - 1: `i + 2` may be
+ // 2^53 + 1, whose double is 2^53, so `i + 1 === i + 2` holds at the end.
+ let same = 0;
+ let above = 0;
+ let mixed = 0;
+ for (let i = start; i <= end; i++) {
+ if (i + 1 === i + 2) same++;
+ if (i + 2 > 9007199254740992) above++;
+ mixed += (i * 2) % 3;
+ }
+ return `${same} ${above} ${mixed}`;
+}
+
+console.log(heapSort(new Int32Array([5, -3, 9, 0, 9, 2147483647, -2147483648, 7, 1])));
+console.log(heapSort(new Int32Array([])), heapSort(new Int32Array([4])));
+console.log(neighbors(new Float64Array([1, 2, 3, 4])));
+const sorted = new Int32Array([-5, -1, 0, 0, 3, 8, 8, 13]);
+console.log([-6, -1, 0, 1, 8, 13, 14].map((v) => lowerBound(sorted, v)).join());
+console.log(nearLimit(9007199254740988));
+console.log(nearLimit(-9007199254740991));
+console.log(atLimit(9007199254740988, 9007199254740991));
+console.log(atLimit(-9007199254740991, -9007199254740988));
diff --git a/tests/corpus/8112-error-factory-own-key-order.js b/tests/corpus/8112-error-factory-own-key-order.js
new file mode 100644
index 000000000..3e7cf277a
--- /dev/null
+++ b/tests/corpus/8112-error-factory-own-key-order.js
@@ -0,0 +1,73 @@
+"use strict";
+// Errors built by a constructor factory, the way zod 4 builds ZodError: a
+// class extending the parent passed in, then an initializer that assigns
+// `name`, defines non-enumerable fields and an enumerable `message`
+// accessor, and a second initializer that reassigns `name`. JSON and
+// Object.keys list `name` before `message`, as they were created, and
+// `name` holds the last assignment.
+
+const messageDescriptor = {
+ get() {
+ return this.state.message ?? JSON.stringify(this.state.issues);
+ },
+ set(value) {
+ this.state.message = value;
+ },
+ enumerable: true,
+ configurable: true,
+};
+
+function factory(name, initializer, parent) {
+ const Parent = parent ?? Object;
+ class Definition extends Parent {}
+ Object.defineProperty(Definition, "name", { value: name });
+ function construct(def) {
+ const instance = parent ? new Definition() : this;
+ Object.defineProperty(instance, "state", { value: { issues: def }, enumerable: false });
+ initializer(instance, def);
+ return instance;
+ }
+ Object.defineProperty(construct, "init", { value: initializer });
+ Object.defineProperty(construct, "name", { value: name });
+ return construct;
+}
+
+const baseInit = (instance, def) => {
+ instance.name = "$BaseError";
+ Object.defineProperty(instance, "issues", { value: def, enumerable: false });
+ Object.defineProperty(instance, "message", messageDescriptor);
+};
+const BaseError = factory("$BaseError", baseInit, Error);
+const ReportError = factory(
+ "ReportError",
+ (instance, def) => {
+ BaseError.init(instance, def);
+ instance.name = "ReportError";
+ },
+ Error,
+);
+
+const report = new ReportError([{ code: "too_small", path: ["title"] }]);
+console.log(report.name, report instanceof Error, Object.keys(report).join(","));
+console.log(JSON.stringify(report));
+console.log(JSON.stringify({ success: false, error: report }));
+report.message = "replaced";
+console.log(JSON.stringify(report), report.message);
+report.name = "Renamed";
+console.log(JSON.stringify(report), report.name);
+
+const base = new BaseError([1, 2]);
+console.log(base.name, Object.keys(base).join(","), JSON.stringify(base));
+
+// The same steps on a class extending Error directly.
+function build(flag) {
+ class Direct extends Error {}
+ const instance = flag ? new Direct() : {};
+ instance.name = "First";
+ Object.defineProperty(instance, "message", messageDescriptor);
+ Object.defineProperty(instance, "state", { value: { issues: ["i"] }, enumerable: false });
+ instance.name = "Second";
+ return instance;
+}
+const direct = build(true);
+console.log(Object.keys(direct).join(","), JSON.stringify(direct), direct.name);
diff --git a/tests/corpus/8113-error-class-field-before-name.js b/tests/corpus/8113-error-class-field-before-name.js
new file mode 100644
index 000000000..e10a9456b
--- /dev/null
+++ b/tests/corpus/8113-error-class-field-before-name.js
@@ -0,0 +1,78 @@
+"use strict";
+// Error subclasses with a property bag that a later dynamic definition or
+// write commits. Object.keys and JSON keep the order in which an instance
+// created its properties: a class field before an own `name` that is
+// assigned after construction, a `name` a constructor assigns before a
+// later property, and a `message` that Error's constructor created before
+// all of them.
+
+const messageDescriptor = {
+ get() {
+ return "described";
+ },
+ enumerable: true,
+ configurable: true,
+};
+
+// A factory-built class: the field, then an initializer that assigns `name`
+// and defines `message`.
+function factory(initializer) {
+ class Definition extends Error {
+ code = "E_FIELD";
+ }
+ return function construct() {
+ const instance = new Definition();
+ initializer(instance);
+ return instance;
+ };
+}
+const Fielded = factory((instance) => {
+ instance.name = "Fielded";
+ Object.defineProperty(instance, "message", messageDescriptor);
+});
+const fielded = new Fielded();
+console.log(Object.keys(fielded).join(","), JSON.stringify(fielded));
+fielded.extra = 1;
+console.log(Object.keys(fielded).join(","), JSON.stringify(fielded));
+fielded.name = "Renamed";
+console.log(Object.keys(fielded).join(","), JSON.stringify(fielded), fielded.name);
+
+// A class whose constructor passes a message to Error and then assigns
+// `name`; the message is redefined after construction.
+class Direct extends Error {
+ code = "E_DIRECT";
+ constructor() {
+ super("constructed");
+ this.name = "Direct";
+ }
+}
+function redefine(instance) {
+ Object.defineProperty(instance, "message", messageDescriptor);
+ instance.extra = 2;
+ return instance;
+}
+const direct = redefine(new Direct());
+console.log(Object.keys(direct).join(","), JSON.stringify(direct), direct.name);
+
+// Constructors that assign `name` and then another property, which their
+// instances create in that order, with and without a message passed to
+// Error, before the message is redefined.
+class Named extends Error {
+ constructor() {
+ super();
+ this.name = "Named";
+ this.detail = "named";
+ }
+}
+const named = redefine(new Named());
+console.log(Object.keys(named).join(","), JSON.stringify(named), named.name);
+
+class NamedMessage extends Error {
+ constructor() {
+ super("constructed");
+ this.name = "NamedMessage";
+ this.detail = "named";
+ }
+}
+const namedMessage = redefine(new NamedMessage());
+console.log(Object.keys(namedMessage).join(","), JSON.stringify(namedMessage), namedMessage.name);
diff --git a/tests/corpus/8160-assert-expected-from-fields.cjs b/tests/corpus/8160-assert-expected-from-fields.cjs
new file mode 100644
index 000000000..4c16bd78f
--- /dev/null
+++ b/tests/corpus/8160-assert-expected-from-fields.cjs
@@ -0,0 +1,58 @@
+// Expected errors stored in an object literal's fields: a JavaScript
+// object keeps its fields checked-dynamic, and assert.throws, assert.match
+// and the expected-object form convert them to the regex or string the
+// fields hold.
+"use strict";
+const assert = require("assert");
+
+const errMessages = {
+ auth: / auth/,
+ state: / state/,
+ length: /Invalid initialization vector/,
+ code: "ERR_CRYPTO_INVALID_IV",
+};
+
+const error = (message, code) => {
+ const e = new Error(message);
+ if (code) e.code = code;
+ return e;
+};
+
+assert.throws(() => {
+ throw error("Unsupported state or unable to authenticate data");
+}, errMessages.auth);
+assert.throws(() => {
+ throw error("Invalid state for operation getAuthTag");
+}, errMessages.state);
+assert.throws(
+ () => {
+ throw error("Invalid initialization vector", "ERR_CRYPTO_INVALID_IV");
+ },
+ { code: errMessages.code, message: errMessages.length },
+);
+assert.match("unable to auth", errMessages.auth);
+assert.doesNotMatch("no match here", errMessages.state);
+
+const report = (label, run) => {
+ try {
+ run();
+ console.log(label, "passed");
+ } catch (error) {
+ console.log(label, error.name, error.code, error.message.split("\n")[0]);
+ }
+};
+report("wrong message", () =>
+ assert.throws(() => {
+ throw error("something else");
+ }, errMessages.length),
+);
+report("wrong code", () =>
+ assert.throws(
+ () => {
+ throw error("Invalid initialization vector", "ERR_OTHER");
+ },
+ { code: errMessages.code },
+ ),
+);
+report("match", () => assert.match("plain text", errMessages.auth));
+console.log("done");
diff --git a/tests/corpus/split-cjs-require-binding/chain-wrap.cjs b/tests/corpus/split-cjs-require-binding/chain-wrap.cjs
new file mode 100644
index 000000000..435dcaeab
--- /dev/null
+++ b/tests/corpus/split-cjs-require-binding/chain-wrap.cjs
@@ -0,0 +1,3 @@
+var chained;
+chained = require("./chain.cjs");
+module.exports = chained;
diff --git a/tests/corpus/split-cjs-require-binding/chain.cjs b/tests/corpus/split-cjs-require-binding/chain.cjs
new file mode 100644
index 000000000..a279f456e
--- /dev/null
+++ b/tests/corpus/split-cjs-require-binding/chain.cjs
@@ -0,0 +1 @@
+module.exports = require("./whole.cjs");
diff --git a/tests/corpus/split-cjs-require-binding/lib.cjs b/tests/corpus/split-cjs-require-binding/lib.cjs
new file mode 100644
index 000000000..b318eb85c
--- /dev/null
+++ b/tests/corpus/split-cjs-require-binding/lib.cjs
@@ -0,0 +1,5 @@
+console.log("lib: init");
+exports.n = 7;
+exports.f = function (x) {
+ return x * 2;
+};
diff --git a/tests/corpus/split-cjs-require-binding/main.cjs b/tests/corpus/split-cjs-require-binding/main.cjs
new file mode 100644
index 000000000..974421bd3
--- /dev/null
+++ b/tests/corpus/split-cjs-require-binding/main.cjs
@@ -0,0 +1,55 @@
+console.log("main: start");
+var lib, whole;
+lib = require("./lib.cjs");
+whole = require("./whole.cjs");
+console.log("main: after", lib.n, lib.f(3), whole(4));
+exports.fromLib = lib.f;
+exports.whole = whole;
+console.log("main: exported", exports.fromLib(5), whole(6));
+console.log("main: identity", exports.fromLib === lib.f, exports.whole === whole);
+const re = require("./reexport.cjs");
+console.log("main: reexport", re(7));
+let later;
+later = require("./lib.cjs");
+console.log("main: let", later.n);
+const rx = require("./reexport-extra.cjs");
+console.log("main: reexport extra", rx(8), rx.extra);
+const splitObject = { whole };
+console.log("main: shorthand value", splitObject.whole(10));
+var libAgain;
+libAgain = require("./lib.cjs");
+const { n: splitN, f: splitF } = libAgain;
+console.log("main: destructured split", splitN, splitF(4));
+var chained;
+chained = require("./chain-wrap.cjs");
+console.log("main: chained", chained(11));
+
+// Any later write, including a destructuring target, keeps ordinary storage.
+var shorthand;
+shorthand = require("./whole.cjs");
+({ shorthand } = { shorthand: "shorthand" });
+console.log("main: shorthand write", shorthand);
+var renamed;
+renamed = require("./whole.cjs");
+({ value: renamed } = { value: "renamed" });
+console.log("main: renamed write", renamed);
+var nested;
+nested = require("./whole.cjs");
+({ a: { nested } } = { a: { nested: "nested" } });
+console.log("main: nested write", nested);
+var rest;
+rest = require("./whole.cjs");
+({ ...rest } = { a: 1, b: 2 });
+console.log("main: rest write", rest.a, rest.b);
+var array;
+array = require("./whole.cjs");
+[array] = ["array"];
+console.log("main: array write", array);
+var arrayRest;
+arrayRest = require("./whole.cjs");
+[...arrayRest] = [1, 2];
+console.log("main: array-rest write", arrayRest[0], arrayRest[1]);
+var each;
+each = require("./whole.cjs");
+for ({ each } of [{ each: "each" }]) {}
+console.log("main: for-of write", each);
diff --git a/tests/corpus/split-cjs-require-binding/package.json b/tests/corpus/split-cjs-require-binding/package.json
new file mode 100644
index 000000000..a3c15a7a6
--- /dev/null
+++ b/tests/corpus/split-cjs-require-binding/package.json
@@ -0,0 +1 @@
+{ "type": "commonjs" }
diff --git a/tests/corpus/split-cjs-require-binding/reexport-extra.cjs b/tests/corpus/split-cjs-require-binding/reexport-extra.cjs
new file mode 100644
index 000000000..584de98fe
--- /dev/null
+++ b/tests/corpus/split-cjs-require-binding/reexport-extra.cjs
@@ -0,0 +1,6 @@
+var whole;
+whole = require("./whole.cjs");
+console.log("extra: before", whole.extra, whole(9));
+module.exports = whole;
+module.exports.extra = 1;
+console.log("extra: after", whole.extra, module.exports.extra);
diff --git a/tests/corpus/split-cjs-require-binding/reexport.cjs b/tests/corpus/split-cjs-require-binding/reexport.cjs
new file mode 100644
index 000000000..9a931bdef
--- /dev/null
+++ b/tests/corpus/split-cjs-require-binding/reexport.cjs
@@ -0,0 +1,4 @@
+console.log("reexport: init");
+var whole;
+whole = require("./whole.cjs");
+module.exports = whole;
diff --git a/tests/corpus/split-cjs-require-binding/whole.cjs b/tests/corpus/split-cjs-require-binding/whole.cjs
new file mode 100644
index 000000000..31b476c4e
--- /dev/null
+++ b/tests/corpus/split-cjs-require-binding/whole.cjs
@@ -0,0 +1,4 @@
+console.log("whole: init");
+module.exports = function whole(x) {
+ return x + 1;
+};
diff --git a/tests/corpus/split-cjs-require-cycle/even.cjs b/tests/corpus/split-cjs-require-cycle/even.cjs
new file mode 100644
index 000000000..94d74ea87
--- /dev/null
+++ b/tests/corpus/split-cjs-require-cycle/even.cjs
@@ -0,0 +1,10 @@
+var odd;
+exports.name = "even";
+exports.isEven = function (n) {
+ return n === 0 ? true : odd.isOdd(n - 1);
+};
+exports.describe = function (n) {
+ return n + " is " + (exports.isEven(n) ? "even" : "odd") + " (asked " + odd.name + ")";
+};
+odd = require("./odd.cjs");
+console.log("even: loaded");
diff --git a/tests/corpus/split-cjs-require-cycle/main.cjs b/tests/corpus/split-cjs-require-cycle/main.cjs
new file mode 100644
index 000000000..9ec7997db
--- /dev/null
+++ b/tests/corpus/split-cjs-require-cycle/main.cjs
@@ -0,0 +1,12 @@
+// Split require bindings (`var m; m = require("./x.cjs")`) that close a
+// CommonJS cycle: each module publishes its exports before its require, and
+// reads the other module's binding only in functions that run after both
+// modules finish, the cycle shape the const form compiles too. One
+// assignment parenthesizes its require.
+console.log("main: start");
+var even, odd;
+even = require("./even.cjs");
+odd = require("./odd.cjs");
+console.log(even.isEven(10), odd.isOdd(7), even.isEven(3));
+console.log(even.describe(4), "|", even.describe(9));
+console.log("odd calls", odd.calls(), even.name, odd.name);
diff --git a/tests/corpus/split-cjs-require-cycle/odd.cjs b/tests/corpus/split-cjs-require-cycle/odd.cjs
new file mode 100644
index 000000000..d449c28e8
--- /dev/null
+++ b/tests/corpus/split-cjs-require-cycle/odd.cjs
@@ -0,0 +1,12 @@
+var even;
+let calls = 0;
+exports.name = "odd";
+exports.isOdd = function (n) {
+ calls++;
+ return n === 0 ? false : even.isEven(n - 1);
+};
+exports.calls = function () {
+ return calls;
+};
+even = (require("./even.cjs"));
+console.log("odd: loaded");
diff --git a/tests/corpus/split-cjs-require-cycle/package.json b/tests/corpus/split-cjs-require-cycle/package.json
new file mode 100644
index 000000000..a3c15a7a6
--- /dev/null
+++ b/tests/corpus/split-cjs-require-cycle/package.json
@@ -0,0 +1 @@
+{ "type": "commonjs" }
diff --git a/tests/corpus/typed-array-masked-index-stores.ts b/tests/corpus/typed-array-masked-index-stores.ts
new file mode 100644
index 000000000..88a25b4c5
--- /dev/null
+++ b/tests/corpus/typed-array-masked-index-stores.ts
@@ -0,0 +1,40 @@
+// Several typed array accesses in one counted loop share an index that is
+// not an affine function of the counter (a masked or remaindered counter).
+// The loop versioning analysis records that such an index has no affine
+// shape and reads the recorded result again at each later access.
+const length = 64;
+const bytes = new Uint8Array(length);
+const words = new Int32Array(16);
+const totals = new Float64Array(16);
+
+for (let i = 0; i < bytes.length; i++) {
+ const slot = i & 15;
+ words[slot] = words[slot]! + i;
+ totals[slot] = totals[slot]! + i * 0.5;
+ bytes[i] = slot;
+}
+console.log(Array.from(words).join(","));
+console.log(Array.from(totals).join(","));
+console.log(bytes.reduce((sum, value) => sum + value, 0));
+
+const ring = new Uint16Array(8);
+const seen = new Uint8Array(8);
+for (let i = 0; i < length; i++) {
+ const at = i % 8;
+ ring[at] = ring[at]! * 3 + i;
+ seen[at] = seen[at]! + 1;
+}
+console.log(Array.from(ring).join(","), Array.from(seen).join(","));
+
+function scatter(source: Uint8Array, mask: number): Uint32Array {
+ const out = new Uint32Array(mask + 1);
+ const counts = new Uint32Array(mask + 1);
+ for (let i = 0; i < source.length; i++) {
+ const bucket = (source[i]! + i) & mask;
+ out[bucket] = out[bucket]! + source[i]!;
+ counts[bucket] = counts[bucket]! + 1;
+ }
+ for (let i = 0; i < out.length; i++) out[i] = out[i]! * 10 + counts[i]!;
+ return out;
+}
+console.log(Array.from(scatter(bytes, 7)).join(","));
diff --git a/tests/diagnostics/fs-url-path-fences.ts b/tests/diagnostics/fs-url-path-fences.ts
index 080c561e1..6b65bfac0 100644
--- a/tests/diagnostics/fs-url-path-fences.ts
+++ b/tests/diagnostics/fs-url-path-fences.ts
@@ -1,15 +1,16 @@
// URL paths whose conversion would run in a different order than Node's.
import * as fs from "node:fs";
+import * as fsp from "node:fs/promises";
const url = new URL("file:///tmp/fs-url-path-fences");
-const encoding: BufferEncoding = process.argv.length > 9 ? "latin1" : "utf8";
const recursive = process.argv.length > 9;
-// Node validates the string source before converting the URL destination.
-fs.copyFileSync("/tmp/source", url);
// Node validates the options before the path.
-console.log(fs.readFileSync(url, encoding));
fs.mkdirSync(url, { recursive });
+void fsp.mkdir(url, { recursive });
// A literal's members are lowered one by one, so they cannot be evaluated
// ahead of the conversion as one value.
console.log(fs.statSync(url, { throwIfNoEntry: process.argv.length > 9 }));
+// fs.promises checks the string source inside the call, after the
+// conversion would have run.
+void fsp.copyFile("/tmp/source", url);
diff --git a/tests/diagnostics/index-signature-values.ts b/tests/diagnostics/index-signature-values.ts
index 6c53b141c..1f3a01ba5 100644
--- a/tests/diagnostics/index-signature-values.ts
+++ b/tests/diagnostics/index-signature-values.ts
@@ -8,10 +8,6 @@
const bySymbol: { [s: symbol]: number } = {};
console.log(bySymbol);
-// Storage is supported, but composite typed-array inspection is not.
-const buffers: Record = {};
-console.log(buffers);
-
// Dual signatures whose value types intern DIFFERENTLY cannot share the
// one store (tsc requires only assignability, but the store gives one
// answer per key).
diff --git a/tests/diagnostics/split-cjs-require-cycle/b.js b/tests/diagnostics/split-cjs-require-cycle/b.js
new file mode 100644
index 000000000..ad5a7441a
--- /dev/null
+++ b/tests/diagnostics/split-cjs-require-cycle/b.js
@@ -0,0 +1,7 @@
+var a;
+a = require('./main.js');
+exports.fromB = 'b-value';
+exports.readA = function () {
+ return a.early + '/' + a.late;
+};
+console.log('b sees a.early:', a.early, 'a.late:', a.late);
diff --git a/tests/diagnostics/split-cjs-require-cycle/main.js b/tests/diagnostics/split-cjs-require-cycle/main.js
new file mode 100644
index 000000000..286de459a
--- /dev/null
+++ b/tests/diagnostics/split-cjs-require-cycle/main.js
@@ -0,0 +1,11 @@
+// A split require binding (`var b; b = require("./b.js")`) whose require
+// closes a CommonJS cycle that top-level code can observe: b.js reads a.js's
+// partial exports while a.js is still evaluating, which the compiled export
+// table does not model. Preflight refuses it with SC1016, as it refuses the
+// `const b = require("./b.js")` form; tests/corpus/split-cjs-require-cycle
+// compiles the cycle shape both forms admit.
+exports.early = 'a-early';
+var b;
+b = require('./b.js');
+exports.late = 'a-late';
+console.log('a sees', b.fromB, b.readA());
diff --git a/tests/diagnostics/split-cjs-require-cycle/package.json b/tests/diagnostics/split-cjs-require-cycle/package.json
new file mode 100644
index 000000000..a3c15a7a6
--- /dev/null
+++ b/tests/diagnostics/split-cjs-require-cycle/package.json
@@ -0,0 +1 @@
+{ "type": "commonjs" }
diff --git a/tests/diagnostics/split-cjs-require-early/lib.js b/tests/diagnostics/split-cjs-require-early/lib.js
new file mode 100644
index 000000000..2a9669e12
--- /dev/null
+++ b/tests/diagnostics/split-cjs-require-early/lib.js
@@ -0,0 +1,8 @@
+'use strict';
+
+/** @param {string} who */
+function greet(who) {
+ return `hello ${who}`;
+}
+
+module.exports = { greet };
diff --git a/tests/diagnostics/split-cjs-require-early/main.js b/tests/diagnostics/split-cjs-require-early/main.js
new file mode 100644
index 000000000..ae1d8d770
--- /dev/null
+++ b/tests/diagnostics/split-cjs-require-early/main.js
@@ -0,0 +1,21 @@
+// A split require binding (`let lib;` then `lib = require(...)`) that code
+// above the assignment can reach: Node reads undefined there, a value the
+// compiled module alias cannot hold, so the assignment keeps a named SC1013
+// fence. Assigned before any such code, the binding compiles
+// (tests/corpus/split-cjs-require-binding).
+'use strict';
+
+let lib;
+console.log('boot', helper());
+lib = require('./lib.js');
+
+// The same with the require parenthesized.
+let wrapped;
+console.log('peek', wrapped === undefined);
+wrapped = (require('./lib.js'));
+
+function helper() {
+ return lib === undefined ? 'not loaded' : lib.greet('late');
+}
+
+console.log(helper());
diff --git a/tests/diagnostics/split-cjs-require-early/package.json b/tests/diagnostics/split-cjs-require-early/package.json
new file mode 100644
index 000000000..a3c15a7a6
--- /dev/null
+++ b/tests/diagnostics/split-cjs-require-early/package.json
@@ -0,0 +1 @@
+{ "type": "commonjs" }
diff --git a/tests/diagnostics/unions.ts b/tests/diagnostics/unions.ts
index ccf62f40e..ddacb8015 100644
--- a/tests/diagnostics/unions.ts
+++ b/tests/diagnostics/unions.ts
@@ -60,14 +60,6 @@ const bareN = null;
// honest text (JS prints "[object Object]") and stays fenced.
const templated = `${mkAB()}`;
-// `x == null` lowers as the null-or-undefined tag test — but a union with
-// BOTH unit arms needs the operand twice, so an effectful operand (a call)
-// gets its own fence pointing at a const binding.
-function mkNullish(): string | null | undefined {
- return null;
-}
-const looseCall = mkNullish() == null;
-
// A DECLARED `T | string` param infers nothing for T (no union
// unification); the body's use of T then fails with the instantiation.
function tagOr(x: T | string): T {
diff --git a/tests/fixtures/event-loop/stdin-for-await-buffers.mjs b/tests/fixtures/event-loop/stdin-for-await-buffers.mjs
new file mode 100644
index 000000000..d64ae460a
--- /dev/null
+++ b/tests/fixtures/event-loop/stdin-for-await-buffers.mjs
@@ -0,0 +1,10 @@
+// for await over process.stdin yields Buffers: Buffer.isBuffer holds and
+// indexOf takes a byte and a start offset (only chunking-independent facts
+// are printed).
+const chunks = [];
+for await (const chunk of process.stdin) chunks.push(chunk);
+console.log('buffers', chunks.every((chunk) => Buffer.isBuffer(chunk)));
+const all = Buffer.concat(chunks);
+const newlines = [];
+for (let at = all.indexOf(10); at !== -1; at = all.indexOf(10, at + 1)) newlines.push(at);
+console.log('newlines', newlines.join(','), all.subarray(newlines[0] + 1).indexOf('#'));
diff --git a/tests/fixtures/event-loop/stdio-readable-backpressure.mjs b/tests/fixtures/event-loop/stdio-readable-backpressure.mjs
new file mode 100644
index 000000000..df2afb18f
--- /dev/null
+++ b/tests/fixtures/event-loop/stdio-readable-backpressure.mjs
@@ -0,0 +1,27 @@
+// 'readable', 'data' and resume() together, with no read() for a while:
+// 'readable' keeps the stream paused, so it stops reading at its read-ahead
+// mark until a read drains it, and 'data' sees what read() returns.
+const input = process.stdin;
+let draining = false;
+let peak = 0;
+let dataBytes = 0;
+let readBytes = 0;
+const drain = () => {
+ let chunk;
+ while ((chunk = input.read()) !== null) readBytes += chunk.length;
+};
+input.on('data', (chunk) => {
+ dataBytes += chunk.length;
+});
+input.on('readable', () => {
+ peak = Math.max(peak, input.readableLength);
+ if (draining) drain();
+});
+input.resume();
+setTimeout(() => {
+ peak = Math.max(peak, input.readableLength);
+ console.log('held below 128 KiB while not draining', peak <= 131072);
+ draining = true;
+ drain();
+}, 400);
+input.on('end', () => console.log('read', readBytes, 'data', dataBytes));
diff --git a/tests/fixtures/event-loop/stdio-readable-large-size.mjs b/tests/fixtures/event-loop/stdio-readable-large-size.mjs
new file mode 100644
index 000000000..f1f44edce
--- /dev/null
+++ b/tests/fixtures/event-loop/stdio-readable-large-size.mjs
@@ -0,0 +1,9 @@
+// read(size) above the 64 KiB read-ahead mark: 'readable' keeps firing as
+// input arrives until the requested size is buffered.
+const input = process.stdin;
+const sizes = [];
+input.on('readable', () => {
+ let chunk;
+ while ((chunk = input.read(150000)) !== null) sizes.push(chunk.length);
+});
+input.on('end', () => console.log(sizes.join(',')));
diff --git a/tests/fixtures/event-loop/stdio-readable-once-rest.mjs b/tests/fixtures/event-loop/stdio-readable-once-rest.mjs
new file mode 100644
index 000000000..81eae144b
--- /dev/null
+++ b/tests/fixtures/event-loop/stdio-readable-once-rest.mjs
@@ -0,0 +1,12 @@
+// A once('readable') listener that reads part of the input: the rest stays
+// buffered for a 'data' listener added afterwards.
+const input = process.stdin;
+input.once('readable', () => {
+ const head = input.read(3);
+ console.log('head', head === null ? null : head.toString('ascii'));
+ setTimeout(() => {
+ const rest = [];
+ input.on('data', (chunk) => rest.push(chunk.toString('ascii')));
+ input.on('end', () => console.log('rest', rest.join('')));
+ }, 50);
+});
diff --git a/tests/fixtures/event-loop/stdio-readable-size.mjs b/tests/fixtures/event-loop/stdio-readable-size.mjs
new file mode 100644
index 000000000..9ff76dafe
--- /dev/null
+++ b/tests/fixtures/event-loop/stdio-readable-size.mjs
@@ -0,0 +1,18 @@
+// Fixed-size reads of stdin: read(n) answers null until n bytes are
+// buffered, and the rest once the input ended.
+const input = process.stdin;
+const chunks = [];
+const counts = [];
+input.on('readable', () => {
+ let reads = 0;
+ let chunk;
+ while ((chunk = input.read(4)) !== null) {
+ chunks.push(chunk.toString('ascii'));
+ reads++;
+ }
+ counts.push(reads);
+});
+input.on('end', () => {
+ console.log(chunks.join('|'));
+ console.log(counts.join(','));
+});
diff --git a/tests/fixtures/event-loop/stdio-readable.mjs b/tests/fixtures/event-loop/stdio-readable.mjs
new file mode 100644
index 000000000..324a4b57b
--- /dev/null
+++ b/tests/fixtures/event-loop/stdio-readable.mjs
@@ -0,0 +1,19 @@
+// process.stdin in paused mode through 'readable': each event drains the
+// buffer with read(), one more event follows EOF, and 'end' comes after the
+// read that finds the buffer empty.
+const input = process.stdin;
+const events = [];
+let text = '';
+input.on('readable', () => {
+ let reads = 0;
+ let chunk;
+ while ((chunk = input.read()) !== null) {
+ text += String(chunk);
+ reads++;
+ }
+ events.push(`readable ${reads}`);
+});
+input.on('end', () => {
+ console.log(events.join(', '));
+ console.log(JSON.stringify(text), input.readableLength);
+});
diff --git a/tests/fixtures/node-types/fs-url-fallible.ts b/tests/fixtures/node-types/fs-url-fallible.ts
new file mode 100644
index 000000000..da86879d5
--- /dev/null
+++ b/tests/fixtures/node-types/fs-url-fallible.ts
@@ -0,0 +1,19 @@
+import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
+import { readFile } from "node:fs/promises";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { pathToFileURL } from "node:url";
+
+const dir = mkdtempSync(join(tmpdir(), "node-types-url-"));
+const file = pathToFileURL(join(dir, "a.txt"));
+writeFileSync(file, "hi");
+// existsSync answers false for a URL it cannot convert.
+console.log(existsSync(file), existsSync(new URL("http://example.com/")));
+// fs.promises functions reject for a URL they cannot convert.
+readFile(file, "utf8")
+ .then((text) => console.log(text))
+ .then(() => readFile(new URL("file://example.com/a.txt"), "utf8"))
+ .catch((error: unknown) => {
+ if (error instanceof Error) console.log((error as NodeJS.ErrnoException).code);
+ rmSync(dir, { recursive: true });
+ });
diff --git a/tests/fixtures/node-types/fs-url-path-fences.ts b/tests/fixtures/node-types/fs-url-path-fences.ts
deleted file mode 100644
index 4ae142189..000000000
--- a/tests/fixtures/node-types/fs-url-path-fences.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { existsSync } from "node:fs";
-import { readFile, stat } from "node:fs/promises";
-
-const url = new URL("file:///tmp/node-types-url-fences");
-// existsSync answers false for a URL it cannot convert.
-console.log(existsSync(url));
-// The fs.promises functions reject for a URL they cannot convert.
-void readFile(url, "utf8");
-void stat(url);
diff --git a/tests/fixtures/npm/cases/hono-zod-default-hook/main.ts b/tests/fixtures/npm/cases/hono-zod-default-hook/main.ts
new file mode 100644
index 000000000..9981e7154
--- /dev/null
+++ b/tests/fixtures/npm/cases/hono-zod-default-hook/main.ts
@@ -0,0 +1,38 @@
+// @hono/zod-validator without a hook: a failed check answers 400 with the
+// whole safeParse result serialized, so the ZodError's own enumerable
+// properties (name, then message) and their order reach the client.
+import { zValidator } from "@hono/zod-validator";
+import { Hono } from "hono";
+import { z } from "zod";
+
+const Item = z.object({ title: z.string().min(2), qty: z.number().int().positive() });
+
+const app = new Hono();
+app.post("/items", zValidator("json", Item), (c) => {
+ const item = c.req.valid("json");
+ return c.json({ created: item.title, qty: item.qty }, 201);
+});
+app.get("/items", zValidator("query", z.object({ limit: z.coerce.number().int().max(5) })), (c) => {
+ return c.json({ limit: c.req.valid("query").limit });
+});
+
+const post = (body: unknown) =>
+ app.request("/items", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
+
+const show = async (label: string, res: Response | Promise) => {
+ const r = await res;
+ console.log(label, r.status, await r.text());
+};
+
+await show("ok", post({ title: "pen", qty: 2 }));
+await show("bad-title", post({ title: "p", qty: 2 }));
+await show("bad-both", post({ title: 3, qty: -1 }));
+await show("query-ok", app.request("/items?limit=4"));
+await show("query-bad", app.request("/items?limit=9"));
+
+const result = Item.safeParse({ title: "x", qty: 0 });
+if (!result.success) {
+ const e = result.error;
+ console.log(e.name, e instanceof Error, Object.keys(e).join(","));
+ console.log(JSON.stringify(e));
+}
diff --git a/tests/fixtures/npm/cases/hono-zod-default-hook/package.json b/tests/fixtures/npm/cases/hono-zod-default-hook/package.json
new file mode 100644
index 000000000..a734aaddb
--- /dev/null
+++ b/tests/fixtures/npm/cases/hono-zod-default-hook/package.json
@@ -0,0 +1 @@
+{ "name": "hono-zod-default-hook-case", "private": true, "type": "module" }
diff --git a/tests/harness/__snapshots__/fs-url-path-fences.ts.txt b/tests/harness/__snapshots__/fs-url-path-fences.ts.txt
index 1a53f8df5..1b4642d32 100644
--- a/tests/harness/__snapshots__/fs-url-path-fences.ts.txt
+++ b/tests/harness/__snapshots__/fs-url-path-fences.ts.txt
@@ -1,35 +1,35 @@
-fs-url-path-fences.ts:9:1 - error SC2020: 'copyFileSync with a string path before a URL path' is part of the standard library types but has no scriptc lowering yet
+fs-url-path-fences.ts:9:19 - error SC2020: 'mkdirSync with a URL path and options that are not literals' is part of the standard library types but has no scriptc lowering yet
- 8 | // Node validates the string source before converting the URL destination.
- 9 | fs.copyFileSync("/tmp/source", url);
- | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- 10 | // Node validates the options before the path.
-
- hint: convert the URL first: fileURLToPath(url) from node:url
-
-fs-url-path-fences.ts:11:34 - error SC2020: 'readFileSync with a URL path and options that are not literals' is part of the standard library types but has no scriptc lowering yet
-
- 10 | // Node validates the options before the path.
- 11 | console.log(fs.readFileSync(url, encoding));
- | ^~~~~~~~
- 12 | fs.mkdirSync(url, { recursive });
-
- hint: convert the URL first: fileURLToPath(url) from node:url
-
-fs-url-path-fences.ts:12:19 - error SC2020: 'mkdirSync with a URL path and options that are not literals' is part of the standard library types but has no scriptc lowering yet
-
- 11 | console.log(fs.readFileSync(url, encoding));
- 12 | fs.mkdirSync(url, { recursive });
+ 8 | // Node validates the options before the path.
+ 9 | fs.mkdirSync(url, { recursive });
| ^~~~~~~~~~~~~
- 13 | // A literal's members are lowered one by one, so they cannot be evaluated
+ 10 | void fsp.mkdir(url, { recursive });
hint: convert the URL first: fileURLToPath(url) from node:url
-fs-url-path-fences.ts:15:30 - error SC2020: 'statSync with a URL path and a later literal that has effects' is part of the standard library types but has no scriptc lowering yet
+fs-url-path-fences.ts:10:21 - error SC2020: 'fs.promises.mkdir with a URL path and options that are not literals' is part of the standard library types but has no scriptc lowering yet
- 14 | // ahead of the conversion as one value.
- 15 | console.log(fs.statSync(url, { throwIfNoEntry: process.argv.length > 9 }));
+ 9 | fs.mkdirSync(url, { recursive });
+ 10 | void fsp.mkdir(url, { recursive });
+ | ^~~~~~~~~~~~~
+ 11 | // A literal's members are lowered one by one, so they cannot be evaluated
+
+ hint: convert the URL first: fileURLToPath(url) from node:url
+
+fs-url-path-fences.ts:13:30 - error SC2020: 'statSync with a URL path and a later literal that has effects' is part of the standard library types but has no scriptc lowering yet
+
+ 12 | // ahead of the conversion as one value.
+ 13 | console.log(fs.statSync(url, { throwIfNoEntry: process.argv.length > 9 }));
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- 16 |
+ 14 | // fs.promises checks the string source inside the call, after the
+
+ hint: convert the URL first: fileURLToPath(url) from node:url
+
+fs-url-path-fences.ts:16:6 - error SC2020: 'fs.promises.copyFile with a string path before a URL path' is part of the standard library types but has no scriptc lowering yet
+
+ 15 | // conversion would have run.
+ 16 | void fsp.copyFile("/tmp/source", url);
+ | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ 17 |
hint: convert the URL first: fileURLToPath(url) from node:url
\ No newline at end of file
diff --git a/tests/harness/__snapshots__/index-signature-values.ts.txt b/tests/harness/__snapshots__/index-signature-values.ts.txt
index 084d6a259..1c037086b 100644
--- a/tests/harness/__snapshots__/index-signature-values.ts.txt
+++ b/tests/harness/__snapshots__/index-signature-values.ts.txt
@@ -7,18 +7,11 @@ index-signature-values.ts:8:7 - error SC2006: values of type '{ [s: symbol]: num
hint: string- and number-keyed index signatures over the supported value types compile directly; symbol keys and dual signatures with unequal value types have no lowering
-index-signature-values.ts:13:13 - error SC1090: console.log of '{ [key: string]: Uint8Array }' values (Uint8Array and Buffer values inside composites have no inspect lowering yet (Buffer's form vs Uint8Array's is not recorded in the static type here)) are not supported yet
+index-signature-values.ts:14:7 - error SC2006: values of type '{ [k: string]: string | number; [n: number]: number; }' cannot be compiled: this index signature is outside the supported shape (string or number keys; values limited to numbers, strings, booleans, records, classes, arrays, unions, functions, Maps, Sets, RegExps, Dates, Promises, or 'unknown')
- 12 | const buffers: Record = {};
- 13 | console.log(buffers);
- | ^~~~~~~
- 14 |
-
-index-signature-values.ts:18:7 - error SC2006: values of type '{ [k: string]: string | number; [n: number]: number; }' cannot be compiled: this index signature is outside the supported shape (string or number keys; values limited to numbers, strings, booleans, records, classes, arrays, unions, functions, Maps, Sets, RegExps, Dates, Promises, or 'unknown')
-
- 17 | // answer per key).
- 18 | const dual: { [k: string]: string | number; [n: number]: number } = {};
+ 13 | // answer per key).
+ 14 | const dual: { [k: string]: string | number; [n: number]: number } = {};
| ^~~~
- 19 | console.log(dual);
+ 15 | console.log(dual);
hint: string- and number-keyed index signatures over the supported value types compile directly; symbol keys and dual signatures with unequal value types have no lowering
\ No newline at end of file
diff --git a/tests/harness/__snapshots__/split-cjs-require-cycle/main.js.txt b/tests/harness/__snapshots__/split-cjs-require-cycle/main.js.txt
new file mode 100644
index 000000000..6c39c35d6
--- /dev/null
+++ b/tests/harness/__snapshots__/split-cjs-require-cycle/main.js.txt
@@ -0,0 +1,7 @@
+split-cjs-require-cycle/main.js:1:1 - error SC1016: circular imports (split-cjs-require-cycle/main.js → split-cjs-require-cycle/b.js → split-cjs-require-cycle/main.js; the cycle closes through a require() edge: split-cjs-require-cycle/main.js uses its exports at split-cjs-require-cycle/main.js:10 outside the export statements that precede its first require, so a module of the cycle can observe them before they settle) are not supported yet
+
+ 1 | // A split require binding (`var b; b = require("./b.js")`) whose require
+ | ^
+ 2 | // closes a CommonJS cycle that top-level code can observe: b.js reads a.js's
+
+ hint: ES-module cycles of let/const/function bindings compile as-is; rewrite the named construct (a named import instead of a namespace, a const instead of a default-export expression or var, a class declared before the code that can reach it) or break the named edge
\ No newline at end of file
diff --git a/tests/harness/__snapshots__/split-cjs-require-early/main.js.txt b/tests/harness/__snapshots__/split-cjs-require-early/main.js.txt
new file mode 100644
index 000000000..0496cd125
--- /dev/null
+++ b/tests/harness/__snapshots__/split-cjs-require-early/main.js.txt
@@ -0,0 +1,13 @@
+split-cjs-require-early/main.js:10:7 - error SC1013: require() binding 'lib' assigned below code that can reach it (Node reads undefined before the assignment runs, which is not modeled — move the assignment above the code that can run first) is not supported yet
+
+ 9 | console.log('boot', helper());
+ 10 | lib = require('./lib.js');
+ | ^~~~~~~~~~~~~~~~~~~
+ 11 |
+
+split-cjs-require-early/main.js:15:12 - error SC1013: require() binding 'wrapped' assigned below code that can reach it (Node reads undefined before the assignment runs, which is not modeled — move the assignment above the code that can run first) is not supported yet
+
+ 14 | console.log('peek', wrapped === undefined);
+ 15 | wrapped = (require('./lib.js'));
+ | ^~~~~~~~~~~~~~~~~~~
+ 16 |
\ No newline at end of file
diff --git a/tests/harness/__snapshots__/unions.ts.txt b/tests/harness/__snapshots__/unions.ts.txt
index bba468c40..8e801b0fe 100644
--- a/tests/harness/__snapshots__/unions.ts.txt
+++ b/tests/harness/__snapshots__/unions.ts.txt
@@ -14,16 +14,9 @@ unions.ts:61:22 - error SC1090: string conversions of unions with object arms (n
| ^~~~~~
62 |
-unions.ts:69:19 - error SC1090: '== null' / '!= null' on a 'null | string | undefined' value that isn't a plain read (both unit arms need the operand twice — bind it to a const first) is not supported yet
+unions.ts:92:10 - error SC1090: reading 'valueOf' on a union-typed value (every arm must be an object/record with a same-typed field 'valueOf'; narrow first: check a discriminant field, or compare with '!== undefined'/'!== null' for unit arms) is not supported yet
- 68 | }
- 69 | const looseCall = mkNullish() == null;
- | ^~~~~~~~~~~~~~~~~~~
- 70 |
-
-unions.ts:100:10 - error SC1090: reading 'valueOf' on a union-typed value (every arm must be an object/record with a same-typed field 'valueOf'; narrow first: check a discriminant field, or compare with '!== undefined'/'!== null' for unit arms) is not supported yet
-
- 99 | function chainSubUnion(x: number | string | undefined): number {
- 100 | return x?.valueOf === undefined ? 0 : 1;
- | ^~~~~~~~~~
- 101 | }
\ No newline at end of file
+ 91 | function chainSubUnion(x: number | string | undefined): number {
+ 92 | return x?.valueOf === undefined ? 0 : 1;
+ | ^~~~~~~~~~
+ 93 | }
\ No newline at end of file
diff --git a/tests/harness/corpus-costs/6935-fs-url-paths-ordered.ts.json b/tests/harness/corpus-costs/6935-fs-url-paths-ordered.ts.json
new file mode 100644
index 000000000..f2dfcb1a1
--- /dev/null
+++ b/tests/harness/corpus-costs/6935-fs-url-paths-ordered.ts.json
@@ -0,0 +1 @@
+{ "plain": 3746, "san": 5148 }
diff --git a/tests/harness/corpus-costs/7189-non-null-assertion-nullish-destination.ts.json b/tests/harness/corpus-costs/7189-non-null-assertion-nullish-destination.ts.json
new file mode 100644
index 000000000..7473d65aa
--- /dev/null
+++ b/tests/harness/corpus-costs/7189-non-null-assertion-nullish-destination.ts.json
@@ -0,0 +1 @@
+{ "plain": 2653, "san": 3208 }
diff --git a/tests/harness/corpus-costs/7392-non-null-operands.ts.json b/tests/harness/corpus-costs/7392-non-null-operands.ts.json
new file mode 100644
index 000000000..a591cc420
--- /dev/null
+++ b/tests/harness/corpus-costs/7392-non-null-operands.ts.json
@@ -0,0 +1 @@
+{ "plain": 4200, "san": 4800 }
diff --git a/tests/harness/corpus-costs/7453-untyped-overload-results.cjs.json b/tests/harness/corpus-costs/7453-untyped-overload-results.cjs.json
new file mode 100644
index 000000000..46a1181bd
--- /dev/null
+++ b/tests/harness/corpus-costs/7453-untyped-overload-results.cjs.json
@@ -0,0 +1 @@
+{ "plain": 2991, "san": 4866 }
diff --git a/tests/harness/corpus-costs/7624-key-order-boxed-callback-records/main.ts.json b/tests/harness/corpus-costs/7624-key-order-boxed-callback-records/main.ts.json
new file mode 100644
index 000000000..fb3015dce
--- /dev/null
+++ b/tests/harness/corpus-costs/7624-key-order-boxed-callback-records/main.ts.json
@@ -0,0 +1 @@
+{ "plain": 6931, "san": 8003 }
diff --git a/tests/harness/corpus-costs/7680-typed-array-search.ts.json b/tests/harness/corpus-costs/7680-typed-array-search.ts.json
new file mode 100644
index 000000000..37f8b6e89
--- /dev/null
+++ b/tests/harness/corpus-costs/7680-typed-array-search.ts.json
@@ -0,0 +1 @@
+{ "plain": 2204, "san": 3060 }
diff --git a/tests/harness/corpus-costs/7681-typed-array-search-dynamic.mjs.json b/tests/harness/corpus-costs/7681-typed-array-search-dynamic.mjs.json
new file mode 100644
index 000000000..b6fcd5eff
--- /dev/null
+++ b/tests/harness/corpus-costs/7681-typed-array-search-dynamic.mjs.json
@@ -0,0 +1 @@
+{ "plain": 1649, "san": 958 }
diff --git a/tests/harness/corpus-costs/7682-constructor-function-globals.js.json b/tests/harness/corpus-costs/7682-constructor-function-globals.js.json
new file mode 100644
index 000000000..90fcede92
--- /dev/null
+++ b/tests/harness/corpus-costs/7682-constructor-function-globals.js.json
@@ -0,0 +1 @@
+{ "plain": 1623, "san": 962 }
diff --git a/tests/harness/corpus-costs/7745-typed-array-unions.ts.json b/tests/harness/corpus-costs/7745-typed-array-unions.ts.json
new file mode 100644
index 000000000..ee546c4fd
--- /dev/null
+++ b/tests/harness/corpus-costs/7745-typed-array-unions.ts.json
@@ -0,0 +1 @@
+{ "plain": 3450, "san": 4000 }
diff --git a/tests/harness/corpus-costs/7747-typed-array-class-values.ts.json b/tests/harness/corpus-costs/7747-typed-array-class-values.ts.json
new file mode 100644
index 000000000..89e0d92e7
--- /dev/null
+++ b/tests/harness/corpus-costs/7747-typed-array-class-values.ts.json
@@ -0,0 +1 @@
+{ "plain": 2400, "san": 2350 }
diff --git a/tests/harness/corpus-costs/7748-typed-array-number-array-unions.ts.json b/tests/harness/corpus-costs/7748-typed-array-number-array-unions.ts.json
new file mode 100644
index 000000000..89fb8ddd2
--- /dev/null
+++ b/tests/harness/corpus-costs/7748-typed-array-number-array-unions.ts.json
@@ -0,0 +1 @@
+{ "plain": 2550, "san": 2300 }
diff --git a/tests/harness/corpus-costs/7749-typed-array-element-searches.ts.json b/tests/harness/corpus-costs/7749-typed-array-element-searches.ts.json
new file mode 100644
index 000000000..7d76a0583
--- /dev/null
+++ b/tests/harness/corpus-costs/7749-typed-array-element-searches.ts.json
@@ -0,0 +1 @@
+{ "plain": 2650, "san": 2850 }
diff --git a/tests/harness/corpus-costs/7750-typed-array-union-inspect.ts.json b/tests/harness/corpus-costs/7750-typed-array-union-inspect.ts.json
new file mode 100644
index 000000000..c0aef3bb7
--- /dev/null
+++ b/tests/harness/corpus-costs/7750-typed-array-union-inspect.ts.json
@@ -0,0 +1 @@
+{ "plain": 2700, "san": 3900 }
diff --git a/tests/harness/corpus-costs/7796-boxed-subclass-identity.js.json b/tests/harness/corpus-costs/7796-boxed-subclass-identity.js.json
new file mode 100644
index 000000000..3dfdb2b6d
--- /dev/null
+++ b/tests/harness/corpus-costs/7796-boxed-subclass-identity.js.json
@@ -0,0 +1 @@
+{ "plain": 4600, "san": 5500 }
diff --git a/tests/harness/corpus-costs/7982-integer-view-compares.ts.json b/tests/harness/corpus-costs/7982-integer-view-compares.ts.json
new file mode 100644
index 000000000..39d9e2b8a
--- /dev/null
+++ b/tests/harness/corpus-costs/7982-integer-view-compares.ts.json
@@ -0,0 +1 @@
+{ "plain": 3700, "san": 4600 }
diff --git a/tests/harness/corpus-costs/8112-error-factory-own-key-order.js.json b/tests/harness/corpus-costs/8112-error-factory-own-key-order.js.json
new file mode 100644
index 000000000..9013dc548
--- /dev/null
+++ b/tests/harness/corpus-costs/8112-error-factory-own-key-order.js.json
@@ -0,0 +1 @@
+{ "plain": 2200, "san": 10300 }
diff --git a/tests/harness/corpus-costs/8113-error-class-field-before-name.js.json b/tests/harness/corpus-costs/8113-error-class-field-before-name.js.json
new file mode 100644
index 000000000..29164fca3
--- /dev/null
+++ b/tests/harness/corpus-costs/8113-error-class-field-before-name.js.json
@@ -0,0 +1 @@
+{ "plain": 1900, "san": 9700 }
diff --git a/tests/harness/corpus-costs/8160-assert-expected-from-fields.cjs.json b/tests/harness/corpus-costs/8160-assert-expected-from-fields.cjs.json
new file mode 100644
index 000000000..cdfeabea9
--- /dev/null
+++ b/tests/harness/corpus-costs/8160-assert-expected-from-fields.cjs.json
@@ -0,0 +1 @@
+{ "plain": 2656, "san": 5234 }
diff --git a/tests/harness/corpus-costs/split-cjs-require-binding/main.cjs.json b/tests/harness/corpus-costs/split-cjs-require-binding/main.cjs.json
new file mode 100644
index 000000000..9d570f700
--- /dev/null
+++ b/tests/harness/corpus-costs/split-cjs-require-binding/main.cjs.json
@@ -0,0 +1 @@
+{ "plain": 1500, "san": 12000 }
diff --git a/tests/harness/corpus-costs/split-cjs-require-cycle/main.cjs.json b/tests/harness/corpus-costs/split-cjs-require-cycle/main.cjs.json
new file mode 100644
index 000000000..9619c105e
--- /dev/null
+++ b/tests/harness/corpus-costs/split-cjs-require-cycle/main.cjs.json
@@ -0,0 +1 @@
+{ "plain": 2700, "san": 3200 }
diff --git a/tests/harness/corpus-costs/typed-array-masked-index-stores.ts.json b/tests/harness/corpus-costs/typed-array-masked-index-stores.ts.json
new file mode 100644
index 000000000..3e45ee566
--- /dev/null
+++ b/tests/harness/corpus-costs/typed-array-masked-index-stores.ts.json
@@ -0,0 +1 @@
+{ "plain": 2791, "san": 3186 }
diff --git a/tests/harness/errors.test.ts b/tests/harness/errors.test.ts
index f8068ac81..c8849a10f 100644
--- a/tests/harness/errors.test.ts
+++ b/tests/harness/errors.test.ts
@@ -325,6 +325,35 @@ console.log(cells.join(","));
);
});
+ test("a split require closing a CommonJS cycle keeps the const form's SC1016", async () => {
+ // Node answers the closing require with the dependency's partial exports
+ // ({ fromA: 1 } at this point), which the static export model cannot
+ // represent. Preflight treats the split assignment like `const a =
+ // require("./a.cjs")`, so the cycle keeps that form's SC1016 instead of
+ // reading uninitialized export storage. A cycle whose reads are all deferred
+ // compiles (tests/corpus/split-cjs-require-cycle).
+ const r = compileAndRun(
+ "split-require-cycle",
+ `require("./a.cjs");
+`,
+ "cjs",
+ {
+ "a.cjs": `exports.fromA = 1;
+const b = require("./b.cjs");
+exports.fromB = b.fromB;
+`,
+ "b.cjs": `exports.fromB = 2;
+var a;
+a = require("./a.cjs");
+console.log(a.fromA, a.fromB);
+`,
+ },
+ );
+ await expect(r).rejects.toThrow(
+ /SC1016: circular imports \(.*a\.cjs → .*b\.cjs → .*a\.cjs; the cycle closes through a require\(\) edge/,
+ );
+ });
+
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
diff --git a/tests/harness/event-loop-cases.ts b/tests/harness/event-loop-cases.ts
index 40bc7988d..50a7d2b85 100644
--- a/tests/harness/event-loop-cases.ts
+++ b/tests/harness/event-loop-cases.ts
@@ -34,6 +34,66 @@ export const eventLoopCases: EventLoopCase[] = [
fixture: "stdio-paused-exit.mjs",
script: { writes: [], end: false },
},
+ {
+ title: "stdio values: 'readable' drains with read(), then fires once more at EOF",
+ fixture: "stdio-readable.mjs",
+ script: {
+ writes: [
+ { delayMs: 0, data: "alpha " },
+ { delayMs: 60, data: "beta\n" },
+ ],
+ end: true,
+ },
+ },
+ {
+ title: "stdio values: 'readable' with read(size) holds a partial tail until EOF",
+ fixture: "stdio-readable-size.mjs",
+ script: { writes: [{ delayMs: 0, data: "abcdefghij" }], end: true },
+ },
+ {
+ title: "stdio values: input a once('readable') listener leaves stays buffered for 'data'",
+ fixture: "stdio-readable-once-rest.mjs",
+ script: { writes: [{ delayMs: 0, data: "abcdefgh" }], end: true },
+ },
+ {
+ title: "stdio values: 'readable' with a read(size) above the read-ahead mark",
+ fixture: "stdio-readable-large-size.mjs",
+ script: {
+ writes: [
+ { delayMs: 0, data: "a".repeat(70000) },
+ { delayMs: 30, data: "b".repeat(70000) },
+ { delayMs: 30, data: "c".repeat(60000) },
+ ],
+ end: true,
+ },
+ },
+ {
+ title: "stdio values: 'readable' with 'data' and resume() holds input at the read-ahead mark",
+ fixture: "stdio-readable-backpressure.mjs",
+ script: {
+ writes: Array.from({ length: 16 }, (_, i) => ({
+ delayMs: i === 0 ? 0 : 5,
+ data: String.fromCharCode(97 + i).repeat(65536),
+ })),
+ end: true,
+ },
+ },
+ {
+ title: "stdio values: 'readable' on an immediately ended pipe",
+ fixture: "stdio-readable.mjs",
+ script: { writes: [], end: true },
+ },
+ {
+ title: "for-await: process.stdin yields Buffers",
+ fixture: "stdin-for-await-buffers.mjs",
+ script: {
+ writes: [
+ { delayMs: 0, data: "#one\nabcd\n" },
+ { delayMs: 40, data: "#two\nefgh\n" },
+ ],
+ end: true,
+ },
+ },
{
title: "read-all: one instant write, then EOF",
fixture: "stdin-read-all.ts",
diff --git a/tests/harness/npm.test.ts b/tests/harness/npm.test.ts
index ff9e09d61..2333f13da 100644
--- a/tests/harness/npm.test.ts
+++ b/tests/harness/npm.test.ts
@@ -114,10 +114,12 @@ 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, the zod 3 cases (zod3,
* zod3-adoption, zod3-boundary, zod3-infer) compile zod 3's 3,700-line
- * types module, and the zod 4 cases (zod4-boundary, hono-zod-validator)
+ * types module, and the zod 4 cases (zod4-boundary, zod4-record,
+ * hono-zod-validator, hono-zod-default-hook)
* compile zod 4's core, classic API and locales. A sanitized build of any
* of them takes minutes on a CI runner. */
const TIMEOUTS: Record = {
+ "hono-zod-default-hook": 900_000,
"hono-zod-validator": 900_000,
undici5: 600_000,
undici7: 600_000,
diff --git a/tests/harness/project-config.test.ts b/tests/harness/project-config.test.ts
index fd2077a48..7980da17f 100644
--- a/tests/harness/project-config.test.ts
+++ b/tests/harness/project-config.test.ts
@@ -795,20 +795,17 @@ test("node-types: fs functions take URL paths under @types/node's PathLike", asy
expect(stdout).toBe("hello 5 5\n");
});
-test("node-types: existsSync and fs.promises URL paths remain named fences", async () => {
- const dir = outDirFor("node-fs-url-path-fences");
- const result = await compile(join(nodeTypesDir, "fs-url-path-fences.ts"), {
- outPath: join(dir, "fs-url-path-fences"),
+test("node-types: existsSync and fs.promises answer false or reject for unconvertible URLs", async () => {
+ const dir = outDirFor("node-fs-url-fallible");
+ const result = await compile(join(nodeTypesDir, "fs-url-fallible.ts"), {
+ outPath: join(dir, "fs-url-fallible"),
outDir: dir,
sanitize,
});
- expect(result.ok).toBe(false);
- if (result.ok) return;
- expect(result.diagnostics.map((diagnostic) => [diagnostic.code, diagnostic.message])).toEqual([
- ["SC2020", expect.stringContaining("'existsSync with a URL path'")],
- ["SC2020", expect.stringContaining("'fs.promises.readFile with a URL path'")],
- ["SC2020", expect.stringContaining("'fs.promises.stat with a URL path'")],
- ]);
+ expect(result.ok, !result.ok ? JSON.stringify(result.diagnostics, null, 2) : "").toBe(true);
+ if (!result.ok) return;
+ const { stdout } = await execFileAsync(result.binaryPath);
+ expect(stdout).toBe("true false\nhi\nERR_INVALID_FILE_URL_HOST\n");
});
test("node-types: path and os lower statically under @types/node's shapes", async () => {
diff --git a/tests/harness/self-hosting-llvm-emitter.test.ts b/tests/harness/self-hosting-llvm-emitter.test.ts
index cb9cd69c4..a81bac16c 100644
--- a/tests/harness/self-hosting-llvm-emitter.test.ts
+++ b/tests/harness/self-hosting-llvm-emitter.test.ts
@@ -92,6 +92,7 @@ const programs = [
"local-union-storage.ts",
"class-callback-storage.ts",
"byte-record-windows.ts",
+ "typed-array-masked-index-stores.ts",
"constant-callback-dispatch.ts",
"constant-number-remainders.ts",
"numeric-call-specialization.ts",
@@ -99,6 +100,10 @@ const programs = [
"native-worker-roots/main.ts",
"native-worker-shared-views.ts",
];
+// Debug info turns the emitter's loop optimizations off. These programs are
+// emitted without it, so the compiled emitter runs its loop analyses on them
+// (typed-array loop versioning, which caches null index shapes).
+const withoutDebugInfo = new Set(["typed-array-masked-index-stores.ts"]);
for (const backend of ["llvm"] as const) {
test(
@@ -227,10 +232,12 @@ for (const backend of ["llvm"] as const) {
if (!lowered.ok)
throw new Error(`${source}: ${lowered.diagnostics.map((d) => d.message).join("\n")}`);
const mod = deserializeModule(readFileSync(irPath, "utf8"));
- const request: LlvmEmitterRequest = llvmEmitterRequest({
- debug: true,
- sources: [{ file: sourcePath, text: readFileSync(sourcePath, "utf8") }],
- });
+ const request: LlvmEmitterRequest = withoutDebugInfo.has(source)
+ ? llvmEmitterRequest()
+ : llvmEmitterRequest({
+ debug: true,
+ sources: [{ file: sourcePath, text: readFileSync(sourcePath, "utf8") }],
+ });
const text = await emit(mod, source, request);
const llvmPath = join(dir, "program.ll");
const outPath = executable("program");