Merge main and address the review comment

This commit is contained in:
Chris Tate
2026-10-23 22:38:44 -05:00
632 changed files with 43509 additions and 1284 deletions
+40
View File
@@ -135,6 +135,9 @@ With `--sparse`, the profile leaves out the functions the runs never reached. Th
<dt><code>--detached-exit</code></dt>
<dd>On Linux, when the program calls <code>process.exit()</code>, 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.</dd>
<dt><code>--target-cpu &lt;baseline|x86-64-v2|x86-64-v3|x86-64-v4|native&gt;</code></dt>
<dd>On x86-64 targets, compile the program's code for an x86-64 microarchitecture level. The default, <code>baseline</code>, 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. <code>native</code> uses the highest level the build machine supports. Valid for executable, assembly, and object output on x86-64 targets. See <a href="#target-cpu">Target CPU</a>.</dd>
<dt><code>--profile-generate &lt;pattern&gt;</code></dt>
<dd>On Linux, build an instrumented executable for profile-guided optimization. Each run writes a raw profile to the path pattern, where <code>%p</code> expands to the process ID; the <code>LLVM_PROFILE_FILE</code> 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 <code>--optimization=release</code> or <code>speed</code>.</dd>
@@ -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:
<table>
<thead>
<tr>
<th>Level</th>
<th>Adds</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>x86-64-v2</code></td>
<td>SSE3, SSSE3, SSE4.1, SSE4.2, POPCNT</td>
</tr>
<tr>
<td><code>x86-64-v3</code></td>
<td>AVX, AVX2, BMI1, BMI2, F16C, FMA, LZCNT, MOVBE</td>
</tr>
<tr>
<td><code>x86-64-v4</code></td>
<td>AVX-512 (F, BW, CD, DQ, VL)</td>
</tr>
</tbody>
</table>
```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 <code>--emit=llvm</code> to inspect the generated code.
+5 -6
View File
@@ -42,17 +42,16 @@ Parsing, type checking, lowering, and LLVM IR generation run one after another o
### Partitions
The LLVM helper divides an executable's LLVM IR into partitions by its size: one partition per 4 MiB of LLVM text, rounded up to a power of two, and at most 64. `--emit=llvm` writes this text, so its file size shows how many partitions a program gets.
The LLVM helper divides an executable's LLVM IR into partitions by its size. Up to 1 MiB of LLVM text is one partition, and up to 2 MiB is four. Larger programs get one partition per 4 MiB of LLVM text, rounded up to a power of two, and at least 8 and at most 64. `--emit=llvm` writes this text, so its file size shows how many partitions a program gets.
<table>
<thead>
<tr><th>LLVM text</th><th>Partitions</th></tr>
</thead>
<tbody>
<tr><td>Up to 4 MiB</td><td>1</td></tr>
<tr><td>Over 4 MiB, up to 8 MiB</td><td>2</td></tr>
<tr><td>Over 8 MiB, up to 16 MiB</td><td>4</td></tr>
<tr><td>Over 16 MiB, up to 32 MiB</td><td>8</td></tr>
<tr><td>Up to 1 MiB</td><td>1</td></tr>
<tr><td>Over 1 MiB, up to 2 MiB</td><td>4</td></tr>
<tr><td>Over 2 MiB, up to 32 MiB</td><td>8</td></tr>
<tr><td>Over 32 MiB, up to 64 MiB</td><td>16</td></tr>
<tr><td>Over 64 MiB, up to 128 MiB</td><td>32</td></tr>
<tr><td>Over 128 MiB</td><td>64</td></tr>
@@ -63,7 +62,7 @@ The count depends only on the size of the LLVM text, so a program has the same p
These builds are not divided into partitions and compile on one thread: `--emit=asm`, `--emit=obj`, and `--print=native-link-info` output, library builds (`--lib`; `dev` library builds are compiled in pieces, one after another), and `--sanitize` builds, which compile the LLVM IR with the C compiler.
The helper compiles partitions on a pool with one thread per hardware thread of the host, so an executable with two partitions uses at most two cores in the backend. A program of one partition is optimized and compiled on one thread.
The helper compiles partitions on a pool with one thread per hardware thread of the host, so an executable with four partitions uses at most four cores in the backend. A program of one partition is optimized and compiled on one thread.
How partitions are compiled depends on `--optimization`:
@@ -1,3 +1,8 @@
`Buffer.from` accepts untyped strings, byte arrays, arrays, and plain array-like or Buffer JSON objects.
Encoding arguments must be supported literals.
`Buffer.from(arrayBuffer, byteOffset, length)` is a view of the ArrayBuffer or SharedArrayBuffer, with Node's offset and length checks.
Custom input `valueOf` hooks and opaque class or native-handle inputs throw an unsupported-operation error.
The deprecated `Buffer()` and `new Buffer()` take up to three arguments, without spreads, and follow Node: the first call made outside `node_modules` reports DEP0005 on the next tick, a number allocates zero-filled bytes, and other arguments convert as `Buffer.from` does.
In JavaScript, `Buffer` read as a value (the global or the buffer module's `Buffer`) is one function with Node's statics and `poolSize`.
Calling or constructing it through a stored value, reading its `prototype`, and its `copyBytesFrom` and `of` statics throw an unsupported-operation error.
`Buffer.prototype` members read as values are functions that run on Buffer receivers; inherited and symbol-keyed members throw an unsupported-operation error.
@@ -27,22 +27,22 @@
A call on a DiffieHellman value whose arguments no @types/node overload accepts, or whose output encoding is typed `any`, is refused at compile time, as are its `Object.prototype` methods; other property reads, and property writes, throw a coded SC2020 error.
`Object.keys()` and `in` do not see a DiffieHellman's own `verifyError` property, and `JSON.stringify()` and `instanceof` are refused.
After `createDiffieHellman(primeLength)` throws `Invalid DH parameters`, the next OpenSSL error Node throws can list the failed generation's errors in `opensslErrorStack`; errors in a compiled binary list only their own.
`crypto.diffieHellman`, which takes DH, EC, or X25519 KeyObjects, and the `DiffieHellman` and `DiffieHellmanGroup` constructors are not available.
`crypto.diffieHellman` computes ECDH and X25519 secrets from EC and X25519 keys, given as KeyObjects or in any form `createPrivateKey` and `createPublicKey` read, with Node's errors; DH keys have no KeyObjects here, and the `DiffieHellman` and `DiffieHellmanGroup` constructors are not available.
`checkPrime`, `checkPrimeSync`, `generatePrime`, and `generatePrimeSync` support bigint, ArrayBuffer, typed-array, Buffer, and DataView values and the `checks`, `safe`, `add`, `rem`, and `bigint` options on native targets, with Node's errors; the callback forms compute before they return and call back in a later macrotask.
Candidates and sizes longer than 262144 bits throw a coded SC2020 error, and wasm32-wasi builds are refused.
Node calls their callbacks with `undefined` as the error, which a callback whose error parameter is typed `Error | null`, as @types/node declares it, receives as `null`.
RSA, EC, and Ed25519 private and public KeyObjects come from `generateKeyPairSync`, `generateKeyPair`, `createPrivateKey`, and `createPublicKey`, on native targets.
RSA, EC, Ed25519, and X25519 private and public KeyObjects come from `generateKeyPairSync`, `generateKeyPair`, `createPrivateKey`, and `createPublicKey`, on native targets.
`generateKeyPairSync` and `generateKeyPair` check their arguments in Node's order, with Node's errors, including the OpenSSL errors for RSA modulus lengths below 512 or above 2147483647 bits and for public exponents of 0, 1, or an even number, which `generateKeyPair` passes to its callback.
RSA keys generate with even modulus lengths from 1024 to 8192 bits and public exponents up to 2147483647; other lengths and exponents that Node accepts, and `paramEncoding: 'explicit'`, throw a coded SC2020 error.
The EC curves are prime256v1 (P-256), secp384r1 (P-384), secp521r1 (P-521), and secp256k1.
Keys read and write PEM and DER as PKCS#1, SEC1, PKCS#8, and SubjectPublicKeyInfo, and JWK (Ed25519 keys use PKCS#8, SubjectPublicKeyInfo, and JWK); `createPublicKey` also takes a private key or an X.509 certificate.
EC and Ed25519 keys also read and write the `raw-public` and `raw-private` formats, including compressed EC points.
Raw X25519, X448, Ed448, and post-quantum keys, EC scalars of 0 or at least the curve order, and the EC point at infinity, which Node accepts, throw a coded SC2020 error.
Keys read and write PEM and DER as PKCS#1, SEC1, PKCS#8, and SubjectPublicKeyInfo, and JWK (Ed25519 and X25519 keys use PKCS#8, SubjectPublicKeyInfo, and JWK); `createPublicKey` also takes a private key or an X.509 certificate.
EC, Ed25519, and X25519 keys also read and write the `raw-public` and `raw-private` formats, including compressed EC points.
Raw X448, Ed448, and post-quantum keys, EC scalars of 0 or at least the curve order, and the EC point at infinity, which Node accepts, throw a coded SC2020 error.
They support `type`, `asymmetricKeyType`, `asymmetricKeyDetails`, `export()`, `equals()`, and `instanceof KeyObject`.
`crypto.sign`, `crypto.verify`, `createSign`, and `createVerify` support RSA PKCS#1 v1.5 and PSS padding and ECDSA with DER and IEEE P1363 encodings over SHA-1 and SHA-2 digests, and Ed25519 through `crypto.sign` and `crypto.verify` with a `null` or `undefined` algorithm.
Ed25519 signing, verification, and private keys link when a string literal in the program names `ed25519` (in any case); without one they throw a coded SC2020 error, while Ed25519 public keys still import and export.
Sign and Verify values accumulate `update()` data and are not writable streams.
Encrypted private keys (`passphrase`, `cipher`), the other key types (`rsa-pss`, `dsa`, `ed448`, `x25519`, and the rest), and wasm32-wasi builds throw a coded SC2020 error or are refused.
Encrypted private keys (`passphrase`, `cipher`), the other key types (`rsa-pss`, `dsa`, `ed448`, `x448`, and the rest), and wasm32-wasi builds throw a coded SC2020 error or are refused.
Comparing keys of different types with `equals()` answers false; Node also leaves an OpenSSL error queued, which a later key operation can report.
`publicEncrypt`/`privateDecrypt` remain unsupported.
The WebCrypto `crypto` global, which node:crypto also exports as `webcrypto`, supports `randomUUID()` and `getRandomValues()`, also read as values from untyped JavaScript.
@@ -1,5 +1,6 @@
- **Date:** Date unions, spread constructor arguments, setters, statically typed TypeScript identity comparisons, thrown Date values, and locale formatters are unsupported.
Zero-argument, single number/string, and local-time year/month field constructors, storage and passing, `getTime`, `valueOf`, `toISOString`, `toUTCString`, `toJSON` (a `string | null`: null for an invalid date, as in Node), `toString`, `toDateString`, `toTimeString`, `String(date)` and template literal conversion, calendar getters, `getTimezoneOffset`, and single-string `Date.parse` compile.
JavaScript Dates retain identity through checked storage and support Date-copy construction and JSON serialization.
A TypeScript Date converted to `unknown` becomes a Date with the same time value; each conversion creates a new Date, so two `unknown` values converted from one TypeScript Date are not `===`.
- **Date:** Spread constructor arguments, setters, thrown Date values, and locale formatters are unsupported.
Dates are objects: `===` compares identity, `new Date(date)` makes a new Date, and Dates compile in arrays (`includes`, `indexOf`, `lastIndexOf`, `join`, and default `sort` by `toString()` text), in `Date | undefined` and `Date | null` slots, as Map and Set keys and values, and in records, tuples, class fields, closures, promises and generators.
Unions of a Date with other non-nullish types are unsupported.
Zero-argument, single number/string, and local-time year/month field constructors, `getTime`, `valueOf`, `toISOString`, `toUTCString`, `toJSON` (a `string | null`: null for an invalid date, as in Node), `toString`, `toDateString`, `toTimeString`, `String(date)` and template literal conversion, relational comparison, calendar getters, `getTimezoneOffset`, and single-string `Date.parse` compile.
A Date converted to `unknown` is the same Date object.
See [Dates and timezones](#dates-and-timezones) for timezone data.
@@ -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.
@@ -4,6 +4,8 @@ Empty record layouts are also snapshots because they have no property storage.
`scriptc coverage` and `scriptc build` report `SC6001` where a copied value is then written or compared, whether the record was passed as an argument, stored in a typed binding, asserted with `as`, assigned to a typed binding later, or stored as an element of a typed array literal.
Typed records with declared fields, indexed records, and arrays retain identity and mutations through native checked storage.
A record or class instance converted to a string-keyed dictionary type such as `Record<string, string>`, directly or through a `{}`, `object` or `unknown` binding or parameter, keeps its identity when the program writes through, compares or inspects values of that dictionary type.
Every dictionary type with the same value type then uses checked dynamic storage, which is slower than a native dictionary.
Class instances retain identity at their exact type or a base class, and at any interface or object type that a class instance is passed, stored, returned, or asserted as when the program could tell a copy from the instance: values of such a type are checked views of the original object, so writes, identity comparisons, Set and Map keys, `util.inspect`, and `JSON.stringify` through them reach the instance, as in Node.js.
These types use checked dynamic storage, which is slower than native record fields, for all of their values, including plain objects.
A type whose values are only read field by field, from fields the class does not write after construction and does not implement as accessors, stays a native record and holds a copy.
@@ -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.
@@ -11476,9 +11476,9 @@
"inherited": true
},
"static": {
"status": "refused",
"detail": "Explicitly rejected by a named compiler diagnostic.",
"verification": "explicit-refusal"
"status": "partial",
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
@@ -20345,8 +20345,19 @@
"inherited": true
},
"static": {
"status": "refused",
"evidence": "surface-manifest:unsupported:node-builtin.crypto.diffieHellman"
"status": "partial",
"evidence": "surface-manifest:node-builtin.crypto.diffieHellman",
"tests": [
"tests/corpus/1358-crypto-random.ts",
"tests/corpus/1534-crypto-hash-chain.ts",
"tests/corpus/2556-crypto-introspection.ts",
"tests/corpus/2890-crypto-static-utility.ts",
"tests/harness/npm-static.test.ts",
"tests/corpus/node24-crypto-sha2.ts",
"tests/corpus/node24-crypto-kdf.ts",
"tests/corpus/node24-crypto-kdf-errors.ts",
"tests/corpus/node24-crypto-kdf-options.ts"
]
},
"anchorSource": "exact"
},
+3
View File
@@ -70,6 +70,8 @@ async function tryFastPath(): Promise<number | null> {
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 ||
+100
View File
@@ -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<string> => {
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);
+62 -27
View File
@@ -917,6 +917,14 @@ interface Buffer<
readBigUint64LE(offset?: number): bigint;
}
interface BufferConstructor {
/** @deprecated since v10.0.0 - Use `Buffer.from(string[, encoding])` instead. */
new (str: string, encoding?: BufferEncoding): Buffer;
/** @deprecated since v10.0.0 - Use `Buffer.alloc()` instead. */
new (size: number): Buffer;
/** @deprecated since v10.0.0 - Use `Buffer.from(array)` instead. */
new (array: ArrayLike<number>): Buffer;
/** @deprecated since v10.0.0 - Use `Buffer.from(arrayBuffer[, byteOffset[, length]])` instead. */
new (arrayBuffer: ArrayBuffer | SharedArrayBuffer): Buffer;
from(data: string, encoding?: BufferEncoding): Buffer;
/* The ArrayBuffer form is the VIEW construction: x.buffer with an
* optional byte offset/length shares x's storage (the subarray rule). */
@@ -1582,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,
@@ -2223,7 +2231,7 @@ declare module "fs/promises" {
stat(): Promise<import("node:fs").Stats>;
[Symbol.asyncDispose](): Promise<void>;
}
export function open(path: string, flags?: string, mode?: number): Promise<FileHandle>;
export function open(path: string | URL, flags?: string, mode?: number): Promise<FileHandle>;
export function link(
existingPath: string | Uint8Array | URL,
newPath: string | Uint8Array | URL,
@@ -2260,14 +2268,14 @@ declare module "fs/promises" {
atime: number | string | Date,
mtime: number | string | Date,
): Promise<void>;
export function readFile(path: string, encoding: "utf8" | "utf-8"): Promise<string>;
export function readFile(path: string | URL, encoding: "utf8" | "utf-8"): Promise<string>;
export function readFile(
path: string,
path: string | URL,
options: BufferEncoding | { encoding: BufferEncoding },
): Promise<string>;
export function readFile(path: string): Promise<Buffer>;
export function readFile(path: string | URL): Promise<Buffer>;
export function writeFile(
path: string,
path: string | URL,
data: string,
options?: {
mode?: number;
@@ -2276,14 +2284,18 @@ declare module "fs/promises" {
[option: string]: unknown;
},
): Promise<void>;
export function writeFile(path: string, data: string, encoding: "utf8" | "utf-8"): Promise<void>;
export function writeFile(
path: string | URL,
data: string,
encoding: "utf8" | "utf-8",
): Promise<void>;
export function mkdir(
path: string,
path: string | URL,
options?: { recursive?: boolean; mode?: number },
): Promise<void>;
export function readdir(path: string): Promise<string[]>;
export function readdir(path: string | URL): Promise<string[]>;
export function readdir(
path: string,
path: string | URL,
options: {
encoding?: "utf8" | "utf-8";
withFileTypes: true;
@@ -2300,12 +2312,12 @@ declare module "fs/promises" {
pattern: string | readonly string[],
options: import("node:fs").GlobOptionsWithFileTypes,
): AsyncIterableIterator<import("node:fs").Dirent>;
export function rm(path: string): Promise<void>;
export function rm(path: string | URL): Promise<void>;
export function rm(
path: string,
path: string | URL,
options: { recursive?: boolean; force?: boolean; maxRetries?: number; retryDelay?: number },
): Promise<void>;
export function stat(path: string): Promise<import("node:fs").Stats>;
export function stat(path: string | URL): Promise<import("node:fs").Stats>;
export function statfs(
path: string | Uint8Array | URL,
options?: import("node:fs").StatFsOptions & { bigint?: false },
@@ -2318,21 +2330,21 @@ declare module "fs/promises" {
path: string | Uint8Array | URL,
options?: import("node:fs").StatFsOptions,
): Promise<import("node:fs").StatsFs | import("node:fs").BigIntStatsFs>;
export function realpath(path: string): Promise<string>;
export function lstat(path: string): Promise<import("node:fs").Stats>;
export function unlink(path: string): Promise<void>;
export function chmod(path: string, mode: number): Promise<void>;
export function chown(path: string, uid: number, gid: number): Promise<void>;
export function lchown(path: string, uid: number, gid: number): Promise<void>;
export function access(path: string, mode?: number): Promise<void>;
export function truncate(path: string, len?: number): Promise<void>;
export function appendFile(path: string, data: string | Uint8Array): Promise<void>;
export function copyFile(src: string, dest: string): Promise<void>;
export function mkdtemp(prefix: string): Promise<string>;
export function rmdir(path: string): Promise<void>;
export function rename(oldPath: string, newPath: string): Promise<void>;
export function realpath(path: string | URL): Promise<string>;
export function lstat(path: string | URL): Promise<import("node:fs").Stats>;
export function unlink(path: string | URL): Promise<void>;
export function chmod(path: string | URL, mode: number): Promise<void>;
export function chown(path: string | URL, uid: number, gid: number): Promise<void>;
export function lchown(path: string | URL, uid: number, gid: number): Promise<void>;
export function access(path: string | URL, mode?: number): Promise<void>;
export function truncate(path: string | URL, len?: number): Promise<void>;
export function appendFile(path: string | URL, data: string | Uint8Array): Promise<void>;
export function copyFile(src: string | URL, dest: string | URL): Promise<void>;
export function mkdtemp(prefix: string | URL): Promise<string>;
export function rmdir(path: string | URL): Promise<void>;
export function rename(oldPath: string | URL, newPath: string | URL): Promise<void>;
export function opendir(
path: string,
path: string | URL,
options?: import("node:fs").OpenDirOptions,
): Promise<import("node:fs").Dir>;
}
@@ -2992,6 +3004,24 @@ declare module "crypto" {
options: ED25519KeyPairOptions<"der", "der">,
): KeyPairSyncResult<Buffer, Buffer>;
export function generateKeyPairSync(type: "ed25519", options?: {}): KeyPairKeyObjectResult;
export interface X25519KeyPairOptions<PubF extends KeyFormat, PrivF extends KeyFormat> {
publicKeyEncoding: { type: "spki"; format: PubF };
privateKeyEncoding: { type: "pkcs8"; format: PrivF };
}
export function generateKeyPairSync(
type: "x25519",
options: X25519KeyPairOptions<"pem", "pem">,
): KeyPairSyncResult<string, string>;
export function generateKeyPairSync(
type: "x25519",
options: X25519KeyPairOptions<"der", "der">,
): KeyPairSyncResult<Buffer, Buffer>;
export function generateKeyPairSync(type: "x25519", options?: {}): KeyPairKeyObjectResult;
export function generateKeyPair(
type: "x25519",
options: {} | undefined,
callback: (err: Error | null, publicKey: KeyObject, privateKey: KeyObject) => void,
): void;
export function generateKeyPair(
type: "ed25519",
options: {} | undefined,
@@ -3020,6 +3050,11 @@ declare module "crypto" {
export interface VerifyKeyObjectInput extends SigningOptions {
key: KeyObject;
}
export function diffieHellman(options: { privateKey: KeyObject; publicKey: KeyObject }): Buffer;
export function diffieHellman(
options: { privateKey: KeyObject; publicKey: KeyObject },
callback: (err: Error | null, secret: Buffer) => void,
): void;
export function sign(
algorithm: string | null | undefined,
data: ArrayBufferView,
@@ -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,
@@ -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<Node>();
const ownedNodes = new Set<Node>();
const used = new Set<number>();
@@ -139,7 +139,6 @@ export class LlvmDebugInfo {
let id: string;
switch (type.kind) {
case "f64":
case "date":
case "procStream":
id = this.add(
`!DIBasicType(name: ${quoted(type.kind === "f64" ? "number" : type.kind)}, size: 64, encoding: DW_ATE_float)`,
@@ -190,7 +189,7 @@ export class LlvmDebugInfo {
? []
: [
this.add(
`!DIDerivedType(tag: DW_TAG_member, name: "arm${i}", baseType: ${this.type(arm)}, size: ${arm.kind === "bool" ? 8 : arm.kind === "f64" || arm.kind === "date" ? 64 : this.pointerBits})`,
`!DIDerivedType(tag: DW_TAG_member, name: "arm${i}", baseType: ${this.type(arm)}, size: ${arm.kind === "bool" ? 8 : arm.kind === "f64" ? 64 : this.pointerBits})`,
),
],
);
@@ -239,9 +238,7 @@ export class LlvmDebugInfo {
// Scalar forward captures use an array-backed TDZ box. Expose that
// exceptional representation opaquely rather than reporting a bad value.
const opaqueBox =
local.boxed &&
local.tdz &&
(local.type.kind === "f64" || local.type.kind === "bool" || local.type.kind === "date");
local.boxed && local.tdz && (local.type.kind === "f64" || local.type.kind === "bool");
const type = opaqueBox
? this.pointer(
this.add(
+7 -8
View File
@@ -469,7 +469,7 @@ export class LlDyn {
/** The value LLVM type of a dynCheck result / toDyn operand for `t`. */
private valTy(t: IrType): string {
return t.kind === "f64" || t.kind === "date" || t.kind === "procStream"
return t.kind === "f64" || t.kind === "procStream"
? "double"
: t.kind === "bool"
? "i1"
@@ -1954,10 +1954,10 @@ export class LlDyn {
};
switch (t.kind) {
case "date": {
host.declare(`declare double @scr_dyn_native_date_value(ptr)`);
host.declare(`declare ptr @scr_dyn_native_date_check(ptr, ptr)`);
const r = B.tmp();
B.line(`${r} = call double @scr_dyn_native_date_value(ptr %d)`);
B.terminate(`ret double ${r}`);
B.line(`${r} = call ptr @scr_dyn_native_date_check(ptr %d, ptr %path)`);
B.terminate(`ret ptr ${r}`);
break;
}
case "url":
@@ -2818,11 +2818,10 @@ export class LlDyn {
let sourceAccessor: { name: string; release: string } | null = null;
switch (t.kind) {
case "date": {
// A Date handle holding the same time value; native Dates are
// immutable values, and so is the handle.
host.declare(`declare ptr @scr_dyn_native_date_from_ms(double)`);
// A Date handle holding the same Date object.
host.declare(`declare ptr @scr_dyn_native_date(ptr)`);
const r = B.tmp();
B.line(`${r} = call ptr @scr_dyn_native_date_from_ms(double %v)`);
B.line(`${r} = call ptr @scr_dyn_native_date(ptr %v)`);
B.terminate(`ret ptr ${r}`);
break;
}
@@ -304,11 +304,7 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string {
// ScrGen owns the frame), so its OUT slot address is fixed.
let out: string | null = null;
const yieldKind = fn.generator?.yieldT.kind;
if (
!emitter.wasi &&
fn.async !== true &&
(yieldKind === "f64" || yieldKind === "date" || yieldKind === "bool")
) {
if (!emitter.wasi && fn.async !== true && (yieldKind === "f64" || yieldKind === "bool")) {
emitter.declare(`declare ptr @scr_gen_of_fiber(ptr)`);
const gen = B.tmp();
B.line(`${gen} = call ptr @scr_gen_of_fiber(ptr ${self})`);
@@ -704,7 +700,6 @@ export function emitGenScaffolding(emitter: LlEmitter): string[] {
tr.push(` br label %done ; void body: the done value is undefined (NONE)`);
break;
case "f64":
case "date":
emitter.declare(`declare void @scr_gen_out_f64(ptr, double)`);
tr.push(` call void @scr_gen_out_f64(ptr %g, double %r)`, ` br label %done`);
break;
@@ -1053,7 +1048,6 @@ export function emitAsyncAwaitCall(
emitter.declare(`declare void @scr_await_void(ptr)`);
return ` call void @scr_await_void(ptr ${promise})`;
case "f64":
case "date":
emitter.declare(`declare double @scr_await_f64(ptr)`);
return ` ${dest} = call double @scr_await_f64(ptr ${promise})`;
case "bool":
@@ -1108,7 +1102,7 @@ export function emitAsyncAwaitCall(
const kind =
ret.kind === "void"
? "void"
: ret.kind === "f64" || ret.kind === "date"
: ret.kind === "f64"
? "f64"
: ret.kind === "bool"
? "bool"
@@ -1187,7 +1181,6 @@ export function emitWasiFulfill(emitter: LlEmitter, v: LlValue | null): void {
case "void":
return;
case "f64":
case "date":
emitter.declare(`declare void @scr_gen_out_f64(ptr, double)`);
emitter.B.line(`call void @scr_gen_out_f64(ptr ${gen}, double ${v!.name})`);
return;
@@ -1212,7 +1205,6 @@ export function emitWasiFulfill(emitter: LlEmitter, v: LlValue | null): void {
emitter.B.line(`call void @scr_promise_fulfill_void(ptr ${pr})`);
break;
case "f64":
case "date":
emitter.declare(`declare void @scr_promise_fulfill_f64(ptr, double)`);
emitter.B.line(`call void @scr_promise_fulfill_f64(ptr ${pr}, double ${v!.name})`);
break;
@@ -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) {
@@ -1,7 +1,6 @@
import { expect, test } from "vitest";
import {
BOOL,
DATE_T,
F64,
PROCSTREAM_T,
STRING,
@@ -202,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 });
@@ -287,7 +296,7 @@ test("shared exceptional exits preserve the function's scalar return ABI", () =>
});
test("exceptional exits use each scalar kind's LLVM return type", () => {
for (const type of [F64, DATE_T, BOOL, PROCSTREAM_T]) {
for (const type of [F64, BOOL, PROCSTREAM_T]) {
for (const ownedLocals of [false, true]) {
const module = moduleFor([call(), call(), { kind: "throw", value: literal("failed"), loc }]);
const work = module.functions[1]!;
+24 -5
View File
@@ -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({
@@ -2044,7 +2051,6 @@ export class LlEmitter {
if (POINTER_KINDS.has(t.kind)) return "ptr";
switch (t.kind) {
case "f64":
case "date":
return "double";
case "bool":
return "i1";
@@ -2177,7 +2183,6 @@ export class LlEmitter {
this.declare(`declare void @scr_promise_fulfill_void(ptr)`);
return [` call void @scr_promise_fulfill_void(ptr ${promise})`];
case "f64":
case "date":
this.declare(`declare void @scr_promise_fulfill_f64(ptr, double)`);
return [` call void @scr_promise_fulfill_f64(ptr ${promise}, double %r)`];
case "bool":
@@ -2407,7 +2412,7 @@ export class LlEmitter {
return { name: "", type };
}
const t = B.tmp();
if (type.kind === "f64" || type.kind === "date") {
if (type.kind === "f64") {
this.declare(`declare double @scr_coro_direct_f64(ptr)`);
B.line(`${t} = call double @scr_coro_direct_f64(ptr ${record})`);
} else if (type.kind === "bool") {
@@ -3803,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
@@ -3820,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
@@ -184,7 +184,7 @@ export function emitIntrinsicExpr(host: LlvmEmitterContext, e: ExprOf<"intrinsic
}
const v = host.emitExpr(e.args[0]!);
const t = e.args[0]!.type;
if (t.kind === "f64" || t.kind === "date") {
if (t.kind === "f64") {
host.declare(`declare void @scr_promise_fulfill_f64(ptr, double)`);
B.line(`call void @scr_promise_fulfill_f64(ptr ${p}, double ${v.name})`);
} else if (t.kind === "bool") {
@@ -500,7 +500,7 @@ export function emitAsyncExpr(
host.declare(`declare void @scr_async_gen_hop_done()`);
B.line(`call void @scr_async_gen_hop_done()`);
}
if (yt.kind === "f64" || yt.kind === "date") {
if (yt.kind === "f64") {
if (host.currentCoro !== null) {
const coro = host.currentCoro;
if (coro.out !== null) host.emitGenSlotStore(coro.out, "f64", v.name);
@@ -582,7 +582,7 @@ export function emitAsyncExpr(
return { name: "", type: e.type };
}
const t = B.tmp();
if (e.type.kind === "f64" || e.type.kind === "date") {
if (e.type.kind === "f64") {
host.declare(`declare double @scr_gen_take_in_f64()`);
B.line(`${t} = call double @scr_gen_take_in_f64()`);
return { name: t, type: e.type };
@@ -710,12 +710,12 @@ export function emitAsyncExpr(
throw new InternalCompilerError(`llvm emitter bug: genResume ${e.mode} on async`);
const gb = emitBorrowedInput(host, e.gen);
if (e.mode === "value") {
if (!host.wasi && (e.type.kind === "f64" || e.type.kind === "date"))
if (!host.wasi && e.type.kind === "f64")
return { name: host.emitGenSlotTake(host.genOutSlot(gb.name), "f64"), type: e.type };
if (!host.wasi && e.type.kind === "bool")
return { name: host.emitGenSlotTake(host.genOutSlot(gb.name), "bool"), type: e.type };
const t = B.tmp();
if (e.type.kind === "f64" || e.type.kind === "date") {
if (e.type.kind === "f64") {
host.declare(`declare double @scr_gen_take_out_f64(ptr)`);
B.line(`${t} = call double @scr_gen_take_out_f64(ptr ${gb.name})`);
return { name: t, type: e.type };
@@ -752,7 +752,7 @@ export function emitAsyncExpr(
store(a.name, e.arg!.type);
};
const parkIn = (name: string, t: IrType): void => {
if (t.kind === "f64" || t.kind === "date") {
if (t.kind === "f64") {
host.declare(`declare void @scr_gen_in_f64(ptr, double)`);
B.line(`call void @scr_gen_in_f64(ptr ${g.name}, double ${name})`);
} else if (t.kind === "bool") {
@@ -787,7 +787,7 @@ export function emitAsyncExpr(
const a = host.emitExpr(e.arg);
const t = e.arg.type;
if (isRefCounted(t)) host.toHeap(a);
if (t.kind === "f64" || t.kind === "date") {
if (t.kind === "f64") {
host.declare(`declare ptr @scr_async_gen_next_f64(ptr, double)`);
call = `call ptr @scr_async_gen_next_f64(ptr ${g.name}, double ${a.name})`;
} else if (t.kind === "bool") {
@@ -806,7 +806,7 @@ export function emitAsyncExpr(
const a = host.emitExpr(e.arg);
const t = e.arg.type;
if (isRefCounted(t)) host.toHeap(a);
if (t.kind === "f64" || t.kind === "date") {
if (t.kind === "f64") {
host.declare(`declare ptr @scr_async_gen_return_f64(ptr, double)`);
call = `call ptr @scr_async_gen_return_f64(ptr ${g.name}, double ${a.name})`;
} else if (t.kind === "bool") {
@@ -849,7 +849,7 @@ export function emitAsyncExpr(
B.line(`call void @scr_gen_ret_none(ptr ${g.name})`);
} else {
sendArg((name, t) => {
if (t.kind === "f64" || t.kind === "date") {
if (t.kind === "f64") {
host.declare(`declare void @scr_gen_ret_f64(ptr, double)`);
B.line(`call void @scr_gen_ret_f64(ptr ${g.name}, double ${name})`);
} else if (t.kind === "bool") {
@@ -960,7 +960,7 @@ export function emitAsyncExpr(
return { name: "", type: e.type };
}
const t = B.tmp();
if (e.type.kind === "f64" || e.type.kind === "date") {
if (e.type.kind === "f64") {
const f64Await = host.awaitResultFn("f64");
host.declare(`declare double @${f64Await}(ptr)`);
B.line(`${t} = call double @${f64Await}(ptr ${pr.name})`);
@@ -1050,7 +1050,7 @@ export function emitAsyncExpr(
const peek = host.unionPeek(u.name, def.id);
if (host.coroAwait) host.emitCoroAwait(peek);
let awaited: LlValue;
if (inner.kind === "f64" || inner.kind === "date") {
if (inner.kind === "f64") {
const f64Await = host.awaitResultFn("f64");
host.declare(`declare double @${f64Await}(ptr)`);
const x = B.tmp();
@@ -1136,7 +1136,6 @@ export function emitAsyncExpr(
let resolve: string;
const kindNums: Partial<Record<IrType["kind"], number>> = {
f64: 0,
date: 0,
bool: 1,
string: 2,
void: 3,
@@ -1191,7 +1190,6 @@ export function emitAsyncExpr(
B.line(`${p} = call ptr @scr_promise_new()`);
const kindNums: Partial<Record<IrType["kind"], number>> = {
f64: 0,
date: 0,
bool: 1,
string: 2,
void: 3,
@@ -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.
@@ -914,6 +914,12 @@ export function emitArrIntrinsic(
B.line(`${t} = call ptr @${helper}(ptr ${r.name}, ptr ${sep.name})`);
return host.own({ name: t, type: e.type });
}
if (elem.kind === "date") {
host.declare(`declare ptr @scr_date_arr_join(ptr, ptr)`);
const t = B.tmp();
B.line(`${t} = call ptr @scr_date_arr_join(ptr ${r.name}, ptr ${sep.name})`);
return host.own({ name: t, type: e.type });
}
host.declare(`declare ptr @scr_arr_join(ptr, ptr)`);
const t = B.tmp();
B.line(`${t} = call ptr @scr_arr_join(ptr ${r.name}, ptr ${sep.name})`);
@@ -938,6 +944,16 @@ export function emitArrIntrinsic(
case "sortPrimitive":
case "toSortedPrimitive": {
if (e.method === "sortPrimitive") host.emitPublishedArrayGuard(r.name, e.receiver.type, 7);
if (elem.kind === "date") {
// Dates order by their toString() text.
host.declare(`declare ptr @scr_arr_sort_by_text(ptr, i1 zeroext, ptr)`);
host.declare(`declare ptr @scr_date_object_to_string(ptr)`);
const sorted = B.tmp();
B.line(
`${sorted} = call ptr @scr_arr_sort_by_text(ptr ${r.name}, i1 ${e.method === "toSortedPrimitive" ? 1 : 0}, ptr @scr_date_object_to_string)`,
);
return host.own({ name: sorted, type: e.type });
}
host.declare(`declare ptr @scr_arr_sort_primitive(ptr, i1 zeroext)`);
const result = B.tmp();
B.line(
@@ -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<string, string> = {
"<": "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<string, string> = {
"<": "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(
@@ -122,12 +122,7 @@ export function dynPromiseAdapter(host: LlvmEmitterContext, inner: IrType): stri
if (inner.kind === "void") {
host.declare(`declare void @scr_promise_fulfill_void(ptr)`);
B.line(`call void @scr_promise_fulfill_void(ptr %dst)`);
} else if (
inner.kind === "f64" ||
inner.kind === "date" ||
inner.kind === "procStream" ||
inner.kind === "bool"
) {
} else if (inner.kind === "f64" || inner.kind === "procStream" || inner.kind === "bool") {
const fn = inner.kind !== "bool" ? "scr_promise_fulfill_f64" : "scr_promise_fulfill_bool";
host.declare(
`declare void @${fn}(ptr, ${host.llType(inner)}${inner.kind === "bool" ? " zeroext" : ""})`,
@@ -998,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)`);
@@ -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();
@@ -5,11 +5,12 @@ import { emitTlsLibCall } from "./lib-tls.js";
import { emitHttp2LibCall } from "./lib-http2.js";
import { emitDatagramLibCall } from "./lib-datagram.js";
import { emitTestLibCall } from "./lib-test.js";
import { FN_ATTRS, arrNewCall, traceArg, vAdapters } from "./shapes.js";
/* Focused LLVM library-call emission extracted from emitter.ts. */
import { InternalCompilerError } from "../../errors.js";
import { MAY_THROW_LIB_FNS } from "../../ir/builtin-effects.js";
import type { IrExpr, IrType } from "../../ir/ir.js";
import { F64, type IrExpr, type IrType } from "../../ir/ir.js";
import { LlvmUnsupportedError } from "./unsupported.js";
import { emitJsLibCall } from "./jsval.js";
import type { LlvmEmitterContext, LibCallExpr, LibCallPrefix, LlValue } from "./expr-context.js";
@@ -239,6 +240,33 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
host.emitPendingCheck();
return output;
}
if (e.fn === "promise.allTuple") return emitPromiseAllTuple(host, e);
if (e.fn === "promise.fulfilledValue") {
// The payload of a fulfilled promise (borrowed operand): the scalar
// kinds that lower to a double (numbers, Dates, process streams) and
// booleans by value, strings and references +1.
const promise = host.emitExpr(e.args[0]!);
const out = B.tmp();
const ty = host.llType(e.type);
if (ty === "double") {
host.declare(`declare double @scr_promise_payload_f64(ptr)`);
B.line(`${out} = call double @scr_promise_payload_f64(ptr ${promise.name})`);
return { name: out, type: e.type };
}
if (ty === "i1") {
host.declare(`declare zeroext i1 @scr_promise_payload_bool(ptr)`);
B.line(`${out} = call zeroext i1 @scr_promise_payload_bool(ptr ${promise.name})`);
return { name: out, type: e.type };
}
if (e.type.kind === "string") {
host.declare(`declare ptr @scr_promise_payload_str(ptr)`);
B.line(`${out} = call ptr @scr_promise_payload_str(ptr ${promise.name}) ; +1`);
return host.own({ name: out, type: e.type });
}
host.declare(`declare ptr @scr_promise_payload_ref(ptr)`);
B.line(`${out} = call ptr @scr_promise_payload_ref(ptr ${promise.name}) ; +1`);
return host.own({ name: out, type: e.type });
}
if (e.fn === "record.sourceSpread") {
const built = host.emitReadReceiver(e.args[0]!);
const value = host.emitReadReceiver(e.args[1]!);
@@ -638,16 +666,66 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
? "scr_stream_dyn_async_iterator_open"
: LIB_FN_SYMS[e.fn]);
if (sym === undefined) throw new LlvmUnsupportedError(`libCall:${e.fn}`, e.loc);
// The runtime's Date functions take and answer time values: a Date
// operand passes its time value, read at once from the borrowed object,
// and a Date result is a new Date object.
const args = READ_ONLY_STRING_LIB_FNS.has(e.fn)
? emitBorrowedInputs(host, e.args)
: e.args.map((a) =>
borrowsJsonInputs(e.fn) || borrowsIteratorInputs(e.fn)
? emitBorrowedInput(host, a)
: host.emitExpr(a),
a.type.kind === "date"
? { name: emitDateTimeArg(host, a), type: F64 }
: borrowsJsonInputs(e.fn) || borrowsIteratorInputs(e.fn)
? emitBorrowedInput(host, a)
: host.emitExpr(a),
);
// getTime and valueOf answer the time value the argument already is.
if ((e.fn === "date.getTime" || e.fn === "date.valueOf") && e.args[0]?.type.kind === "date")
return { name: args[0]!.name, type: e.type };
// The deep comparisons' number test: computed here, with no runtime call.
if (e.fn === "assert.sameValue")
return { name: emitSameValueF64(host, args[0]!.name, args[1]!.name), type: e.type };
const callArgs = args;
if (e.type.kind === "date") {
const ms = emitSymbolLibCall(host, { ...e, type: F64 }, sym, callArgs);
host.declare(`declare ptr @scr_date_new(double)`);
const date = B.tmp();
B.line(`${date} = call ptr @scr_date_new(double ${ms.name})`);
return host.own({ name: date, type: e.type });
}
return emitSymbolLibCall(host, e, sym, callArgs);
}
/** The time value of a Date argument. A Date that a runtime Date function
* produces for this argument alone (`new Date(ms).getTime()`, a
* time-value local's read) passes the function's time value and creates
* no Date object. */
function emitDateTimeArg(host: LlvmEmitterContext, date: IrExpr): string {
// A time-value local's read is its time value itself.
if (date.kind === "libCall" && date.fn === "date.fromTime")
return host.emitExpr(date.args[0]!).name;
if (date.kind === "libCall" && date.fn.startsWith("date."))
return emitGenericLibCall(host, { ...date, type: F64 }).name;
return emitDateTime(host, emitBorrowedInput(host, date).name);
}
/** A Date object's time value (ScrDate.ms). */
function emitDateTime(host: LlvmEmitterContext, date: string): string {
const B = host.B;
const field = B.tmp();
const ms = B.tmp();
B.line(`${field} = getelementptr inbounds i8, ptr ${date}, i64 8 ; ->ms`);
B.line(`${ms} = load double, ptr ${field}`);
return ms;
}
/** The call of runtime function `sym` with evaluated arguments. */
function emitSymbolLibCall(
host: LlvmEmitterContext,
e: LibCallExpr,
sym: string,
args: readonly LlValue[],
): LlValue {
const B = host.B;
const argDecls = args.map((a) => {
const ty = host.llType(a.type);
return ty === "i1" ? "i1 zeroext" : ty;
@@ -975,6 +1053,7 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
case "disposable":
case "punycode":
case "constants":
case "promise":
return host.emitGenericLibCall(e);
case "test":
return emitTestLibCall(host, e);
@@ -989,3 +1068,63 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
}
}
}
/** Promise.all's tuple form (promise.allTuple): the combinator over the
* entries stores each fulfilled entry promise, and a settlement adapter on
* the combinator calls the builder with them and fulfills the result with
* the tuple inside the combinator's settlement; a rejection settles the
* result the same way. The combinator itself counts as handled. */
function emitPromiseAllTuple(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
const B = host.B;
const build = e.args[1]!;
if (build.kind !== "closure" || e.type.kind !== "promise")
throw new InternalCompilerError("llvm emitter bug: promise.allTuple shape");
const entriesT = e.args[0]!.type;
const entries = host.emitExpr(e.args[0]!);
const tuple = e.type.inner;
const key = `allTuple:${build.fnName}`;
let adapter = host.resolveThunks.get(key);
if (adapter === undefined) {
adapter = `sc_all_tuple_${host.resolveThunks.size}`;
host.resolveThunks.set(key, adapter);
const rc = vAdapters(host.shapeHost, tuple);
host.declare(`declare ptr @scr_promise_payload_ref(ptr)`);
host.declare(`declare void @scr_promise_fulfill_ref(ptr, ptr, ptr, ptr, ptr)`);
host.resolveThunkDefs.push(
`define internal void @${adapter}(ptr %dst, ptr %src) ${FN_ATTRS} { ; Promise.all tuple`,
`entry:`,
` %entries = call ptr @scr_promise_payload_ref(ptr %src) ; +1, moves into the builder`,
` %tuple = call ptr @${host.callTarget(build.fnName)}(ptr %entries)`,
` call void @scr_promise_fulfill_ref(ptr %dst, ptr %tuple, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(host.shapeHost, tuple)})`,
` ret void`,
`}`,
``,
);
}
host.declare(`declare ptr @scr_promise_all(ptr, ptr, ptr)`);
host.declare(`declare void @scr_promise_all_store_promise(ptr, double, ptr)`);
host.declare(`declare ptr @scr_promise_new()`);
host.declare(`declare void @scr_promise_race_add(ptr, ptr, ptr)`);
host.declare(`declare void @scr_promise_mark_handled(ptr)`);
host.declare(`declare void @scr_promise_release(ptr)`);
host.declare(`declare double @scr_arr_len(ptr)`);
if (entriesT.kind !== "array")
throw new InternalCompilerError("llvm emitter bug: promise.allTuple entries");
const len = B.tmp();
const cap = B.tmp();
B.line(`${len} = call double @scr_arr_len(ptr ${entries.name})`);
B.line(`${cap} = fptoui double ${len} to ${host.sizeType}`);
const values = B.tmp();
B.line(`${values} = ${arrNewCall(host.shapeHost, entriesT.elem, cap)}`);
host.own({ name: values, type: entriesT });
const all = B.tmp();
B.line(
`${all} = call ptr @scr_promise_all(ptr ${entries.name}, ptr ${values}, ptr @scr_promise_all_store_promise)`,
);
B.line(`call void @scr_promise_mark_handled(ptr ${all})`);
const result = B.tmp();
B.line(`${result} = call ptr @scr_promise_new()`);
B.line(`call void @scr_promise_race_add(ptr ${result}, ptr ${all}, ptr @${adapter})`);
B.line(`call void @scr_promise_release(ptr ${all})`);
return host.own({ name: result, type: e.type });
}
@@ -333,6 +333,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"date.parse": "scr_date_parse",
"date.getTime": "scr_date_get_time",
"date.valueOf": "scr_date_get_time",
"date.fromTime": "scr_date_get_time",
"date.parseGetTime": "scr_date_parse",
"date.utc": "scr_date_utc",
"date.newLocal": "scr_date_new_local",
@@ -454,6 +455,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"buffer.poolFromDyn": "scr_buffer_from_dyn_pooled",
"buffer.poolFromDynRef": "scr_buffer_from_dyn_ref_pooled",
"buffer.poolFromDynOffset": "scr_buffer_from_dyn_offset_pooled",
"buffer.poolFromDynRange": "scr_buffer_from_dyn_range_pooled",
"buffer.poolSize": "scr_buffer_pool_size",
"buffer.setPoolSize": "scr_buffer_set_pool_size",
"buffer.byteLenDyn": "scr_buffer_byte_length_dyn",
@@ -479,6 +481,12 @@ export const LIB_FN_SYMS: Record<string, string> = {
"bytes.equalsChk": "scr_bytes_equals_chk",
"bytes.compareChk": "scr_bytes_compare_chk",
"buffer.newStringFail": "scr_buffer_new_string_fail",
"buffer.deprecation": "scr_buffer_deprecation",
"buffer.deprecationCount": "scr_buffer_deprecation_count",
"buffer.constructor": "scr_buffer_constructor_value",
"buffer.installValue": "scr_buffer_install_value",
"buffer.staticValue": "scr_buffer_static_value",
"buffer.prototypeMember": "scr_buffer_prototype_member",
"fs.toUnixTimestamp": "scr_fs_to_unix_timestamp",
"fs.existsChk": "scr_fs_exists_async",
"fs.mkdtempSyncChk": "scr_fs_mkdtemp_sync_chk",
@@ -650,6 +658,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"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.
@@ -703,6 +712,9 @@ export const LIB_FN_SYMS: Record<string, string> = {
"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",
@@ -1070,6 +1082,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"crypto.generateKeyPairSync": "scr_crypto_generate_key_pair_sync",
"crypto.generateKeyPairCb": "scr_crypto_generate_key_pair_async",
"crypto.signOneshot": "scr_crypto_sign_oneshot",
"crypto.diffieHellman": "scr_crypto_diffie_hellman",
"crypto.verifyOneshot": "scr_crypto_verify_oneshot",
"crypto.createSign": "scr_crypto_create_sign",
"crypto.createVerify": "scr_crypto_create_verify",
@@ -1654,6 +1667,7 @@ export function mockTimerSym(
export const USES_TIMERS_LIB_FNS = new Set<string>([
"crypto.generateKeyPairCb",
"crypto.diffieHellman",
"crypto.checkPrimeCb",
"crypto.generatePrimeCb",
"process.emitWarning",
@@ -1679,6 +1693,8 @@ export const USES_TIMERS_LIB_FNS = new Set<string>([
"url.legacyParse",
"url.legacyFormat",
"url.legacyResolve",
// The deprecated Buffer constructor reports DEP0005 on the next tick.
"buffer.deprecation",
// The wrappers schedule warnings and callbacks on the tick queue.
"util.deprecate",
"util.callbackify",
@@ -596,10 +596,10 @@ export class PublishWalkers {
}
}
/** Unpublishable types stored as scalars (dates are time values, process
* streams file descriptors): no pointer to push, and always a value. */
/** Unpublishable types stored as scalars (process streams are file
* descriptors): no pointer to push, and always a value. */
function scalarRefusal(t: IrType): boolean {
return t.kind === "date" || t.kind === "procStream";
return t.kind === "procStream";
}
/** The Node.js implementation's refusal for a value of type `t`. */
@@ -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" ||
+2 -7
View File
@@ -1100,6 +1100,7 @@ export function arrNewCall(
elem.kind === "stats" || // fs.Stats snapshots: scr_stats_* adapters, no trace
elem.kind === "dyn" || // native collection seeds/drains: traced REF
elem.kind === "regex" || // RegExp values: scr_regex_* adapters, no trace (no refs inside)
elem.kind === "date" || // Date objects: scr_date_* adapters, no trace (no refs inside)
(elem.kind === "array" && traceAdapter(host, elem) !== null);
if (!useRef) {
if (small) {
@@ -1163,7 +1164,6 @@ const SCR_ARR_OPS_RELEASE_NOTHING = 2;
export function boxNewCall(host: ShapeHost, t: IrType): string {
const plain: Partial<Record<IrType["kind"], number>> = {
f64: 0,
date: 0,
procStream: 0,
bool: 1,
string: 2,
@@ -1188,11 +1188,7 @@ export function boxNewCall(host: ShapeHost, t: IrType): string {
/** Box accessor suffix (boxAccess): scalars unboxed, ref kinds pointers. */
export function boxAccess(t: IrType): "f64" | "bool" | "ref" {
return t.kind === "f64" || t.kind === "date" || t.kind === "procStream"
? "f64"
: t.kind === "bool"
? "bool"
: "ref";
return t.kind === "f64" || t.kind === "procStream" ? "f64" : t.kind === "bool" ? "bool" : "ref";
}
/* ── record shapes ────────────────────────────────────────────────────── */
@@ -1208,7 +1204,6 @@ export function llFieldType(
if (POINTER_KINDS.has(t.kind)) return "ptr";
switch (t.kind) {
case "f64":
case "date":
case "procStream":
return "double";
case "bool":
@@ -105,13 +105,7 @@ export class StackCaptures {
// sites, including duplicated finally bodies and versioned loops.
if (!this.locals.has(id) || this.remaining < 48) return null;
const ref = isRefCounted(type);
if (
!ref &&
type.kind !== "f64" &&
type.kind !== "date" &&
type.kind !== "procStream" &&
type.kind !== "bool"
)
if (!ref && type.kind !== "f64" && type.kind !== "procStream" && type.kind !== "bool")
return null;
this.remaining -= 48;
const B = host.B;
@@ -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;
}
@@ -234,6 +234,10 @@ export class LlWalkers {
this.host.declare(`declare void @scr_jb_put_json_str(ptr, ptr)`);
B.line(`call void @scr_jb_put_json_str(ptr %b, ptr %v)`);
break;
case "date":
this.host.declare(`declare void @scr_jb_put_date(ptr, ptr)`);
B.line(`call void @scr_jb_put_date(ptr %b, ptr %v) ; toJSON(): ISO text, null if invalid`);
break;
case "dyn":
// A checked-dynamic value serializes itself (runtime walker).
// Bare stringify of dyn stays frontend-fenced, and overflow
@@ -927,6 +931,17 @@ export class LlWalkers {
case "string":
this.putScrStr(B, buf, this.unionPeek(B, u, unionId));
break;
case "date": {
host.declare(`declare ptr @scr_date_object_to_string(ptr)`);
host.declare(`declare void @scr_str_release(ptr)`);
const text = B.tmp();
B.line(
`${text} = call ptr @scr_date_object_to_string(ptr ${this.unionPeek(B, u, unionId)})`,
);
this.putScrStr(B, buf, text);
B.line(`call void @scr_str_release(ptr ${text})`);
break;
}
case "f64": {
host.declare(`declare double @scr_union_get_f64(ptr)`);
host.declare(`declare void @scr_jb_put_number(ptr, double)`);
@@ -6,19 +6,29 @@ export const NATIVE_CODEGEN_LLVM_VERSION = "22.1.8";
export type NativeCodegenOutputKind = "asm" | "obj";
/** LLVM text per partition, and the most partitions one executable uses. */
/** LLVM text that stays one module, LLVM text per partition, the fewest
* partitions (MIN_PROGRAM_PARTITIONS, of at least MIN_PARTITION_BYTES each)
* a divided module gets, and the most one executable uses. */
const SINGLE_PARTITION_BYTES = 1024 * 1024;
const PROGRAM_PARTITION_BYTES = 4 * 1024 * 1024;
const MIN_PARTITION_BYTES = 512 * 1024;
const MIN_PROGRAM_PARTITIONS = 8;
const MAX_PROGRAM_PARTITIONS = 64;
/** Executable objects this large are divided into partitions that the helper
* simplifies, optimizes and compiles concurrently, importing small functions
* across partitions for inlining, and reuses individually across builds. The
/** Executable objects larger than SINGLE_PARTITION_BYTES are divided into
* partitions that the helper simplifies, optimizes and compiles concurrently,
* importing small functions across partitions for inlining, and reuses
* individually across builds. A divided module gets at least
* MIN_PROGRAM_PARTITIONS so that mid-size programs use several cores. The
* count is a power of two that depends only on the module's size, on every
* target, so artifacts never vary with the host and ordinary edits keep every
* function in its partition. */
export function nativeProgramPartitions(llvmBytes: number): number {
const required = Math.ceil(llvmBytes / PROGRAM_PARTITION_BYTES);
if (required < 2) return 1;
if (llvmBytes <= SINGLE_PARTITION_BYTES) return 1;
const required = Math.max(
Math.ceil(llvmBytes / PROGRAM_PARTITION_BYTES),
Math.min(MIN_PROGRAM_PARTITIONS, Math.ceil(llvmBytes / MIN_PARTITION_BYTES)),
);
return Math.min(MAX_PROGRAM_PARTITIONS, 2 ** Math.ceil(Math.log2(required)));
}
@@ -352,11 +352,13 @@ test("sanitized native artifacts fail before helper resolution", async () => {
test("program partitions depend only on module size", () => {
const megabyte = 1024 * 1024;
expect(nativeProgramPartitions(4 * megabyte)).toBe(1);
expect(nativeProgramPartitions(4 * megabyte + 1)).toBe(2);
expect(nativeProgramPartitions(megabyte)).toBe(1);
expect(nativeProgramPartitions(megabyte + 1)).toBe(4);
expect(nativeProgramPartitions(2 * megabyte)).toBe(4);
expect(nativeProgramPartitions(2 * megabyte + 1)).toBe(8);
expect(nativeProgramPartitions(32 * megabyte)).toBe(8);
// Powers of two keep every function in its partition across small edits.
expect(nativeProgramPartitions(13 * megabyte)).toBe(4);
expect(nativeProgramPartitions(16 * megabyte)).toBe(4);
expect(nativeProgramPartitions(32 * megabyte + 1)).toBe(16);
expect(nativeProgramPartitions(100 * megabyte)).toBe(32);
expect(nativeProgramPartitions(1024 * megabyte)).toBe(64);
expect(nativePartitionPaths("/build.d/program.o", 3)).toEqual([
@@ -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);
});
+118
View File
@@ -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<Record<string, string>> = {
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;
}
+28 -1
View File
@@ -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<number> {
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<number> {
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<number> {
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<number> {
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<number> {
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<number> {
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<number> {
...(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
+7
View File
@@ -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 <baseline|x86-64-v2|x86-64-v3|x86-64-v4|native>
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 <pattern>
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 },
+7
View File
@@ -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
@@ -656,7 +656,7 @@ export function genericSignatureTypeDiag(typeText: string, loc: SrcLoc): ScrDiag
export function indexSignatureTypeDiag(typeText: string, loc: SrcLoc): ScrDiagnostic {
return {
code: "SC2006",
message: `values of type '${typeText}' 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, Promises, or 'unknown')`,
message: `values of type '${typeText}' 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')`,
loc,
milestone: "M2",
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",
@@ -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" },
@@ -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<EarlyExecutableRouteOptions, "nativeEnvironment"
if (options.strip) hash.update("strip\0");
if (options.windowsSubsystem === "gui") hash.update("windows-subsystem-gui\0");
if (options.detachedExit) hash.update("detached-exit\0");
for (const part of targetCpuKeyParts(options.targetCpu)) hash.update(`${part}\0`);
if (options.profile !== undefined) hash.update(`${options.profile}\0`);
if (options.checker !== undefined) hash.update(`checker:${options.checker}\0`);
if (options.youngNursery) hash.update("young-nursery\0");
+101 -10
View File
@@ -185,8 +185,24 @@ class TypePrinter {
readonly declarations: string[] = [];
private readonly inProgress = new Map<number, { name: string | null }>();
/** While set, printing registers no recursive alias (projectable()). */
private dry = false;
constructor(private readonly checker: ts.TypeChecker) {}
/** Whether a type projects, without naming it in the generated file. */
projectable(type: ts.Type): boolean {
if (type.flags & ANYISH) return false;
const was = this.dry;
this.dry = true;
try {
const text = this.print(type, 0);
return text !== null && text !== "unknown";
} finally {
this.dry = was;
}
}
/** The alias naming a projectable type, or null. Top-level any/unknown
* never projects. */
project(type: ts.Type): string | null {
@@ -212,7 +228,12 @@ class TypePrinter {
if (f & ts.TypeFlags.Null) return "null";
if (f & ts.TypeFlags.Undefined) return "undefined";
if (f & ts.TypeFlags.EnumLiteral || f & ts.TypeFlags.Enum) return null;
if (f & ts.TypeFlags.BooleanLiteral) return type.intrinsicName === "true" ? "true" : "false";
// A boolean literal type carries its value (`intrinsicName` is not
// set for it by every checker facade).
if (f & ts.TypeFlags.BooleanLiteral)
return (type as ts.BooleanLiteralType).value === true || type.intrinsicName === "true"
? "true"
: "false";
if (f & ts.TypeFlags.StringLiteral) return JSON.stringify(String(type.value));
if (f & ts.TypeFlags.NumberLiteral) {
const v = Number(type.value);
@@ -341,7 +362,7 @@ class TypePrinter {
if (props.length > 0 && readonlyCount === props.length) return null;
const text = `{ ${members.join("; ")} }`;
if (frame.name !== null) {
this.declarations.push(`type ${frame.name} = ${text};`);
if (!this.dry) this.declarations.push(`type ${frame.name} = ${text};`);
return frame.name;
}
return text;
@@ -517,9 +538,17 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection |
const declSf = input.declProgram.getSourceFile(file);
if (jsSf === undefined || declSf === undefined || jsSf.text !== declSf.text) continue;
const fileEdits: Edit[] = [];
/** Candidate expressions (by js node) with their alias. */
const exprCandidates = new Map<ts.Node, string>();
/** Candidate expressions (by js node) with their declared type, named
* only for the casts that survive suppression. */
const exprCandidates = new Map<ts.Node, ts.Type>();
const exprOrder: ts.Node[] = [];
/** Reads whose projectable receiver the JavaScript world types `any`:
* kept only when nothing types the receiver. */
const deferred = new Set<ts.Node>();
/** Package calls whose declared type does not project. */
const unprojected = new Set<ts.Node>();
/** The declaration-world twin of every walked node. */
const twins = new Map<ts.Node, ts.Node>();
const considerType = (jsNode: ts.Node, declNode: ts.Node): boolean => {
if (
@@ -603,8 +632,18 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection |
} else if (ts.isPropertyAccessExpression(d) || ts.isElementAccessExpression(d)) {
const recvDecl = typeAt(declChecker, d.expression);
const recvJs = typeAt(jsChecker, (jsNode as ts.PropertyAccessExpression).expression);
const recvProjectable = recvDecl !== null && printer.project(recvDecl) !== null;
const recvProjectable = recvDecl !== null && printer.projectable(recvDecl);
produced = !recvProjectable || (recvJs !== null && !isAnyish(recvJs));
// A projectable receiver the JavaScript world types `any` is
// usually typed by another candidate. One that is a package value
// nothing projects stays `any`, such as a safeParse result narrowed
// to its success arm (the whole result is not data: its error arm
// is a class): the read crosses here instead, if the suppression
// below confirms the receiver stays untyped.
if (!produced) {
produced = true;
deferred.add(jsNode);
}
} else if (ts.isAwaitExpression(d)) {
const inner = unwrapTransparent(d.expression);
produced =
@@ -633,6 +672,12 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection |
declaredInPackage(resolveAlias(raw));
}
if (!produced) return;
if (isCall && !deferred.has(jsNode)) {
// Recorded before the contextual-type test below: a package value
// whose type does not project stays `any` wherever it flows.
const t = typeAt(declChecker, d);
if (t === null || !printer.projectable(t)) unprojected.add(jsNode);
}
// A consumer that takes anything needs no conversion.
try {
const ctx = declChecker.getContextualType(d);
@@ -641,10 +686,8 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection |
// no contextual type
}
const declType = typeAt(declChecker, d);
if (declType === null) return;
const alias = printer.project(declType);
if (alias === null) return;
exprCandidates.set(jsNode, alias);
if (declType === null || !printer.projectable(declType)) return;
exprCandidates.set(jsNode, declType);
exprOrder.push(jsNode);
};
@@ -703,6 +746,7 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection |
const walk = (jsNode: ts.Node, declNode: ts.Node): void => {
if (jsNode.kind !== declNode.kind) return;
twins.set(jsNode, declNode);
if (ts.isTypeNode(jsNode) || ts.isExpressionWithTypeArguments(jsNode)) {
if (considerType(jsNode, declNode)) return;
} else {
@@ -749,21 +793,67 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection |
result = willBeTyped(decl.initializer, depth + 1);
else if (ts.isParameter(decl) && annotated.has(decl)) result = true;
else if (ts.isBindingElement(decl)) result = false;
else if (
(ts.isVariableDeclaration(decl) || ts.isParameter(decl)) &&
decl.type !== undefined
)
result = annotationWillBeTyped(decl.type);
}
symbolTyped.set(sym, result);
return result;
}
return false;
};
/** An annotation in this file whose declaration-world type projects:
* after the rewrite it names the projected structure (a package type
* is replaced by its alias, a user alias of one has its right-hand
* side replaced). */
const annotationWillBeTyped = (type: ts.TypeNode): boolean => {
const twin = twins.get(type);
if (twin === undefined) return false;
let declType: ts.Type | null;
try {
declType = declChecker.getTypeFromTypeNode(twin as ts.TypeNode);
} catch {
return false;
}
return declType !== null && printer.projectable(declType);
};
const receiverOf = (e: ts.Node): ts.Node | null => {
if (ts.isPropertyAccessExpression(e) || ts.isElementAccessExpression(e)) return e.expression;
if (ts.isIdentifier(e)) return null;
return null;
};
const kept: ts.Node[] = [];
/** A receiver that stays `any` after the rewrite: a package call whose
* type does not project, or a user const bound to one. */
const staysUntyped = (e: ts.Node): boolean => {
const u = unwrapTransparent(e as ts.Expression);
if (unprojected.has(u)) return true;
if (!ts.isIdentifier(u)) return false;
let sym: ts.Symbol | undefined;
try {
sym = jsChecker.getSymbolAtLocation(u);
} catch {
return false;
}
const decls = sym?.declarations ?? [];
if (decls.length !== 1) return false;
const decl = decls[0]!;
return (
ts.isVariableDeclaration(decl) &&
decl.type === undefined &&
decl.initializer !== undefined &&
decl.parent !== undefined &&
ts.isVariableDeclarationList(decl.parent) &&
(decl.parent.flags & ts.NodeFlags.Const) !== 0 &&
unprojected.has(unwrapTransparent(decl.initializer))
);
};
for (const e of exprOrder) {
const recv = receiverOf(e);
if (recv !== null && willBeTyped(recv)) continue;
if (deferred.has(e) && (recv === null || !staysUntyped(recv))) continue;
// An identifier candidate that is a user binding never gets here
// (only package imports are candidates).
kept.push(e);
@@ -800,7 +890,8 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection |
)
continue;
}
const alias = exprCandidates.get(e)!;
const alias = printer.project(exprCandidates.get(e)!);
if (alias === null) continue;
const start = ts.tokenStartOfNode(e);
const stmt = e.parent;
let prefix = "(";
@@ -1454,6 +1454,7 @@ function lowerPkiCall(
],
sign: ["crypto.signOneshot", 3, 3, "sign(algorithm, data, key)"],
verify: ["crypto.verifyOneshot", 4, 4, "verify(algorithm, data, key, signature)"],
diffieHellman: ["crypto.diffieHellman", 1, 2, "diffieHellman(options[, callback])"],
createSign: ["crypto.createSign", 1, 1, "createSign(algorithm)"],
createVerify: ["crypto.createVerify", 1, 1, "createVerify(algorithm)"],
};
@@ -1474,6 +1475,7 @@ function lowerPkiCall(
return { kind: "libCall", fn, args: values, type: VOID, loc };
const call: IrExpr = { kind: "libCall", fn, args: values, type: DYN, loc };
const result = lowerer.mapTypeOf(lowerer.typeOf(expr));
if (result === null || result.kind === "dyn") return call;
// diffieHellman's callback form answers undefined.
if (result === null || result.kind === "dyn" || result.kind === "void") return call;
return lowerer.coerceInto(expr, call, result);
}
@@ -53,7 +53,13 @@ export function lowerDateCall(
call: ts.CallExpression,
access: ts.PropertyAccessExpression,
): IrExpr | null {
if (call.questionDotToken || access.questionDotToken) return null;
// An optional chain lowers the call again once it has narrowed the
// receiver (Lowerer.chainHandled).
if (
(call.questionDotToken && !lowerer.chainHandled.has(call)) ||
(access.questionDotToken && !lowerer.chainHandled.has(access))
)
return null;
const loc = locOf(call);
const member = lowerer.stdlibGlobalMember(access, "Date");
if (member === "now" || member === "parse" || member === "UTC")
@@ -265,7 +265,7 @@ export function lowerFsPromisesOpenCall(
"use open(path[, stringFlags[, numericMode]])",
);
}
const path = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING);
const path = lowerFsPath(lowerer, expr.arguments.nodes[0]!);
const defaultFlags: IrExpr = { kind: "strLit", value: "r", type: STRING, loc };
const defaultMode: IrExpr = { kind: "numLit", value: 0o666, type: F64, loc };
const flags = expr.arguments.nodes[1]
@@ -705,7 +705,7 @@ export function lowerFsPromisesEncodedRead(
)
return null;
}
const path = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING);
const path = lowerFsPath(lowerer, expr.arguments.nodes[0]!);
const encoding: IrExpr =
optionsType.kind === "string"
? lowerer.lowerExprExpecting(options, STRING)
@@ -853,7 +853,7 @@ export function lowerFsPromisesMkdirCall(
);
}
const { recursive, modeNode } = options;
const path = lowerer.lowerExprExpecting(expr.arguments.nodes[0]!, STRING);
const path = lowerFsPath(lowerer, expr.arguments.nodes[0]!);
const mode: IrExpr | null = modeNode ? lowerer.lowerExprExpecting(modeNode, F64) : null;
const type: IrType = { kind: "promise", inner: VOID };
if (!recursive) {
@@ -22,7 +22,10 @@ import { builtinImportOf } from "./module-bindings.js";
* lib/internal/fs/glob.js). The pattern and options cross as dyn values so
* the runtime validates them in Node's order. The result form (strings or
* Dirent rows) comes from the call's mapped result type, so withFileTypes
* must be statically known; the runtime checks that the option agrees. */
* must be statically known; the runtime checks that the option agrees.
* Because of that check, the form follows the checker's resolved overload
* even where an untyped options argument leaves the overload undecided
* (dynamicOverloadChoice). */
/** The interned Dirent record array (type-mapper.ts), or null. */
function direntArray(lowerer: Lowerer, type: IrType | null | undefined): boolean {
@@ -67,7 +70,7 @@ export function lowerFsGlobSyncCall(
loc: SrcLoc,
): IrExpr {
const args = globArgs(lowerer, call, "fs.globSync");
const callType = lowerer.mapTypeOf(lowerer.typeOf(call));
const callType = lowerer.mapTypeOf(lowerer.checker.getTypeAtLocation(call));
const types = direntArray(lowerer, callType);
if (types) fenceDirentExclude(lowerer, call, "fs.globSync");
if (!types && !(callType?.kind === "array" && callType.elem.kind === "string")) {
@@ -132,7 +135,9 @@ export function lowerFspGlobCall(lowerer: Lowerer, call: ts.CallExpression, loc:
}
const args = globArgs(lowerer, call, "fs/promises glob");
// The async iterator's yielded type (its first type argument).
const yielded = lowerer.checker.getTypeArguments(lowerer.typeOf(call) as ts.TypeReference)[0];
const yielded = lowerer.checker.getTypeArguments(
lowerer.checker.getTypeAtLocation(call) as ts.TypeReference,
)[0];
const element = yielded ? lowerer.mapTypeOf(yielded) : null;
const rows: IrType | null = element ? { kind: "array", elem: element } : null;
const types = direntArray(lowerer, rows);
@@ -5,30 +5,40 @@
* conversion (url.fileURLToPathUrl). The later arguments through the last
* one with side effects are evaluated into hidden locals before the
* conversion, in source order, so they run even when it throws and each
* reads what it reads in Node. The conversion keeps Node's error order when every
* earlier path is a URL too and each argument Node validates before the
* path is absent or a literal that passes that validation; other URL
* forms fence with SC2020, as do the fs.promises functions, whose
* conversion errors are rejections rather than throws, and existsSync,
* which answers false for a URL it cannot convert. */
* reads what it reads in Node. Before the conversion, an earlier string
* path gets its null-byte check (copyFileSync, renameSync) and a runtime
* readFileSync encoding its name check, which Node runs first. existsSync
* and the fs.promises functions convert to a value instead
* (url.fileURLToPathValue) and answer false or a rejected promise for a
* conversion error. An argument Node validates before the path must
* otherwise be absent or a literal that passes that validation; other
* forms fence with SC2020. */
import * as ts from "../../ts7/adapter.js";
import { type Lowerer } from "../lowerer.js";
import { locOf } from "../../program.js";
import { sideEffectFreeOptionValue } from "../surfaces.js";
import { stripTypeWrappers } from "../typed-arguments.js";
import { type IrExpr, type IrStmt, STRING, URL_T } from "../../../ir/ir.js";
import { boolLit, varRef } from "../../../ir/build.js";
import { BOOL, DYN, type IrExpr, type IrStmt, STRING, URL_T, VOID } from "../../../ir/ir.js";
import { InternalCompilerError } from "../../../errors.js";
/** The fs functions whose path parameters accept a URL: the path
* parameter indexes, and whether Node validates the paths before every
* other argument (otherwise the options come first). */
const FS_URL_PATHS: Readonly<
Record<string, { readonly paths: readonly number[]; readonly pathsFirst: boolean }>
> = {
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<Record<string, FsPathRow>> = {
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<Record<string, readonly number[]>> = {
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<Record<string, FsPathRow>> = {
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<number, string>();
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
);
}
@@ -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]),
);
@@ -2,7 +2,7 @@ import { keyedHelperName } from "../stable-names.js";
import { checkedPromiseAll, thenReader } from "../checked-promise-all.js";
import { InternalCompilerError } from "../../../errors.js";
import * as ts from "../../ts7/adapter.js";
import { newFnCtx, type Lowerer } from "../lowerer.js";
import type { Lowerer } from "../lowerer.js";
import { varRef } from "../../../ir/build.js";
import { maybePromiseResolve, maybePromiseUnionOf } from "../maybe-promise.js";
import { isJsSourceFile, locOf } from "../../program.js";
@@ -17,31 +17,23 @@ import {
UNDEFINED_T,
VOID,
arrayOf,
funcOf,
isUnitType,
typeEquals,
typeKey,
} from "../../../ir/ir.js";
/** `Promise.all([p, q, r])` over DIFFERENT promise types — the checker's
* tuple overload, `Promise<[A, B, C]>`. One interned async helper per
* entry/result signature takes the entries as arguments, awaits the
* runtime combinator over the same promises (each widened to
* Promise<void>, a type-only view), so settlement is Node's exactly: the
* first rejection in settlement order wins and every later one counts as
* handled; then it reads each (now fulfilled) entry and returns the
* tuple. Entries keep their identity — no value converts through the
* checked-dynamic tree. Non-promise entries (numbers, strings, booleans,
* unit values — never thenable) pass straight into their slot; a
* checked-dynamic entry (a JavaScript function's result) joins through
* PromiseResolve: the promise it holds, a promise that adopts a thenable,
* or a promise fulfilled with its plain value. The tuple
* fulfills one microtask turn after the combinator does. Null when the
* result type or an entry has no typed home here. */
function lowerTuplePromiseAll(
* tuple overload, `Promise<[A, B, C]>`: the result type and tuple shape,
* or null when the result type or an entry has no typed home here.
* Entries are promises, checked-dynamic values (a JavaScript function's
* result) and plain numbers, strings, booleans and unit values, which are
* never thenable. */
function tuplePromiseAllShape(
lowerer: Lowerer,
call: ts.CallExpression,
elems: IrExpr[],
): IrExpr | null {
elems: readonly IrExpr[],
): { resultT: IrType & { kind: "promise" }; tuple: IrType & { kind: "record" } } | null {
const resultT = lowerer.mapTypeOf(lowerer.typeOf(call));
if (resultT?.kind !== "promise" || resultT.inner.kind !== "record") return null;
const shape = lowerer.shapes.get(resultT.inner.shapeId);
@@ -53,135 +45,159 @@ function lowerTuplePromiseAll(
const settles = (t: IrType): boolean => t.kind === "promise" || t.kind === "dyn";
if (!elems.some((e) => settles(e.type))) return null;
if (!elems.every((e) => settles(e.type) || plainValue(e.type))) return null;
return { resultT, tuple: resultT.inner };
}
/** `Promise.all([p, q, r])` over different promise types. Each entry
* becomes a promise in input order (a plain value through Promise.resolve,
* a checked-dynamic one through its PromiseResolve view: the promise it
* holds, a promise that adopts a thenable, or a promise fulfilled with the
* value), widened to Promise<void>, a type-only view. The runtime
* combinator over them gives Node's settlement: the first rejection in
* settlement order wins and every later one counts as handled. The
* result settles inside the combinator's own settlement, so it and its
* awaits land on Node's microtask turns; a fulfillment runs one interned
* function per entry/result signature that reads the fulfilled entries
* into the tuple. Entries keep their identity: no value converts through
* the checked-dynamic tree. */
function lowerTuplePromiseAll(
lowerer: Lowerer,
call: ts.CallExpression,
elems: IrExpr[],
): IrExpr | null {
const plan = tuplePromiseAllShape(lowerer, call, elems);
if (!plan) return null;
const { resultT, tuple } = plan;
const loc = locOf(call);
const key = `${elems.map((e) => typeKey(e.type)).join(",")}->${typeKey(resultT.inner)}`;
const voidPromise: IrType & { kind: "promise" } = { kind: "promise", inner: VOID };
const entriesT = arrayOf(voidPromise);
const dynPromise: IrType = { kind: "promise", inner: DYN };
// A checked-dynamic entry's PromiseResolve can run a `then` getter, which
// Node runs after the whole array literal evaluated. With such an entry,
// every entry evaluates into a hidden local first.
const staged: IrStmt[] = [];
const values = !elems.some((e) => e.type.kind === "dyn")
? elems
: elems.map((e): IrExpr => {
if (e.kind === "unitLit") return e;
if (isUnitType(e.type)) {
staged.push({ kind: "exprStmt", expr: e, loc });
return {
kind: "unitLit",
unit: e.type.kind === "nullT" ? "null" : "undefined",
type: e.type,
loc,
};
}
const local = lowerer.declareHiddenLocal("%allValue", e.type);
staged.push({ kind: "varDecl", localId: local.id, init: e, loc });
return varRef(local.id, e.type, loc);
});
const entries = values.map((e): IrExpr => {
let promise: IrExpr;
if (e.type.kind === "promise") promise = e;
else if (e.type.kind === "dyn")
promise = {
kind: "libCall",
fn: "dyn.promiseResolve",
args: [e, thenReader(lowerer, loc)],
type: dynPromise,
loc,
};
else if (isUnitType(e.type)) {
// A unit entry's value is its type's one value; the promise only
// takes its place in the combinator.
const resolved: IrExpr = {
kind: "intrinsic",
name: "promise.resolve",
args: [],
type: voidPromise,
loc,
};
promise =
e.kind === "unitLit"
? resolved
: {
kind: "seqExpr",
stmts: [{ kind: "exprStmt", expr: e, loc }],
result: resolved,
type: voidPromise,
loc,
};
} else
promise = {
kind: "intrinsic",
name: "promise.resolve",
args: [e],
type: { kind: "promise", inner: e.type },
loc,
};
return promise.type.kind === "promise" && promise.type.inner.kind === "void"
? promise
: { kind: "promiseVoidWiden", value: promise, type: voidPromise, loc };
});
const key = `${elems.map((e) => typeKey(e.type)).join(",")}->${typeKey(tuple)}`;
let name = lowerer.arrHofHelpers.get(`promise.allTuple:${key}`);
if (name === undefined) {
name = keyedHelperName(lowerer, "%promise.allTuple", `promise.allTuple:${key}`);
lowerer.arrHofHelpers.set(`promise.allTuple:${key}`, name);
const ctx = newFnCtx(false, null, null, resultT.inner);
ctx.isAsync = true;
lowerer.fnStack.push(ctx);
try {
const params = elems.map((e, i) => ({
localId: `entry.${i}`,
name: `entry${i}`,
type: e.type,
}));
const ref = (i: number): IrExpr => varRef(`entry.${i}`, elems[i]!.type, loc);
const dynPromise: IrType = { kind: "promise", inner: DYN };
const body: IrStmt[] = [];
// Each dyn entry's promise view, in entry order: the promise it
// holds, a promise that adopts a thenable's settlement, or a promise
// fulfilled with the plain value (PromiseResolve's rule).
const views = new Map<number, IrExpr>();
elems.forEach((e, i) => {
if (e.type.kind !== "dyn") return;
const view = lowerer.declareHiddenLocal("%allEntry", dynPromise);
body.push({
kind: "varDecl",
localId: view.id,
init: {
kind: "libCall",
fn: "dyn.promiseResolve",
args: [ref(i), thenReader(lowerer, loc)],
type: dynPromise,
loc,
},
loc,
});
views.set(i, varRef(view.id, dynPromise, loc));
});
const widened: IrExpr[] = [];
elems.forEach((e, i) => {
const view = views.get(i);
if (view !== undefined) {
widened.push({
kind: "promiseVoidWiden",
value: view,
type: { kind: "promise", inner: VOID },
loc,
});
return;
}
if (e.type.kind !== "promise") return;
widened.push(
e.type.inner.kind === "void"
? ref(i)
: {
kind: "promiseVoidWiden",
value: ref(i),
type: { kind: "promise", inner: VOID },
const shape = lowerer.shapes.get(tuple.shapeId)!;
const entriesRef = varRef("entries.0", entriesT, loc);
// Shape fields are sorted by name ("10" before "2"); a tuple field's
// name is its position.
const fields = shape.fields.map((field) => {
const i = Number(field.name);
const type = elems[i]!.type;
const inner = type.kind === "promise" ? type.inner : type;
const value: IrExpr =
inner.kind === "void"
? { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc }
: isUnitType(inner)
? {
kind: "unitLit",
unit: inner.kind === "nullT" ? "null" : "undefined",
type: inner,
loc,
},
);
});
const settled: IrExpr = {
kind: "awaitExpr",
value: {
kind: "intrinsic",
name: "promise.all",
args: [
{
kind: "arrayLit",
elems: widened,
type: arrayOf({ kind: "promise", inner: VOID }),
loc,
},
],
type: { kind: "promise", inner: VOID },
loc,
},
type: VOID,
loc,
};
// Shape fields are sorted by name ("10" before "2"); a tuple field's
// name is its position.
const fields = shape.fields.map((field) => {
const i = Number(field.name);
const entry = elems[i]!;
let value: IrExpr;
const view = views.get(i);
if (view !== undefined) {
value = { kind: "awaitExpr", value: view, type: DYN, loc };
} else if (entry.type.kind === "promise") {
value =
entry.type.inner.kind === "void"
? { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc }
: { kind: "awaitExpr", value: ref(i), type: entry.type.inner, loc };
} else {
value = ref(i);
}
return { name: field.name, value: lowerer.coerceToExpected(value, field.type) };
});
body.push(
{ kind: "exprStmt", expr: settled, loc },
{
kind: "return",
value: { kind: "recordLit", fields, type: resultT.inner, loc },
loc,
},
);
lowerer.liftedFns.push({
name,
params,
returnType: resultT.inner,
locals: [
...params.map((p) => ({ id: p.localId, name: p.name, type: p.type, mutable: false })),
...lowerer.ctx.locals,
],
body,
async: true,
loc,
});
} catch (e) {
lowerer.arrHofHelpers.delete(`promise.allTuple:${key}`);
throw e;
} finally {
lowerer.fnStack.pop();
}
}
: {
kind: "libCall",
fn: "promise.fulfilledValue",
args: [
{
kind: "arrayGet",
arr: entriesRef,
index: { kind: "numLit", value: i, type: F64, loc },
type: voidPromise,
loc,
},
],
type: inner,
loc,
};
return { name: field.name, value: lowerer.coerceToExpected(value, field.type) };
});
lowerer.liftedFns.push({
name,
params: [{ localId: "entries.0", name: "entries", type: entriesT }],
returnType: tuple,
locals: [{ id: "entries.0", name: "entries", type: entriesT, mutable: false }],
body: [{ kind: "return", value: { kind: "recordLit", fields, type: tuple, loc }, loc }],
loc,
});
}
return { kind: "call", callee: name, args: elems, type: resultT, loc };
const all: IrExpr = {
kind: "libCall",
fn: "promise.allTuple",
args: [
{ kind: "arrayLit", elems: entries, type: entriesT, loc },
{ kind: "closure", fnName: name, captures: [], type: funcOf([entriesT], tuple), loc },
],
type: resultT,
loc,
};
return staged.length === 0
? all
: { kind: "seqExpr", stmts: staged, result: all, type: resultT, loc };
}
/** `Promise.race([...])` on THE Promise global: the entries lower
@@ -611,7 +611,6 @@ export function checkDefaultParamBodyType(
if (
bodyType.kind === "void" ||
bodyType.kind === "regex" ||
bodyType.kind === "date" ||
bodyType.kind === "dyn" ||
isUnitType(bodyType)
) {
@@ -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("<literal>")` 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 = <value>` 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 `= <split binding>`), 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<ts.Symbol>();
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 = <split binding>`,
* 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("<literal>")` 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)) {
@@ -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<string, string[]>();
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<string, string>();
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);
@@ -46,6 +46,13 @@ export function collectClassMethodMutations(
const typedClasses: (ts.ClassDeclaration | ts.ClassExpression)[] = [];
const symbolMethods = new Set<string>();
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);
};
@@ -125,11 +132,13 @@ export function collectClassMethodMutations(
};
// Whether no function value can inhabit `type`. Primitives qualify, and so
// do non-callable object types a function is not assignable to: one that
// requires a property functions lack, or a weak (all-optional) type that
// requires a property functions lack, a weak (all-optional) type that
// shares no property with functions (TypeScript's weak-type rule rejects
// callable sources too).
const functionFree = (type: ts.Type): boolean => {
if (type.isUnionType()) return ts.constituentTypes(type).every(functionFree);
// callable sources too), or a dictionary with no properties whose index
// signatures hold function-free values (functions have no index
// signature). `seen` stops recursive dictionary types.
const functionFree = (type: ts.Type, seen: Set<ts.Type> = new Set()): boolean => {
if (type.isUnionType()) return ts.constituentTypes(type).every((t) => functionFree(t, seen));
if (
(type.flags &
(ts.TypeFlags.StringLike |
@@ -149,7 +158,12 @@ export function collectClassMethodMutations(
)
return false;
const properties = lowerer.checker.getPropertiesOfType(type);
if (properties.length === 0) return false;
if (properties.length === 0) {
if (seen.has(type)) return true;
seen.add(type);
const indexes = lowerer.checker.getIndexInfosOfType(type);
return indexes.length > 0 && indexes.every((index) => functionFree(index.type, seen));
}
const required = properties.filter(
(property) => (property.flags & ts.SymbolFlags.Optional) === 0,
);
@@ -163,14 +177,32 @@ export function collectClassMethodMutations(
// A string index whose values cannot be functions excludes callable
// method slots. Class instances have no implicit index signature, so such
// a dictionary is only a class view when the class declares that index
// signature, and then every method must be assignable to its value type.
const functionFreeDictionary = (value: ts.Expression): boolean =>
lowerer.checker
.getIndexInfosOfType(lowerer.typeOf(value))
// signature, and then every method must be assignable to its value type,
// or when the dictionary keeps checked identity: it then holds the
// existing objects it receives, class instances included.
// A string-keyed dictionary that keeps checked identity holds the
// existing objects it receives, class instances included, so any write
// through it can replace a method.
const dictionaryView = (value: ts.Expression): boolean => {
const type = lowerer.typeOf(value);
return (
lowerer.checker.getPropertiesOfType(type).length === 0 &&
lowerer.checker
.getIndexInfosOfType(type)
.some((index) => (index.keyType.flags & ts.TypeFlags.StringLike) !== 0) &&
lowerer.classImplementedProtocol(type)
);
};
const functionFreeDictionary = (value: ts.Expression): boolean => {
const type = lowerer.typeOf(value);
if (lowerer.classImplementedProtocol(type)) return false;
return lowerer.checker
.getIndexInfosOfType(type)
.some(
(index) =>
(index.keyType.flags & ts.TypeFlags.StringLike) !== 0 && functionFree(index.type),
);
};
const literalLoopKeys = (key: ts.Expression): (string | number)[] | null => {
if (!ts.isIdentifier(key)) return null;
const symbol = lowerer.checker.getSymbolAtLocation(key);
@@ -442,11 +474,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" ||
@@ -456,7 +489,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];
@@ -570,7 +603,10 @@ export function collectClassMethodMutations(
);
for (const { target, site } of propertyWrites) {
const name = target.name.text;
if (functionFree(lowerer.typeOf(target)) || !methodOwnerCanInhabit(target.expression, name))
if (
!dictionaryView(target.expression) &&
(functionFree(lowerer.typeOf(target)) || !methodOwnerCanInhabit(target.expression, name))
)
continue;
if (lowerer.prototypeMethodAccesses.has(name)) lowerer.noteMethodSite(name, site);
else lowerer.noteMethodAccess(name, site);
@@ -611,7 +647,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) {
@@ -620,7 +656,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)
@@ -10,6 +10,9 @@ export class ClassProtocols {
private readonly accepted = new Map<ts.Type, ts.Symbol[]>();
/** What markIdentityFlows learned about the program, while it runs. */
private facts: FlowFacts | null = null;
/** The value types of the string-keyed dictionaries that keep checked
* identity (see markDictionaryViews). */
private readonly dictionaryViews = new Set<ts.Type>();
constructor(
private readonly checker: ts.TypeChecker,
@@ -323,6 +326,7 @@ export class ClassProtocols {
else if (names !== "all") names.add(name);
}
}
this.markDictionaryViews(sites, sourceNodes, observedNodes, writeNodes);
for (let i = 0; i < looseArguments.length; i++) {
const type = typeOf(looseNodes[i]!);
if (type === undefined) continue;
@@ -697,6 +701,163 @@ export class ClassProtocols {
return source !== undefined && this.isProgramClassInstance(source);
}
/** String-keyed dictionaries (`Record<string, V>`, `{ [key: string]: V }`)
* that receive an existing record or class instance. Natively a
* dictionary is a key/value record, and converting another object into
* one copies its fields, so writes through the dictionary and identity
* checks would miss the original. When the program writes through, or
* observes, a value of such a dictionary type, every dictionary with that
* value type keeps checked identity instead (records with the same value
* type share one native layout, so values pass between them). A source
* counts when it is an existing object (a binding, member, element or
* `this`, not a fresh literal or call result) whose type is a program
* class instance or a record with declared properties, or a constant
* `{}`, `object` or `unknown` binding initialized with one. */
private markDictionaryViews(
sites: readonly {
source: ts.Expression;
target: () => ts.Type | undefined;
contextual?: ts.Expression;
}[],
sourceNodes: readonly ts.Expression[],
observedNodes: readonly ts.Expression[],
writeNodes: readonly ts.Expression[],
): void {
// The calls the sites' arguments belong to, by callee name, built on
// first use.
let calls: Map<string, (ts.CallExpression | ts.NewExpression)[]> | null = null;
const callsNamed = (name: string): readonly (ts.CallExpression | ts.NewExpression)[] => {
if (calls === null) {
calls = new Map();
for (const site of sites) {
const call = site.contextual?.parent;
if (call === undefined || (!ts.isCallExpression(call) && !ts.isNewExpression(call)))
continue;
const callee = call.expression;
const named = ts.isIdentifier(callee)
? callee.text
: ts.isPropertyAccessExpression(callee)
? callee.name.text
: undefined;
if (named === undefined) continue;
const list = calls.get(named) ?? [];
if (!list.includes(call)) list.push(call);
calls.set(named, list);
}
}
return calls.get(name) ?? [];
};
const candidates: { index: number; sourceType: ts.Type }[] = [];
for (let i = 0; i < sites.length; i++) {
const source = sourceNodes[i]!;
if (!isExistingObject(source)) continue;
const sourceType = attempt(() => this.checker.getTypeAtLocation(source));
if (sourceType !== undefined && this.dictionarySource(source, sourceType, callsNamed))
candidates.push({ index: i, sourceType });
}
if (candidates.length === 0) return;
// One batch for the contextual targets of the candidate sites.
attempt(() =>
this.checker.prefetchContextualTypes(
candidates.flatMap(({ index }) => {
const contextual = sites[index]!.contextual;
return contextual === undefined ? [] : [contextual];
}),
),
);
const received = new Set<ts.Type>();
for (const { index, sourceType } of candidates) {
const target = attempt(sites[index]!.target);
const parts =
target === undefined ? [] : target.isUnionType() ? ts.constituentTypes(target) : [target];
for (const part of parts) {
const value = this.dictionaryValueType(part);
// A dictionary of `unknown` or `any` values is checked-dynamic already.
if (
value !== undefined &&
(value.flags & (ts.TypeFlags.Unknown | ts.TypeFlags.Any)) === 0 &&
this.dictionaryValueType(sourceType) !== value
)
received.add(value);
}
}
if (received.size === 0) return;
// The dictionary values the program writes through or observes.
const seen = new Set<ts.Type>();
for (const node of [...writeNodes, ...observedNodes]) {
const type = attempt(() => this.checker.getTypeAtLocation(node));
const parts =
type === undefined ? [] : type.isUnionType() ? ts.constituentTypes(type) : [type];
for (const part of parts) {
const value = this.dictionaryValueType(part);
if (value !== undefined) seen.add(value);
}
}
for (const value of received) if (seen.has(value)) this.dictionaryViews.add(value);
}
/** Whether `source` holds an existing object that a dictionary
* conversion would copy. */
private dictionarySource(
source: ts.Expression,
type: ts.Type,
callsNamed: (name: string) => readonly (ts.CallExpression | ts.NewExpression)[],
): boolean {
if (this.isProgramClassInstance(type) || this.isRecordWithFields(type)) return true;
if (!isDynamicView(this.checker, type) || !ts.isIdentifier(source)) return false;
const decl = attempt(() => this.checker.getSymbolAtLocation(source))?.valueDeclaration;
if (decl === undefined) return false;
const holdsObject = (expr: ts.Expression): boolean => {
const held = attempt(() => this.checker.getTypeAtLocation(this.flowSourceNode(expr)));
return (
held !== undefined && (this.isProgramClassInstance(held) || this.isRecordWithFields(held))
);
};
if (ts.isParameter(decl)) {
// A parameter holds what its callers pass: the calls of the function
// by name whose signature resolves to it.
const fn = decl.parent;
if (fn === undefined || !ts.isFunctionLike(fn)) return false;
const index = fn.parameters.nodes.indexOf(decl);
const name = functionName(fn);
if (name === undefined || index < 0) return false;
return callsNamed(name).some((call) => {
const argument = call.arguments?.nodes[index];
return (
argument !== undefined &&
!ts.isSpreadElement(argument) &&
attempt(() => this.checker.getResolvedSignature(call)?.declaration) === fn &&
holdsObject(argument)
);
});
}
return (
ts.isVariableDeclaration(decl) &&
decl.initializer !== undefined &&
ts.isVariableDeclarationList(decl.parent) &&
(decl.parent.flags & ts.NodeFlags.Const) !== 0 &&
holdsObject(decl.initializer)
);
}
/** A program object type with declared properties that is not a class
* instance, array, tuple, function or library type. */
private isRecordWithFields(type: ts.Type): boolean {
return this.isStructuralTarget(type) && this.checker.getPropertiesOfType(type).length > 0;
}
/** The value type of a string-keyed dictionary with no declared
* properties, or undefined for any other type. */
private dictionaryValueType(type: ts.Type): ts.Type | undefined {
if ((type.flags & ts.TypeFlags.Object) === 0) return undefined;
if (this.checker.isArrayType(type) || this.checker.isTupleType(type)) return undefined;
if (this.checker.getCallSignatures(type).length > 0) return undefined;
if (this.checker.getPropertiesOfType(type).length > 0) return undefined;
return this.checker
.getIndexInfosOfType(type)
.find((index) => (index.keyType.flags & ts.TypeFlags.String) !== 0)?.type;
}
private isProgramClassInstance(type: ts.Type): boolean {
const symbol = type.getSymbol();
return (
@@ -822,7 +983,13 @@ export class ClassProtocols {
if ((type.flags & ts.TypeFlags.Object) === 0) return false;
const previous = this.answers.get(type);
if (previous !== undefined) return previous;
const answer = this.protocol(type);
// A dictionary keeps checked identity when the protocols already mark
// it (a literal whose methods read `this`) or when it receives existing
// objects (markDictionaryViews).
const dictionaryValue = this.dictionaryValueType(type);
const answer =
this.protocol(type) ||
(dictionaryValue !== undefined && this.dictionaryViews.has(dictionaryValue));
this.answers.set(type, answer);
return answer;
}
@@ -922,6 +1089,44 @@ const IDENTITY_METHODS = new Set([
"includes",
]);
/** An expression that denotes an object that already exists, not a fresh
* literal or call result. */
function isExistingObject(node: ts.Expression): boolean {
return (
ts.isIdentifier(node) ||
ts.isPropertyAccessExpression(node) ||
ts.isElementAccessExpression(node) ||
node.kind === ts.SyntaxKind.ThisKeyword
);
}
/** `{}`, `object` and `unknown`: types whose values are checked-dynamic
* and keep the original object. */
function isDynamicView(checker: ts.TypeChecker, type: ts.Type): boolean {
if (type.flags & (ts.TypeFlags.Unknown | ts.TypeFlags.NonPrimitive)) return true;
return (
(type.flags & ts.TypeFlags.Object) !== 0 &&
checker.getPropertiesOfType(type).length === 0 &&
checker.getIndexInfosOfType(type).length === 0 &&
checker.getCallSignatures(type).length === 0 &&
checker.getConstructSignatures(type).length === 0
);
}
/** The name calls use for a function, method or function-valued binding. */
function functionName(fn: ts.Node): string | undefined {
if ((ts.isFunctionDeclaration(fn) || ts.isMethodDeclaration(fn)) && fn.name !== undefined)
return ts.isIdentifier(fn.name) ? fn.name.text : undefined;
if (
(ts.isArrowFunction(fn) || ts.isFunctionExpression(fn)) &&
fn.parent !== undefined &&
ts.isVariableDeclaration(fn.parent) &&
ts.isIdentifier(fn.parent.name)
)
return fn.parent.name.text;
return undefined;
}
function isNullishLiteral(node: ts.Expression): boolean {
while (ts.isParenthesizedExpression(node)) node = node.expression;
return (
@@ -1,4 +1,5 @@
import type { IrType } from "../../../ir/ir.js";
import type * as ts from "../../ts7/adapter.js";
/** Per-compilation conversion state. Recursive plans are scoped to the
* active planning stack; only fully validated copy routes are memoized. */
@@ -22,4 +23,7 @@ export class CoercionState {
/** Unions widenReducedReturn produced: only these read through the
* width-converting extraction (widthNarrowHelper). */
readonly widenedReturns = new Set<string>();
/** widenReducedReturn's results by declaration and inferred return type,
* for the results computed without cutting a recursive widening. */
readonly widenedReturnTypes = new Map<ts.Node, Map<ts.Type, IrType>>();
}
@@ -2358,10 +2358,11 @@ function reduceHelper(
* the left element first. Undefined values sink without reaching the user
* comparator. The callback sequence differs from V8's TimSort, so exact
* order and count parity are not claimed; results agree for consistent
* comparators. Without a comparator, string, number and boolean arrays use
* native stable ordering: their String conversions cannot call user code.
* Strings compare UTF-16 units; numbers compare their decimal spellings
* ([10, 9, 1] → [1, 10, 9]). Other element types still need a comparator. */
* comparators. Without a comparator, string, number, boolean and Date
* arrays use native stable ordering: their String conversions cannot call
* user code. Strings compare UTF-16 units; numbers compare their decimal
* spellings ([10, 9, 1] → [1, 10, 9]); Dates compare their toString() text.
* Other element types still need a comparator. */
export function lowerArraySortCall(
lowerer: Lowerer,
call: ts.CallExpression,
@@ -2374,7 +2375,7 @@ export function lowerArraySortCall(
const copyFirst = method === "toSorted";
if (
call.arguments.nodes.length === 0 &&
(elem.kind === "string" || elem.kind === "f64" || elem.kind === "bool")
(elem.kind === "string" || elem.kind === "f64" || elem.kind === "bool" || elem.kind === "date")
) {
return {
kind: "arrIntrinsic",
@@ -2389,7 +2390,7 @@ export function lowerArraySortCall(
lowerer.noLowering(
`.${method} with ${call.arguments.nodes.length} arguments`,
call,
"default ordering supports string[], number[] and boolean[]; other element types need a comparator",
"default ordering supports string[], number[], boolean[] and Date[]; other element types need a comparator",
);
}
const receiver = lowerer.lowerExpr(access.expression);
@@ -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",
@@ -1081,14 +1081,25 @@ export function lowerArrayMethodCall(
(lowerer.unions
.get(elem.unionId)
?.arms.every(
(a) => a.kind === "f64" || a.kind === "string" || a.kind === "bool" || isUnitType(a),
(a) =>
a.kind === "f64" ||
a.kind === "string" ||
a.kind === "bool" ||
a.kind === "date" ||
isUnitType(a),
) ??
false);
if (elem.kind !== "f64" && elem.kind !== "string" && elem.kind !== "bool" && !joinableUnion) {
if (
elem.kind !== "f64" &&
elem.kind !== "string" &&
elem.kind !== "bool" &&
elem.kind !== "date" &&
!joinableUnion
) {
lowerer.unsupported(
"SC1090",
call,
"'.join()' on arrays of this element type (number, string, and boolean arrays join — unions of those with undefined/null arms too, the units printing empty like JS)",
"'.join()' on arrays of this element type (number, string, boolean and Date arrays join — unions of those with undefined/null arms too, the units printing empty like JS)",
);
}
const receiver = lowerer.lowerExpr(access.expression);
@@ -1,7 +1,9 @@
import { keyedHelperName } from "../stable-names.js";
import { boolLit, dynUndefinedExpr, numLit, varRef } from "../../../ir/build.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,
@@ -15,6 +17,7 @@ import {
type IrLibFn,
type IrLocal,
type IrParam,
type IrFunction,
type IrStmt,
type IrType,
STRING,
@@ -30,7 +33,8 @@ import {
import { isJsSourceFile, locOf } from "../../program.js";
import { stdlibGlobalNameOf } from "../surfaces.js";
import type { Lowerer } from "../lowerer.js";
import { newFnCtx, own } from "../lowerer.js";
import { ENTRY_NAME, newFnCtx, own } from "../lowerer.js";
import { everyStmt } from "../../../ir/traverse.js";
import { buildBytesSortFn, closureKeyOrder } from "../lower-array-sort.js";
import { isSafeToDiscard } from "../expressions/evaluation-safety.js";
import { lowerDynObjectLiteral } from "../expressions/object-literals.js";
@@ -40,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";
@@ -903,6 +907,126 @@ function lowerDataViewNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
return lowerArrayBufferView(lowerer, args, "dv", locOf(expr));
}
/** `receiver.set(src, offset?)` on a lowered typed-array receiver. */
function bytesSetIntrinsic(
lowerer: Lowerer,
receiver: IrExpr,
argNodes: readonly ts.Expression[],
loc: SrcLoc,
): IrExpr {
const src = lowerer.lowerExpr(argNodes[0]!);
const method = src.type.kind === "bytes" ? "setFrom" : "setFromDyn";
const args = [src.type.kind === "bytes" ? src : lowerer.coerceInto(argNodes[0]!, src, DYN)];
if (argNodes.length === 2)
args.push(
lowerOptionalArgument(lowerer, argNodes[1]!, F64, {
kind: "numLit",
value: 0,
type: F64,
loc,
}),
);
return { kind: "bytesIntrinsic", method, receiver, args, type: VOID, loc };
}
/** `Uint8Array.prototype.set.call(target, src, offset?)`, through any typed
* array constructor's prototype (they share %TypedArray%.prototype.set). A
* target the checker types as a typed array or Buffer lowers like
* `target.set(src, offset)`. Any other target, a DataView included, is
* checked at run time: a typed array calls its `set`, and
* anything else throws Node's TypeError "this is not a typed array.",
* after every argument is evaluated. */
export function lowerBytesPrototypeSetCall(
lowerer: Lowerer,
call: ts.CallExpression,
access: ts.PropertyAccessExpression,
): IrExpr | null {
if (
call.questionDotToken ||
access.questionDotToken ||
access.name.text !== "call" ||
!ts.isPropertyAccessExpression(access.expression)
)
return null;
const method = access.expression;
const proto = method.expression;
if (
method.questionDotToken ||
method.name.text !== "set" ||
!ts.isPropertyAccessExpression(proto) ||
proto.questionDotToken ||
proto.name.text !== "prototype" ||
!ts.isIdentifier(proto.expression) ||
!lowerer.isStdlibMember(method)
)
return null;
const ctor = lowerer.resolveValueSymbol(proto.expression);
if (!ctor || !lowerer.isStdlibSymbol(ctor) || !own(BYTES_CTORS, ts.symbolName(ctor))) return null;
const argNodes = call.arguments.nodes;
if (argNodes.length < 2 || argNodes.length > 3 || argNodes.some(ts.isSpreadElement)) return null;
const loc = locOf(call);
const target = lowerer.lowerExpr(argNodes[0]!);
// A DataView lowers to the same bytes type, so the checker type decides:
// only a typed array (or a Buffer) takes the method's path.
const targetSymbol = lowerer.typeOf(argNodes[0]!).getSymbol();
if (
target.type.kind === "bytes" &&
targetSymbol !== undefined &&
lowerer.isStdlibSymbol(targetSymbol) &&
(own(BYTES_CTORS, ts.symbolName(targetSymbol)) !== undefined ||
ts.symbolName(targetSymbol) === "Buffer")
)
return bytesSetIntrinsic(lowerer, target, argNodes.slice(1), loc);
const slot = (value: IrExpr): { decl: IrStmt; read: IrExpr } => {
const local = lowerer.declareHiddenLocal("%setArg", DYN);
return {
decl: { kind: "varDecl", localId: local.id, init: value, loc },
read: varRef(local.id, DYN, loc),
};
};
const slots = [
slot(lowerer.coerceInto(argNodes[0]!, target, DYN)),
...argNodes.slice(1).map((arg) => slot(lowerer.lowerExprExpecting(arg, DYN))),
];
const isTypedArray: IrExpr = {
kind: "libCall",
fn: "util.typeIs",
args: [slots[0]!.read, { kind: "strLit", value: "isTypedArray", type: STRING, loc }],
type: BOOL,
loc,
};
return {
kind: "seqExpr",
stmts: [
...slots.map((s) => s.decl),
{
kind: "if",
cond: { kind: "unary", op: "!", operand: isTypedArray, type: BOOL, loc },
then: [
{
kind: "exprStmt",
expr: nodeThrowExpr(1, "", "this is not a typed array.", VOID, loc),
loc,
},
],
else_: null,
loc,
},
],
result: {
kind: "dynInvoke",
recv: slots[0]!.read,
method: "set",
calleeName: ts.getTextOfNode(method),
args: slots.slice(1).map((s) => s.read),
type: DYN,
loc,
},
type: DYN,
loc,
};
}
/** Method calls on typed-array/Buffer receivers: slice (copy), subarray
* (shared view), toReversed, copyWithin, set(src,
* offset?), and the u8-only Buffer surface: toString(enc?) plus the
@@ -910,6 +1034,119 @@ function lowerDataViewNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
* 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<Record<string, IrBytesIntrinsicMethod>> = {
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,
@@ -918,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);
@@ -964,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);
@@ -1106,21 +1369,7 @@ export function lowerBytesMethodCall(
lowerer.noLowering(`.set with ${nArgs} arguments on typed arrays`, call);
}
const receiver = lowerer.lowerExprExpecting(access.expression, receiverIr);
const src = lowerer.lowerExpr(call.arguments.nodes[0]!);
const method = src.type.kind === "bytes" ? "setFrom" : "setFromDyn";
const args = [
src.type.kind === "bytes" ? src : lowerer.coerceInto(call.arguments.nodes[0]!, src, DYN),
];
if (nArgs === 2)
args.push(
lowerOptionalArgument(lowerer, call.arguments.nodes[1]!, F64, {
kind: "numLit",
value: 0,
type: F64,
loc,
}),
);
return { kind: "bytesIntrinsic", method, receiver, args, type: VOID, loc };
return bytesSetIntrinsic(lowerer, receiver, call.arguments.nodes, loc);
}
if (name === "toString") {
// Buffer's toString(encoding?) — utf8 by default. A 0-arg toString
@@ -1271,6 +1520,8 @@ export function lowerBytesMethodCall(
"the numeric families read/write Buffer bytes",
);
}
if (!numericArguments(lowerer, call))
return dynNumericCall(lowerer, call, access, name, receiverIr, loc);
const write = name.startsWith("write");
const required = write ? 1 : 0; // value; offset defaults to 0
if (nArgs < required || nArgs > required + 1) {
@@ -1448,6 +1699,39 @@ const BUF_BIGINT_METHODS: Record<string, { sign: boolean; le: boolean } | undefi
writeBigUint64LE: { sign: false, le: true },
};
/** Whether every argument of a numeric read/write is statically a number. */
function numericArguments(lowerer: Lowerer, call: ts.CallExpression): boolean {
return call.arguments.nodes.every((a) => lowerer.mapTypeOf(lowerer.typeOf(a))?.kind === "f64");
}
/** A numeric read/write with an argument that is not a number (a string
* offset from untyped JavaScript): the checked-dynamic Buffer method runs
* Node's argument validation at runtime. */
function dynNumericCall(
lowerer: Lowerer,
call: ts.CallExpression,
access: ts.PropertyAccessExpression,
name: string,
receiverIr: IrType,
loc: SrcLoc,
): IrExpr {
const receiver = lowerer.lowerExprExpecting(access.expression, receiverIr);
return {
kind: "dynCheck",
value: {
kind: "dynInvoke",
recv: { kind: "dynFrom", value: receiver, type: DYN, loc, liveRef: true },
method: name,
calleeName: ts.getTextOfNode(access),
args: call.arguments.nodes.map((a) => lowerer.lowerExprExpecting(a, DYN)),
type: DYN,
loc,
},
type: F64,
loc,
};
}
/** The variable-width quartet (offset + byteLength) → sign/endian token. */
const BUF_NUM_VAR_METHODS: Record<string, string | undefined> = {
readUIntBE: "ube",
@@ -1807,7 +2091,12 @@ function knownBufferProducer(
* or empty encoding and throws ERR_UNKNOWN_ENCODING for an unknown name; an
* ArrayBuffer takes the second argument as its byte offset; other sources
* ignore it. */
function dynBufferFromParts(source: IrExpr, encoding: IrExpr, loc: SrcLoc): IrExpr {
function dynBufferFromParts(
source: IrExpr,
encoding: IrExpr,
loc: SrcLoc,
length?: IrExpr,
): IrExpr {
const utf8: IrExpr = { kind: "strLit", value: "utf8", type: STRING, loc };
const isString = (x: IrExpr): IrExpr => ({
kind: "dynTest",
@@ -1848,20 +2137,48 @@ function dynBufferFromParts(source: IrExpr, encoding: IrExpr, loc: SrcLoc): IrEx
type: BYTES_U8,
loc,
},
// An ArrayBuffer takes the second argument as its byte offset
// (scr_buffer_from_dyn_offset_pooled); other values convert with utf8.
else_: {
kind: "libCall",
fn: "buffer.poolFromDynOffset",
args: [source, encoding],
type: BYTES_U8,
loc,
},
// An ArrayBuffer takes the second argument as its byte offset and the
// third as its length (scr_buffer_from_dyn_range_pooled); other values
// convert with utf8.
else_: length
? {
kind: "libCall",
fn: "buffer.poolFromDynRange",
args: [source, encoding, length],
type: BYTES_U8,
loc,
}
: {
kind: "libCall",
fn: "buffer.poolFromDynOffset",
args: [source, encoding],
type: BYTES_U8,
loc,
},
type: BYTES_U8,
loc,
};
}
/** Buffer.from(arrayBuffer, byteOffset[, length]): each argument in order
* as a checked-dynamic value, read by the runtime as Node's
* fromArrayBuffer reads them. */
function bufferFromArrayBufferRange(
lowerer: Lowerer,
args: readonly ts.Expression[],
loc: SrcLoc,
first?: IrExpr,
): IrExpr {
const operands = [0, 1, 2].map((i) =>
i === 0 && first
? first
: args[i]
? lowerer.lowerExprExpecting(args[i]!, DYN)
: dynUndefinedExpr(loc),
);
return { kind: "libCall", fn: "buffer.poolFromDynRange", args: operands, type: BYTES_U8, loc };
}
/** Lowers `source` and `encNode` (or undefined) into hidden dyn locals, in
* source order, and builds the result from their references. */
function withDynBufferArguments(
@@ -1909,52 +2226,81 @@ function dynBufferFromRuntimeEncoding(
);
}
/** The deprecated `new Buffer(arg[, encoding])` in a dependency's
* JavaScript, through the global or an alias of it (buffer-crc32 and yazl
* keep it as the fallback for runtimes without Buffer.alloc/Buffer.from).
* Node never emits DEP0005 for calls from node_modules, so the construction
* is exactly Node's: a number allocates zero-filled bytes (and throws
* ERR_INVALID_ARG_TYPE beside a string second argument), anything else
* converts as Buffer.from does. Null elsewhere: other callers owe the
* deprecation warning. */
export function lowerNodeModulesBufferConstruct(
/** The deprecated `Buffer(arg[, encodingOrOffset[, length]])`, called or
* constructed through the global or an alias of it, as Node's Buffer
* function runs it: the arguments evaluate in order, DEP0005 is reported
* on the next tick (once, and never for a call made from node_modules,
* which only counts toward Node's 10000-call check), then a number
* allocates zero-filled bytes (and throws ERR_INVALID_ARG_TYPE beside a
* string second argument) and anything else converts as Buffer.from does.
* Null for spread arguments and more than three. */
export function lowerBufferConstruct(
lowerer: Lowerer,
expr: ts.NewExpression,
expr: ts.NewExpression | ts.CallExpression,
): IrExpr | null {
const sf = expr.getSourceFile();
if (!isJsSourceFile(sf) || !/[\\/]node_modules[\\/]/.test(sf.fileName)) return null;
if (ts.isCallExpression(expr) && expr.questionDotToken) return null;
const args = expr.arguments?.nodes ?? [];
if (args.length < 1 || args.length > 2 || args.some(ts.isSpreadElement)) return null;
if (args.length > 3 || args.some(ts.isSpreadElement)) return null;
if (stdlibGlobalNameOf(lowerer, expr.expression) !== "Buffer") return null;
const loc = locOf(expr);
const value = withDynBufferArguments(
lowerer,
lowerer.lowerExpr(args[0]!),
args[1],
loc,
(source, encoding) => ({
const locals = [0, 1, 2].slice(0, Math.max(args.length, 2)).map((i) => {
const local = lowerer.declareHiddenLocal("%bufferArgument", DYN);
const node = args[i];
const init = node === undefined ? dynUndefinedExpr(loc) : lowerer.lowerExprExpecting(node, DYN);
return { local, init };
});
const [source, second, third] = locals.map(({ local }) => varRef(local.id, DYN, loc)) as [
IrExpr,
IrExpr,
IrExpr | undefined,
];
const inNodeModules = /[\\/]node_modules[\\/]/.test(expr.getSourceFile().fileName);
const value: IrExpr = {
kind: "ternary",
cond: { kind: "dynTest", test: "number", value: source, type: BOOL, loc },
then: {
kind: "ternary",
cond: { kind: "dynTest", test: "number", value: source, type: BOOL, loc },
then: {
kind: "ternary",
cond: { kind: "dynTest", test: "string", value: encoding, type: BOOL, loc },
then: { kind: "libCall", fn: "buffer.newStringFail", args: [source], type: BYTES_U8, loc },
else_: {
kind: "libCall",
fn: "buffer.alloc",
args: [{ kind: "dynCheck", value: source, type: F64, loc }],
type: BYTES_U8,
loc,
},
cond: { kind: "dynTest", test: "string", value: second, type: BOOL, loc },
then: { kind: "libCall", fn: "buffer.newStringFail", args: [source], type: BYTES_U8, loc },
else_: {
kind: "libCall",
fn: "buffer.alloc",
args: [{ kind: "dynCheck", value: source, type: F64, loc }],
type: BYTES_U8,
loc,
},
else_: dynBufferFromParts(source, encoding, loc),
type: BYTES_U8,
loc,
}),
);
return { kind: "libCall", fn: "buffer.brand", args: [value], type: BYTES_U8, loc };
},
else_: dynBufferFromParts(source, second, loc, third),
type: BYTES_U8,
loc,
};
return {
kind: "seqExpr",
stmts: [
...locals.map(({ local, init }): IrStmt => ({
kind: "varDecl",
localId: local.id,
init,
loc,
})),
{
kind: "exprStmt",
expr: {
kind: "libCall",
fn: inNodeModules ? "buffer.deprecationCount" : "buffer.deprecation",
args: [],
type: VOID,
loc,
},
loc,
},
],
result: { kind: "libCall", fn: "buffer.brand", args: [value], type: BYTES_U8, loc },
type: BYTES_U8,
loc,
};
}
/** Buffer.from(array or bytes, extra): Node copies an array-like source and
@@ -2018,7 +2364,16 @@ function lowerBufferStaticValue(
lowerer.typeOf(args[0]!).getSymbol(),
)
) {
return lowerArrayBufferView(lowerer, args, "u8", loc);
// A byte offset or length follows Node's fromArrayBuffer (its
// conversions and ERR_BUFFER_OUT_OF_BOUNDS), not the typed-array view.
if (args.length === 1) return lowerArrayBufferView(lowerer, args, "u8", loc);
return bufferFromArrayBufferRange(lowerer, args, loc);
}
// Buffer.from(value, byteOffset, length) over an untyped value: an
// ArrayBuffer's range, or the two-argument conversion of other values.
if (args.length === 3 && !args.some(ts.isSpreadElement)) {
const first = lowerer.lowerExpr(args[0]!);
if (first.type.kind === "dyn") return bufferFromArrayBufferRange(lowerer, args, loc, first);
}
// Buffer.from(x.buffer[, byteOffset[, length]]): a u8 VIEW sharing
// x's storage (Node shares the ArrayBuffer; length is in BYTES).
@@ -2445,11 +2800,101 @@ function lowerBufferStaticValue(
const s = lowerer.lowerExprExpecting(args[0]!, STRING);
return { kind: "libCall", fn: "buffer.isEncoding", args: [s], type: BOOL, loc };
}
// Any other single argument: Node answers false for non-strings.
if (args.length === 1 && !ts.isSpreadElement(args[0]!)) {
const value = lowerer.declareHiddenLocal("%bufferEncoding", DYN);
const ref = varRef(value.id, DYN, loc);
return {
kind: "seqExpr",
stmts: [
{
kind: "varDecl",
localId: value.id,
init: lowerer.lowerExprExpecting(args[0]!, DYN),
loc,
},
],
result: {
kind: "logical",
op: "&&",
left: { kind: "dynTest", test: "string", value: ref, type: BOOL, loc },
right: {
kind: "libCall",
fn: "buffer.isEncoding",
args: [{ kind: "dynCheck", value: ref, type: STRING, loc }],
type: BOOL,
loc,
},
type: BOOL,
loc,
},
type: BOOL,
loc,
};
}
lowerer.noLowering(
"Buffer.isEncoding with this argument shape",
call,
"one string-typed argument is the lowered form",
"one argument is the lowered form",
);
}
return null; // of, copyBytesFrom, ... → the SC2020 member fence
}
/** `Buffer.prototype.<name>` or `Buffer.prototype[key]` read in JavaScript
* (smart-buffer's `typeof Buffer.prototype[method]` probe): the member as
* scr_buffer_prototype_member answers it. Null for other expressions. */
export function lowerBufferPrototypeMember(
lowerer: Lowerer,
expr: ts.PropertyAccessExpression | ts.ElementAccessExpression,
): IrExpr | null {
if (expr.questionDotToken || !isJsSourceFile(expr.getSourceFile())) return null;
const receiver = expr.expression;
if (
!ts.isPropertyAccessExpression(receiver) ||
receiver.questionDotToken ||
receiver.name.text !== "prototype" ||
stdlibGlobalNameOf(lowerer, receiver.expression) !== "Buffer"
)
return null;
const loc = locOf(expr);
const key: IrExpr = ts.isPropertyAccessExpression(expr)
? {
kind: "dynFrom",
value: { kind: "strLit", value: expr.name.text, type: STRING, loc },
type: DYN,
loc,
}
: lowerer.lowerExprExpecting(expr.argumentExpression, DYN);
return { kind: "libCall", fn: "buffer.prototypeMember", args: [key], type: DYN, loc };
}
const BUFFER_VALUE_FNS: ReadonlySet<string> = new Set([
"buffer.constructor",
"buffer.staticValue",
"buffer.prototypeMember",
]);
/** A program that reads the Buffer function's value installs it in %main,
* so a Buffer's `.constructor` answers the same function. */
export function installBufferValue(functions: IrFunction[]): void {
let found = false;
const visitor = {
expr: (e: IrExpr): boolean => {
if (e.kind === "libCall" && BUFFER_VALUE_FNS.has(e.fn)) found = true;
return !found;
},
stmt: (): boolean => !found,
};
for (const fn of functions) {
for (const s of fn.body) everyStmt(s, visitor);
if (found) break;
}
const main = functions.find((fn) => fn.name === ENTRY_NAME);
if (!found || !main) return;
main.body.unshift({
kind: "exprStmt",
expr: { kind: "libCall", fn: "buffer.installValue", args: [], type: VOID, loc: main.loc },
loc: main.loc,
});
}
@@ -0,0 +1,207 @@
import * as ts from "../ts7/adapter.js";
import type { Lowerer } from "./lowerer.js";
/** Date methods whose receiver is only read for its time value: a Date
* local used as their receiver can store the time value instead of a Date
* object (builtins/dates.ts lowers them). */
const TIME_VALUE_METHODS = new Set([
"getTime",
"valueOf",
"toISOString",
"toJSON",
"toString",
"toDateString",
"toTimeString",
"toUTCString",
"getFullYear",
"getUTCFullYear",
"getMonth",
"getUTCMonth",
"getDate",
"getUTCDate",
"getDay",
"getUTCDay",
"getHours",
"getUTCHours",
"getMinutes",
"getUTCMinutes",
"getSeconds",
"getUTCSeconds",
"getMilliseconds",
"getUTCMilliseconds",
"getTimezoneOffset",
]);
const RELATIONAL = new Set([
ts.SyntaxKind.LessThanToken,
ts.SyntaxKind.LessThanEqualsToken,
ts.SyntaxKind.GreaterThanToken,
ts.SyntaxKind.GreaterThanEqualsToken,
]);
/** True when a local `const` (or a `let` that is never assigned again)
* initialized with `new Date(...)` can keep the Date's time value instead
* of a Date object: no use observes the Date's identity. Every use reads the time value as
* a Date method receiver, a String() or template operand, a relational or
* string `+` operand, or `new Date(d)`'s argument, inside the declaring
* function, or inside the block or loop that scopes a module-level
* binding (no closure captures it). Top-level module bindings keep the
* object. Other uses box the time value into a new Date at the use. */
export function isTimeValueDateDeclaration(
lowerer: Lowerer,
decl: ts.VariableDeclaration,
): boolean {
const known = lowerer.timeValueDateDecisions.get(decl);
if (known !== undefined) return known;
const answer = decideTimeValueDate(lowerer, decl);
lowerer.timeValueDateDecisions.set(decl, answer);
return answer;
}
function decideTimeValueDate(lowerer: Lowerer, decl: ts.VariableDeclaration): boolean {
if (!ts.isIdentifier(decl.name) || decl.initializer === undefined) return false;
// Only a Date the declaration creates: another reference to an existing
// Date can mutate it (`const d = a; a.setTime(0)`), and d must see that.
if (!createsDate(lowerer, decl.initializer)) return false;
const sf = decl.getSourceFile();
if (sf.isDeclarationFile || /\.[cm]?jsx?$/.test(sf.fileName)) return false;
const list = decl.parent;
if (!ts.isVariableDeclarationList(list)) return false;
const isConst = (list.flags & ts.NodeFlags.Const) !== 0;
const isLet = (list.flags & ts.NodeFlags.Let) !== 0;
if (!isConst && !isLet) return false;
if (!ts.isVariableStatement(list.parent) && !ts.isForStatement(list.parent)) return false;
const fn = enclosingFunction(decl);
// The uses to check: the function body, or for a binding inside a block
// or loop at module level (a local of the module's initializer, not a
// module binding) the block or loop that scopes it.
let scope: ts.Node | undefined;
if (fn !== null) scope = fn.body;
else if (!inClassStaticBlock(decl)) scope = moduleBlockScope(decl);
if (scope === undefined) return false;
// The lib Date interface by its symbol first: mapping every candidate
// binding's type would intern its unions before the lowering does.
const typeSymbol = lowerer.typeOf(decl.name).getSymbol();
if (
typeSymbol === undefined ||
ts.symbolName(typeSymbol) !== "Date" ||
!typeSymbol.declarations.some(
(d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()),
)
)
return false;
if (lowerer.mapTypeOf(lowerer.typeOf(decl.name))?.kind !== "date") return false;
const symbol = lowerer.checker.getSymbolAtLocation(decl.name);
if (!symbol) return false;
let ok = true;
const visit = (node: ts.Node): void => {
if (!ok) return;
if (ts.isIdentifier(node) && node !== decl.name && node.text === decl.name.text) {
if (lowerer.checker.getSymbolAtLocation(node) === symbol) {
if (enclosingFunction(node) !== fn || !readsTimeValue(lowerer, node)) ok = false;
}
return;
}
ts.forEachChild(node, visit);
};
visit(scope);
return ok;
}
/** True for `new Date(...)` of the standard Date, through parentheses and
* type-only wrappers: a Date no other reference can reach. */
function createsDate(lowerer: Lowerer, init: ts.Expression): boolean {
let e = init;
while (
ts.isParenthesizedExpression(e) ||
ts.isAsExpression(e) ||
ts.isSatisfiesExpression(e) ||
ts.isTypeAssertion(e) ||
ts.isNonNullExpression(e)
)
e = e.expression;
return ts.isNewExpression(e) && lowerer.isStdlibGlobal(e.expression, "Date");
}
/** The block or for statement that scopes a module-level binding which is
* not a module binding: a declaration in a block or loop body, or in a
* for statement's initializer. Undefined for a top-level declaration. */
function moduleBlockScope(decl: ts.VariableDeclaration): ts.Node | undefined {
const list = decl.parent;
if (!list || !ts.isVariableDeclarationList(list)) return undefined;
if (ts.isForStatement(list.parent)) return list.parent;
const statement = list.parent;
if (!ts.isVariableStatement(statement) || ts.isSourceFile(statement.parent)) return undefined;
for (let cur: ts.Node | undefined = statement.parent; cur; cur = cur.parent) {
if (ts.isBlock(cur) || ts.isCaseBlock(cur)) return cur;
if (ts.isSourceFile(cur)) return undefined;
}
return undefined;
}
function inClassStaticBlock(node: ts.Node): boolean {
for (let cur = node.parent; cur; cur = cur.parent) {
if (ts.isClassStaticBlockDeclaration(cur)) return true;
if (ts.isSourceFile(cur)) return false;
}
return false;
}
function enclosingFunction(node: ts.Node): ts.FunctionLikeDeclaration | null {
for (let cur = node.parent; cur; cur = cur.parent) {
if (
ts.isFunctionDeclaration(cur) ||
ts.isFunctionExpression(cur) ||
ts.isArrowFunction(cur) ||
ts.isMethodDeclaration(cur) ||
ts.isConstructorDeclaration(cur) ||
ts.isGetAccessorDeclaration(cur) ||
ts.isSetAccessorDeclaration(cur)
)
return cur;
if (ts.isClassStaticBlockDeclaration(cur) || ts.isSourceFile(cur)) return null;
}
return null;
}
/** A use of the binding that reads only its time value. */
function readsTimeValue(lowerer: Lowerer, id: ts.Identifier): boolean {
let node: ts.Node = id;
while (ts.isParenthesizedExpression(node.parent)) node = node.parent;
const parent = node.parent;
if (ts.isPropertyAccessExpression(parent) && parent.expression === node) {
const call = parent.parent;
return (
!parent.questionDotToken &&
ts.isCallExpression(call) &&
call.expression === parent &&
!call.questionDotToken &&
TIME_VALUE_METHODS.has(parent.name.text) &&
lowerer.isStdlibMember(parent)
);
}
if (ts.isTemplateSpan(parent)) return parent.expression === node;
if (ts.isCallExpression(parent)) {
return (
parent.arguments.nodes.length === 1 &&
parent.arguments.nodes[0] === node &&
ts.isIdentifier(parent.expression) &&
lowerer.isStdlibGlobal(parent.expression, "String")
);
}
if (ts.isNewExpression(parent)) {
return (
parent.arguments?.nodes.length === 1 &&
parent.arguments.nodes[0] === node &&
ts.isIdentifier(parent.expression) &&
lowerer.isStdlibGlobal(parent.expression, "Date")
);
}
if (ts.isBinaryExpression(parent)) {
const other = parent.left === node ? parent.right : parent.left;
const otherKind = lowerer.mapTypeOf(lowerer.typeOf(other))?.kind;
if (RELATIONAL.has(parent.operatorToken.kind)) return otherKind === "date";
if (parent.operatorToken.kind === ts.SyntaxKind.PlusToken) return otherKind === "string";
}
return false;
}
@@ -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;
@@ -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
@@ -764,10 +765,21 @@ export function ensureString(lowerer: Lowerer, e: IrExpr, node: ts.Node): IrExpr
(lowerer.unions
.get(elem.unionId)
?.arms.every(
(a) => a.kind === "f64" || a.kind === "string" || a.kind === "bool" || isUnitType(a),
(a) =>
a.kind === "f64" ||
a.kind === "string" ||
a.kind === "bool" ||
a.kind === "date" ||
isUnitType(a),
) ??
false);
if (elem.kind === "f64" || elem.kind === "string" || elem.kind === "bool" || joinableUnion) {
if (
elem.kind === "f64" ||
elem.kind === "string" ||
elem.kind === "bool" ||
elem.kind === "date" ||
joinableUnion
) {
const sep: IrExpr = { kind: "strLit", value: ",", type: STRING, loc: e.loc };
return {
kind: "arrIntrinsic",
@@ -2195,14 +2207,41 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
loc,
};
if (bothStr) return { kind: "strCmp", op: cmpOp, left, right, type: BOOL, loc };
// A Date converts to a new checked-dynamic Date on each crossing, so
// comparisons keep their Date fence instead of comparing copies.
if (
left.type.kind !== "date" &&
right.type.kind !== "date" &&
lowerer.dynConvertible(left.type) &&
lowerer.dynConvertible(right.type)
) {
// Dates compare by time value, the number hint of ToPrimitive. Both
// operands evaluate before either converts, so a right operand that
// mutates the left Date (`a < (a.setTime(t), b)`) is seen: with such
// a right operand, both Dates bind first and their times read after.
if (left.type.kind === "date" && right.type.kind === "date") {
const time = (d: IrExpr): IrExpr => ({
kind: "libCall",
fn: "date.getTime",
args: [d],
type: F64,
loc,
});
if (isSafeToRepeat(right))
return { kind: "bin", op: cmpOp, left: time(left), right: time(right), type: BOOL, loc };
const leftDate = lowerer.declareHiddenLocal("%date", left.type);
const rightDate = lowerer.declareHiddenLocal("%date", right.type);
return {
kind: "seqExpr",
stmts: [
{ kind: "varDecl", localId: leftDate.id, init: left, loc },
{ kind: "varDecl", localId: rightDate.id, init: right, loc },
],
result: {
kind: "bin",
op: cmpOp,
left: time(varRef(leftDate.id, left.type, loc)),
right: time(varRef(rightDate.id, right.type, loc)),
type: BOOL,
loc,
},
type: BOOL,
loc,
};
}
if (lowerer.dynConvertible(left.type) && lowerer.dynConvertible(right.type)) {
return {
kind: "libCall",
fn: "dyn.compare",
@@ -2416,6 +2455,7 @@ export function lowerStrictEquality(
idLeft.type.kind === "map" ||
idLeft.type.kind === "set" ||
idLeft.type.kind === "regex" ||
idLeft.type.kind === "date" ||
idLeft.type.kind === "url" ||
idLeft.type.kind === "object" ||
idLeft.type.kind === "record" ||
@@ -2452,14 +2492,7 @@ export function lowerStrictEquality(
loc,
};
}
// A Date converts to a new checked-dynamic Date on each crossing; its
// identity has no representation, so equality keeps the fence.
if (
idLeft.type.kind !== "date" &&
idRight.type.kind !== "date" &&
lowerer.dynConvertible(idLeft.type) &&
lowerer.dynConvertible(idRight.type)
) {
if (lowerer.dynConvertible(idLeft.type) && lowerer.dynConvertible(idRight.type)) {
return {
kind: "dynScalarEq",
left: lowerer.coerceToExpected(idLeft, DYN),
@@ -71,6 +71,14 @@ 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
* string-key element access (`a["length"]`) and string indexing (`s[0]`
@@ -104,6 +112,8 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre
const en = lowerEnumAccess(lowerer, expr);
if (en) return en;
}
const bufferMember = lowerBufferPrototypeMember(lowerer, expr);
if (bufferMember) return bufferMember;
const cachedModule = lowerRequireCacheElement(lowerer, expr);
if (cachedModule) return cachedModule;
const namespaceElement = lowerModuleNamespaceElement(lowerer, expr);
@@ -299,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).
@@ -1518,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
@@ -1702,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 {
@@ -2243,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");
}
@@ -9,6 +9,7 @@ import { type Lowerer, PoisonError } from "../lowerer.js";
import {
BOOL,
BYTES_ELEMENT_NAME,
DATE_T,
DYN,
F64,
type IrExpr,
@@ -24,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 {
@@ -171,6 +177,12 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc:
// when the checker types this USE as a single arm, maybeNarrow
// bridges the tagged representation with a unionNarrow.
const local = lowerer.resolveLocal(expr);
// A Date local holding its time value reads as that Date: only uses
// that read the time value reach here (date-values.ts).
if (local && lowerer.timeValueDateLocals.has(local)) {
const time: IrExpr = { kind: "varRef", localId: local.id, type: F64, loc };
return { kind: "libCall", fn: "date.fromTime", args: [time], type: DATE_T, loc };
}
if (local) {
// A switch can dispatch past a lexical declaration. Keep this
// previously refused codec form out until switch-scope TDZ boxes
@@ -744,6 +756,20 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc:
return { kind: "libCall", fn: "webcrypto.object", args: [], type: DYN, loc };
const nativeAlias = stdlibGlobalNameOf(lowerer, expr);
if (nativeAlias === "Buffer" && expr.text !== "Buffer") {
// An alias through a cast of globalThis carries the cast's static type,
// which the identity token below does not have.
const aliasSymbol = identifierReadSymbol(lowerer, expr);
if (aliasSymbol && lowerer.castGlobalAliases.has(aliasSymbol)) {
lowerer.unsupported(
"SC1090",
expr,
"the Buffer constructor read through a cast of globalThis as a value",
"test it in a condition, read its typeof, or call its members",
);
}
// JavaScript reads the Buffer function's value (builtinGlobalValue).
if (isJsSourceFile(ts.getSourceFileOfNode(expr)))
return { kind: "libCall", fn: "buffer.constructor", args: [], type: DYN, loc };
return { kind: "strLit", value: "[builtin Buffer]", type: STRING, loc };
}
if (lowerer.isStdlibGlobal(expr, "fetch")) {
@@ -958,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.
@@ -1,6 +1,7 @@
import { widensReducedRecords } from "../function-returns.js";
import { widthNarrowHelper } from "../coercions/unions.js";
/* 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";
@@ -156,6 +157,7 @@ import {
lowerStringCodesValue,
} from "../lower-builtin-values.js";
import { lowerArrayFromValue } from "../containers/array-construction.js";
import { lowerBufferPrototypeMember } from "../containers/bytes.js";
import { lowerNetAddressStaticRead } from "../builtins/net-address.js";
import { STATS_TYPE_TESTS } from "../builtins/filesystem.js";
import { arrayValueRead, arrayValueType } from "../array-values.js";
@@ -313,6 +315,10 @@ export function lowerPropertyAccessExpr(
// Cloudflare probe) folds like `navigator.userAgent`.
const navigator = lowerNavigatorMember(lowerer, expr, NODE_COMPAT_VERSION);
if (navigator) return navigator;
{
const member = lowerBufferPrototypeMember(lowerer, expr);
if (member) return member;
}
// A dgram socket's method read as a value in JavaScript (`emit:
// socket.emit.bind(socket)`): the socket crosses as its checked-dynamic
// handle, whose methods are function values that dispatch on their
@@ -1809,6 +1815,28 @@ export function lowerIntrinsicProperty(
return lowerNumberParserValue(lowerer, numberMember, locOf(expr));
if (lowerer.stdlibGlobalMember(expr, "Buffer") === "poolSize")
return { kind: "libCall", fn: "buffer.poolSize", args: [], type: F64, loc: locOf(expr) };
{
const member = lowerer.stdlibGlobalMember(expr, "Buffer");
if (member === "name")
return { kind: "strLit", value: "Buffer", type: STRING, loc: locOf(expr) };
if (member === "length") return numLit(3, locOf(expr));
// JavaScript reads the statics from the Buffer function's value, so
// each has one identity and Node's name and length.
if (
member !== null &&
BUFFER_VALUE_STATICS.has(member) &&
isJsSourceFile(ts.getSourceFileOfNode(expr))
) {
const loc = locOf(expr);
return {
kind: "libCall",
fn: "buffer.staticValue",
args: [{ kind: "strLit", value: member, type: STRING, loc }],
type: DYN,
loc,
};
}
}
const netAddressStatic = lowerNetAddressStaticRead(lowerer, expr);
if (netAddressStatic) return netAddressStatic;
if (lowerer.stdlibGlobalMember(expr, "Buffer") === "isBuffer")
@@ -3385,6 +3413,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,
@@ -4213,6 +4248,8 @@ export function builtinFunctionValue(name: string, loc: SrcLoc): IrExpr {
* null when it stays an identity token (JavaScript) or fenced. */
export function builtinGlobalValue(canonical: string, loc: SrcLoc, js = false): IrExpr | null {
if (js && GLOBAL_FUNCTION_VALUES.has(canonical)) return builtinFunctionValue(canonical, loc);
if (js && canonical === "Buffer")
return { kind: "libCall", fn: "buffer.constructor", args: [], type: DYN, loc };
if (GLOBAL_CONSTRUCTOR_VALUES.has(canonical)) {
return {
kind: "libCall",
@@ -4340,6 +4377,22 @@ function fenceBuiltinConstructorStaticRead(
return null;
}
/** The Buffer statics the Buffer function's value holds
* (scr_buffer_constructor_value). */
const BUFFER_VALUE_STATICS: ReadonlySet<string> = new Set([
"from",
"copyBytesFrom",
"of",
"alloc",
"allocUnsafe",
"allocUnsafeSlow",
"isBuffer",
"compare",
"isEncoding",
"concat",
"byteLength",
]);
/** Object.prototype members a plain record inherits, read as values. */
const OBJECT_PROTOTYPE_VALUE_READS: ReadonlySet<string> = new Set([
"constructor",
@@ -88,6 +88,12 @@ export function lowerTypeofExpr(lowerer: Lowerer, expr: ts.TypeOfExpression, loc
if (builtinClassRefOf(lowerer, expr.expression) !== null) {
return { kind: "strLit", value: "function", type: STRING, loc };
}
// The Buffer constructor and its aliases (`const B = globalThis.Buffer`,
// `(globalThis as { Buffer?: B }).Buffer`): a value read of an alias is an
// identity token, so its typeof is decided here.
if (stdlibGlobalNameOf(lowerer, expr.expression) === "Buffer") {
return { kind: "strLit", value: "function", type: STRING, loc };
}
if (ts.isPropertyAccessExpression(expr.expression)) {
const member = expr.expression;
const prototype = member.expression;
@@ -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,
@@ -191,6 +196,10 @@ export function lowerPrefixUnary(lowerer: Lowerer, expr: ts.PrefixUnaryExpressio
operatorUnsupported(lowerer, expr, "+", [raw.type]);
}
case ts.SyntaxKind.ExclamationToken: {
// `!runtimeBuffer` over an alias of the Buffer global: Node always
// defines Buffer (the condition twin is lowerCondition's).
if (lowerer.isStdlibGlobal(expr.operand, "Buffer"))
return { kind: "boolLit", value: false, type: BOOL, loc };
// `!x` is ToBoolean-then-negate: f64/string operands go through toBool.
const operand = lowerer.ensureBool(
deferredSlotOr(
@@ -386,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.
@@ -19,6 +19,7 @@ import { functionCanReturnUndefined } from "../function-completion.js";
import { jsBindingAppendsOnly, jsBindingHasOpenWrites } from "./lower-stmts.js";
import type { FnCtx } from "./function-context.js";
import { constituentTypes } from "../ts7/checker.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. */
@@ -260,6 +261,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 (
@@ -528,6 +545,10 @@ export function blockBodyOf(decl: ts.FunctionLikeDeclaration): ts.Block | null {
}
const widening = new Set<ts.Node>();
/** How many times widenReducedReturn answered a declaration it was still
* widening with its unwidened type. A result computed while that happened
* may lack arms, so it is not kept. */
let wideningCuts = 0;
/** An inferred return type is the checker's SUBTYPE-REDUCED union of the
* returned values' types: `return aborted` (`{ status }`) beside `return
@@ -557,7 +578,36 @@ function widenReducedReturn(
const arms =
mapped.kind === "union" ? (lowerer.unions.get(mapped.unionId)?.arms ?? []) : [mapped];
const recordArms = arms.filter((a) => a.kind === "record");
if (recordArms.length === 0 || widening.has(decl)) return mapped;
if (recordArms.length === 0) return mapped;
if (widening.has(decl)) {
wideningCuts++;
return mapped;
}
// Functions that forward to the same callees along several return
// branches read each callee's widened type once.
const known = lowerer.coercions.widenedReturnTypes.get(decl)?.get(retTsType);
if (known !== undefined) return known;
const cutsBefore = wideningCuts;
const widened = widenReturnArms(lowerer, decl, retTsType, mapped, arms, recordArms);
if (wideningCuts === cutsBefore) {
let byType = lowerer.coercions.widenedReturnTypes.get(decl);
if (byType === undefined) lowerer.coercions.widenedReturnTypes.set(decl, (byType = new Map()));
byType.set(retTsType, widened);
}
return widened;
}
/** widenReducedReturn's scan of the returned values, for a declaration not
* already being widened. */
function widenReturnArms(
lowerer: Lowerer,
decl: ts.SignatureDeclaration,
retTsType: ts.Type,
mapped: IrType,
arms: readonly IrType[],
recordArms: readonly IrType[],
): IrType {
const body = (decl as { body?: ts.Node }).body!;
const checkerArms = new Set<ts.Type>(
retTsType.flags & ts.TypeFlags.Union ? constituentTypes(retTsType) : [retTsType],
);
@@ -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<IrExpr["kind"]>([
"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<IrType["kind"]>(["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;
}
@@ -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) => {
@@ -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<string>, seen: Set<string>, 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<string>,
seen: Set<string>,
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<string>();
const opaqueSeen = new Set<string>();
@@ -412,6 +433,7 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio
* declaration's order, the order their table holds. */
const declaredShapes = new Set<string>();
const declaredSeen = new Set<string>();
const boxedSeen = new Set<string>();
/** 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);
@@ -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") {
@@ -119,7 +119,11 @@ import {
} from "./containers/indexed-objects.js";
import { lowerTupleReadMethodCall } from "./containers/array-methods.js";
import { tryFuseIndexRead } from "./array-values.js";
import { lowerBytesStaticCall } from "./containers/bytes.js";
import {
lowerBufferConstruct,
lowerBytesPrototypeSetCall,
lowerBytesStaticCall,
} from "./containers/bytes.js";
import {
lowerRegexMethodCall,
lowerStringIndexCall,
@@ -256,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";
@@ -363,6 +368,13 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
const codeGeneration = codeGenerationCalleeOf(lowerer, expr.expression);
if (codeGeneration !== null) return lowerCodeGenerationCall(lowerer, expr, codeGeneration);
}
// The deprecated Buffer(...) call (containers/bytes.ts). The checker
// types it `any`, so the bytes cross as a checked-dynamic value, which
// keeps the Buffer brand for inspection.
{
const buffer = lowerBufferConstruct(lowerer, expr);
if (buffer) return { kind: "dynFrom", value: buffer, type: DYN, loc };
}
// A package shim's runtime intrinsic (a bodiless `declare function`
// named scriptcUnixDgram* in a shipped shim): a direct runtime call.
if (ts.isIdentifier(expr.expression) && isShimIntrinsicName(expr.expression.text)) {
@@ -1579,6 +1591,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
lowerer.lowerTestMethodCall(expr, expr.expression) ??
lowerer.lowerTimeoutMethodCall(expr, expr.expression) ??
lowerArrayPrototypeBorrowCall(lowerer, expr, expr.expression) ??
lowerBytesPrototypeSetCall(lowerer, expr, expr.expression) ??
lowerObjectOwnPrototypeCall(lowerer, expr, expr.expression) ??
lowerObjectPrototypeCall(lowerer, expr, expr.expression) ??
lowerStringPrototypeCall(lowerer, expr, expr.expression) ??
@@ -5494,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
@@ -47,7 +47,6 @@ import {
DATE_T,
DYN,
F64,
bytesOf,
type IrClassDef,
type IrExpr,
type IrFunction,
@@ -116,8 +115,7 @@ import { isCjsRequireGlobal } from "./lower-node-module.js";
import { lowerArrayConstructor } from "./containers/array-construction.js";
import { lowerMapSeedNew, lowerSetSeedNew } from "./containers/collection-methods.js";
import { lowerObjectConstructor } from "./containers/indexed-objects.js";
import { knownBufEncoding, lowerNodeModulesBufferConstruct } from "./containers/bytes.js";
import { isSafeToRepeat } from "./expressions/evaluation-safety.js";
import { knownBufEncoding, lowerBufferConstruct } from "./containers/bytes.js";
import { lowerSearchParamsNew } from "./builtins/url.js";
import { lowerTextCodecNew } from "./builtins/text-codecs.js";
import {
@@ -1426,7 +1424,7 @@ function undefArmedFieldType(lowerer: Lowerer, p: ts.Symbol): IrType | null {
const arms =
mapped.kind === "union" ? (lowerer.unions.get(mapped.unionId)?.arms ?? []) : [mapped];
for (const a of arms) {
if (a.kind === "map" || a.kind === "regex" || a.kind === "date" || a.kind === "generator") {
if (a.kind === "map" || a.kind === "regex" || a.kind === "generator") {
return null;
}
byKey.set(typeKey(a), a);
@@ -3983,7 +3981,6 @@ export function collectClassShapeInner(
declared.kind !== "union" &&
declared.kind !== "dyn" &&
declared.kind !== "map" &&
declared.kind !== "date" &&
declared.kind !== "generator" &&
declared.kind !== "void" &&
declared.kind !== "caught";
@@ -9782,9 +9779,9 @@ function isCollectionOrErrorClass(ref: BuiltinClassRef | null): boolean {
export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
const loc = locOf(expr);
// A dependency's deprecated `new Buffer(...)` fallback (containers/bytes.ts).
// The deprecated `new Buffer(...)` (containers/bytes.ts).
{
const buffer = lowerNodeModulesBufferConstruct(lowerer, expr);
const buffer = lowerBufferConstruct(lowerer, expr);
if (buffer) return buffer;
}
// new Function(...): Node's EvalError under
@@ -10424,8 +10421,10 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
if (arg.type.kind === "string") {
return { kind: "libCall", fn: "date.newString", args: [arg], type: DATE_T, loc };
}
// new Date(date): a new Date object with the same time value.
if (arg.type.kind === "date") {
return arg;
const time: IrExpr = { kind: "libCall", fn: "date.getTime", args: [arg], type: F64, loc };
return { kind: "libCall", fn: "date.newMs", args: [time], type: DATE_T, loc };
}
lowerer.noLowering(
`new Date of '${lowerer.fmt(arg.type)}' values`,
@@ -10628,7 +10627,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
expr,
`Map values of type '${lowerer.checker.typeToString(targs[1])}' ` +
`(Map values must be number, string, boolean, records, class instances, functions, ` +
`typed arrays, arrays, promises, Maps, Sets, or supported unions and checked values)`,
`typed arrays, arrays, promises, Dates, Maps, Sets, or supported unions and checked values)`,
);
}
lowerer.badType(expr, tsType);
@@ -10863,43 +10862,6 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
// families carry pointed hints: each states WHY no honest static
// lowering exists (or what to use instead).
if (lowerer.isStdlibSymbol(symbol ?? undefined)) {
// The deprecated `new Buffer(string, encoding?)` ctor's string arm
// with a NON-STRING first argument and a string second: Node
// throws ERR_INVALID_ARG_TYPE synchronously (and DEP0005 never
// fires on this throwing path, so the compiled silence matches).
// Every other new Buffer form keeps the fence below — the
// constructing forms would owe the deprecation warning.
if (
expr.expression.text === "Buffer" &&
expr.arguments?.nodes.length === 2 &&
!expr.arguments.nodes.some(ts.isSpreadElement) &&
lowerer.mapTypeOf(lowerer.typeOf(expr.arguments.nodes[0]!))?.kind !== "string" &&
lowerer.mapTypeOf(lowerer.typeOf(expr.arguments.nodes[1]!))?.kind === "string"
) {
const first = lowerer.lowerExpr(expr.arguments.nodes[0]!);
if (
first.kind === "unitLit" ||
first.type.kind === "dyn" ||
lowerer.dynConvertible(first.type)
) {
const got: IrExpr =
first.type.kind === "dyn" ? first : { kind: "dynFrom", value: first, type: DYN, loc };
// The encoding argument still evaluates in Node before the
// throw only via the ctor body's later reads — it does NOT
// observe it before throwing, so dropping it is exact for
// effect-free operands; effectful ones keep the fence.
const enc = lowerer.lowerExpr(expr.arguments.nodes[1]!);
if (enc.kind === "strLit" || isSafeToRepeat(enc)) {
return {
kind: "libCall",
fn: "buffer.newStringFail",
args: [got],
type: bytesOf("u8"),
loc,
};
}
}
}
const ctorHints: Record<string, string | undefined> = {
RegExp: "use a regex literal (/pattern/flags) — constructed regexes have no lowering",
String:
@@ -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>([
ts.SyntaxKind.EqualsEqualsEqualsToken,
ts.SyntaxKind.ExclamationEqualsEqualsToken,
ts.SyntaxKind.EqualsEqualsToken,
ts.SyntaxKind.ExclamationEqualsToken,
]);
const RELATIONAL_OPERATORS = new Set<ts.SyntaxKind>([
ts.SyntaxKind.LessThanToken,
ts.SyntaxKind.GreaterThanToken,
ts.SyntaxKind.LessThanEqualsToken,
ts.SyntaxKind.GreaterThanEqualsToken,
]);
const NUMERIC_OPERATORS = new Set<ts.SyntaxKind>([
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>([
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,
@@ -1087,6 +1297,10 @@ export function lowerCondition(lowerer: Lowerer, expr: ts.Expression): IrExpr {
lowerer.isStdlibGlobal(e.expression, "Object")
)
return { kind: "boolLit", value: true, type: BOOL, loc: locOf(e) };
// `if (runtimeBuffer)` over an alias of the Buffer global (a
// capability probe): Node always defines Buffer.
if (lowerer.isStdlibGlobal(e, "Buffer"))
return { kind: "boolLit", value: true, type: BOOL, loc: locOf(e) };
if (ts.isPropertyAccessExpression(e)) {
const presence = lowerPromiseThenPresence(lowerer, e, {
kind: "boolLit",
@@ -333,7 +333,7 @@ export function stepDrivable(genT: GenType): boolean {
/** Whether a yield channel's values have an OUT-slot form `value` reads. */
function hasOutSlot(yt: IrType): boolean {
return yt.kind === "f64" || yt.kind === "date" || yt.kind === "bool" || isRefCounted(yt);
return yt.kind === "f64" || yt.kind === "bool" || isRefCounted(yt);
}
/** `step` and `value` resumes of a generator-typed borrowed read. */
@@ -14,7 +14,7 @@ import { InternalCompilerError } from "../../errors.js";
import * as ts from "../ts7/adapter.js";
import { unsignedHex } from "../../format-integer.js";
import type { Lowerer } from "./lowerer.js";
import { jsFuncNameOf, type Lowerer } from "./lowerer.js";
import { isJsSourceFile } from "../program.js";
import {
BOOL,
@@ -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 <Buffer ..> 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;
@@ -950,7 +946,15 @@ export function inspectExpr(
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":
@@ -2181,15 +2185,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
@@ -2416,6 +2422,12 @@ function directCallableInspect(lowerer: Lowerer, node: ts.Expression, loc: SrcLo
}
const decl = sym.declarations[0];
if (!decl) return null;
// A JavaScript function can hold own properties (`f.x = 1`, computed
// keys), which the runtime walk prints after its name.
if (isJsSourceFile(node.getSourceFile())) {
const type = lowerer.mapTypeOf(lowerer.typeOf(node));
if (type?.kind === "func" && lowerer.dynConvertible(type)) return null;
}
if (ts.isFunctionDeclaration(decl) && decl.name) {
return strLit(`[Function: ${decl.name.text}]`, loc);
}
@@ -2476,7 +2488,17 @@ export function lowerInspectValue(
loc,
);
}
const value = lowerer.lowerExpr(valueNode);
let value = lowerer.lowerExpr(valueNode);
// A JavaScript function renders through the runtime walk, which prints
// its own properties after its name.
if (
value.type.kind === "func" &&
isJsSourceFile(valueNode.getSourceFile()) &&
lowerer.dynConvertible(value.type)
) {
const name = jsFuncNameOf(valueNode);
value = { kind: "dynFrom", value, type: DYN, ...(name !== null ? { fnName: name } : {}), loc };
}
if (value.type.kind === "bytes") {
// One runtime representation serves Buffer AND Uint8Array; their
// renderings differ (<Buffer aa> vs Uint8Array(1) [ 170 ]), so the
@@ -2510,7 +2532,10 @@ export function lowerInspectValue(
value,
(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,
@@ -2519,7 +2544,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);
@@ -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 &&
@@ -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";
@@ -29,6 +36,7 @@ import { lowerOptionalIndex } from "./undefined-index.js";
import { processDataMemberValue, programReadsExitCode } from "./builtins/process.js";
import { requireInitFlagStmt } from "./require-initialization.js";
import { isJsonRequireDecl } from "./json-require.js";
import { isTimeValueDateDeclaration } from "./date-values.js";
import { bindingSource } from "../binding-source.js";
import { lowerConstructorReturn } from "./constructor-returns.js";
import type { Lowerer } from "./lowerer.js";
@@ -38,6 +46,11 @@ import {
cjsTableMethodEntry,
inlineProgramRequireOf,
requireNestedPatternsOf,
splitRequireAssignmentOf,
splitRequireBindingOf,
splitRequireBindingOfIdent,
splitRequireEarlyReadOf,
wholeExportSymbolOfModule,
} from "./cjs-inline-require.js";
import {
cjsExportMemberWriteGlobalOf,
@@ -64,6 +77,7 @@ import {
BOOL,
BYTES_U8,
CAUGHT,
DATE_T,
DYN,
F64,
type IrExpr,
@@ -1932,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
@@ -5415,6 +5446,16 @@ export function lowerVarDecl(
if (alias) return alias;
}
// A Date local whose identity no use observes stores its time value.
if (isTimeValueDateDeclaration(lowerer, decl)) {
const loc = locOf(decl);
const date = lowerer.lowerExprExpecting(decl.initializer!, DATE_T);
const local = lowerer.declareLocal(decl.name, decl.name.text, F64, isLet);
lowerer.timeValueDateLocals.add(local);
const init: IrExpr = { kind: "libCall", fn: "date.getTime", args: [date], type: F64, loc };
return { kind: "varDecl", localId: local.id, init, loc };
}
// A NULLISH generic binding (`const i: I<A & B> = null as any` — the
// declared type has no mapping, the value is provably null/undefined
// forever): no storage exists; reads know the value (member reads and
@@ -6885,7 +6926,7 @@ function lowerDynSwitch(
loc: locOf(expression),
};
}
if (test.type.kind === "date" || !lowerer.dynConvertible(test.type)) {
if (!lowerer.dynConvertible(test.type)) {
lowerer.unsupported(
"SC1100",
expression,
@@ -7525,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 (
@@ -8435,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(
@@ -8749,13 +8820,20 @@ function destrSpellingOf(rhs: ts.Expression): string | null {
return base === null ? null : `${base}.${e.name.text}`;
}
// `table[key]` and `list[0]`; V8 prints a numeric key in its canonical
// number form (`o[1.50]` reads "o[1.5]").
// number form (`o[1.50]` reads "o[1.5]"), and a string key as a property
// name with its text decoded (`o["a.b"]` and `o[\`a.b\`]` read "o.a.b").
// The message is a C string, so a key holding NUL keeps the refusal.
if (ts.isElementAccessExpression(e) && !e.questionDotToken) {
const base = destrSpellingOf(e.expression);
const key = e.argumentExpression;
if (base === null) return null;
if (ts.isIdentifier(key)) return `${base}[${key.text}]`;
if (ts.isNumericLiteral(key)) return `${base}[${String(Number(key.text))}]`;
if (
(ts.isStringLiteral(key) || ts.isNoSubstitutionTemplateLiteral(key)) &&
!key.text.includes("\0")
)
return `${base}.${key.text}`;
return null;
}
if (ts.isCallExpression(e) && !e.questionDotToken) {
@@ -10142,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;
@@ -10706,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);
@@ -10800,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");
}
@@ -10808,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());
@@ -148,12 +148,18 @@ 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";
import { installBufferValue } from "./containers/bytes.js";
import { ClassProtocols } from "./class-protocols.js";
import { earlyReadVarSymbols } from "./var-hoisting.js";
import { recordTextCodecClass } from "../../ir/ir.js";
@@ -343,10 +349,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 +435,7 @@ import {
collectSignatureInner,
lambdaSignature,
} from "./call-signatures.js";
import { dynamicOverloadChoice, guessedOverloadIdentifier } from "./overload-choice.js";
import {
implicitAnyParamSymbolsOf,
implicitLocalFnNodeOf,
@@ -2291,6 +2301,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<ts.SourceFile, IrGlobal>();
/** `var m; m = require("./x")` alias bindings (cjs-inline-require.ts).
* Null entries cache declarations that keep ordinary storage. */
readonly cjsSplitRequireBindings = new Map<ts.VariableDeclaration, SplitRequireBinding | null>();
/** Per-module whole-export symbol (cjs-inline-require.ts). Null means the
* module's exports are a table. */
readonly cjsWholeExportSymbols = new Map<ts.SourceFile, ts.Symbol | null>();
/** Per-module live member-export classification (cjs-inline-require.ts). */
readonly cjsLiveMemberExports = new Map<ts.SourceFile, boolean>();
/** 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<ts.CallExpression>();
@@ -2341,6 +2359,10 @@ export class Lowerer {
readonly runtimeOptionalFields = new Set<string>();
/** Bindings initialized by a Date's `toJSON()` (analyzeDateJsonResults). */
readonly dateJsonBindings = new Set<ts.Symbol>();
/** isTimeValueDateDeclaration's answers, and the locals that hold a
* Date's time value instead of a Date object (date-values.ts). */
readonly timeValueDateDecisions = new Map<ts.Node, boolean>();
readonly timeValueDateLocals = new Set<IrLocal>();
/** Record bindings whose flow class reaches storage outside the class:
* they keep the declared layout, so an initializer from unchecked reads
* converts once instead of the binding adopting the widened variant. */
@@ -2825,6 +2847,8 @@ export class Lowerer {
/** Parameters read as `any` in the error-path copy of an error-first
* callback. */
readonly anySymbols = new Set<ts.Symbol>();
/** overload-choice's answers per call, binding and function. */
readonly dynamicOverloadAnswers = new Map<ts.Node, boolean>();
/** 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). */
@@ -2900,6 +2924,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<string, string>();
/** Indexed-union safe-read helpers mapped to their numeric twins, for the
* fusion of an immediate ToNumber (tryLowerNumericIndexRead). */
readonly indexedUnionNumericTwins = new Map<string, string>();
private readonly unionLiteralOwners = new Map<
string,
{ owners: Map<string, number> | null; shapes: number; unions: number }
@@ -2939,6 +2966,10 @@ export class Lowerer {
* checks resolve through this map (stdlibGlobalNameOf), declarations
* emit nothing. */
readonly stdlibGlobalAliases = new Map<ts.Symbol, string>();
/** The stdlibGlobalAliases bound through a cast of the global object
* (`(globalThis as { Buffer?: B }).Buffer`). Their static type is the
* cast's, so their value reads are refused (identifiers.ts). */
readonly castGlobalAliases = new Set<ts.Symbol>();
/** CJS export-table ACCESSORS (`module.exports = { get path() {...} }`),
* lifted lazily as module-level functions and interned per accessor
* declaration: member reads call the getter (lower-exprs). */
@@ -3137,8 +3168,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<ts.Expression, { localId: string; type: IrType } | null>();
/** The staged arguments that follow each such URL path, in order. */
readonly stagedAfterPath = new Map<ts.Expression, ts.Expression[]>();
/** What each fs path argument of a URL-path call lowers to (the plans
* of builtins/fs-paths.ts). */
readonly fsPathPlans = new Map<ts.Expression, FsPathPlan>();
/** 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<ts.CallExpression>();
readonly moduleNsParamOverrides = new Map<
ts.ParameterDeclaration,
IrType & { kind: "moduleNs" }
@@ -4123,6 +4159,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;
}
@@ -4516,8 +4557,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;
@@ -4622,6 +4663,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` /
@@ -4929,6 +4973,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
@@ -7725,9 +7776,11 @@ export class Lowerer {
finalizeClassMethodValues(this, functions);
finalizePrivateBrands(this, functions);
trackClassFieldCreation(this, functions);
boxClassInstancesExactly(this, functions);
guardClassAttributeWrites(this, functions);
guardFrozenWrites(this, functions);
applyHandleProperties(this, functions);
installBufferValue(functions);
// Globals typed by a class that never REGISTERED (a JS class whose
// collection fenced — Symbol-keyed fields, an unsupported base): the
@@ -8989,6 +9042,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
@@ -13909,6 +13976,9 @@ export class Lowerer {
loc,
};
}
// The buffer module's Buffer is the global constructor's value.
if (bi.module === "buffer" && bi.member === "Buffer")
return { kind: "libCall", fn: "buffer.constructor", args: [], type: DYN, loc };
if (bi.module === "fs" && (bi.member === "realpathSync" || bi.member === "realpathSync.native"))
return fsRealpathValue(this, bi.member === "realpathSync.native", loc);
const fn = builtinModuleFnOf(this, bi.module, bi.member);
@@ -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 {
@@ -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);
}
@@ -6,6 +6,20 @@ function unparenthesized(e: ts.Expression): ts.Expression {
while (ts.isParenthesizedExpression(e)) e = e.expression;
return e;
}
/** `e` without parentheses and type-only wrappers (as, satisfies, type
* assertions, non-null assertions), which change no runtime value. */
function withoutTypeCasts(e: ts.Expression): ts.Expression {
while (
ts.isParenthesizedExpression(e) ||
ts.isAsExpression(e) ||
ts.isSatisfiesExpression(e) ||
ts.isTypeAssertion(e) ||
ts.isNonNullExpression(e)
)
e = e.expression;
return e;
}
import { UNSUPPORTED } from "../../diagnostics/diagnostic.js";
import {
BOOL,
@@ -1563,6 +1577,7 @@ export const BUILTIN_MODULE_FNS: Record<
generateKeyPair: { fn: "crypto.generateKeyPairCb", params: [], result: VOID },
sign: { fn: "crypto.signOneshot", params: [], result: DYN },
verify: { fn: "crypto.verifyOneshot", params: [], result: DYN },
diffieHellman: { fn: "crypto.diffieHellman", params: [], result: DYN },
pbkdf2Sync: {
fn: "crypto.pbkdf2",
params: [BYTES_U8, BYTES_U8, F64, F64, STRING],
@@ -2070,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"],
@@ -3101,9 +3125,6 @@ export const BUILTIN_MODULE_FENCE_HINTS: Record<
"createSign, createVerify, createECDH, and createDiffieHellman",
]),
),
diffieHellman:
"crypto.diffieHellman takes DH, EC or X25519 KeyObjects and has no lowering; " +
"the objects of createDiffieHellman, getDiffieHellman and createECDH compute shared secrets",
setFips:
"a compiled binary has no FIPS provider to enable, and Node itself throws on setFips(true) " +
"in a non-FIPS build — getFips() answers 0 here",
@@ -3440,6 +3461,20 @@ export function stdlibGlobalNameOf(lowerer: Lowerer, expr: ts.Expression): strin
return ts.symbolName(resolved) === "global" ? "globalThis" : ts.symbolName(resolved);
}
if (ts.isPropertyAccessExpression(expr) && !expr.questionDotToken) {
// The buffer module's Buffer (`require("buffer").Buffer`, `buffer_1.Buffer`)
// is the global constructor.
if (bufferNamespaceMember(lowerer, expr)) return "Buffer";
// A cast of the global object (`(globalThis as { Buffer?: B }).Buffer`,
// `(globalThis as any).Buffer`) changes only its static type: the
// member is the global object's own.
const uncast = withoutTypeCasts(expr.expression);
if (uncast !== unparenthesized(expr.expression)) {
if (!ts.isIdentifier(uncast) || stdlibGlobalNameOf(lowerer, uncast) !== "globalThis")
return null;
const member = lowerer.checker.getPropertyOfType(lowerer.typeOf(uncast), expr.name.text);
if (!member || !lowerer.isStdlibSymbol(member)) return null;
return ts.symbolName(member) === "global" ? "globalThis" : ts.symbolName(member);
}
if (stdlibGlobalNameOf(lowerer, expr.expression) !== "globalThis") return null;
// Through a detection chain the checker types the receiver as the
// union of every arm; the selected arm (the global object) owns the
@@ -3572,9 +3607,28 @@ export function stdlibGlobalAliasDecl(
const symbol = lowerer.checker.getSymbolAtLocation(nameNode);
if (!symbol) return false;
lowerer.stdlibGlobalAliases.set(symbol, name);
if (init !== undefined && castGlobalAliasSource(lowerer, init))
lowerer.castGlobalAliases.add(symbol);
return true;
}
/** True when an alias initializer reads its global through a cast of the
* global object (`(globalThis as { Buffer?: B }).Buffer`), directly or as
* the left operand of `||` (stdlibGlobalAliasNameOf's guard), or copies an
* alias made that way. Such an alias carries the cast's static type. */
function castGlobalAliasSource(lowerer: Lowerer, init: ts.Expression): boolean {
let source = unparenthesized(init);
if (ts.isBinaryExpression(source) && source.operatorToken.kind === ts.SyntaxKind.BarBarToken)
source = unparenthesized(source.left);
if (ts.isPropertyAccessExpression(source))
return withoutTypeCasts(source.expression) !== unparenthesized(source.expression);
if (ts.isIdentifier(source)) {
const symbol = lowerer.checker.getSymbolAtLocation(source);
return symbol !== undefined && lowerer.castGlobalAliases.has(symbol);
}
return false;
}
/** True iff the symbol is declared ONLY by the adopted @types/node
* surface (no es-lib or shipped-ambient declaration merges in) — chooses
* the SC2020 fence's wording: "typed by @types/node", not "standard
+2 -1
View File
@@ -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()
@@ -1,4 +1,5 @@
import type {
CallExpression,
ExportDeclaration,
Identifier,
SourceFile,
@@ -30,6 +31,13 @@ export class ProgramAnalysis {
readonly requireEarlyReferences = new Map<VariableDeclaration, Identifier | null>();
/** Per file: the first top-level statement that can run user code. */
readonly requireFirstRunnable = new Map<SourceFile, number>();
/** 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<VariableDeclaration, CallExpression>
>();
clear(): void {
this.createRequireReasons.clear();
@@ -41,5 +49,6 @@ export class ProgramAnalysis {
this.moduleEarlyBindings = null;
this.requireEarlyReferences.clear();
this.requireFirstRunnable.clear();
this.splitRequireBindings.clear();
}
}
+224 -17
View File
@@ -1378,6 +1378,11 @@ function suppressedJsStrictness7(d: ts.Diagnostic): boolean {
if (JS_ANY_OPERATOR_CODES.has(d.code)) {
return ts.flattenDiagnosticMessageText(d, "\n").includes("'any'");
}
// The deprecated Buffer(...) call without `new`, which Node's Buffer
// function accepts (containers/bytes.ts lowers it).
if (d.code === 2348) {
return ts.flattenDiagnosticMessageText(d, "\n").includes("'BufferConstructor'");
}
return false;
}
@@ -1418,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 });
@@ -1436,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("<literal>")` 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<ts.VariableDeclaration, ts.CallExpression> {
const memo = program.analysis.splitRequireBindings;
const cached = memo.get(sf);
if (cached !== undefined) return cached;
const out = new Map<ts.VariableDeclaration, ts.CallExpression>();
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<string>();
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.Symbol, number>();
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 (
@@ -1575,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;
@@ -1702,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);
@@ -1760,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 => {
@@ -1983,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";
}
@@ -3494,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;
@@ -5770,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 => {
@@ -5778,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;
@@ -5788,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),
@@ -5822,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");
});
@@ -5995,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);
@@ -6745,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,
+22 -15
View File
@@ -183,6 +183,7 @@ export const CRYPTO_KEY_OPTION_DYN_TYPES: ReadonlySet<string> = new Set([
"PrivateKeyInput",
"PublicKeyInput",
"RSAKeyPairKeyObjectOptions",
"X25519KeyPairOptions",
"RSAKeyPairOptions",
"SignKeyObjectInput",
"SignPrivateKeyInput",
@@ -1392,10 +1393,18 @@ function genChannels(
: mapType(nextTs, ctx);
if (!nextT || nextT.kind === "void" || nextT.kind === "nullT") return null;
const ownChannel = (t: IrType): boolean =>
t.kind === "f64" || t.kind === "bool" || t.kind === "string" || t.kind === "record";
t.kind === "f64" || t.kind === "bool" || t.kind === "string";
// A record yield channel keeps its type beside a dyn return (an
// `IterableIterator<Row>`). A record return channel beside a dyn yield
// does not: the value a delegating `yield*` answers (Effect.gen's
// generators) is often an object whose methods a record would drop.
const dynMix =
(yieldT.kind === "dyn" && retT.kind !== "dyn" && retT.kind !== "void" && !ownChannel(retT)) ||
(retT.kind === "dyn" && yieldT.kind !== "dyn" && yieldT.kind !== "void" && !ownChannel(yieldT));
(retT.kind === "dyn" &&
yieldT.kind !== "dyn" &&
yieldT.kind !== "void" &&
!ownChannel(yieldT) &&
yieldT.kind !== "record");
if (dynMix) return { yieldT: DYN, retT: DYN, nextT };
return { yieldT, retT, nextT };
}
@@ -3222,10 +3231,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
}
const collectionView = mapCollectionView(widened, ctx);
if (collectionView) return collectionView;
// Date: a TimeClip'd epoch-millisecond scalar in the static runtime.
// This supports stored/passed values and the read-only getter slice;
// identity and mutation stay fenced because the scalar deliberately
// carries no object identity.
// Date: a refcounted object holding its TimeClip'd time value (ScrDate),
// shared with the checked-dynamic Date handle, so it keeps its identity
// through every slot and through `unknown`. Setters stay fenced.
if (isStdlibInterface("Date")) return DATE_T;
// RegExp: a reference to the lib RegExp interface — provenance, not the
// name (a user's own `interface RegExp` maps as a record). Regex values
@@ -3957,10 +3965,11 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
// argument — fenced at the call).
// Mixed dyn/concrete value channels share checked storage, so either
// branch of the result record has the same representation. A number,
// boolean, string or record channel keeps its own type beside a dyn one
// (the `Generator<number>` and `IterableIterator<Row>` defaults): its
// values ride OUT typed and box only when the result record is built, so
// for-of and spread read them back unchecked.
// boolean or string channel keeps its own type beside a dyn one (the
// `Generator<number>` default), and so does a record yield channel beside
// a dyn return (`IterableIterator<Row>`): its values ride OUT typed and box
// only when the result record is built, so for-of and spread read them
// back unchecked.
if (
isStdlibInterface("Generator") ||
isStdlibInterface("AsyncGenerator") ||
@@ -5058,7 +5067,6 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
(a) =>
a.kind === "void" ||
a.kind === "union" ||
a.kind === "date" ||
a.kind === "dyn" ||
// A generator arm only comes from an Iterable<T> (an optional
// one keeps its unit arms beside the iterable union's).
@@ -6611,7 +6619,7 @@ export function genResultRecord(
for (const a of unions.get(t.unionId)?.arms ?? []) byKey.set(typeKey(a), a);
return true;
}
if (t.kind === "regex" || t.kind === "date" || t.kind === "generator") {
if (t.kind === "regex" || t.kind === "generator") {
return false; // no legal union arm exists for these kinds
}
byKey.set(typeKey(t), t);
@@ -6700,15 +6708,15 @@ export function withUnitArm(
if (t.kind === "union") {
const def = unions.get(t.unionId);
if (!def) return null;
if (def.arms.some((a) => a.kind === "date")) return null;
if (def.arms.some((a) => a.kind === kind)) return t;
const arms = [...def.arms, unit];
// Date and Promise arms beside other data arms have no union home.
if (!unionContainerArmsOk(arms)) return null;
arms.sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1));
return { kind: "union", unionId: unions.transform(def, arms) };
}
if (
t.kind === "void" ||
t.kind === "date" ||
t.kind === "dyn" ||
// A bare unit field type cannot occur (units live only inside unions),
// but guard against constructing a single-arm union from one.
@@ -8298,7 +8306,6 @@ function armHasUnionHome(arm: IrType): boolean {
case "void":
case "union":
case "regex":
case "date":
case "generator":
case "dyn":
return false;
+16
View File
@@ -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 } : {}),
@@ -739,6 +739,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"url.set",
"url.setChecked",
"url.fileURLToPathUrl",
"fs.checkPath",
"fs.checkReadEncoding",
"url.fileURLToPathChecked",
"url.fileURLToPathOptions",
"url.fileURLToPathBuffer",
@@ -835,12 +837,15 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"buffer.poolFromDyn",
"buffer.poolFromDynRef",
"buffer.poolFromDynOffset",
"buffer.poolFromDynRange",
// The checked-dynamic compare/equals validators: Node's argument
// ladders throw ERR_INVALID_ARG_TYPE / ERR_OUT_OF_RANGE catchably.
"buffer.compareChk",
"bytes.equalsChk",
"bytes.compareChk",
"buffer.newStringFail",
// Inherited and symbol-keyed Buffer.prototype members throw SC2020.
"buffer.prototypeMember",
"fs.toUnixTimestamp",
"fs.existsChk",
"fs.mkdtempChk",
@@ -1031,6 +1036,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"crypto.generateKeyPairCb",
"crypto.signOneshot",
"crypto.verifyOneshot",
"crypto.diffieHellman",
"crypto.createSign",
"crypto.createVerify",
]);
@@ -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,
@@ -3016,6 +3024,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
/** poolFromDyn that also copies an array crossing by reference. */
"buffer.poolFromDynRef": { argTypes: [DYN, STRING], result: BYTES_U8 },
"buffer.poolFromDynOffset": { argTypes: [DYN, DYN], result: BYTES_U8 },
/** Buffer.from(value, byteOffset, length): an ArrayBuffer's range with
* Node's fromArrayBuffer checks; other values as poolFromDynOffset. */
"buffer.poolFromDynRange": { argTypes: [DYN, DYN, DYN], result: BYTES_U8 },
"buffer.poolSize": { argTypes: [], result: F64 },
"buffer.setPoolSize": { argTypes: [F64], result: VOID },
/** Buffer.byteLength(string, enc) — enc a NORMALIZED literal like
@@ -3050,6 +3061,12 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
* always throws Node's ERR_INVALID_ARG_TYPE ("The \"string\" argument
* must be of type string. Received ..."). Borrowed dyn; may-throw. */
"buffer.newStringFail": { argTypes: [DYN], result: BYTES_U8 },
"buffer.deprecation": { argTypes: [], result: VOID },
"buffer.deprecationCount": { argTypes: [], result: VOID },
"buffer.constructor": { argTypes: [], result: DYN },
"buffer.installValue": { argTypes: [], result: VOID },
"buffer.staticValue": { argTypes: [STRING], result: DYN },
"buffer.prototypeMember": { argTypes: [DYN], result: DYN },
/** fs._toUnixTimestamp(time) over a dyn value: numeric strings and
* finite numbers coerce (negatives answer now/1000, Node's shape);
* everything else throws Node's ERR_INVALID_ARG_TYPE. May-throw. */
@@ -3194,6 +3211,18 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
// or an optional Error | null slot (same success shape as fs.rename).
"process.stdoutWriteBytesCb": { argTypes: [BYTES_U8, STRING, null], result: BOOL },
"process.stderrWriteBytesCb": { argTypes: [BYTES_U8, STRING, null], result: BOOL },
/** The value of a FULFILLED promise, read without a microtask turn (+1
* for references): the tuple form of Promise.all reads its entries this
* way once the combinator fulfills, each entry widened to Promise<void>
* and read at the type it was built with (program-dependent). */
"promise.fulfilledValue": { argTypes: [null], result: VOID },
/** Promise.all's tuple form as a value: the entries (each widened to
* Promise<void>) join the runtime combinator, and when it fulfills the
* function reads the fulfilled entries into the tuple inside that
* settlement, so the result settles on the combinator's turn; a
* rejection settles it the same way. The result is the tuple's promise
* (program-dependent). */
"promise.allTuple": { argTypes: [null, null], result: VOID },
"fsp.readFile": { argTypes: [STRING, STRING], result: { kind: "promise", inner: STRING } },
"fsp.writeFile": { argTypes: [STRING, STRING], result: { kind: "promise", inner: VOID } },
/** fs.promises.writeFile(path, data, { mode }): the settled-promise
@@ -4431,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 },
@@ -4822,6 +4854,10 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
"date.getMilliseconds": { argTypes: [DATE_T], result: F64 },
"date.getUTCMilliseconds": { argTypes: [DATE_T], result: F64 },
"date.getTimezoneOffset": { argTypes: [DATE_T], result: F64 },
/** A Date object for a time value a local holds (date-values.ts): a
* lib-call argument passes the time value itself, and any other use
* creates the Date. */
"date.fromTime": { argTypes: [F64], result: DATE_T },
/** Date.prototype.toString / toDateString / toTimeString and String(date):
* V8's local-time formats with the host zone's long name, "Invalid Date"
* for an invalid date. Owned (+1) string; never throws. */
@@ -4874,6 +4910,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
"crypto.generateKeyPairSync": { argTypes: [DYN, DYN], result: DYN },
"crypto.generateKeyPairCb": { argTypes: [DYN, DYN, DYN], result: VOID },
"crypto.signOneshot": { argTypes: [DYN, DYN, DYN], result: DYN },
"crypto.diffieHellman": { argTypes: [DYN, DYN], result: DYN },
"crypto.verifyOneshot": { argTypes: [DYN, DYN, DYN, DYN], result: DYN },
"crypto.createSign": { argTypes: [DYN], result: DYN },
"crypto.createVerify": { argTypes: [DYN], result: DYN },
+2
View File
@@ -449,6 +449,8 @@ const DYN_READ_ONLY = new Set<string>([
"error.new",
"error.newOptions",
"fs.utimesSync",
// Buffer.prototype members read as values: a lookup by key.
"buffer.prototypeMember",
"insp.dyn",
"insp.dynS",
// The dynamic-tier operators run their dyn twins (scr_js_value.c); the
+17 -4
View File
@@ -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;
+2 -1
View File
@@ -57,7 +57,8 @@ describe("IR kind sets", () => {
test("distinguishes pointer values from object-like scalars", () => {
expect(POINTER_KINDS.has("record")).toBe(true);
expect(POINTER_KINDS.has("date")).toBe(false);
expect(POINTER_KINDS.has("date")).toBe(true);
expect(POINTER_KINDS.has("procStream")).toBe(false);
});
test("distinguishes typed-rest closure ABIs", () => {
+33 -6
View File
@@ -440,6 +440,7 @@ const POINTER_KIND_LIST = [
"url",
"searchParams",
"symbol",
"date",
...POINTER_HANDLE_KINDS,
"func",
"object",
@@ -463,7 +464,7 @@ export const POINTER_KINDS = irKindSet(POINTER_KIND_LIST);
export const RUNTIME_RC_STEMS: Record<IrType["kind"], string> = {
f64: "",
bigint: "scr_bigint",
date: "",
date: "scr_date",
string: "scr_str",
bool: "",
array: "scr_arr",
@@ -645,6 +646,7 @@ export function isSupportedArrayElem(t: IrType): boolean {
case "func":
case "promise":
case "regex":
case "date":
case "child":
case "netServer":
// fs.Stats snapshots (`await Promise.all(paths.map(stat))`): immutable
@@ -689,7 +691,8 @@ export function isIdentityCollectionKey(t: IrType): boolean {
// functions intern one immortal closure, inner closures allocate once
// per evaluation), so pointer identity is JS function identity.
// Closures are cycle-headered and traced like records.
t.kind === "func"
t.kind === "func" ||
t.kind === "date"
);
}
@@ -797,6 +800,9 @@ export function isSupportedMapValue(t: IrType): boolean {
// the pre-settle closures.
case "secureCtx":
return true;
// A Date object: immutable, holds no references.
case "date":
return true;
// A promise (Map<string, Promise<SecureContext>> — the SNI callback's
// in-flight dedupe map): refcounted, with trace/cycle support already
// in place (scr_promise_trace_v — the race machinery's refcounting).
@@ -849,8 +855,9 @@ export function funcOf(params: IrType[], ret: IrType): IrType {
* promises nullable-only; declared maybe-promise unions are admitted by
* maybePromiseUnionArmsOk. */
export function unionContainerArmsOk(arms: IrType[]): boolean {
return arms.every(
(a, i) => a.kind !== "promise" || arms.every((b, j) => j === i || isUnitType(b)),
return (
arms.every((a, i) => a.kind !== "promise" || arms.every((b, j) => j === i || isUnitType(b))) &&
dateUnionArmsOk(arms)
);
}
@@ -861,7 +868,13 @@ export function unionContainerArmsOk(arms: IrType[]): boolean {
* non-thenable path). Data arms must not be thenable; the type mapper
* checks that against the checker types before minting such a union. */
export function maybePromiseUnionArmsOk(arms: IrType[]): boolean {
return arms.filter((a) => a.kind === "promise").length <= 1;
return arms.filter((a) => a.kind === "promise").length <= 1 && dateUnionArmsOk(arms);
}
/** A Date arm has a union home beside undefined and null arms only: no
* narrowing contract tells it from other data arms yet. */
function dateUnionArmsOk(arms: IrType[]): boolean {
return arms.every((a, i) => a.kind !== "date" || arms.every((b, j) => j === i || isUnitType(b)));
}
/** Canonical, injective text form of an IrType — the building block of
@@ -1486,6 +1499,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
@@ -2494,6 +2511,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"
@@ -4039,6 +4064,9 @@ function isJsonSafeAt(
case "string":
case "bool":
return true;
// JSON.stringify writes a Date's toJSON(); JSON text never parses to one.
case "date":
return stringify;
case "array":
return isJsonSafeAt(
t.elem,
@@ -4137,7 +4165,6 @@ function isJsonSafeAt(
// Maps are not JSON (JSON.stringify(new Map()) is "{}" in Node — an
// empty-object husk nobody wants; stringify/dynCheck reject instead).
case "map":
case "date":
// URLSearchParams stringifies as the same "{}" husk — rejected; use
// sp.toString() instead.
case "searchParams":
+21 -1
View File
@@ -100,6 +100,8 @@ const DYN_ASYNC_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"process.setDeprecationFlag",
"process.setEmitWarning",
"punycode.warn",
// DEP0005 for the deprecated Buffer constructor.
"buffer.deprecation",
"timers.immediatePromise",
// The performance timeline (scr_perf.c, linked with this unit for its
// MaxPerformanceEntryBufferExceededWarning).
@@ -366,6 +368,10 @@ const SYMBOL_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"dyn.usingDispose",
"dyn.usingMethod",
"util.isDeepStrictEqual",
// The Buffer function value's Symbol(kIsEncodingSymbol) member
"buffer.constructor",
"buffer.staticValue",
"buffer.prototypeMember",
]);
/** Entry points that link the querystring unit. */
@@ -732,6 +738,7 @@ export const PKI_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"crypto.generateKeyPairSync",
"crypto.signOneshot",
"crypto.verifyOneshot",
"crypto.diffieHellman",
]);
/** The WebCrypto algorithms scr_webcrypto_pki.c implements over the pki
@@ -906,6 +913,7 @@ const RUNTIME_FEATURE_RULES = {
...UTIL_CALLABLE_LIB_FNS,
...BUILTIN_CLASS_LIB_FNS,
"util.promisifyDyn",
"buffer.prototypeMember",
],
// Constant-key reads of Object.prototype member names answer the
// inherited member through scr_dyn_invoke.c.
@@ -934,10 +942,22 @@ const RUNTIME_FEATURE_RULES = {
/** Buffer.prototype methods called on checked-dynamic values
* (scr_bytes_dyn.c). */
dynBuffer: rule({
// Buffer.prototype members read as values dispatch to the same entries.
fns: ["buffer.prototypeMember"],
expr: (node, _tables) => node.kind === "dynInvoke" && DYN_BUFFER_METHODS.has(node.method),
}),
/** Builtin functions read as values (scr_builtin_values.c). */
builtinValues: rule({ fns: ["buffer.byteLenValue", "buffer.allocValue", "fs.realpathValue"] }),
builtinValues: rule({
fns: [
"buffer.byteLenValue",
"buffer.allocValue",
"fs.realpathValue",
"buffer.constructor",
"buffer.installValue",
"buffer.staticValue",
"buffer.prototypeMember",
],
}),
/** The stream unit emits through the emitter unit, also when the
* program constructs no stream (a pipeline of functions). */
/** scr_ecdh.c over the mbedTLS archive; DEP0031 reports through the
+46 -4
View File
@@ -55,6 +55,7 @@ import {
typeEquals,
typeKey,
maybePromiseUnionArmsOk,
unionContainerArmsOk,
VOID,
} from "./ir.js";
import { DYN_CLASS_PROPERTIES, callSiteReturnType, classValueConstructor } from "./ir.js";
@@ -803,7 +804,6 @@ export function validateModule(mod: IrModule): IrValidationError[] {
arm.kind === "void" ||
arm.kind === "union" ||
arm.kind === "dyn" ||
arm.kind === "date" ||
(arm.kind === "generator" && iterableArmsElem(u.arms) === null)
) {
errors.push({ message: `union ${u.id}: arm ${i} is ${arm.kind}`, loc: noLoc });
@@ -814,6 +814,9 @@ export function validateModule(mod: IrModule): IrValidationError[] {
loc: noLoc,
});
}
if (arm.kind === "date" && !unionContainerArmsOk(u.arms)) {
errors.push({ message: `union ${u.id}: date arm ${i} beside non-unit arms`, loc: noLoc });
}
if (arm.kind === "record" && !recordsById.has(arm.shapeId)) {
errors.push({
message: `union ${u.id}: arm ${i} references undeclared shape "${arm.shapeId}"`,
@@ -1507,6 +1510,7 @@ function validateFunction(
e.left.type.kind === "map" ||
e.left.type.kind === "set" ||
e.left.type.kind === "regex" ||
e.left.type.kind === "date" ||
e.left.type.kind === "url" ||
e.left.type.kind === "object" ||
e.left.type.kind === "record" ||
@@ -2192,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).
@@ -2372,9 +2379,13 @@ function validateFunction(
(e.method === "sortPrimitive" || e.method === "toSortedPrimitive") &&
elem.kind !== "string" &&
elem.kind !== "f64" &&
elem.kind !== "bool"
elem.kind !== "bool" &&
elem.kind !== "date"
) {
err(`arrIntrinsic ${e.method} requires primitive elements, got ${elem.kind}`, e.loc);
err(
`arrIntrinsic ${e.method} requires primitive or Date elements, got ${elem.kind}`,
e.loc,
);
}
if (e.method === "sortValues" || e.method === "sortDense") {
const arms = elem.kind === "union" ? unions.get(elem.unionId)?.arms : undefined;
@@ -2456,13 +2467,18 @@ function validateFunction(
elem.kind !== "f64" &&
elem.kind !== "string" &&
elem.kind !== "bool" &&
elem.kind !== "date" &&
!(
elem.kind === "union" &&
(unions
.get(elem.unionId)
?.arms.every(
(a) =>
a.kind === "f64" || a.kind === "string" || a.kind === "bool" || isUnitType(a),
a.kind === "f64" ||
a.kind === "string" ||
a.kind === "bool" ||
a.kind === "date" ||
isUnitType(a),
) ??
false)
)
@@ -5339,6 +5355,32 @@ function validateFunction(
}
function checkLibSpecialCall(e: IrExpr & { kind: "libCall" }): boolean {
if (e.fn === "promise.fulfilledValue") {
const promise = e.args[0]?.type;
if (
promise?.kind !== "promise" ||
e.type.kind === "void" ||
isUnitType(e.type) ||
promise.inner.kind !== "void"
)
err(`libCall ${e.fn} must read a Promise<void> entry at a non-void type`, e.loc);
return true;
}
if (e.fn === "promise.allTuple") {
const [entries, build] = e.args;
if (
entries?.type.kind !== "array" ||
entries.type.elem.kind !== "promise" ||
entries.type.elem.inner.kind !== "void" ||
build?.kind !== "closure" ||
build.captures.length > 0 ||
build.type.kind !== "func" ||
e.type.kind !== "promise" ||
!typeEquals(build.type.ret, e.type.inner)
)
err(`libCall ${e.fn} must join Promise<void> entries into its builder's tuple`, e.loc);
return true;
}
if (e.fn === "als.bindCallback") {
// The bound function, at its closure argument's own type.
const fn = e.args[0]?.type;

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